Comments, Docstrings, and the Style That Makes Python Readable
1 · The lesson
readOne of Python's design rules is "readability counts". Python even has an official style guide, PEP 8, and almost every Python project follows it. If you learn these conventions early, your code will look like the code experienced Python programmers write (people call this Pythonic code).
This is a short lesson, but it matters. Ignoring these conventions is one of the most common reasons code looks like a beginner wrote it.
1. Comments: the WHY, Not the WHAT
A comment starts with #. Everything after it on that line is ignored by Python.
# This is a comment x = 5 # this is an inline comment # Block comments can span # multiple lines like this
Beginners often write comments that say what the code does:
# BAD: the comment only repeats what the code says x = x + 1 # increment x by 1
Experienced programmers write comments that say why the code does what it does:
# GOOD: the comment explains why, which the code cannot show x = x + 1 # skip the header row before reading data
If a comment only repeats what the code says, delete it. Good names should explain the code instead.
2. Docstrings: Comments That Tools Can Read
A docstring is a string literal placed as the first statement of a module, function, class, or method. Python keeps it while the program runs, and you can read it as obj.__doc__. Tools like help(), IDEs, and documentation generators all read them.
def calculate_tip(amount, percent=15): """Calculate the tip on a bill amount. Args: amount: The bill amount in dollars. percent: Tip percentage (default 15). Returns: The tip as a float. """ return amount * percent / 100 # The docstring is now discoverable: print(calculate_tip.__doc__) help(calculate_tip) # opens the full doc page
Use triple double-quotes """...""" for docstrings (PEP 257 convention). Single-line docstrings sit on one line:
def square(x): """Return x squared.""" return x * x
3. PEP 8: The Style Rules You Will Use Most
PEP 8 is long. These are the rules you will need most often:
Indentation: 4 spaces, never tabs
def greet(name): if name: # indented 4 spaces print(f"Hi, {name}") # 4 more spaces (8 in total)
Lines under ~80 characters
If a line is getting long, break it:
# OK: this line fits comfortably total = item_price * quantity + tax + shipping # Too long, so break it total = ( item_price * quantity + tax + shipping_for(destination) )
setup added so this can run · defines shipping, tax, shipping_for, destination, item_price, quantity
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) shipping = _AutoMock('shipping') tax = _AutoMock('tax') def shipping_for(*_a, **_kw): print('-> shipping_for() called') return _AutoMock('shipping_for()') destination = _AutoMock('destination') item_price = _AutoMock('item_price') quantity = _AutoMock('quantity')
Naming conventions
my_variable = 1 # snake_case for variables and functions def calculate_total(): # snake_case for functions too pass class UserAccount: # PascalCase for classes pass MAX_RETRIES = 5 # SCREAMING_SNAKE for constants _private_thing = "x" # a leading underscore means "internal, don't use"
Spaces around operators and after commas
# GOOD x = 1 + 2 fn(a, b, c) # UGLY x=1+2 fn(a,b,c)
setup added so this can run · defines fn, a, b, c
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) def fn(*_a, **_kw): print('-> fn() called') return _AutoMock('fn()') a = _AutoMock('a') b = _AutoMock('b') c = _AutoMock('c')
Blank lines to separate code
- 2 blank lines between top-level functions/classes
- 1 blank line between methods inside a class
- 1 blank line inside functions to separate logical chunks
4. Imports: Where and How
PEP 8 has a clear convention for imports: put them at the top of the file, in three groups separated by a blank line.
# 1. Standard library import os import sys from pathlib import Path # 2. Third-party packages import requests import pandas as pd # 3. Your own modules from myapp.config import settings from myapp.utils import helpers
One import per line for import x, but from x import a, b, c is fine on one line.
# GOOD import os import sys # BAD (works, but not Pythonic) import os, sys
5. The Tools That Enforce All This
You don't have to remember all of PEP 8. There are tools that check it for you.
| Tool | What it does |
|---|---|
| black | Formats your code automatically. Fast, with almost no settings to choose. Very widely used. |
| ruff | Linter (and now also a formatter). Reports style problems and suggests fixes. Very fast. |
| flake8 | Older linter, still common. |
| isort | Sorts your imports into the three PEP 8 groups. |
| mypy | Static type checker (relevant later, when you get to type hints). |
Most editors run these on save. Set up black and ruff once, and you won't need to format code by hand.
6. The Zen of Python
Python has a built-in poem about its philosophy. Type import this in any Python REPL to see it:
import thisSome of the best-known lines:
- Beautiful is better than ugly.
- Explicit is better than implicit.
- Simple is better than complex.
- Readability counts.
- There should be one — and preferably only one — obvious way to do it.
- If the implementation is hard to explain, it's a bad idea.
Python programmers really do use these ideas when they review each other's code.
7. Before-and-After: Real-Code Cleanup
Before:
def f(x,y): if x>0 and y>0: z=x*y # multiply return z else: return 0
After:
def area_of_rectangle(width, height): """Return the area, or 0 if either side is non-positive.""" if width > 0 and height > 0: return width * height return 0
What changed:
- Function name says what it does
- 4-space indent (the original used only 1 space per level)
- Docstring explains intent
- Spaces around operators and after commas
- Redundant
# multiplycomment removed - Redundant
elseremoved (the earlyreturnalready ends the function) - Temporary
zremoved: the function returns the expression directly
Each change is small, but together they make the code much easier to read.
8. Mistakes You'll Hit
1. Mixing tabs and spaces
Python 3 stops with an error if tabs and spaces are mixed inconsistently, so your script won't run. Configure your editor to insert spaces when you press Tab.
2. Naming variables in CamelCaseuserName = "alice" works, but style checkers will warn about it. Use user_name. CamelCase is for class names only.
3. Skipping docstrings on public functions
If someone else (or you, months later) might call this function, give it a one-line docstring at minimum.
🎯 Your Turn: Fix the Comments, Not the Code
The function below works correctly. Every comment in it is useless. Each one
describes what the line does, which the line already says. What a reader
really needs to know is why the rules are what they are.
Rewrite it: delete the noise comments, add a docstring, give the variables real
names, and leave exactly one comment explaining the non-obvious rule.
def calc(x, y): # multiply x by y t = x * y # if t is more than 500 then discount if t > 500: # multiply by 0.9 t = t * 0.9 # add 18 percent t = t * 1.18 # return t return t
The non-obvious part: orders over ₹500 get a 10% discount, and GST of 18% is
applied after the discount, not before. That ordering is a tax rule, not a
programming decision, and it is the one thing a future reader cannot infer.
Skeleton:
def order_total(unit_price, quantity): # TODO 1: write a docstring saying what it returns # TODO 2: name the running amount something meaningful # TODO 3: keep only ONE comment, the one that says why discount comes before GST ... print(round(order_total(100, 3), 2)) # 354.0 (below the threshold, so only GST is added) print(round(order_total(100, 8), 2)) # 849.6 (the discount first, then GST)
Hint 1: A comment that repeats the code makes the code worse
# multiply by 0.9 tells you nothing that t * 0.9 did
not. Worse, if the rate changes to 0.85 and someone forgets the comment, it now
lies. The only comments worth keeping are the ones the code cannot express.
Hint 2: Names explain more than comments do
t forces the reader to hold "t is the running total" in their head.
total does not. Renaming is usually a better fix than commenting:
a good name explains the code, and it can never get out of date.
Show full solution
GST_RATE = 0.18 BULK_DISCOUNT_RATE = 0.10 BULK_DISCOUNT_THRESHOLD = 500 def order_total(unit_price, quantity): """Return the payable total for an order, including GST. Orders above the bulk threshold receive a discount before tax. """ total = unit_price * quantity if total > BULK_DISCOUNT_THRESHOLD: total = total * (1 - BULK_DISCOUNT_RATE) # GST applies to the discounted amount, not the list price. Taxing the # pre-discount total would overcharge the customer. return total * (1 + GST_RATE) print(round(order_total(100, 3), 2)) # 354.0 print(round(order_total(100, 8), 2)) # 849.6
Five comments became one. The rates moved into named constants, so the numbers
explain themselves and there is a single place to change them. The one surviving
comment answers the one question the code cannot answer: why this order.
What You Learned
- Comments explain why. They should not just repeat what the code does.
- Docstrings (triple-quoted, first statement) are machine-readable.
- PEP 8 is the style guide everyone follows. Highlights: 4-space indent,
snake_casefor variables and functions,PascalCasefor classes,SCREAMING_SNAKEfor constants. - Tools like
black,ruff, andisortenforce style automatically. import thisfor the philosophy.
This lesson is a discipline lesson. Other lessons teach you what Python can do, and this one teaches you how to do it the way everyone else does. That helps you read other people's code, and it helps other people read yours.
Next: Input & Output.
Practice this
on practicepython.inShort exercises that run in your browser and tell you what your code did, as well as whether a test passed.
Found this useful? Share it with a friend who’s learning Python.