While You Are Coding
12 min read
Core idea
Coding is not transcription
The topic opens with the book's most contrarian claim: the idea that coding is a "mechanical phase" — typing up a design that's already complete — is the single largest reason software projects fail. Coding is where most of the real decisions get made, not where pre-made decisions get executed. Every minute of typing involves judgments about naming, structure, error handling, edge cases, and assumptions. A developer on autopilot is a developer making those decisions badly.
Thomas and Hunt's topic, the longest in the book, is a catalogue of disciplines that keep your mind active during the keystrokes. Some are perceptual (notice when your lizard brain is uneasy, notice when you don't know why your code works). Some are analytical (estimate algorithmic complexity, write tests as design probes). Some are mechanical (refactor continuously, sanitize input, choose names with care). All of them oppose the same enemy: the comfortable autopilot where code accumulates without anyone actively thinking about it.
Eight topics, one stance
The eight topics divide into two clusters. The first three — Listen to Your Lizard Brain, Programming by Coincidence, Algorithm Speed — are about awareness: keeping your conscious and nonconscious minds in dialogue with the code. The next five — Refactoring, Test to Code, Property-Based Testing, Stay Safe Out There, Naming Things — are about active intervention: things you do, on purpose, while coding, to keep the code healthy.
Tip 60: Listen to Your Inner Lizard.
Why it matters
Most bad code is written on autopilot
Postmortems of failed systems almost never trace to one bad decision. They trace to thousands of small unexamined ones — a function nobody quite knew worked, but didn't break, so they shipped it; a variable named for what it was three months ago; an algorithm that's fine at 100 records and lethal at 100,000. Each of the disciplines in this topic is a guardrail against one specific autopilot failure.
Active coding is faster, not slower
The topic's hidden argument is that being deliberate while coding produces faster delivery, not slower. Refactoring as you go is faster than refactoring once a quarter. Tests written before code are faster than tests added after. Good names are faster than bad ones, because the next reader (often you, in a month) doesn't have to decode them. The "stop and think" disciplines feel like overhead in the moment and pay back many times over the lifetime of the code.
Key takeaways
The topics in this topic
Topic 37 — Listen to Your Lizard Brain
The opening topic borrows from Gavin de Becker's The Gift of Fear: humans have a nonconscious pattern-recognition system that fires before our conscious mind catches up. In programming, that system speaks in feelings — nervousness, queasiness, "this is too much work," reluctance to start. These feelings are not weakness. They are accumulated experience trying to deliver a warning your conscious mind hasn't yet decoded.
Tip 60: Listen to Your Inner Lizard.
The topic identifies two common scenarios where the lizard speaks. Fear of the blank page — staring at an empty editor, can't begin. Fighting yourself — every line is muddy, every step backwards two. In both cases, the recommended response is the same:
- Stop coding. Walk, eat, talk to a non-programmer.
- Externalize. Sketch, explain to a rubber duck, write the problem on paper.
- If still stuck, prototype. Tell yourself "this doesn't count — I'm just learning something." Put a sticky note on the screen: I'm prototyping. The pressure drops; code starts flowing again; whatever you learn either solves the original problem or reveals what's actually blocking it.
The deeper claim is that you cannot brute-force your way through lizard-brain resistance. Pushing harder produces worse code. The only viable path is to convert the feeling into a question, then answer the question.
Topic 38 — Programming by Coincidence
A trap with a memorable name. Programming by coincidence is when your code works, but you don't know why — you tried things until something stopped breaking, and you shipped that. The code passes the test you wrote, but you have no model of why; you can't predict how it behaves in cases your test didn't cover; you can't safely change it; you can't reuse the pattern. It's a house of cards built on what happens to be true today.
Tip 61: Don't Program by Coincidence.
The authors identify a few sub-patterns:
- Accidents of implementation. You depend on undocumented behavior of a library that works today and will silently change in v2.3.
- Accidents of context. Your code works because the file is always sorted, or because the timezone is always UTC — facts you didn't write down and didn't enforce.
- Lucky bug interactions. Two bugs cancel each other out. Fix one and the other surfaces.
The cure is to deliberately program: know why each line is there, test your assumptions explicitly, document context dependencies, refuse to ship code you don't understand. If you find yourself debugging by changing things at random and seeing what helps, stop — you're not debugging, you're flailing. Form a hypothesis, test it, eliminate it, move on.
Topic 39 — Algorithm Speed
Big-O notation gets a refresher: most code runs in O(1), O(log n), O(n), or O(n log n), and those are fine. The dangers are O(n²), O(n³), and worst of all O(2^n) — algorithms that are tolerable at small input and lethal at scale.
Tip 62: Estimate the Order of Your Algorithms.
The authors are not asking you to count operations to derive a formal complexity. They are asking you to develop a gut feel for when an inner loop hits an outer loop's data and you've just written O(n²) without noticing. A common example: scanning a list inside a loop over the same list ("for each user, find their manager by scanning the user table") is O(n²). For 100 users, fine. For 100,000, your service times out.
The matching discipline is measure, don't guess. Estimates of speed are guides; actual profiling under realistic load is ground truth. The authors close with a corollary: test your estimates. If you predicted O(n) and the timings show O(n²), find out why before you ship.
Tip 63: Test Your Estimates.
Topic 40 — Refactoring
Code is a garden, not a building. It grows, gets weedy, gets pruned, occasionally needs a section dug up and replanted. Refactoring is the constant small pruning that keeps the garden alive. The opposite — leaving the weeds until you have to bulldoze and start over — is what produces the legacy systems everyone hates.
Tip 64: Refactor Early, Refactor Often.
Martin Fowler's discipline applies: refactor in small, safe, behavior-preserving steps. Run tests between each step. Don't combine refactoring with adding behavior — do one, then the other. The temptation is always to "fix this whole module" at once; the discipline is to do it in a sequence of small renames, extractions, and inlines, each individually trivial, each leaving the system green.
When to refactor: when you notice duplication; when names no longer match what the code does; when a function has grown past one screen; when a comment is needed to explain what (not why) the code does; when adding a feature requires "shotgun surgery" across multiple files.
When not to refactor: when you should be rewriting; when the test coverage is so thin that you can't tell if you broke something; when a deadline is in two hours; when nobody actually needs to change this code.
Topic 41 — Test to Code
The topic's most subtly important topic. Most programmers think of testing as bug-finding. Thomas and Hunt invert this: testing is design feedback. The bugs are a side effect. The real value comes from what trying to write the test tells you about the code.
Tip 65: Testing Is Not About Finding Bugs.
Tip 66: A Test Is the First User of Your Code.
When you write a test for a function, you instantly discover whether:
- The function is reasonable to call (or has too many parameters, weird types, complex setup).
- The function is reasonable to mock around (or it's tangled into a dozen collaborators).
- The function has a clear contract (or you can't articulate what it should return).
- The function is doing one thing or six.
These are design findings, not testing findings. They tell you to refactor the code under test, not to write more assertions. This is what Test-Driven Development is actually about — using tests as a design tool, with bug-prevention as a secondary benefit.
Tip 67: Build End-to-End, Not Top-Down or Bottom-Up.
The authors close the topic by repeating the value: tests are most powerful when written during (or before) code, not after. After-the-fact tests catch regressions; before-the-fact tests shape the API.
Topic 42 — Property-Based Testing
Example-based tests check that specific inputs produce specific outputs. Property-based tests check that every input satisfies some property. The framework generates hundreds of random inputs and looks for violations. When it finds one, it shrinks the input — finds the smallest example that still fails — and hands you a minimal reproducer.
Tip 68: Use Property-Based Tests to Validate Your Assumptions.
The technique catches a category of bug that unit tests systematically miss: the edge case you didn't think of. Empty input, single-element input, repeated elements, very large inputs, Unicode in places you assumed ASCII, negative numbers, dates in February. Humans write tests for cases they imagine; property-based testing tests cases they didn't.
Useful properties to assert: roundtrips (encode then decode equals input), invariants (sorted list is still sorted after insertion), commutativity (A + B == B + A), idempotence (running twice equals running once), comparison with a reference implementation (your fast version agrees with a slow obvious one).
Tools: QuickCheck (Haskell), Hypothesis (Python), fast-check (JS), PropEr (Erlang). The investment in writing a generator pays back the first time it finds a bug your unit tests missed — and it always finds one.
Topic 43 — Stay Safe Out There
A short defensive-coding topic. The internet is hostile. Code that processes any input from outside its trust boundary is under attack — sometimes by adversaries, sometimes by accident, always by entropy. The Pragmatic Programmer ships with a few baseline habits.
Tip 69: Keep It Simple and Minimize Attack Surfaces.
The authors' baseline:
- Minimize attack surface. Every feature, dependency, port, and endpoint is a potential entry. Less is safer.
- Principle of least privilege. Code, services, users — give them exactly the access they need, no more.
- Default to deny. Allowlists, not denylists. Decide what's permitted; everything else is rejected.
- Encrypt sensitive data. Especially anything personal, and especially at rest.
- Authenticate and authorize. Every request, every boundary. Never trust on the basis of network location.
- Sanitize input. Always. Treat untrusted input as hostile by default.
- Patch. Keep dependencies current. Yesterday's CVE is today's exploit.
Tip 70: Apply Security Patches Quickly.
Security is not a separate phase; it is a tax paid on every line. The cheapest way to be secure is to write less code, with fewer dependencies, with smaller boundaries, with tighter contracts.
Topic 44 — Naming Things
The topic — and the book's coding advice — closes with what the authors call one of the hardest things in software: naming. A name encodes intent. It tells the next reader what a thing is, what it does, what it means. A good name reads like prose. A bad name forces the reader to recover the meaning from the implementation, every time.
Tip 71: Name Well; Rename When Needed.
The authors' practical advice:
- Pick names that describe the intent, not the type.
userListis worse thanpendingApprovals. - Match the level of abstraction. A high-level function should have a high-level name; a low-level helper should be specific.
- Be aware of cultural baggage. Some terms imply more than you intend (e.g.
master/slave, gendered defaults, jargon that excludes). - Rename without ceremony. When a name no longer matches the code, rename it immediately. The cost grows the longer you wait.
The closing observation is unexpectedly philosophical: names define the reality your code lives in. Bad names produce a codebase nobody really understands. Good names produce one that explains itself.
Mental model
Practical application
A coder's checklist for every change
Before you commit a change, walk through these eight questions — one per topic in the topic:
- Lizard: Does anything about this code feel off? If so, stop. Externalize. Find the doubt.
- Coincidence: Do I understand why this works, or just that it does? If the latter, I'm not done.
- Speed: What's the complexity of the hot loop? Will it scale to 100× the current input?
- Refactor: Did I leave the code cleaner than I found it? Any small renaming or extraction I can do right now?
- Test: Did the test feel hard to write? If so, what does that say about the design?
- Property: Are there invariants I could state as properties and check with generated input?
- Safety: Did I introduce any new input or trust boundary? Is it sanitized? Authorized?
- Names: Does every new identifier read like prose? Would a colleague understand it without context?
If you can answer yes to all eight, the code is ready. If not, you have specific work to do before committing — which is much better than discovering it later.
When the lizard says "stop"
A reusable pattern when you feel stuck or anxious about a piece of code:
- Step away physically (walk, coffee, anything five minutes away from the screen).
- Write the problem in plain English on paper. Often the act of writing reveals the misframing.
- Explain it to a non-programmer (or a duck). The constraint of explaining without jargon forces clarity.
- If still stuck, write a sticky note "I'm prototyping" and hack something throwaway to learn the unknown. The pressure to be correct evaporates; speed returns.
The whole loop usually takes under an hour and is dramatically faster than the alternative — pushing through, producing bad code, debugging it for two days, refactoring it next month.
Example
A refactor that uncovered a coincidence
A backend engineer is asked to add an "export to CSV" feature to a reporting service. The existing generateReport() function returns a dict the front end renders to HTML. The CSV format needs the same data.
She starts by writing a test: generateReport() should return a dict with keys summary, rows, and metadata. The test fails — the actual function returns summary, rows, and meta (singular, abbreviated). She fixes the test, then realizes: why was she sure of the keys? She checks the source. The function builds the dict from a class that mutates state as it iterates the database cursor. The keys actually depend on the order in which fields appear in the result set.
This is programming by coincidence. The current order works because the database happens to return columns in declaration order. A future schema migration that reorders columns would silently break every report.
She refactors: extract the dict-building into a function with an explicit contract (preconditions on the row format, postconditions on the dict shape). Adds a property-based test that generates random valid row data and checks the resulting dict always has the expected keys. The property test immediately finds a bug — empty result sets crash with a KeyError on metadata, because the loop that sets it never runs.
She fixes the bug, adds the CSV export (now trivial because the dict shape is documented and stable), and ships. Total time: an afternoon. The bug she found would have surfaced eventually as a customer-visible report failure costing days of triage. The CSV feature was the visible work; the latent bug was the real win — and she only found it because she let the test and the lizard brain do their jobs.
Related lessons
Related concepts
- Refactoringlinked concept
- Property-Based Testinglinked concept
- Programming by Coincidencelinked concept
- Naminglinked concept