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.

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

ThingConventionExample
Variable, function, methodlower_case_with_underscoresorder_total
ConstantUPPER_CASEMAX_RETRIES
Class, exceptionCapWordsInvoiceLine, ParseError
Module, packageshort lowercaseinvoices
Internalleading underscore_cache
Avoiding a keyword clashtrailing underscoreclass_, 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
WeakBetterWhy
datainvoicesSay what it holds
tempprevious_totalTemporary is a lifetime, not a meaning
flagis_verifiedA boolean reads as a question
process()send_invoice()Verbs say what happens
list1, list2paid, unpaidNumbers 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 = 0

Docstrings

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 years
A 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

ToolDoes
A formatterRewrites layout automatically, ending style arguments
A linterFinds unused imports, shadowed names, likely bugs
A type checkerVerifies annotations without running the code
python -W errorTurns warnings into failures
doctestRuns 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 tests

The Zen of Python

import this
Beautiful 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

  1. Take a function that does four things and split it into four, then test each one.
  2. Rewrite a deeply nested validation function using guard clauses.
  3. Add complete docstrings, with a runnable example, to three functions.
  4. Find every magic number in a file and give each a name.
  5. 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.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Python notes →
Python

Testing with unittest

A test is a small program that checks another program. unittest ships with Python, finds your tests automatically and tells you exactly what broke.

Read more

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.