Back in the day I read one of Zed Shaw's "Learn X the hard way" courses (I think it was his C++ course) and he began the course with a disclaimer that said something like "do not copy and paste the examples; type them out" because he believed strongly that doing so improved your comprehension of the material.
These days he recommends something even stronger:
The best way to do this is when you complete an exercise delete what you did and do it again, but try to use your memory. If you get stuck then look at the exercise for clues, but try your best to recreate what you just did from memory. At first you'll be constantly referring to the exercise and the code, but eventually you'll get to where you can do it on your own. This will also improve your ability to remember more code, your ability to summarize how something works, and generally improve your skills faster than if you did it once and moved on.
This is known as the generation effect in cognitive psychology: the active generation of new content improves comprehension compared to passive consumption of the same material. Or as Richard Feynman put it: "What I cannot create, I do not understand".
I'm not a researcher, but I would still like to add my own spin as a lay programmer on why I believe this phenomenon matters for improving programming ability, even in the age of agentic coding. In fact, I think it's even more important now than ever for people to reacquaint themselves with this "ancient wisdom" because otherwise we'll end up with an entire generation of programmers wondering how anyone ever programmed without the help of a coding agent.
In this post I will lay out the case for why you should occasionally type out the code, character-by-character, as best as possible from memory. That doesn't mean that you should never use developer tools of any sort to streamline the coding process, but you also should occasionally stretch your ability to code without those affordances, too.
I believe in the importance of occasionally spelling out details when programming, and I don't just mean things like performance characteristics or UI design decisions. I mean basic stuff like:
syntax and structure
… meaning fluency with keywords, punctuation, and language constructs
types and schemas
… including familiarity and comfort with the type system and data model
names of things
… such as accurate recall of functions, methods, classes, imports, and files
These are the things that need to live in your brain so that you can "freecode" from memory and in my experience programmers who do this significantly outperform other programmers even on tasks which seem unrelated to freecoding proficiency. I also don't think this is simple correlation but rather causation: freecoding cultivates broader programming excellence.
Why? Because, if you're unable to type out what you have in mind to a certain degree of precision1 that means you didn't really have it in mind. You were hallucinating understanding, the same as an LLM.
"But Gabby," you might retort, "I'm pretty sure I can proficiently understand and manipulate the code at a high level without getting bogged down in details like syntax or names."
No, I'm not convinced that this works as well as some people think and I'll spell out why.
Let's start with syntax, which is probably the most controversial thing for me to advocate grinding on. After all, it's 2026 and our IDE or coding agent can get the syntax right for us, so why should we bother to think about the syntax at all?
Well, for starters, if you struggle with syntax then I'll wager there are many other things (even non-programming things) that you struggle with. For example, if you have difficulty balancing parentheses I'd begin to wonder how fluently you can connect someone else's logical premise with their conclusion.
I would go even further and connect disregard for the details with the epidemic of people who are "functionally inarticulate": unable to logically follow or chain together sentences. I'm talking about the sort of people who communicate based on vibes rather than reasoning in a clear-headed way about the meaning of words.
To illustrate what I mean, I recently audited an LLM prompt and stumbled across this:
Never suggest external tools or alternatives that aren't part of the skills listed above. If a task requires capabilities beyond the available skills, say so.
This is the sort of prompt that I would read as functionally inarticulate: it sounds sensible if you don't read closely but when you do you might notice the prompt is actually giving the model two diametrically opposed instructions.
I find that functionally inarticulate people, almost to a person, also struggle to spell or form grammatically correct sentences on their own because you can't separate the small stuff from the big stuff: when you skimp on getting details correct that bleeds into everything you do and you begin to get the big picture wrong, too.
Similarly, people who struggle with syntax often struggle with abstract thought. They probably think of syntax as a detail that gets in the way of big picture thinking but nothing could be further from the truth! Syntax is a mental tool that compresses and powers higher-level thinking, affording us a precise, clear, and compact internal language for reasoning.
Or to put it another way, syntax is the difference between this:
xis a an array of objects, each of which has a requireddomainproperty storing a string and an optionalportproperty storing a number
… and this:
x : { domain: string, port?: number }[]
… which brings me to the next thing you should grind on:
I cannot overstate how important it is to know your type system and data model like the back of your own hand2. As Fred Brooks said in The Mythical Man Month:
Show me your flowchart and conceal your tables, and I shall continue to be mystified. Show me your tables, and I won't usually need your flowchart; it'll be obvious.
For example, if you use a database then you need to know your project's tables and column names and their relationships backwards. Don't lazily query this information "on demand"; you should proactively gather this information and keep it top of mind in order to do any sort of effective system-level design. People who don't put in this work create database schemas that are as confused as their own thoughts: denormalized and full of duplicate/similar data (no single source of truth).
This applies to types and the type system, too! Anyone who has used a very strongly-typed language (e.g. Rust or Haskell) can vouch for the benefit of maintaining a strong mental model of the types. Moreover, the exercise of cultivating an accurate "mental type checker" or "mental borrow checker" comes in handy even when programming in a weakly-typed language3. When you exercise a mental muscle like that you improve your ability to reason abstractly about projects and when you don't you stumble on data modeling basics like making invalid states unrepresentable.
Guess what happens if you don't understand how all the types fit together. You (or your coding agent) will begin sprinkling as any type assertions in your TypeScript code because you won't know how to fix the errors you run into. After all, if you can't handle the discomfort of thinking through the types on the happy path (authoring code), you definitely won't be able to stomach the unhappy path (debugging type errors).
I also believe that you should able to easily recall commonly used function/method/class/import/package/file names within your project or dependencies. You also should keep that knowledge up-to-date as the project evolves.
This is a special case of a more general rule: you should familiarize yourself with prior art (things that have already been built, whether in the open source world or within your own organization). A lot of the reason people lean on coding agents to do the work is that they're not aware of reusable prior art that does what they want so they end up asking their agent to reinvent the wheel (and the agent happily obliges).
For example, if you didn't know that SaaS boilerplate projects existed you'd think that you need a coding agent to scaffold all of that for you. Cloning a battle-tested open source project created for that very purpose is faster, cheaper, and more reliable than asking an agent to do the same4.
The same applies when reusing code within the same project or company, too. If you struggle to remember names, then you will also struggle to reuse your colleagues' code. You can't build off of someone else's work if you don't even know what you're looking for.
An agent can help there, but now you have a new problem: can you meaningfully review the agent's output? For example, how can you tell if the coding agent is duplicating functionality if you don't even know what other functionality exists? How can you review the agent's output for quality if you understand the code worse than the agent does5?
You can ask an agent to generate tests, but in my experience many agentic coders can't even be bothered to scrutinize those tests, which is how you end up with test code like this:
// This is a real example I've seen in the wild
describe("abort detection logic", () => {
it("detects aborted stopReason in messages", () => {
const messages = [
{ role: "assistant", stopReason: "aborted", content: [] },
];
const isAborted = messages.some((m: any) => m.stopReason === "aborted");
expect(isAborted).toBe(true);
});
it("detects abort in error string", () => {
const error = "The operation was aborted";
const isAborted = error.includes("abort");
expect(isAborted).toBe(true);
});
it("does not false-positive on normal errors", () => {
const error = "Network timeout";
const isAborted = error.includes("abort");
expect(isAborted).toBe(false);
});
it("does not false-positive on normal stop reasons", () => {
const messages = [
{ role: "assistant", stopReason: "stop", content: [] },
];
const isAborted = messages.some((m: any) => m.stopReason === "aborted");
expect(isAborted).toBe(false);
});
});
Software development is full of everyday frictions and the way we respond to small frictions affects how we respond to bigger frictions. When we decide that small frictions (like remembering a name) are not worth surmounting we eventually decide that bigger frictions (like scrutinizing tests) are also not worth surmounting.
You might notice some common threads shared between these examples:
you cannot compartmentalize gumption
Eustress is good: when you challenge yourself to do and learn new things you build up your tolerance for smaller discomforts and pave the way for overcoming bigger discomforts. If you always avoid discomfort then you descend into a vicious cycle of increasing helplessness and frustration.
you cannot compartmentalize proficiency
When you get really good at one thing that bleeds into getting better at other things because it's all connected. In the same way that LLMs generalize from their training data, so do humans! Cultivating precision, recall, and structured thinking in one domain translates into the same improvements in other domains, too.
If you like this post, you might also be interested in another post of mine: Software engineers are not (and should not be) technicians. There I explain how software development by its nature entails regularly leaving one's comfort zone.
Importantly, English is not a precise language, unless you contort it into something as detailed as code. ↩
Counterpoint: I don't actually know the back of my hand all that well ↩
That doesn't necessarily mean that I believe Haskellers or Rustaceans write better code than other people (I've seen them write some really shitty code sometimes ngl), but all other things equal it does help to be fluent with types ↩
I once saw someone predict that in a few years an LLM would be able to build an entire browser. All I could think was: "I can already do that today by forking Chromium" and it would be much easier to modify and maintain. ↩
Also, coding agents pick up on the mental habits of the people driving them. Your instructions to your agent are a major chunk of your agent's context and if you are consistently intellectually lazy you will drive your agent straight into a similarly intellectually lazy basin. Or to use a non-agent analogy: intellectually lazy managers engender intellectually lazy reports. ↩