Readable code in practice

Readable Python Code: Beyond PEP 8

Readable code in practice · · Published 2026-10-27

Rows of students reading at long desks in the Library of Congress reading room, around 1900, with marble columns behind
Students in the Reading Room of the Library of Congress, photographed by Frances Benjamin Johnston. Library of Congress, public domain.

A style checker can confirm that code is laid out consistently. It cannot tell you whether the next person will understand it. PEP 8 starts from the observation that code is read much more often than it is written; this article is about the part of readability that no tool measures.

The Zen of Python

Type import this in a Python interpreter and it prints "The Zen of Python, by Tim Peters", nineteen aphorisms published as PEP 20. PEP 8 quotes one of them, "Readability counts". Several others are practical advice about readable code:

They are deliberately loose, and sometimes in tension ("practicality beats purity"), but they give a shared vocabulary for review comments that go beyond whitespace.

A before and after

This function passes pycodestyle except for one warning, and it works:

def f(d, t):
    r = []
    for k in d:
        if d[k]["active"] == True:
            if d[k]["age"] >= 18:
                if t in d[k]["tags"]:
                    r.append(k)
    return r

To understand it, a reader has to decode every single-letter name, keep three levels of conditions in mind, and guess what 18 means. Here is the same logic rewritten:

ADULT_AGE = 18


def is_eligible(member, tag):
    """Return True if the member is active, adult and tagged."""
    is_adult = member["age"] >= ADULT_AGE
    return member["active"] and is_adult and tag in member["tags"]


def eligible_member_ids(members, tag):
    """Return the ids of members eligible for a tag."""
    return [
        member_id
        for member_id, member in members.items()
        if is_eligible(member, tag)
    ]

Both versions return ['a1'] for the same test data. The second is longer in lines, and quicker to read: the name of each function states what it returns, the condition reads as a sentence, the constant explains the number, and members.items() removes the repeated d[k] lookups. The == True comparison (E712 in pycodestyle) is gone as well.

Names do most of the work

PEP 8 governs how names are formatted; our naming guide covers those rules. What a name says is a separate skill:

Flat is better than nested

Each level of nesting adds a condition the reader must remember. Guard clauses, which handle the special cases first and return early, keep the main path at the left margin:

def shipping_cost(order):
    if not order.items:
        return 0
    if order.is_pickup:
        return 0
    base = 4.90 if order.weight_kg < 2 else 8.90
    return base * (2 if order.express else 1)

The same applies to loops: continue to skip an item early often removes a level of indentation. When a block is nested three or four levels deep, it is usually a sign that part of it wants to become a function.

Small functions, one level of detail

A function that fits on a screen and does one thing can be understood without scrolling and tested on its own. A useful test is whether you can describe a function without "and": "load the configuration" is one job; "load the configuration and connect to the database and start the scheduler" is three. Keep each function at one level of abstraction, so that high-level steps are not mixed with low-level string slicing.

Idioms that say what they mean

Readers recognise idioms faster than equivalent hand-written loops:

The counterweight is "sparse is better than dense". A comprehension with two loops and three conditions, or a chain of five method calls on one line, saves lines at the expense of the reader. When an expression needs a pause to decode, give its parts names.

Comments and consistency

Readable code needs fewer comments, not none: our article on writing useful comments explains what remains worth saying. And PEP 8's first principle still applies here: consistency within a module beats any individual preference. A reader who has learned one pattern in a file should meet the same pattern further down.

Checking readability

Some proxies can be measured: cyclomatic complexity (flake8's mccabe check, ruff's C901, pylint's design checks), function length and nesting depth. They are useful alarms, not definitions. The reliable test is a reader: ask a colleague to explain your code back to you, or review it yourself after a day away. Our code review checklist turns these questions into a routine, and the checker on our home page takes care of the mechanical layer first.

Sources