We migrated a backend from Python to Go and Rust midway through building it with coding agents in the loop, and the most instructive artifact of the migration wasn't the code. It was the diff between the two CLAUDE.md files.

The Python version ran roughly 480 lines, and it spent most of that budget on things no principal engineer should have to write down. Keep handlers, service logic, and persistence in separate layers. Even a two-layer split, let alone three. No inline imports unless a legacy dependency genuinely forces one. Where modules live, what a module is allowed to know about, which patterns from the standard tutorials are banned here. The majority of those lines were basic enterprise hygiene, the kind of thing you'd expect any mid-level hire to arrive already knowing.

The replacement stands at about 335 lines, and it covers two languages. That's not a 30% reduction, it's a 65% reduction per language, and the composition changed more than the count did. Almost none of it is hygiene. It's project-specific. Here's our error-wrapping convention, here's how we name our interfaces, here's the one place we deviate from the standard layout and why. The agents showed up already writing the kind of code the Python file had to beg for.

The line counts also understate the gap, because a rule that has to live in an instruction file is a rule that gets broken, and broken rules bill twice. Our review layer, Copilot and /review passes, spent a real fraction of its tokens bouncing Python changes for violating the very file they were checked against, with inline imports the repeat offender. Every bounce is a full cycle: the tokens to generate the violation, the tokens to catch it, the tokens to regenerate it. That loop simply doesn't exist on the Go side, because the draft that would have bounced never survives the compiler.

The interesting part is what the file stopped needing to say, and what the review queue stopped needing to catch.

It's not corpus size, it's corpus entropy

The reflexive explanation is training data volume, and it's wrong in an instructive way. Python is one of the largest corpora any model has ever seen. If volume were the variable, agents would write their best code in Python and their worst in Go, and the actual experience is closer to the reverse, at least for the kind of code we care about, which is supportable, layered, enterprise SaaS code on both ends of the stack.

The variable that matters is variance. Condition a model on "Go HTTP service" and the distribution of plausible next structures is narrow. The corpus behind that context is overwhelmingly server-side software written by teams, formatted by one tool, and arranged the way the language's own documentation arranges it. Condition the same model on "Python backend" and the distribution spans Flask tutorials, Django monoliths, ML training scripts, Jupyter notebooks flattened into files, and twenty years of scripts that were never meant to outlive the afternoon. The model isn't dumber in Python. It's sampling from a wider prior, and every session it can land somewhere different in that spread.

That's why the Python instruction file felt like a losing battle. A rule in CLAUDE.md is a nudge applied against the full weight of the prior, and I've written before, in the context of deprecated codepaths, about how weak a nudge is against a strong prior. An agent arrives cold each session and re-derives its habits from the strongest signals available. In a codebase, that's the naming. In a language, it's the corpus. You can out-shout a corpus with enough instruction, but you'll pay for the shouting in tokens and review attention, per session, forever.

A word on that corpus, because it's easy to phrase this as a shot at the people who wrote it and that would be both unkind and inaccurate. Notebook code is optimized for iteration speed and disposability, which is exactly what it should be optimized for. The problem isn't that scientists write bad code. It's that code optimized for a different objective flooded the corpus, and the model can't tell which objective your project has unless you spend instruction budget telling it, every single time.

Two mechanisms, and Go sits at the intersection

There are actually two things going on, and separating them matters because they fail independently.

The first is the prior, above. The second is in-loop verification. Go doesn't discourage inline imports through good examples. It makes them grammatically impossible, imports exist only at the top of a file, and an unused one is a compile error, not a lint warning someone silenced in 2019. gofmt collapses formatting to a single canonical shape. An agent iterating against that toolchain gets fast, machine-checkable correction on exactly the axes where drift happens, and agents benefit from strict tooling even more than humans do, because they iterate fast, they iterate cheap, and they have no ego invested in the first draft.

Verification placement is also where the unit economics of the instruction tax live. A rule the compiler holds is deterministic and free at the margin, since a violating draft dies before anyone reads it. A rule only an instruction file holds is probabilistic, and every failure of the nudge gets caught, if it gets caught, by the most expensive verifier in the pipeline, an LLM review pass. The instruction file is the fixed cost. The review bounces are the variable cost, and the variable cost is the one that scales with every session for the life of the codebase. Moving a constraint out of the review layer and into the toolchain isn't a style preference, it's a cost reduction that compounds.

