Readable Python Rules (/readable-py)
Apply these rules when writing, reviewing, or refactoring Python code. Inspired by The Art of Readable Code by Dustin Boswell and Trevor Foucher.
Core principle: Code should be easy to understand. The time it takes someone else (or future you) to understand the code is the ultimate metric.
1. Keep Functions Short and Focused
- A function should do one thing. If you can describe what it does with "and", split it.
- Aim for functions that fit on one screen (~15-25 lines). If it's longer, extract sub-tasks.
- Each function should operate at a single level of abstraction — don't mix high-level logic with low-level details in the same function.
2. Flatten Control Flow — No Deep Nesting
- Never nest more than 2 levels deep. If you have a loop inside a loop, or an
if inside a loop inside an if, extract the inner block into a helper function with a descriptive name.
- Use early returns / guard clauses to handle edge cases at the top, keeping the main logic flat.
- Prefer
continue or break to skip iterations rather than wrapping the body in a conditional.
- Replace complex conditionals with well-named helper functions or variables that explain the intent.
# Bad: nested and hard to follow
for user in users:
if user.is_active:
for order in user.orders:
if order.is_pending:
process(order)
# Good: flat, each function name explains what it does
active_users = get_active_users(users)
for user in active_users:
process_pending_orders(user.orders)
3. Name Things Clearly
- Pack information into names. Use specific, concrete words —
fetch_page not get, num_retries not n.
- Avoid generic names like
tmp, data, result, val, info, handle — unless the scope is tiny (2-3 lines).
- Use names that can't be misconstrued. If a range is inclusive, say
max_items not limit. If a boolean, use is_, has_, should_, can_ prefixes.
- Match the name length to the scope. Short names for small scopes, descriptive names for wide scopes.
- Don't use abbreviations unless they're universally understood (
num, max, min, err are fine; svc_mgr_cfg is not).
4. Make Control Flow Easy to Follow
- Put the changing/interesting value on the left side of comparisons:
if length > 10 not if 10 < length.
- Order
if/else blocks: positive case first, simpler case first, or the more interesting case first.
- Minimize the number of variables the reader has to track. Reduce the mental footprint of each block.
- Avoid the ternary operator for anything non-trivial — if it's not immediately obvious, use an
if/else.
5. Break Down Giant Expressions
- Use explaining variables to break complex expressions into named pieces.
- Use summary variables to capture a long expression that's used more than once.
- Apply De Morgan's laws to simplify negated boolean expressions.
# Bad
if not (age >= 18 and has_id and not is_banned):
deny()
# Good
is_eligible = age >= 18 and has_id and not is_banned
if not is_eligible:
deny()
6. Extract Unrelated Subproblems
- If a block of code is solving a subproblem unrelated to the main goal of the function, extract it.
- The helper function should be pure and self-contained — it shouldn't need to know about the calling context.
- This is the single most effective way to improve readability: separate what you're doing from how.
7. One Task at a Time
- Each section of code should do one task. If a function is doing parsing AND validation AND transformation, split them into separate steps.
- List the tasks a function does. If there's more than one, reorganize so each task is in its own block or function.
8. Reduce Variable Scope
- Declare variables close to where they're used. Don't declare at the top of a function if it's only used 30 lines later.
- Minimize the "live time" of a variable — the fewer lines between its assignment and last use, the easier it is to follow.
- Prefer write-once variables. Variables that are assigned once and never modified are easier to reason about.
- Eliminate unnecessary variables. If a variable is used only once and doesn't clarify anything, inline it.
9. No Magic Numbers or Strings
- Replace magic numbers and strings with named constants:
if retries > MAX_RETRIES not if retries > 3.
- If a value has meaning, give it a name. The name documents the intent.
- Group related constants together.
10. Fewer Function Arguments
- Aim for 3 or fewer arguments per function. More than that is a smell.
- Group related arguments into an object, dataclass, or named tuple.
- If a function needs many config-like options, pass a single config/options object.
- Boolean flag arguments are a sign the function does two things — split it instead.
11. Consistency
- If the codebase does something one way, do it the same way. Don't mix styles.
- Consistent naming patterns, consistent structure, consistent error handling.
- When joining an existing codebase, match the existing conventions even if you'd prefer a different style.
- Surprise is the enemy of readability — predictable code is readable code.
12. Write Less Code
- The best code is no code at all. Question whether a feature is truly needed before implementing.
- Don't over-engineer. Solve the problem at hand, not hypothetical future problems.
- Remove dead code. Commented-out code is dead code.
- Use standard libraries before writing custom solutions.
13. Comments: Explain Why, Not What
- Don't comment what the code does — the code already says that. Comment why it does it.
- Comment flaws and workarounds:
// TODO:, // HACK:, // XXX: with explanation.
- Comment surprising behavior or non-obvious decisions — things where a reader would ask "why?".
- Don't comment bad code — rewrite it. If you need a comment to explain what a block does, extract it into a well-named function instead.
14. Design Code to Survive Auto-Formatting
- Write code that looks good after the auto-formatter runs. If a chained expression or repeated pattern would be broken across 4+ lines by the formatter, extract a helper function instead.
- Prefer one-line helper calls over long inline chains that the formatter will expand vertically.
- The formatter is your reader's first impression. Run it before committing — if the result looks ugly, that's a signal to refactor, not to disable the formatter.
# Bad: black wraps this into a multi-line mess
result = (
client.get_session()
.query(User)
.filter(User.active == True)
.options(joinedload(User.orders))
.order_by(User.created_at.desc())
.limit(page_size)
.all()
)
# Good: extract a helper so the call site stays clean
def get_active_users(session, page_size: int) -> list[User]:
return (
session.query(User)
.filter(User.active == True)
.options(joinedload(User.orders))
.order_by(User.created_at.desc())
.limit(page_size)
.all()
)
users = get_active_users(client.get_session(), page_size=20)
# Bad: dict comprehension with inline chain — black expands to 5+ lines
config = {
k: settings.get(k, defaults.get(k, fallbacks.get(k, None)))
for k in required_keys
}
# Good: extract the lookup
def resolve_setting(key, settings, defaults, fallbacks):
return settings.get(key, defaults.get(key, fallbacks.get(key)))
config = {k: resolve_setting(k, settings, defaults, fallbacks) for k in required_keys}
Python-Specific Rules
15. Prefer Comprehensions — But Keep Them Simple
- Use list/dict/set comprehensions for simple transforms and filters.
- If a comprehension needs a nested loop AND a conditional, it's too complex — use a regular loop or extract a helper.
- Generator expressions for large sequences to avoid materializing the whole list.
# Good: simple and readable
names = [user.name for user in users if user.is_active]
# Bad: too much going on
result = [transform(item) for group in data for item in group.items if item.valid and item.type == "A"]
# Good: break it up
valid_items = get_valid_items(data, item_type="A")
result = [transform(item) for item in valid_items]
16. Use Unpacking
- Tuple unpacking over index access:
name, age = get_user() not result[0], result[1].
- Star unpacking for head/tail:
first, *rest = items.
- Dict unpacking with
** for merging dicts.
- Unpacking makes the structure of the data explicit in the code.
17. Use enumerate, zip, and Itertools
- Use
enumerate(items) — never track indices manually with i += 1.
- Use
zip(a, b) to iterate in parallel — never index into parallel lists.
- Use
itertools (chain, groupby, islice) before writing manual iteration logic.
18. Use Dataclasses and NamedTuples Over Raw Dicts/Tuples
- If a dict always has the same keys, it should be a
dataclass or NamedTuple.
- If a function returns more than 2 values, return a
dataclass or NamedTuple — not a raw tuple.
- This gives you names, type hints, and readable attribute access for free.
19. Use pathlib for File Paths
- Use
pathlib.Path instead of os.path.join and string manipulation.
Path objects are readable, composable (/ operator), and cross-platform.