Readable Python Code: Beyond PEP 8

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:
- "Explicit is better than implicit."
- "Simple is better than complex. Complex is better than complicated."
- "Flat is better than nested."
- "Sparse is better than dense."
- "Errors should never pass silently. Unless explicitly silenced."
- "If the implementation is hard to explain, it's a bad idea."
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:
- Name for the problem, not the data structure.
membersrather thandormember_dict;overdue_invoicesrather thanfiltered_list. - Let the length of a name follow its scope. A loop index used on one line can be
i; a module-level function needs a full name. - Make booleans read as statements:
is_adult,has_access,should_retry. - Name functions after their result or effect:
parse_date(),send_reminder(); avoid vague verbs likehandle,processordounless the context makes them precise. - Keep one name per concept. If it is a "customer" in one module, do not call it a "client" in the next.
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:
for index, item in enumerate(items):rather thanrange(len(items)).for name, score in zip(names, scores):to walk two sequences together.any(...)andall(...)for "is there one" and "are they all".pathlib.Pathfor file paths,withfor resources, f-strings for formatting.dataclasses.dataclassor aNamedTupleinstead of dictionaries with fixed keys, so thatmember.agereplacesmember["age"]and typos become errors.
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
- PEP 20 – The Zen of Python, Tim Peters.
- PEP 8, "A Foolish Consistency is the Hobgoblin of Little Minds".
- Python documentation,
dataclasses; built-in functionsenumerate,zip,any,all.
PEP 8, section by sectionNaming conventions
snake_case for functions, CapWords for classes, capitals for constants, and what one or two underscores really do, with the tools that check names.
Readable code in practicePython code review checklist
Automate the style, then review what tools cannot see: behaviour, tests, names, error handling and security, plus how to phrase comments people can act on.
Docstrings, comments & typesWriting useful comments
Comments that contradict the code are worse than none: PEP 8's comment rules, what is worth explaining, and what to delete instead.