Rust, which carries part of our migrated backend, is the evidence that the second mechanism can carry the load almost alone. Its corpus is moderate and salted with learning-phase code, and the language has far more degrees of freedom than Go. What it has is the strongest in-loop verifier in mainstream use. The borrow checker converts a whole class of architectural sloppiness into compile errors, and the agent-written Rust in our tree comes out correspondingly tight. Rust argues that verification dominates. Go argues that the prior dominates. The honest reading is that Go is the one mainstream language where both mechanisms are strong at the same time over a corpus that's large enough to matter, and that intersection is the sweet spot.

There's a natural experiment that would separate the mechanisms cleanly, and I'll offer it as a prediction rather than a finding. Rails has one of the most convention-dense corpora in existence, convention over configuration is literally the slogan, but it's dynamically typed with weak compile-time feedback. If the prior does the heavy lifting, agent-written Rails should be architecturally clean. If verification does, it should be well-shaped but runtime-buggy. My money is on both being true at once, good structure, shakier correctness, which would say the prior handles architecture and the compiler handles correctness. If you run this experiment, I want to hear about it.

The frontend ran the same experiment and got the same answer

We saw the identical pattern on the other end of the stack, moving a consumer platform's frontend from JavaScript and React to TypeScript and Angular. Consistency and quality improved immediately, and for the same two reasons wearing different clothes.

TypeScript is the verifier. Angular is the prior, and it's worth being precise about why, because the tempting phrasing is that the model can somehow visualize Angular's layout, and the real mechanism is more mundane and more interesting. Angular's conventions mean the correct file structure is predictable from almost no context. user.service.ts, user.component.ts, the injection patterns, the module boundaries. The framework has effectively pre-tokenized its own architecture, and the corpus written in it agrees with itself. React, by contrast, is a library with a state-management ecosystem bolted on, and its corpus contains a dozen mutually incompatible schools of structure. Ask an agent for a React app and it averages across those schools. Ask it for an Angular app and there's mostly one school to sample from.

The concession, and it's real: the smaller, narrower corpus has a staleness cost. Expect the agent's Angular to occasionally lag the framework, reaching for NgModule-era patterns where standalone components belong, or missing newer primitives entirely, and budget review attention for it. The trade still clears, because Angular's conventions are stable enough that stale code is at least consistently shaped and mechanically upgradeable. A corpus that's slightly behind but agrees with itself beats a corpus that's current and at war with itself. Staleness is a linear cost. Heterogeneity is a per-session one.

The map

Put the two corpus properties on axes and most of the experience so far falls into place.

                 low entropy                 high entropy
              (one sanctioned shape)     (many schools of thought)

high volume   Go, SQL,                   Python, JavaScript
              TypeScript-in-Angular      (the instruction-tax zone)
              (the sweet spot)

low volume    Erlang/OTP, Elixir,        Common Lisp
              Gleam                      (the anti-Go)
              (the open question)

The top-left is where agents read as senior engineers with the house style already installed. The top-right is where they read as talented contractors who each trained at a different shop. The bottom row is where it gets falsifiable.

Bottom-left is the open question this piece deliberately doesn't answer, and I'll come back to it. Bottom-right has a perfect pathological occupant. Common Lisp's central cultural convention is building your own domain-specific language, which makes its corpus maximally heterogeneous by design. Every serious Lisp codebase is, on purpose, unlike every other one. The framework here predicts agents should be at their worst there, structurally, not syntactically, and that's a claim anyone with a Lisp project can go test against me.

The counterintuitive summary of the whole map is that moving to a smaller, stricter corpus improved our output on both ends of the stack. Closer to a DSL turned out to be an advantage. Forcing enterprise Python out of an agent is a constant fight against the prior. Go essentially is enterprise Python's target state, natively, with a compiler enforcing the parts the prior misses.

Consistency is not capability

