I learned to write code long before I knew how to read it. This is, I now believe, the central malformation of how we teach software engineering. We hand new engineers a problem and ask them to produce code; we never hand them code and ask them to produce understanding. The asymmetry is so deep that we mostly don't notice it. We don't even have a word for it.

In the literature departments next door — the ones where my partner spent her undergraduate years — students do the opposite. They read for a year before they write a paragraph. The first essays they produce are responses to other essays. They learn that what writing is for is to be read, and that reading is the slow, patient practice of finding out what writing actually does. By the time they're asked to write a thesis, they have spent thousands of hours doing what we, in software, almost never do: sitting with a single piece of writing for long enough that they can defend its choices line by line.

We should learn this too. We can.

The asymmetry, in full.

Most software curricula are organized around production. The first lesson teaches you to write print("hello"). The second lesson teaches you to write a loop. By the end of the first month, you've written perhaps a hundred small programs, and you have read essentially none. Reading happens in passing — on Stack Overflow, in a tutorial, in the documentation when you're stuck — but it is never the assignment. The assignment is always to produce something.

By the time you reach a job, you can write competent code. But the first six months of that job are usually about reading: a codebase someone else built, in conventions you didn't choose, with assumptions you can't yet name. The reading skill — the one you weren't taught — is suddenly the only thing that matters. Most of us muddle through. Most of us never get all that good at it. We become engineers who can write but cannot read, the way someone might become a writer of paragraphs who has never finished a book.

I've been at Cloudflare for nearly a decade and have onboarded dozens of engineers. The single best predictor of how someone will perform in their first year is not how cleverly they write code in the interview. It is how patiently they read code in the second week.

This is not a discovery about Cloudflare. It is a discovery about engineering. The senior engineers I most admire are the ones who can sit with an unfamiliar function for an hour and emerge knowing not just what it does but what it commits its author to, where it would resist change, and which of its assumptions are load-bearing. They are good readers. The good readers are good engineers. The relationship is not coincidental.

What "reading as literature" actually means.

I do not mean: skim for the gist. I do not mean: trace the call graph until you find the bug. I do not mean what most engineers think they're being asked to do when they're handed a foreign codebase.

I mean what literary critics mean by close reading. Sit with a single function for an hour. Read it word by word. Annotate the assumptions. Ask why this if is here and not three lines later. Ask why this name was chosen and what it commits the author to. Ask, when you reach a comment, what the comment is doing — is it explaining? Apologizing? Pre-empting the question you're about to ask?

Read the surrounding code as context. Trace the values that flow into and out of this function. Read the tests. Read the commit that introduced the function. Read the commits that touched it since.

Read the issue that prompted them. By the time you have done all of this for a single function, you will know the function as well as its author does — better, in fact, because you will have read it as a stranger and as the person who has to live with it. The author wrote it in an afternoon. You spent an hour with it. The hour is well spent.

This is a skill. It is not innate. It can be taught.

A small case study.

Let me give an example. Here, lightly paraphrased, is a function from the Linux kernel — kfree, the function that frees a previously-allocated block of memory:

void kfree(const void *block) {
    if (unlikely(ZERO_OR_NULL_PTR(block)))
        return;
    /* ... cache lookup, free, telemetry, the usual ... */
}

Five lines of code. A two-week class could be taught from them.

Why is the parameter const void *? Why const, when we are about to free the memory it points to — and so the constness will, in moments, be moot? Because const here is a contract with the caller, not with the implementation: it tells the caller they can pass in a pointer they themselves treated as const, without a cast. It commits the function to not modifying the block before deciding what to do with it. A small, considerate decision. Hold onto it.

Why void * rather than a typed pointer? Because the kernel allocator is type-agnostic — it returns and accepts blocks, not objects. The same allocator serves filesystems, network buffers, scheduler structures. void * is the language admitting that this function lives below the type system.

Why unlikely()? Because the compiler will use the hint to lay out the assembly so the common case (a real pointer) is the fall-through path, and the unusual case (a null or sentinel pointer) is the branch. This costs nothing if you're wrong and pays a small but real price if you're right. The author of this function thought about cache lines. You can tell.

Why ZERO_OR_NULL_PTR rather than just !block? Because the kernel allocator returns a small sentinel value (ZERO_SIZE_PTR) for zero-byte allocations, and code in the kernel relies on being able to pass that sentinel back to kfree without a special case. The macro covers both NULL and the sentinel. The decision is invisible from outside the file but expensive to get wrong: if kfree tried to free the sentinel, you'd corrupt the heap.

