Code Quality: PEP 8, Docstrings and Clean Code
Code is read far more often than it is written. Consistent style, honest names and clear structure are what make the second reading cheap.
- Basics
- Data Types
- Operators
- Strings
- Control Flow
- Lists
- Tuples
- Sets
- Dictionaries
- Comprehensions
- Functions
- Advanced Functions
- Recursion
- Exception Handling
- File Handling
- Modules
- Standard Library
- OOP
- Advanced OOP
- Iterators and Generators
- Decorators
- Context Managers
- Descriptors and Dataclasses
- Python Internals
- Concurrency
- Regular Expressions
- Serialization
- Command Line Python
- Testing and Debugging
- Type Hints
- Performance
- Python Security
- DSA with Python
PEP 8 in practice
Layout
"""Module docstring on the first line."""
import os # standard library
import sys
from pathlib import Path
from notesapp import storage # your own code, after a blank line
MAX_RETRIES = 3 # constants near the top
DEFAULT_ENCODING = "utf-8"
def first_function():
"""Two blank lines before a top level definition."""
return 1
def second_function():
return 2
class Example:
"""One blank line between methods inside a class."""
def first_method(self):
pass
def second_method(self):
pass- Four spaces per indent level, never tabs.
- Two blank lines between top level definitions, one between methods.
- Imports at the top, one per line, grouped standard library then third party then local.
- A line length limit; 79 is the classic, and many teams use 88 or 100. Pick one.
Whitespace
# Correct
total = price * quantity
result = function(a, b, key=value)
items = [1, 2, 3]
record = {"name": "Meera"}
if x == 1 and y > 2:
pass
# Wrong
total=price*quantity
result = function( a,b , key = value )
items = [ 1,2,3 ]
if x==1and y>2 :
pass# Spaces around binary operators, but not for a keyword argument default
def draw(width, height, colour="black"): # no spaces around =
pass
draw(width=10, height=20) # no spaces here either
# Except when there is an annotation
def draw(width: int = 10, colour: str = "black") -> None: # spaces WITH a type
pass
# Operator precedence may be shown with spacing
result = x*2 - y*3
answer = (a + b) * (c - d)Naming
| Thing | Convention | Example |
|---|---|---|
| Variable, function, method | lower_case_with_underscores | order_total |
| Constant | UPPER_CASE | MAX_RETRIES |
| Class, exception | CapWords | InvoiceLine, ParseError |
| Module, package | short lowercase | invoices |
| Internal | leading underscore | _cache |
| Avoiding a keyword clash | trailing underscore | class_, id_ |
# Never use these as names: they are indistinguishable in many fonts
# l, O, I
# Do not shadow built-ins
list = [1, 2] # now list() is broken for the rest of the module
id = 5
type = "user"
input = "text"
# Add a suffix instead
items_list = [1, 2]
user_id = 5
user_type = "admin"Names that carry meaning
# Unclear
def calc(d, r, t):
return d * (1 + r) ** t
# Clear
def compound_amount(principal, annual_rate, years):
"""Return the value of principal after compounding annually."""
return principal * (1 + annual_rate) ** years| Weak | Better | Why |
|---|---|---|
data | invoices | Say what it holds |
temp | previous_total | Temporary is a lifetime, not a meaning |
flag | is_verified | A boolean reads as a question |
process() | send_invoice() | Verbs say what happens |
list1, list2 | paid, unpaid | Numbers carry no meaning |
helper() | normalise_name() | Every function is a helper |
# Booleans read as a question
is_active = True
has_permission = False
should_retry = True
can_edit = False
# Collections are plural, single items singular
users = [...]
for user in users:
...
# Counts and indexes say so
user_count = len(users)
current_index = 0Docstrings
def find_overdue(invoices, as_of=None, grace_days=0):
"""Return the invoices that are overdue.
Args:
invoices: An iterable of invoice dictionaries, each with a
``due_date`` and a ``status``.
as_of: The date to compare against. Defaults to today.
grace_days: Days of grace allowed after the due date.
Returns:
A list of the overdue invoices, oldest first.
Raises:
ValueError: If grace_days is negative.
Example:
>>> find_overdue([], grace_days=3)
[]
"""
if grace_days < 0:
raise ValueError("grace_days cannot be negative")
...def slugify(title):
"""Convert a title into a URL safe slug.""" # one line is often enough
class Invoice:
"""A customer invoice with lines, tax and a payment status."""
def total(self):
"""Return the total including tax, rounded to two places."""- Use triple double quotes, always.
- The first line is a short imperative summary ending in a full stop.
- Leave a blank line before a longer description.
- Document parameters, the return value and anything raised, when they are not obvious.
- Document why and what; the code already says how.
def add(a, b):
"""Return the sum of two numbers.
>>> add(2, 3)
5
>>> add(-1, 1)
0
"""
return a + b
if __name__ == "__main__":
import doctest
doctest.testmod(verbose=True)Examples in a docstring can be executed by doctest, which means the documentation cannot silently drift out of date.
Comments
# Useless: repeats the code
count = count + 1 # add one to count
# Useful: explains why
count += 1 # the API numbers pages from one, not zero
# Useful: warns
# Do not reorder: the tax must be applied before the discount,
# per the finance policy dated 2026-03.
total = apply_tax(subtotal)
total = apply_discount(total)
# Useful: marks work
# TODO: handle the multi currency case
# FIXME: this breaks on leap yearsA comment explaining what a line does is usually a sign the line needs a better name. A comment explaining why is information that cannot be expressed in code at all, and is always worth writing.
Functions that stay readable
# Doing too much
def process(data):
cleaned = []
for item in data:
if item and item.strip():
cleaned.append(item.strip().lower())
counts = {}
for item in cleaned:
counts[item] = counts.get(item, 0) + 1
top = sorted(counts.items(), key=lambda p: -p[1])[:3]
print("Top three:")
for word, count in top:
print(f" {word}: {count}")
return top# One job each
def clean(values):
"""Strip, lowercase and drop empty values."""
return [v.strip().lower() for v in values if v and v.strip()]
def count_occurrences(values):
"""Return a mapping of value to how often it appears."""
counts = {}
for value in values:
counts[value] = counts.get(value, 0) + 1
return counts
def most_common(counts, limit=3):
"""Return the most frequent entries, highest first."""
return sorted(counts.items(), key=lambda pair: -pair[1])[:limit]
def print_report(entries):
"""Print a ranked list of entries."""
print("Top three:")
for value, count in entries:
print(f" {value}: {count}")Each piece is now testable on its own, and only the last one touches the screen. Separating input, computation and output is the highest value habit in this note.
Guard clauses
# Nested
def process(order):
if order is not None:
if order.get("paid"):
if order.get("items"):
return "ready"
else:
return "no items"
else:
return "unpaid"
else:
return "no order"
# Flat
def process(order):
if order is None:
return "no order"
if not order.get("paid"):
return "unpaid"
if not order.get("items"):
return "no items"
return "ready"Magic values
# What is 86400? What is 2?
if elapsed > 86400 and status == 2:
...
# Named
SECONDS_PER_DAY = 86400
STATUS_OVERDUE = 2
if elapsed > SECONDS_PER_DAY and status == STATUS_OVERDUE:
...
# Better still
from enum import Enum
class Status(Enum):
PENDING = 1
OVERDUE = 2
PAID = 3
if elapsed > SECONDS_PER_DAY and status is Status.OVERDUE:
...Type hints as documentation
from pathlib import Path
def load_records(path: Path, encoding: str = "utf-8") -> list[dict[str, str]]:
"""Read comma separated records from a file."""
...
def find_user(users: list[dict], user_id: int) -> dict | None:
"""Return the matching user, or None."""
...A signature with types answers most of the questions a docstring would otherwise have to. The type hints note covers this in full.
Tools
| Tool | Does |
|---|---|
| A formatter | Rewrites layout automatically, ending style arguments |
| A linter | Finds unused imports, shadowed names, likely bugs |
| A type checker | Verifies annotations without running the code |
python -W error | Turns warnings into failures |
doctest | Runs the examples in your docstrings |
import doctest
import unittest
def load_tests(loader, tests, ignore):
"""Add doctests to the normal unittest run."""
tests.addTests(doctest.DocTestSuite())
return testsThe Zen of Python
import thisBeautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Flat is better than nested.
Readability counts.
Errors should never pass silently.
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.These are not rules to recite; they are the reasoning behind most of the advice above. "Flat is better than nested" is guard clauses. "Errors should never pass silently" is why except: pass is wrong. "Readability counts" is the whole note.
Common mistakes
- Mixing naming styles within one project.
- Shadowing built in names.
- Writing comments that restate the code.
- Functions long enough to need scrolling.
- Magic numbers and bare strings used as status values.
- Skipping docstrings on anything another person will call.
- Arguing about formatting instead of adopting a formatter.
Best practices
- Adopt a formatter and a linter on day one, and stop discussing layout.
- Name things after what they mean, and booleans as questions.
- One job per function; separate input, computation and output.
- Return early with guard clauses instead of nesting.
- Write docstrings for anything public, and comment the why.
- Replace magic values with named constants or enums.
Practice
- Take a function that does four things and split it into four, then test each one.
- Rewrite a deeply nested validation function using guard clauses.
- Add complete docstrings, with a runnable example, to three functions.
- Find every magic number in a file and give each a name.
- Rename ten poorly named variables in your own code and note which were hardest.
Conclusion
Style is not decoration. Consistent layout removes noise, honest names remove guesswork, small functions remove scrolling, and docstrings remove the need to read the body at all. Adopt a formatter, then spend your attention on the names.