Writing Useful Comments in Python: What PEP 8 Says and What Helps Readers

Comments are the one part of a program that the interpreter never checks. That is their strength, because they can say things code cannot, and their weakness, because nothing stops them from becoming wrong. PEP 8 has a short section on comments; most of what makes one useful comes from asking who will read it and what they will need.
The rules in PEP 8
PEP 8's comment section is brief and mostly about form:
- Comments should be complete sentences. The first word is capitalised, unless it is an identifier that starts with a lowercase letter: "never alter the case of identifiers".
- Block comments are made of paragraphs of complete sentences, each ending with a period, and are indented to the same level as the code they describe. Each line starts with
#and a single space, unless it is indented text inside the comment. Paragraphs are separated by a line containing a single#. - Inline comments, on the same line as a statement, should be used sparingly, separated from the statement by at least two spaces, and start with
#and a space. - Comments should be clear to other speakers of the language you write in, and Python developers from non-English-speaking countries are asked to write in English "unless you are 120% sure that the code will never be read by people who don't speak your language".
- Long comments are wrapped at 72 characters, like docstrings.
pycodestyle checks the mechanical parts: E261 (fewer than two spaces before an inline comment), E262 (inline comment not starting with # ), E265 (block comment not starting with # ) and E266 (block comment starting with ##).
x = 1 # bad: one space before the comment
y = 2 #bad: no space after the hash
#bad block comment
### decorative heading
The rule that matters most
The comments section of PEP 8 opens with its most important sentence: "Comments that contradict the code are worse than no comments. Always make a priority of keeping the comments up-to-date when the code changes!" A missing comment costs the reader some time; a wrong one sends them in the wrong direction with confidence. In review, a change to a function's behaviour should come with a look at the comments around it.
PEP 8 gives a small example of the difference between a useless and a useful inline comment:
x = x + 1 # Compensate for border
"Increment x" would only repeat the code. "Compensate for border" tells the reader something the code cannot: the reason.
Explain why, not what
Good names and small functions already say what the code does. Comments earn their place by carrying information that is not in the code:
- Intent and reasons. Why this approach, and not the obvious one.
- Constraints from outside. An API limit, a legal requirement, a format quirk, a bug in a dependency, with a reference when you have one.
- Surprises. An ordering that matters, a deliberate exception swallowed, a value that looks wrong but is not.
- Units and invariants that types do not express.
TIMEOUT = 25 # Seconds. The upstream gateway closes idle connections at 30.
# Sort by date first, then by amount: the bank statement lists
# same-day transactions in amount order, and reconciliation
# relies on matching that order.
rows.sort(key=lambda row: (row.date, row.amount))
When you find yourself writing a comment that explains what a block does, consider whether the block should be a function whose name says it. apply_late_fee(invoice) needs no comment; ten lines under "# apply late fee" do.
TODOs, notes and tool directives
Comments also carry messages to people and tools.
- TODO and FIXME notes are useful if they are actionable: say what is missing and link to an issue, so the note can be found and closed. Some teams require an author or issue reference; ruff's TD rules (derived from the flake8-todos plugin) can enforce that format, and its FIX rules can flag every TODO, FIXME or XXX left in the code.
- Linter directives such as
# noqa: E501(pycodestyle, flake8, ruff) or# pylint: disable=invalid-namesilence a specific warning on one line. Name the code rather than writing a bare# noqa, which hides every warning on the line, and add a reason if it is not obvious. - Type checker directives such as
# type: ignore[attr-defined]work the same way for mypy and similar tools. - The first two lines may hold a shebang (
#!/usr/bin/env python3) for scripts. An encoding declaration is unnecessary for UTF-8 files, the default since Python 3, and PEP 8 says code in the core distribution should not have one.
Commented-out code
Blocks of code disabled with # are a common sight and almost never useful. They are not run, so they rot; they are not explained, so nobody knows whether they can be deleted; and they make searches return false matches. Version control already keeps every previous version. If code might be needed again, say so in a commit message or an issue, and delete it. ruff can flag commented-out code with its ERA001 rule, derived from the eradicate tool.
Comments versus docstrings
The two are often confused, but they have different readers:
| Docstring | Comment | |
|---|---|---|
| Reader | Someone calling the code | Someone changing the code |
| Content | What it does, arguments, return value, errors | How and why the implementation works this way |
| Where it shows up | help(), editors, generated docs | Only in the source |
| Convention | PEP 257 | PEP 8, "Comments" |
PEP 8 connects the two: public functions get docstrings, while non-public methods can make do with a comment after the def line that describes what they do. Our guide to docstrings and PEP 257 covers the first column.
A quick self-review
- Does each comment say something the code does not?
- Is each one still true after your change?
- Could a better name or an extracted function replace it?
- Are TODOs linked to something that will get them done?
- Is there commented-out code to delete?
The checker on our home page reports E261, E262, E265 and E266 for a pasted snippet. The rest of this list is the kind of thing a reviewer catches; our guide to readable Python code develops the idea that names and structure should carry most of the meaning.
Sources
- PEP 8, "Comments" (block comments, inline comments) and "Source File Encoding".
- pycodestyle documentation, error codes E261 to E266.
- ruff rules reference: ERA001 (from eradicate), TD (from flake8-todos), FIX (from flake8-fixme); flake8 documentation, "Selecting and Ignoring Violations" (
# noqa); mypy documentation (# type: ignore).
Docstrings, comments & typesDocstrings and PEP 257
One-line summaries in the imperative, a blank line, then the details: what PEP 257 asks of docstrings, the common formats and the tools that check them.
Readable code in practiceReadable code, beyond PEP 8
Code can pass every linter and still be hard to follow. Names, flat control flow, small functions and idioms that say what they mean, with a before-and-after.
PEP 8, section by sectionWhitespace and blank lines
Spaces around operators and commas, none inside brackets, two blank lines between functions: the rules, the warnings, and the one place the checker disagrees with PEP 8.