One distinction, stated plainly so nobody has to state it for me. Models benchmark strongest in Python on algorithmic tasks, and if your problem is an isolated function with a tricky invariant, Python is likely still where the model is sharpest. The claim here is not that agents solve harder problems in Go. It's that they produce consistent, supportable architecture in Go with a smaller instruction budget, and for a system you intend to operate for years, consistency under a small budget is the property that compounds. Capability gets you a correct function. Consistency gets you a codebase the next session's agent, and the next year's human, can navigate without a map.

The boundary condition: the constraint has to be checkable

Before this hardens into "rigid languages good," one boundary condition, because the DSL framing overshoots without it.

Constraint pays exactly when the toolchain can enforce it in-loop. SQL is the framing's best friend, a rigid grammar over an enormous corpus, checkable against a live schema, and agents are genuinely good at it. Terraform is the cautionary tale. Syntactically it's as constrained as anything on the map, but the real constraint surface is provider schemas the model can't verify locally, so agents hallucinate resource attributes with total confidence and the plan fails at apply time, or worse, doesn't. A constraint the toolchain can't check is just vibes with extra steps. When you're evaluating a stack for agent-driven work, the question isn't "is this language strict," it's "does the strictness produce an error the agent can see before a human does."

Go was built for this, just not for them

Here's the part I find genuinely funny. None of this is an accident, it's just an accident that it works on machines.

Rob Pike said the quiet part out loud in his 2012 SPLASH talk. Go was designed for engineers early in their careers, and in his words "not capable of understanding a brilliant language", so the language deliberately traded expressiveness for uniformity, one way to do things, enforced by tooling, so that thousands of engineers of varying skill would produce interchangeable code. It was a design goal that read as mildly insulting to humans, and the humans said so at length for a decade.

Then the programmer became a probability distribution, and a language engineered to constrain variance across thousands of junior engineers turned out to be exactly a language engineered to constrain sampling variance across sessions of a stochastic one. Go was designed to make Googlers interchangeable. It accidentally made tokens interchangeable too.

The irony has a second layer. "There should be one obvious way to do it" is Python's own stated Zen. Go is the language that shipped an enforcement mechanism for it.

The open question

The bottom-left quadrant is the one this piece won't resolve, on purpose. Somewhere below Go on the volume axis there's a floor, a corpus size beneath which convention density stops compensating and the agent simply doesn't have enough examples to be competent, however consistent those examples are. Erlang is the most esoteric language I have real hours in, and I haven't run it through an agent pipeline seriously enough to place it. I suspect the ritualized parts of OTP, supervision trees, gen_server callbacks, would survive, because behaviours are convention formalized into an interface, and the unritualized connective tissue would fray. That's a suspicion, not a result.

The question is answerable, though, and here's the ruler. Take languages down the volume axis at similar entropy, Go, Elixir, Erlang, Gleam at the bottom, hand each the same spec with the same fixed instruction budget, and measure where architectural consistency degrades gracefully versus falls off a cliff. I haven't run it. If you do, publish it.

One hedge to plant with the question: the floor is moving. Cross-language transfer means models increasingly generalize structure over syntax, and synthetic data means a vendor can backfill a thin corpus on purpose. Any specific floor named today is stale in two model generations, which is a good reason to publish the ruler rather than the reading.

The durable takeaway is upstream of the floor anyway. Choosing a language for agent-driven development is choosing a corpus, and the corpus is a hiring decision. Python's corpus is a brilliant, enormous team that never agreed on anything. Go's is a smaller team that was forced to agree by its tools. For code you intend to operate, hire the second team, and spend the instruction budget you save on the parts of your system that are actually unique.

Sources

  1. Rob Pike, “Go at Google: Language Design in the Service of Software Engineering” (SPLASH 2012) — the design-goals talk, including the target-audience remarks discussed above.
  2. Tim Peters, PEP 20 — The Zen of Python — “one obvious way to do it,” as aspiration.
  3. Rob Pike, Go Proverbs — “gofmt's style is no one's favorite, yet gofmt is everyone's favorite,” which is this whole article in nine words.
  4. Angular coding style guide — the file-naming and structure conventions that make Angular's architecture predictable from minimal context.

Want to talk shop on language choice, agent pipelines, or making a stack agent-legible? Get in touch.