Brian Kernighan’s “good taste” advice shows how readable code saves time, reduces bugs, and helps real teams move faster than clever tricks.

Brian Kernighan’s name shows up in places many developers use without thinking about it: classic Unix tools, the C ecosystem, and decades of writing that taught people how to explain programs clearly. Whether you remember The C Programming Language (with Dennis Ritchie), The Unix Programming Environment, or his essays and talks, the common thread is an insistence on simple ideas expressed cleanly.
Kernighan’s best advice doesn’t depend on C syntax or Unix conventions. It’s about how humans read: we scan for structure, we rely on naming, we infer intent, and we get confused when code hides meaning behind tricks. That’s why “taste” in readability still matters when you’re writing TypeScript, Python, Go, Java, or Rust.
Languages change. Tooling improves. Teams still ship features under time pressure, and most code is maintained by someone other than the original author (often future-you). Clarity is the multiplier that makes all of that survivable.
This isn’t a tribute to “hero coding” or a call to memorize old-school rules. It’s a practical guide to the habits that make everyday code easier to work with:
Kernighan’s influence matters because it points to a simple, team-friendly goal: write code that communicates. When code reads like a clear explanation, you spend less time decoding it and more time improving it.
“Good taste” in readable code isn’t about personal style, fancy patterns, or compressing a solution into the fewest lines. It’s the habit of choosing the simplest clear option that reliably communicates intent.
A good-taste solution answers a basic question for the next reader: What is this code trying to do, and why is it doing it this way? If that answer requires mental gymnastics, hidden assumptions, or decoding clever tricks, the code is costing the team time.
Most code is read far more often than it’s written. “Good taste” treats reading as the primary activity:
That’s why readability is not just aesthetics (indentation, line width, or whether you like snake_case). Those are helpful, but “good taste” is mainly about making reasoning easy: clear names, obvious control flow, and predictable structure.
A common mistake is optimizing for brevity instead of clarity. Sometimes the clearest code is a little longer because it makes the steps explicit.
For example, compare these two approaches:
The second version might add lines, but it reduces the cognitive load required to verify correctness. It also makes bugs easier to isolate and changes safer to apply.
Good taste is knowing when to stop “improving” a solution with cleverness and instead make the intent plain. If a teammate can understand the code without asking you for a tour, you’ve chosen well.
Clever code often feels like a win in the moment: fewer lines, a neat trick, a “wow” factor in the diff. In a real team, that cleverness turns into a recurring bill—paid in onboarding time, review time, and hesitation every time someone has to touch the code again.
Onboarding slows down. New teammates don’t just need to learn the product; they also have to learn your private dialect of shortcuts. If understanding a function requires decoding clever operators or implicit conventions, people will avoid changing it—or change it with fear.
Reviews get longer and less reliable. Reviewers spend energy proving that the trick is correct rather than assessing whether the behavior matches the intent. Worse, clever code is harder to mentally simulate, so reviewers miss edge cases they would have caught in a straightforward version.
Cleverness compounds during:
A few repeat offenders:
17, 0.618, -1) that encode rules no one can remember.&& / || tricks) that rely on readers knowing subtle evaluation rules.Kernighan’s point about “taste” shows up here: clarity is not about writing more; it’s about making intent obvious. If a “smart” version saves 20 seconds today but costs 20 minutes for every future reader, it isn’t smart—it’s expensive.
Kernighan’s “taste” often shows up in small, repeatable decisions. You don’t need a grand rewrite to make code easier to live with—tiny clarity wins compound every time someone scans a file, searches for a behavior, or fixes a bug under time pressure.
A good name reduces the need for comments and makes mistakes harder to hide.
Aim for intention-revealing names that match how your team talks:
invoiceTotalCents over sum.If a name forces you to decode it, it’s doing the opposite of its job.
Most reading is scanning. Consistent whitespace and structure help the eye find what matters: function boundaries, conditionals, and the “happy path.”
A few practical habits:
When logic gets tricky, readability usually improves by making decisions explicit.
Compare these two styles:
// Harder to scan
if (user && user.active && !user.isBanned && (role === 'admin' || role === 'owner')) {
allow();
}
// Clearer
if (!user) return deny('missing user');
if (!user.active) return deny('inactive');
if (user.isBanned) return deny('banned');
if (role !== 'admin' && role !== 'owner') return deny('insufficient role');
allow();
The second version is longer, but it reads like a checklist—and it’s easier to extend without breaking.
These are “small” choices, but they’re the daily craft of maintainable code: names that stay honest, formatting that guides the reader, and control flow that never makes you do mental gymnastics.
Kernighan’s style of clarity shows up most in how you break work into functions and modules. A reader should be able to skim the structure, guess what each piece does, and be mostly right before reading the details.
Aim for functions that do exactly one thing at one “zoom level.” When a function mixes validation, business rules, formatting, and I/O, the reader has to keep multiple threads in mind.
A quick test: if you find yourself writing comments like “// now do X” inside a function, X is often a good candidate for a separate function with a clear name.
Long parameter lists are a hidden complexity tax: every call site becomes a mini-configuration file.
If several parameters always travel together, group them thoughtfully. Options objects (or small data structs) can make call sites self-explanatory—if you keep the group coherent and avoid dumping everything into one “misc” bag.
Also, prefer passing domain concepts over primitives. UserId beats string, and DateRange beats (start, end) when those values have rules.
Modules are promises: “Everything you need for this concept is here, and the rest is elsewhere.” Keep modules small enough that you can hold their purpose in your head, and design boundaries that minimize side effects.
Practical habits that help:
When you do need shared state, name it honestly and document the invariants. Clarity isn’t about avoiding complexity—it’s about placing it where readers expect it. For more on maintaining these boundaries during changes, see /blog/refactoring-as-a-habit.
Kernighan’s “taste” shows up in how you comment: the goal isn’t to annotate every line, it’s to reduce future confusion. The best comment is the one that prevents a wrong assumption—especially when the code is correct but surprising.
A comment that restates the code (“increment i”) adds clutter and teaches readers to ignore comments altogether. Useful comments explain intent, trade-offs, or constraints that aren’t obvious from the syntax.
# Bad: says what the code already says
retry_count += 1
# Good: explains why the retry is bounded
retry_count += 1 # Avoids throttling bans on repeated failures
If you feel tempted to write “what” comments, it’s often a sign the code should be clearer (better names, smaller function, simpler control flow). Let the code carry the facts; let comments carry the reasoning.
Nothing damages trust faster than a stale comment. If a comment is optional, it will drift over time; if it’s wrong, it becomes an active source of bugs.
A practical habit: treat comment updates as part of the change, not as “nice to have.” During reviews, it’s fair to ask: Does this comment still match behavior? If not, either update it or remove it. “No comment” is better than “wrong comment.”
Inline comments are for local surprises. Broader guidance belongs in docstrings, READMEs, or developer notes—especially for:
A good docstring tells someone how to use the function correctly and what errors to expect, without narrating the implementation. A short /docs or /README note can capture the “why we did it this way” story so it survives refactors.
The quiet win: fewer comments, but each one earns its place.
Most code looks “fine” on the happy path. The real test of taste is what happens when inputs are missing, services time out, or a user does something unexpected. Under stress, clever code tends to hide the truth. Clear code makes failure obvious—and recoverable.
Error messages are part of your product and your debugging workflow. Write them as if the next person reading them is tired and on-call.
Include:
If you have logging, add structured context (like requestId, userId, or invoiceId) so the message is actionable without digging through unrelated data.
There’s a temptation to “handle everything” with a clever one-liner or a generic catch-all. Good taste is choosing the few edge cases that matter and making them visible.
For example, an explicit branch for “empty input” or “not found” often reads better than a chain of transformations that implicitly produce null somewhere in the middle. When a special case is important, name it and put it up front.
Mixing return shapes (sometimes an object, sometimes a string, sometimes false) forces readers to keep a mental decision tree. Prefer patterns that stay consistent:
Clear failure handling reduces surprise—and surprise is where bugs and midnight pages thrive.
Clarity isn’t only about what you meant when you wrote the code. It’s about what the next person expects to see when they open a file at 4:55pm. Consistency turns “reading code” into pattern recognition—fewer surprises, fewer misunderstandings, fewer debates that keep happening every sprint.
A good team style guide is short, specific, and pragmatic. It doesn’t try to encode every preference; it settles the recurring questions: naming conventions, file structure, error-handling patterns, and what “done” looks like for tests.
The real value is social: it prevents the same discussion from resetting with every new pull request. When something is written down, reviews shift from “I prefer X” to “We agreed on X (and here’s why).” Keep it living and easy to find—many teams pin it in the repo (for example, /docs/style-guide.md) so it’s close to the code.
Use formatters and linters for anything that’s measurable and boring:
This frees humans to focus on meaning: naming, API shape, edge cases, and whether the code matches the intent.
Manual rules still matter when they describe design choices—for example, “Prefer early returns to reduce nesting” or “One public entry point per module.” Tools can’t fully judge those.
Sometimes complexity is justified: tight performance budgets, embedded constraints, tricky concurrency, or platform-specific behavior. The agreement should be: exceptions are allowed, but they must be explicit.
A simple standard helps: document the trade-off in a short comment, add a micro-benchmark or measurement when performance is cited, and isolate the complex code behind a clear interface so most of the codebase stays readable.
A good code review should feel less like an inspection and more like a short, focused lesson in “good taste.” Kernighan’s point isn’t that clever code is evil—it’s that cleverness is expensive when other people have to live with it. Reviews are where teams can make that trade-off visible and choose clarity on purpose.
Start by asking: “Can a teammate understand this in one pass?” That usually means looking at naming, structure, tests, and behavior before diving into micro-optimizations.
If the code is correct but hard to read, treat readability as a real defect. Suggest renaming variables to reflect intent, splitting long functions, simplifying control flow, or adding a small test that demonstrates the expected behavior. A review that catches “this works, but I can’t tell why” prevents weeks of future confusion.
A practical ordering that works well:
Reviews go sideways when feedback is framed as scoring points. Instead of “Why would you do this?” try:
Questions invite collaboration and often surface constraints you didn’t know about. Suggestions communicate direction without implying incompetence. This tone is how “taste” spreads across a team.
If you want consistent readability, don’t rely on reviewer mood. Add a few “clarity checks” to your review template and definition of done. Keep them short and specific:
Over time, this turns reviews from policing style into teaching judgment—exactly the kind of everyday discipline Kernighan advocated.
LLM tools can produce working code quickly, but “works” isn’t the bar Kernighan was pointing at—communicates is. If your team uses a vibe-coding workflow (for example, building features via chat and iterating on generated code), it’s worth treating readability as a first-class acceptance criterion.
On platforms like Koder.ai, where you can generate React frontends, Go backends, and Flutter mobile apps from a chat prompt (and export the source code afterward), the same taste-driven habits apply:
Speed is most valuable when the output is still easy for humans to review, maintain, and extend.
Clarity isn’t something you “achieve” once. Code stays readable only if you keep nudging it back toward plain speech as requirements change. Kernighan’s sensibility fits here: prefer steady, understandable improvements over heroic rewrites or “smart” one-liners that impress today and confuse next month.
The safest refactoring is boring: tiny changes that keep behavior identical. If you have tests, run them after every step. If you don’t, add a few focused checks around the area you’re touching—think of them as temporary guardrails so you can improve structure without fear.
A practical rhythm:
Small commits also make code review easier: teammates can judge intent, not hunt for side effects.
You don’t have to purge every “clever” construct in one go. As you touch code for a feature or bugfix, trade clever shortcuts for straightforward equivalents:
This is how clarity wins in real teams: one improved hotspot at a time, exactly where people are already working.
Not all cleanup is urgent. A useful rule: refactor now when the code is actively changing, frequently misunderstood, or likely to cause bugs. Schedule later when it’s stable and isolated.
Make refactoring debt visible: leave a short TODO with context, or add a ticket that describes the pain (“hard to add new payment methods; function does 5 jobs”). Then you can decide deliberately—rather than letting confusing code quietly become the team’s permanent tax.
If you want “good taste” to show up consistently, make it easy to practice. Here’s a lightweight checklist you can reuse in planning, coding, and review—short enough to remember, specific enough to act on.
Before: process(data) does validation, parsing, saving, and logging in one place.
After: Split into validateInput, parseOrder, saveOrder, logResult. The main function becomes a readable outline.
Before: if not valid then return false repeated five times.
After: One upfront guard section (or one validation function) that returns a clear list of issues.
Before: x, tmp, flag2, doThing().
After: retryCount, draftInvoice, isEligibleForRefund, sendReminderEmail().
Before: A loop with three special cases hidden in the middle.
After: Handle special cases first (or extract helpers), then run the straightforward loop.
Pick one improvement to adopt this week: “no new abbreviations,” “happy path first,” “extract one helper per PR,” or “every error message includes next steps.” Track it for seven days, then keep what actually made reading easier.
Kernighan’s influence is less about C and more about a durable principle: code is a communication medium.
Languages and frameworks change, but teams still need code that’s easy to scan, reason about, review, and debug—especially months later and under time pressure.
“Good taste” means consistently choosing the simplest clear option that communicates intent.
A useful test is: can a teammate answer “what does this do, and why is it done this way?” without decoding tricks or relying on hidden assumptions.
Because most code is read far more than it’s written.
Optimizing for readers reduces onboarding time, review friction, and the risk of incorrect changes—especially when the maintainer is “future you” with less context.
The “tax” shows up as:
If the clever version saves seconds now but costs minutes every time it’s touched, it’s a net loss.
Common culprits include:
These patterns tend to hide intermediate state and make edge cases easier to miss in review.
When it reduces cognitive load.
Making steps explicit with named variables (e.g., validate → normalize → compute) can make correctness easier to verify, simplify debugging, and make future changes safer—even if it adds a few lines.
Aim for:
invoiceTotalCents over sum)If you need to decode a name, it’s not doing its job; the name should reduce the need for extra comments.
Prefer simple, explicit branching and keep the “happy path” visible.
Tactics that usually help:
Comment the why, not the what.
Good comments capture intent, trade-offs, constraints, or non-obvious invariants. Avoid narrating obvious code, and treat comment updates as part of the change—stale comments are worse than no comments.
Use tools for the mechanical rules (formatting, imports, simple footguns) and reserve human review for meaning.
A lightweight style guide helps by settling recurring decisions (naming, structure, error-handling patterns) so reviews become about clarity and behavior, not personal preference.
When you do make exceptions for performance or constraints, document the trade-off and isolate complexity behind a clean interface.