| name | readable-code |
| description | Write code that minimizes time-till-understanding using Boswell & Foucher's tactics from *The Art of Readable Code*: naming, aesthetics, comments, control flow, expressions, variables, decomposition, and tests. Use when the user mentions "readable code", "time-till-understanding", "Art of Readable Code", "Boswell", "naming variables", "naming functions", "explaining variable", "summary variable", "minimize nesting", "return early", "Yoda notation", "comment density", "ambiguous name", "extract subproblem", "one task at a time", "rubber duck", "smaller scope", "write-once variable". Also trigger when reviewing a PR for line-level readability, refactoring confusing logic, deciding whether to comment, picking between two names, untangling a giant boolean expression, or shrinking variable scope. Distinct from `clean-code` (Martin: SRP/OOP/error handling) and `refactoring-ui` (visual design): this skill is *micro-level* code readability — names, lines, expressions, and the tight loop a reader runs through them. |
| license | MIT |
| metadata | {"source":"The Art of Readable Code (Boswell & Foucher, O'Reilly 2011)","version":"1.0.0"} |
The Art of Readable Code
A tactical, line-level discipline for writing code that's fast to understand. Apply when writing new code, reviewing PRs, refactoring a confusing function, or deciding between two near-equivalent ways to express the same logic.
Core Principle: The Fundamental Theorem of Readability
Code should be written to minimize the time it would take for someone else to understand it.
"Someone else" includes you, six months from now. Every tactic in this skill serves one metric: time-till-understanding. Shorter is not better. Cleverer is not better. Faster to understand is better.
This theorem trumps other goals when they conflict. Fewer lines, fewer characters, idiomatic one-liners — none of it matters if the reader has to stop and reread. When in doubt, optimize for the reader's clock, not the author's keystrokes.
Scoring
Goal: 10/10. When reviewing or writing code, rate it 0–10 on time-till-understanding. Always give the current score and the smallest change that would raise it.
- 9–10: A new reader understands each function in one pass. Names answer their own questions; control flow is linear; no expression requires re-reading.
- 7–8: Mostly readable. One or two ambiguous names, a deep nest, or a long boolean that forces a pause.
- 5–6: Mixed. Some sections clear, others require mental simulation of variable values or untangling negations.
- 3–4: Reader must trace execution carefully. Generic names (
data, tmp, info), giant expressions, comments that don't help.
- 1–2: Reverse-engineering required. Cryptic names, deeply nested logic, no comments where they're essential, comments where they aren't.
The Four Parts
Part I — Surface-Level Improvements (apply per line, per name, per file)
Most of readability lives at the surface. These four disciplines pay off immediately and compound.
1. Pack Information into Names
Core concept: A name is a tiny comment. Pick names that answer the reader's question without forcing them to read the implementation.
Why it works: Names are read on every encounter with the symbol. A good name eliminates a trip into the function body. A bad name forces it every time.
Six tactics:
- Choose specific words —
Fetch() / Download() beats Get(). NumNodes() / Height() beats Size().
- Avoid generic names like
tmp, retval, foo, data — unless their generic-ness is the meaning (a true short-lived swap value).
- Prefer concrete over abstract —
CanListenOnPort() beats ServerCanStart().
- Attach extra info via suffix/prefix when units or state matter —
delay_secs, size_mb, html_utf8, unsafe_password, plaintext_password.
- Length should match scope —
i is fine in a 3-line loop; clientIndex is required across a 200-line class.
- Use name formatting to convey meaning —
ClassName, methodName, CONSTANT, _private. Be consistent.
| Context | Smell | Better |
|---|
| Variable | tmp = user.name(); tmp += "@" + user.host(); | user_email = ... |
| Function | def Stop(): | Kill() (irreversible) / Pause() (resumable) |
| Boolean | disable_ssl = false | use_ssl = true (no negation) |
| Units | start = Date.now(); | start_ms = Date.now(); |
| Range | Print(stop) (inclusive?) | Print(last) (inclusive) or Print(end) (exclusive) |
See: references/surface-level.md — naming + aesthetics + comments in depth.
2. Names That Can't Be Misconstrued
Core concept: Before committing a name, ask "What other meanings could someone read into this?" — and pick a name no one could misread.
Why it works: Ambiguity costs the reader a function-body trip, every time. Worse, they may guess wrong and write a bug.
Patterns:
filter() → ambiguous; use select() or exclude().
- Inclusive limits →
max_ / min_.
- Inclusive ranges →
first / last. Inclusive-then-exclusive → begin / end.
- Booleans → prefix with
is_ / has_ / can_ / should_; avoid double negatives.
- Match user expectations —
get*() should be cheap; list.size() should be O(1). If getMean() iterates a million items, rename it computeMean().
3. Aesthetics
Core concept: Make code easy on the eyes so the reader can skim. Three principles:
- Consistent layout the reader can pattern-match.
- Similar code looks similar — parallel structure, column alignment when it helps.
- Group related lines into paragraphs.
Tactics:
- Rearrange line breaks so similar items align.
- Extract repeated irregularity into a helper.
- Pick a meaningful order (importance / matches the HTML form / alphabetical) and use it everywhere — declarations, error handling, switch cases.
- Declare in blocks, not interleaved.
- Personal style beats nothing; project consistency beats personal style.
4. Comments — Knowing What to Write, How to Write
Core concept: The purpose of a comment is to help the reader know what the writer knew. Comments aren't a tax — they're a high-value channel for everything the code can't express.
Don't comment:
- What the code already says (
// constructor).
- Bad names — fix the name instead.
- "Filler" comments to satisfy a style guide.
Do comment:
- The why behind a non-obvious choice.
- Flaws and known limits:
// TODO, // HACK, // XXX.
- The reasoning for constants (
// 0.72 = empirically best ratio from A/B test #218).
- Likely pitfalls — "this function may take 60s on large input".
- Big-picture / director's commentary at the top of a file or class.
- Section labels inside a long function (
// --- step 2: normalize ---).
How to write them:
- High information-to-space ratio. Compact illustrations beat prose:
// CategoryType -> (score, weight).
- Avoid ambiguous pronouns ("it", "this") — say which thing.
- Use input/output examples for corner cases (one example beats a paragraph).
- State intent, not mechanism. (
// fast version of std::hash; tested empirically not // compute hash by xor-ing bytes.)
- "Named parameter" comments at call sites:
Connect(/* timeout_ms = */ 10, /* use_encryption = */ false).
- Information-dense words ("heuristic", "brute-force", "canonical form", "raw") instead of long descriptions.
See: references/surface-level.md
Part II — Simplifying Loops and Logic
Once names and layout are clean, the next bottleneck is mental simulation: tracing branches, evaluating expressions, tracking variable values.
5. Make Control Flow Natural
Core concept: Every conditional, loop, and jump should read so naturally the reader doesn't pause.
Tactics:
- Argument order in comparisons: put the thing being interrogated on the left, the stable thing on the right.
if (length >= 10), not if (10 <= length).
- if/else order: positive case before negative; simple/interesting before complex; the "do something" branch first.
- Return early for guard clauses. Don't carry boolean flags down a function just to check them at the end.
- Minimize nesting. Every level deeper is another item the reader has to remember. Prefer a flat sequence of early returns.
- Avoid
do/while, goto, and deeply nested ternaries. ?: is fine for trivial cases (time_str += hour >= 12 ? "pm" : "am"); use if/else for anything more.
- Threads, callbacks, virtual methods, and exceptions all hide control flow. Use them when they pay; don't sprinkle them.
6. Break Down Giant Expressions
Core concept: Humans can hold ~3–4 things in working memory. An expression with more pieces will be misread.
Tactics:
7. Variables and Readability
Core concept: Variables have three costs that compound: count, scope, and mutability.
- Fewer variables — eliminate ones that don't break down expressions, document, or remove duplication. Useless temporaries (
now = datetime.now(); x.t = now), intermediate results that could be returned directly, control-flow flags replaceable with early returns.
- Shrink scope — make variables visible to as little code as possible. Smaller blocks, smaller classes, no globals. Languages help: use
let/const not var; nested classes; pure functions.
- Prefer write-once variables —
const, final, val. A variable that changes in many places forces the reader to mentally simulate the program.
See: references/loops-and-logic.md
Part III — Reorganizing Your Code
Bigger moves: where code lives and how it's split.
8. Extract Unrelated Subproblems
Core concept: Aggressively separate the high-level goal of a block of code from the subproblems it happens to need.
Process:
- Ask: what is the high-level goal of this code?
- For each line, ask: is this directly serving the goal, or is it solving an unrelated subproblem?
- Extract the unrelated parts into separate, oblivious functions (the extracted code shouldn't know why it's being called).
Variants:
- Pure utility code (string formatting, date math).
- Simplifying an existing interface (a thin wrapper that fixes the bad API).
- Reshaping data into the form your code actually wants.
- Spotting missing primitives in your codebase and creating general-purpose helpers.
Warning: Don't over-extract. A one-off helper used in exactly one place may hurt more than it helps — the reader now has to jump.
9. One Task at a Time
Core concept: Defragment code so it does one task at a time, in sequence.
Process:
- List the tasks the code is doing — loosely. "Validate input." "Compute score." "Format output." "Make sure the object is valid."
- Separate them into different functions, or at least clearly delimited sections inside one function.
Broader than "functions do one thing" — even inside one long function, sequencing tasks linearly beats interleaving them.
10. Turning Thoughts into Code (the rubber-duck method)
Core concept: Source code is your primary medium for explaining what the program does. Write it the way you'd explain it.
Process:
- Describe what the code needs to do in plain English, like talking to a colleague.
- Notice the key words and phrases you used.
- Write code that matches that description.
If you can't describe it in English, you don't understand it yet — and the code will reflect that. Works for debugging too: explain the bug aloud and the answer often surfaces.
11. Writing Less Code
Core concept: The most readable code is no code at all.
Tactics:
- Question requirements. Most features go unused; most edge cases never occur. Push back before implementing.
- Stay small. Prune dead code, unused features, over-general code, fields nobody reads.
- Know your standard library. Spend 15 minutes a quarter browsing it. Most "code I need to write" already exists.
- Reuse over reinvent. Unix tools and small scripts often beat full programs.
See: references/reorganizing.md
Part IV — Tests
12. Make Tests Easy to Read and Maintain
Core concept: Test code is unofficial documentation for the real code. If it's intimidating, people stop adding tests and stop changing the real code.
Tactics:
- Hide non-essential setup behind helpers. Each test should read close to one line:
CheckScoresBeforeAfter([1,2,3], [3,2,1]).
- Make failure messages informative — a bad assertion forces a debugger trip; a good one tells you what differed.
- Pick the simplest test inputs that completely exercise the case. Don't test with
[3.14, -8.5, 1.41] when [1, 2] would do.
- One test function per functionality. Name them
Test_<Function>_<Situation>.
- Beware of testing making real code worse. If adding a test forces you to inject 8 mocks and split a clear class into three, the cure may be worse than the disease.
See: references/testing.md
How to Apply This Skill
When writing new code: name first, write the function, then read it as a stranger would. If you stop or reread, fix what made you stop.
When reviewing a PR: read each function once at normal speed. Flag every place you stopped, reread, or had to look something up. Those are the readability bugs — score them 0–10, suggest the minimal change.
When refactoring: pick one lever (a name, a nest level, a giant boolean, a leaky scope) per pass. Don't grand-rewrite — that destroys reviewability and review is readability's main test.
When stuck: rubber-duck. Describe the code aloud. The phrase that comes out of your mouth is usually the better function name.
Distinct from Adjacent Skills
clean-code (Robert Martin): broader — covers SRP, OOP, error handling, architecture. Use when discussing class boundaries, exception strategy, or system design.
refactoring-ui: visual / UI polish, not source readability.
hexagonal-architecture / responsibility-driven-design: module-level structure. Use when deciding where responsibilities live. This skill assumes that's settled — it asks how the code inside reads.
Use readable-code for the line-level, name-level, expression-level decisions a programmer makes hundreds of times a day.