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.mdsource 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
inside a loop inside anif
, extract the inner block into a helper function with a descriptive name.if - Use early returns / guard clauses to handle edge cases at the top, keeping the main logic flat.
- Prefer
orcontinue
to skip iterations rather than wrapping the body in a conditional.break - 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 —
notfetch_page
,get
notnum_retries
.n - Avoid generic names like
,tmp
,data
,result
,val
,info
— unless the scope is tiny (2-3 lines).handle - Use names that can't be misconstrued. If a range is inclusive, say
notmax_items
. If a boolean, uselimit
,is_
,has_
,should_
prefixes.can_ - 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
are fine;err
is not).svc_mgr_cfg
4. Make Control Flow Easy to Follow
- Put the changing/interesting value on the left side of comparisons:
notif length > 10
.if 10 < length - Order
blocks: positive case first, simpler case first, or the more interesting case first.if/else - 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:
notif retries > MAX_RETRIES
.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:
with explanation.// XXX: - 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:
notname, age = get_user()
.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
enumeratezip- Use
— never track indices manually withenumerate(items)
.i += 1 - Use
to iterate in parallel — never index into parallel lists.zip(a, b) - Use
(itertools
,chain
,groupby
) before writing manual iteration logic.islice
18. Use Dataclasses and NamedTuples Over Raw Dicts/Tuples
- If a dict always has the same keys, it should be a
ordataclass
.NamedTuple - If a function returns more than 2 values, return a
ordataclass
— not a raw tuple.NamedTuple - This gives you names, type hints, and readable attribute access for free.
19. Use pathlib
for File Paths
pathlib- Use
instead ofpathlib.Path
and string manipulation.os.path.join
objects are readable, composable (Path
operator), and cross-platform./