Write It Like You Mean It
Code for Humans, Executed by Machines
A curious phenomenon in the natural habitat of the programmer: writing code consumes about ten percent of their time but receives ninety percent of their attention. The remaining ninety percent of their time (spent reading, deciphering, and occasionally cursing at code) receives barely a thought at all.
This asymmetry matters more than almost anything else in programming.
You write a function once. But how many times will it be read? There’s the immediate read when testing it. The confused re-read an hour later when something doesn’t work. The read next week when you need to modify it. The read next month when a bug emerges from whatever dark corner bugs emerge from. The read by a teammate who needs to understand your code without hunting you down for explanations. The read by future-you, six months hence, who has absolutely no memory of writing this and regards it with the same baffled suspicion you’d reserve for an ancient manuscript in an unknown tongue.
All those readings add up. If writing clear code costs an extra five minutes now but spares ten minutes of squinting on every future visit, the math isn’t subtle. You come out ahead, and you keep coming out ahead, paid back in the one currency you actually care about.
Yet we optimize for writing speed anyway. We choose terse variable names because they’re faster to type. We skip comments because explaining feels tedious. We write clever one-liners because we’re feeling clever, and cleverness, like a new hat, demands to be shown off.
This is programming’s original sin: confusing the act of typing with the act of creating value. Typing is ephemeral. Fingers moving, characters appearing, a pleasant sense of productivity. What you type gets read, over and over, by people who didn’t have the context you had when you wrote it. People who are tired, or new to the project, or working on a Friday afternoon when their mind has already departed for the weekend.
Those people, including future-you, deserve better. They deserve code that communicates rather than code that merely functions.
A recipe, after all, isn’t the dinner; it’s the dinner written down so someone in another kitchen can make it again without you hovering at their elbow. Code is the same kind of writing: you’re not just feeding the machine tonight, you’re leaving instructions clear enough that the next cook, future-you included, can follow them.
The Two Audiences
Every piece of code performs for two very different audiences, and their needs agree only by accident. One audience is a machine. The other is a tired human. Guess which one you keep designing for, and guess which one keeps you up at night.
Sometimes these needs align: clear code is often correct code, because clarity of thought precedes clarity of expression. But sometimes they conflict. You can write code that’s incredibly efficient (every byte optimized, every cycle counted) but, to a human reader, as legible as a windscreen in a downpour. Or you can write code that’s beautifully clear but runs a few milliseconds slower.
The modern answer, nine times out of ten, is: optimize for the human. Make the code clear. If it’s too slow (and you’ve measured, not merely assumed), then optimize the critical paths. But clarity should be the default, and cleverness the exception that requires justification.
This wasn’t always the consensus. Early programmers worked in environments where computer time was precious and programmer time was cheap. You optimized ruthlessly because cycles were scarce as hen’s teeth. Now the economics are entirely reversed: computer time is cheap, programmer time is expensive. The advice has flipped accordingly. This is worth remembering when reading old programming books that insist on practices that no longer make economic sense.
The Dark Art of Naming
There’s a famous quote: “There are only two hard things in Computer Science: cache invalidation and naming things.” (Some add “and off-by-one errors.”)
Cache invalidation is a topic for another day, but the naming part is painfully, exquisitely true. Naming is hard because a name is your primary tool for communication: a tiny container into which you must pack meaning, intent, and context. A good name carries meaning across time and space. A bad name is a locked door, and you’re the one who’ll be standing outside it at midnight, having long since lost the key.
Principles for Variable Naming
- Be specific. Not
count, butuserCountorerrorCountorretryCount. Context matters. In isolation,countis meaningless. With specificity, it’s documentation. - Say what it is, not what type it is. Avoid names like
userListormessageDict. Better:users,messagesById. The type is usually obvious from usage, and if it isn’t, you’ll eventually meet TypeScript’s type annotations, which exist for exactly that. Use the name to convey semantic meaning. - Avoid abbreviations unless they’re ubiquitous.
msgsaves you the three keystrokes you’d spend reaching for a doorknob, and costs every reader after you.messageis clearer. The exception is if everyone in your domain uses an abbreviation (HTTP,URL,API). Butusrandtmpandbtnare just making readers work harder. - For booleans, use is/has/should/can. These make conditionals read naturally.
Function names follow similar principles, but they’re actions, so they use verbs. If your function name is getting absurdly long, that’s often a sign the function is doing too much, a suitcase you have to sit on to get shut. calculateAndValidateAndSendPayment suggests a function that should be broken into three functions. When naming becomes difficult, it’s often because the thing being named is itself confused about its purpose.
Comments: The Why, Not The What
Here’s a controversial opinion that shouldn’t be controversial at all: most comments are bad.
Not because comments themselves are bad. Comments are vital, essential, the difference between comprehensible code and archaeological mystery. But most programmers comment the wrong things entirely.
Bad comments explain what the code does. Comments like // Set x to 5 above const x = 5; are worse than useless. They’re noise. A parrot on your shoulder, repeating everything you say. The code already says what it’s doing; repeating it in English adds nothing except clutter. And worse, these comments rot. The code changes, the comment doesn’t get updated, and now the comment lies. A lying comment is worse than no comment at all because it actively misleads.
Good comments explain why: business context (why this tax rate needs annual review), rationale for choices (why binary search? because the list is huge and sorted), gotchas and workarounds (the API is weird, here’s how we handle it).
When should you write a comment?
- When you make a non-obvious choice
- When you work around a known limitation or bug
- When you implement a complex algorithm
- When you encode a business rule that’s not self-evident
- When you leave a TODO for future work
When should you not write a comment?
- When the code already says it clearly
- When better naming would make the comment unnecessary
- When the comment would just restate obvious code
A meta-rule worth remembering: if you’re about to write a comment explaining what a chunk of code does, ask yourself whether you could extract this into a well-named function instead. Code that explains itself is always better than code plus explanation, because the code and the explanation can drift apart over time (the way a caption and a photo slowly stop matching), while a well-named function carries its explanation with it wherever it goes.
Structure: The Visual Grammar
Code has a visual shape, and that shape does far more work than anyone gives it credit for. The human eye is a sucker for patterns. It groups whatever sits together and separates whatever drifts apart, automatically, whether you asked it to or not. Good code quietly leans on this for free.
Those blank lines create visual paragraphs. Each paragraph is a logical step. The structure reveals, at a glance, that this code has three distinct phases: input, validation, persistence, laid out like the prep bowls a cook lines up before the heat goes on. No comment needed. The structure itself communicates.
Keep functions short. A function should fit on one screen. If you’re scrolling to understand a function, the way you’d unspool a long receipt to find the one charge you don’t recognize, it’s too long. Break it into smaller pieces, each with a clear purpose and a name that announces that purpose.
This isn’t about arbitrary line limits or aesthetic preferences; it’s about cognitive load. Humans can hold about seven things in working memory at once. (Yes, seven. Not seventy. Not seven hundred. Seven. Your brain has the RAM of a goldfish with ambition.) A 200-line function requires understanding far more than seven things simultaneously, which means constant re-reading, constant backtracking, constant confusion. A 15-line function? Much more manageable. The brain can actually hold it all at once.
Consistent style matters too. Not because there’s One True Way to format code (there isn’t, and arguments about it have caused more casualties than the tabs-vs-spaces wars), but because inconsistency is distracting. If you use spaces in some places and tabs in others, if you sometimes put braces on the same line and sometimes on the next, readers notice. It’s the pebble in the shoe that you can’t stop thinking about, and they spend their attention on it instead of on what the code actually does.
Pick a style (or adopt your team’s style) and stick with it. Better yet, use an auto-formatter and never think about it again. Your time is better spent on substance than arguing about where braces go.
Code Archaeology: The Detective Approach
Watch the programmer encountering unfamiliar code for the first time. Note the furrowed brow. The tentative scroll. The slight hunch that says “I have no idea what I’m looking at.” This is a natural response. In fact, it’s the correct response. Confidence when facing unknown code is usually misplaced.
You will spend an astonishing amount of your programming life reading code you didn’t write. Code written by teammates, by open-source contributors, by someone who left the company three years ago and took all their context with them, by past-you who might as well be a different person with different memories and different concerns.
Code archaeology is less about excavation and more about investigation. You’re a detective arriving at a scene, piecing together what happened from the evidence left behind. Think Sherlock Holmes, but instead of a smoking gun, you’re looking for a smoking null.
The key insight is this: unfamiliar code is a mystery to be solved, not a wall to be scaled. You don’t need to understand everything at once. You need to ask good questions and follow the clues, one at a time, patiently.
Start with the big picture. What does this code do at the highest level? Ignore the details. They’ll only overwhelm you at this stage. Don’t read line by line; that’s exhausting and ineffective. Instead, scan for the shape of things. Look at function names, class names, module structure. What story do they tell? What narrative emerges?
Then zoom in with purpose. You’re looking for something specific: perhaps where user authentication happens, or how the email system works, or why this particular bug occurs. Find the entry point relevant to your question and trace from there. Follow the thread, and only the thread.
Code archaeology has gained a new dimension. When AI generates code for you, you become the archaeologist of code you didn’t write but are responsible for. The same skills apply: scan for shape, zoom in with purpose, trace backwards from outputs. The difference is that the ‘original author’ is an AI that can explain itself if asked, but whose explanations should be verified against the actual code.
What helps:
- Use the tests. If the codebase has tests (and we’ll cross our fingers that it does), read those first. Tests show you how the code is meant to be used. They’re examples, living documentation, proof of what works.
- Look for the names that matter. Good naming leaves breadcrumbs. A function called
calculateRefundAmounttells you immediately what it does and suggests where refunds are handled. - Trace backwards from outputs. If you’re trying to understand how data flows, start at the end. Find where the result appears (on screen, in a database, in a log file) and work backwards to its sources, the way you’d read a postmark to figure out where a parcel began its journey.
- Accept that some code is genuinely confusing. Not all code is good code. Sometimes the code really is a mess, the names really are misleading, the structure really is inscrutable. That’s not your failure as a reader.
- Leave clues for the next archaeologist. When you finally understand something non-obvious, write it down. Add a comment. Update documentation. Make the path easier for whoever comes after.
Code archaeology is a fundamental skill, because code is rarely written once and forgotten. It’s written, then read, then modified, then read again, then debugged, then read again. The reading vastly outweighs the writing. Becoming a good archaeologist (patient, methodical, willing to follow evidence wherever it leads) makes you a more effective programmer than almost any other skill.
API Design as Communication
An API (Application Programming Interface) is any boundary where one piece of code talks to another. It might be a library’s public functions, a web service’s endpoints, or just the functions your module exposes to the rest of your program. What they all have in common: they’re designed to be used by someone other than the person who built them. Often by many people. Often by people you’ll never meet, who will form firm opinions about you based on nothing but your function names.
This makes API design an exercise in empathy, which is a peculiar thing to demand of anyone at 11pm. You’re not just writing code that works. You’re writing code that strangers can use correctly without reading your implementation, without asking you questions, and ideally without ever cracking open the documentation at all.
The principle of least surprise is your guiding star: when someone uses your API, it should behave the way they expect. Not the way you think is clever. Not the way that made implementation easier. The way that makes intuitive sense to the person calling it.
Naming matters even more at API boundaries
Internal code can be refactored. An API, once published, is a promise. People depend on it. So the names you choose matter enormously. The name should tell you what goes in and what comes out. It should suggest whether there are side effects. It should be honest about what it does.
Consistency creates predictability
If one function is called getUser, another is fetchProduct, and a third is retrieveOrder, you’re making people memorize three words for one idea, and resent you a little for each one. Pick a convention and stick to it. This goes for parameter order, return types, error handling, everything. When your API is consistent, users can guess how new functions work based on ones they already know.
Fail helpfully
Errors are communication too. A cryptic error message like throw new Error("Error") isn’t an error message, it’s a shrug with a stack trace attached. A helpful error tells you what went wrong, what was expected, and how to fix it. It respects the user’s time. It treats them as an intelligent person who made a simple mistake, not as someone who deserves punishment.
The Audience of Code
In the beginning, you write code for the computer. You’re learning syntax, testing logic, making things work. The computer is your only audience, and it’s a simple audience. Code either runs or it doesn’t. There’s a clarity to this that’s almost refreshing.
But here’s what every programmer eventually learns, usually the hard way: the computer is your least important audience.
The computer will execute any syntactically valid code you give it, no matter how unclear, how convoluted, how nightmarish. The computer doesn’t judge. The computer doesn’t get confused. The computer doesn’t maintain your code at 2 AM while a production system burns, phones ring, and managers ask pointed questions with that special tone they reserve for disasters.
Humans do all those things. Humans are your actual audience.
Your real audience is:
- Future-you. The person you’ll be in six months when a bug appears in this code and you can’t remember why you wrote it this way. Future-you has forgotten all the context present-you has. Future-you will be grateful for clear names, helpful comments, and logical structure.
- Your teammates. The people who need to understand your code to work alongside it, to modify it, to fix it when you’re on vacation or asleep or moved to a different project. They don’t have the context you had when you wrote this.
- Future maintainers. The people who’ll work on this codebase after you’ve moved on. Maybe you left the company. Maybe you switched teams. Maybe you were promoted and this is no longer your responsibility. Someone else inherits your code.
The mental shift is this: stop thinking of code as instructions for a computer. Start thinking of code as communication with humans, communication that happens to be executable by machines. The computer is the medium, not the audience.
The Long Game
Programming is unusual among crafts in that you’re constantly collaborating with past and future versions of yourself. This temporal collaboration is one of the stranger aspects of the work: you’re basically in a group project with yourself across time, and past-you is often an unreliable partner.
Past-you wrote this code. Present-you needs to understand it, modify it, debug it. Future-you will inherit whatever present-you creates. It’s a relay race where you hand the baton to yourself, across days and weeks and months.
This time-traveling collaboration only works if you’re kind to your collaborators. Write code that past-you would be proud of. Write code that future-you will thank you for. Write code that your teammates can understand without hunting you down for an explanation.
Clean code is an act of respect: for your team, for your future self, for anyone who’ll maintain this software after you’ve moved on. It’s a gift you give to people you’ll never meet. A kindness that echoes forward through time.
And practically speaking, it’s enlightened self-interest. That confusing code you’re writing today? You’ll be debugging it next month. That comment you’re skipping? You’ll wish it existed when you’re trying to remember why you made this choice. That meaningful variable name you’re too lazy to type? Future-you will curse present-you for the brevity.
The extra minute you spend making code clear is never wasted. It’s the best investment you can make. Because code, ultimately, is communication, and communication only works if the receiver can understand what you’re saying.
Make it easy for them. Make it easy for yourself. Write code you’d want to read.
Building the Habit
The temptation when you’re learning is to write messy code and tell yourself you’ll clean it up later.
Later never comes.
Or rather, later comes when you’re debugging at 2 AM, when you’re trying to add a feature under deadline pressure, when you’re explaining your code to a colleague who is trying very hard not to look confused. These are terrible times to be deciphering messy code. These are the moments when past-you’s shortcuts become present-you’s problems.
Build the habit now: write clear code from the start.
Name variables well the first time. Yes, it takes a few extra seconds to think of a good name. Those seconds pay dividends immediately, even while you’re still writing the code, and they keep paying dividends every time anyone reads it. The few seconds spent thinking are an investment that compounds.
Add comments when you figure something out. You just spent ten minutes understanding how this API works. Write a comment capturing that understanding. Future-you, arriving at this code with no memory of the struggle, will be grateful. Future-you will silently thank past-you for the kindness.
Before you call code “done,” read it. Pretend you’re seeing it for the first time. Does it make sense? Are the names clear? Is the flow logical? Would you want to debug this at midnight, tired, slightly annoyed, and questioning your career choices? If not, it isn’t done.
When AI writes code for you, resist the temptation to accept it as-is just because it works. Read it. Clean up the names. Add a comment where the logic isn’t obvious. Make it yours. Messy AI output accepted uncritically becomes technical debt at machine speed.
This isn’t busywork. This is the craft. Like all craft, it gets easier with practice. Eventually, writing clear code becomes as natural as writing messy code once was. Possibly more natural, because clarity helps you think. Clear code is not just easier to read; it’s easier to write correctly, because you understand what you’re doing.
Learning from Others
The best way to develop good coding habits is to read good code. This is the same advice given to aspiring writers: read widely, read well, notice what works.
Find well-regarded open-source projects in the language you’re learning. Read their source code. Notice what makes it easy or hard to understand. When something is beautifully clear, ask yourself: what makes this clear? What choices did the author make that help me understand? When something is confusing, ask: how could this be clearer? What’s missing?
Patterns will emerge. Good projects tend to have:
- Consistent naming conventions
- Functions that do one thing well
- Clear separation between different concerns
- Comments on the tricky bits, silence on the obvious bits
- Examples in documentation
- Tests that also serve as usage examples
Absorb these patterns. Steal liberally. In programming, nobody’s going to stop you, and honestly, borrowing what works is the whole point. Every great programmer learned by reading other programmers’ code and quietly lifting what worked. This is how craft traditions perpetuate themselves.
Here’s a secret that experienced programmers know but rarely articulate: reading code makes you better at writing code. It builds your vocabulary. It shows you new approaches. It calibrates your sense of what’s normal and what’s exceptional. Just as reading good writing makes you a better writer, reading good code makes you a better coder.
Next: The programming world is larger than what we’ve explored so far. Concurrency, paradigms, and the many ways programmers have learned to think about problems.
Now that we’ve explored how to communicate clearly with humans through code, let’s turn to a more philosophical question: how do different programming languages shape the way we think? Just as natural languages influence thought, programming paradigms influence how we approach problems. Some languages encourage thinking in objects, others in transformations, still others in declarations.