The five lines, read closely, contain decisions about the type system, the compiler, the cache, the allocator's API, and the kernel's social contract with its callers. The author wrote them quickly. They probably didn't think they were writing literature. But you can read them as literature, and when you do, you understand the kernel.

A reader who cannot read three lines of kernel code carefully cannot understand the kernel. A reader who can read three lines of kernel code carefully has a path to understanding any of it. The close reading is what makes the rest possible.

An interlude on annotation.

The students I teach often arrive convinced that annotation is for textbooks, not for code. They have not, I suspect, watched a serious literary scholar work. The work is mostly margin notes. Underlines. Question marks. Tiny disagreements with the author that, over time, become the substance of an interpretation.

I ask my students to annotate code by hand, on paper, for the first three weeks. They print out the function. They read it with a pen. They mark, in the margin, every choice that surprises them. They mark, in a different colour, every choice that they would have made differently — and they have to write a sentence saying why. By the end of three weeks, the function is illegible under all the annotation, and the student knows it intimately.

Then I ask them to do it on screen, with comments. Then I ask them to do it in code review. Then I ask them to do it silently, in their head, when they read.

The form changes. The practice does not.

How we teach it.

At Lattice, every track begins with two weeks of reading before any writing. This is the part of the curriculum students complain about most in week one and miss most after they graduate. We assign code the way an English department assigns novels: with a chapter list, a deadline, and a seminar discussion.

The first reading is small and self-contained — a single Redis data structure, or a single Postgres planner module, or the entire SQLite virtual machine, all of which fit in a long evening if you are patient. The seminar the next morning is two hours of close discussion of the choices the author made. What does this name commit you to? Why is this function in this file and not the one next door? Where would you push on the design? By week two, students are answering these questions about each other's reading. By week three, they are answering them about their own work.

The labs are reverse-engineering exercises. Here is a function. Reconstruct the conversation that produced it. Defend each line. The hardest line in any function is rarely the cleverest one — it is the line whose presence is unjustified to the new reader. We teach students to find those lines and to ask, gently, what they are doing there. Sometimes the answer is "removing it would break a test you can't see yet." Sometimes the answer is "it's a vestige." Both answers are useful.

By week six, students are reading like editors. By week twelve, like critics. We send them out into industry knowing how to read code the way the people who wrote it did. The companies who hire them notice within a fortnight.

The payoff.

What changes, when this skill is in the room, is everything downstream of it.

Code review becomes substantive. The reviewer is not skimming for typos; they are reading. They are noticing the quiet decision the author made on line forty-seven and asking whether it generalizes. The author, who has by now learned to read their own code as a stranger would, is grateful for the question. The conversation in the pull request is, by week three of a new team, about choices and trade-offs and not about formatting.

Onboarding compresses. A reader who can sit with unfamiliar code for an afternoon and emerge with a thesis — here is what this is doing, here is the assumption I would push on, here is the thing I'd ask to change — is productive within a fortnight, not six months. The senior engineers who used to spend their afternoons explaining the codebase get their afternoons back.

APIs improve. An engineer who has spent twelve weeks reading other people's interfaces with the seriousness of a critic writes their own with the seriousness of an author.

They name things. They write the comments that explain why and not what. They notice, halfway through their own design, that an interface has gotten too clever, and they shave it down. The resulting code is not necessarily shorter — it is rarely shorter — but it is easier to read. Everything downstream of "easier to read" is easier.

The bugs we ship are different bugs. Fewer of them, and more interesting. The bugs that survive a team of close readers are the ones nobody could have caught at the function level — bugs that live in the interaction between systems, in the slow drift of an invariant across a year of small commits, in the edges where two reasonable engineers held two reasonable assumptions. Those bugs are still hard. They are at least, finally, the right ones to find.

In closing.

I am, you will have noticed, advocating for a teaching method by listing its results. This is not, I think, accidental. The method is not a clever pedagogy; it is a return to first principles. We are letting the cumulative inheritance of how literature is taught — slowly, with notes in the margins, in conversation with other readers — apply to software.

It does. It always did. We just stopped doing it.

If you take one thing from this essay: this week, pick one piece of code you admire, and read it the way a critic would read a poem. Slowly. With a notebook. With a friend, if you can find one. Read until you can defend each line, and then read until you can argue with one. See what you find.

You will, I think, find that the code is talking to you. That it always was. That the only thing keeping you from hearing it is that no one ever taught you how to listen.


Mira Carvalho · Berlin · April MMXXVI