Pysheeet readable-py

Readable Python code rules inspired by The Art of Readable Code. Use when writing, reviewing, or refactoring Python code. Enforces short functions, flat control flow, clear naming, readable structure, and Pythonic idioms.

install
source · Clone the upstream repo
git clone https://github.com/crazyguitar/pysheeet
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/crazyguitar/pysheeet "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/readable-py" ~/.claude/skills/crazyguitar-pysheeet-readable-py && rm -rf "$T"
manifest: skills/readable-py/SKILL.md
source content

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.