Daniel's got a whole thing about the gap between knowing what a type system is and actually getting a real codebase to conform to one. The theory is well-trodden — static versus dynamic, annotations, TypeScript, mypy. What he wants walked through is the operational ladder. How you go from scattered annotations, to a type checker running as a blocking gate in CI, to strict configs and per-module ratchets applied gradually to a legacy codebase that was never typed, to runtime validation at the trust boundaries with something like Pydantic or Zod for the places where static types stop being able to help at all. And running underneath all of it — what conformity is actually worth. Whether a codebase that passes the checker is measurably less broken, or whether what you mostly bought was better autocomplete and an expensive ritual.
The expensive ritual question is the one that makes people flinch, because the honest answer is — it's some of both. But let's start with the ladder, because if you build it wrong you definitely get the ritual without the payoff.
Walk me through the wrong way first.
The naive approach is — annotate everything, flip on strict mode, fix all the errors, ship. Eightfold's engineering team wrote about this earlier this year. Four million lines of Python. They explicitly rejected that approach. Instagram tried it at similar scale and it took them about eight months just to hit fifty percent coverage. You burn months and you haven't even turned the checker on yet.
So the codebase is in limbo. Half-typed, no enforcement, annotations that might be wrong because nobody's checking them.
And new untyped code keeps arriving. That's the killer. Without a gate, new debt accrues as fast as old debt is paid down. I saw Protean's architecture decision record from July — they measured their codebase. About seven hundred mypy errors under a lenient config. Twenty-four hundred under strict. If you try to fix that in one pull request, it's unreviewable. If you fix it with no CI gate, you're bailing out a boat with a hole in it.
So the first rung is — stop the bleeding.
Stop the bleeding. And the dominant pattern for that is the two-tier config. You have a strict mode and a lenient mode. Strict mode — new projects, fully-typed modules. The config sets disallow untyped defs to true. CI blocks any untyped function. To onboard a module, you add its path to the strict config, run mypy locally to confirm zero errors, and merge. From that moment, no untyped code can enter that module.
And lenient mode is the holding pen.
For legacy modules being gradually typed. Disallow untyped defs is false, so existing untyped code is tolerated. But — and this is the ratchet — a separate CI check ensures every new function added in a pull request is fully annotated. The untyped surface can only shrink.
The ratchet.
It's borrowed from physical engineering. Dusty Burwell wrote about this a few years ago. A ratchet strap only moves in one direction — it spools slack onto an axle, never loosens. Applied to software, you can always make things stricter, never looser. It shows up in linter configs, test coverage targets, API versioning. For types, the cleanest expression is Sorbet's per-file sigil system.
Stripe's Ruby type checker.
Every file starts with a typed sigil. Ignore, false, true, strict, strong. Each level reports all errors from lower levels plus new ones. At strict, all methods must have signatures, all constants and instance variables must have explicit types. At strong, no T dot untyped values are allowed. The migration is file by file, team by team. You move a file up one level, it stays there.
And the escape hatch problem. Because every ratchet has an escape hatch, and the whole question is whether you can keep people from using it.
This is where it gets interesting, because the escape hatches are the same features that make gradual typing possible in the first place. TypeScript has any and as any. Python has hash type colon ignore. Sorbet has T dot untyped. The Sorbet docs are refreshingly honest about this — they say, look, at first blush it seems like you don't want this property in your type system, it lets you make wildly inaccurate claims. But it's precisely this property that enables incremental adoption. You can't do this file by file if every file has to be perfect on day one.
So the escape hatch is the feature. But then you need a second ratchet to close the escape hatch.
And that second ratchet is where the semi-programmatic decisions come in. Daniel mentioned this in the prompt — some enforcement is fully programmatic, some of it is a human deciding where the ratchet stops. The Type Ratchet GitHub action, which shipped in June, is the pure programmatic version. It counts any, as any, hash type colon ignore, and at ts-ignore occurrences in a pull request. If the count goes up, the PR fails. Zero dependency, just a counter.
The creator's rationale was specifically about AI coding agents, wasn't it?
Yeah. The argument is — AI coding agents are very good at making CI green, and the fastest route to green is as any, not a fix. A reviewer can miss one escape hatch in a four-hundred-line diff. A counter can't.
Which is a darkly funny inversion. The thing that's supposed to accelerate development creates a new category of silent type debt, so you build a robot to watch the robot.
And the robot watching the robot is four lines of bash. But not everything can be automated. There are at least three decision points where a human has to show up. First — which modules to strictify first. Eightfold deliberately started from the leaves of the dependency tree. Utility modules, data models. Annotating high-fan-out modules first maximizes the ripple effect, because every downstream consumer immediately gets better type checking.
Start where the blast radius is largest.
Second decision — what to do with Any that's legitimate. Eightfold kept disallow any expr and warn return any off even in strict mode, because in practice Any is unavoidable in a large codebase with third-party libraries and untyped dependencies. A pandas Series of Any is just what it is. The Type Ratchet action lets you set a baseline any parameter for those legitimate uses.
And the third one is the one I think is actually the hardest. Whether a given type ignore is debt or permanent.
The convention that's emerging in several codebases is that every type ignore must include a reason comment. Type ignore attr-defined, third-party lib missing stubs. With warn unused ignores set to true, mypy tells you when the annotation finally makes the ignore unnecessary. The ignore rots on the vine and you prune it.
There's also the annotation review loop. The thing where you run a production type tracer and it gives you back types that are technically correct and completely wrong.
MonkeyType. Eightfold used it. You run it in production, it records the actual types that flow through your functions, and it generates annotation suggestions. The problem is — runtime tracing produces concrete types, not abstract ones. A utility function that works on any list type will produce a union of every concrete type seen in production. List of int, list of str, list of dict, all unioned together. A human has to come in and replace that with a proper TypeVar generic.
So the machine gives you raw material and you still need someone who understands the intent of the code.
The machine tells you what happened. You have to decide what should happen. That's not automatable.
Let's talk about the quarantine model versus the two-tier model. Because they get to the same place but they feel different.
Protean chose quarantine. All modules start failing, then get individually silenced and re-enabled. Eightfold chose two-tier — modules opt into lenient or strict. The quarantine model is more aggressive. CI blocks immediately, even for untyped modules. The two-tier model is more permissive — untracked modules produce zero noise. The quarantine model says we're going to feel the pain now and ratchet it down. The two-tier model says we're going to expand the zone of safety outward.
Which one you pick says something about your organization.
Quarantine requires a team that can tolerate a noisy CI for a while and not learn to ignore it. Two-tier requires discipline to actually move modules into the tiers. The failure mode of quarantine is alert fatigue. The failure pattern of two-tier is — the strict tier stays at three modules forever.
Alright. So we've climbed the ladder. We've got per-module ratchets, we've got CI gates, we've got human decision points for the places the ratchet can't reach. What's at the top? What does a codebase that passes the checker actually buy you?
The landmark study on this is Gao, Bird, and Barr from twenty seventeen. They looked at public JavaScript bugs and asked — how many would Flow or TypeScript have caught? The answer is fifteen percent. With a ninety-five percent confidence interval of eleven and a half to eighteen and a half.
Fifteen percent.
Fifteen percent of public bugs. And that's conservative, because it only measures bugs that survived testing and review — it understates effectiveness during private development. But still. Fifteen percent.
So more than eight out of ten bugs walk right past the type checker.
More than eight out of ten. And the study breaks down what the other eighty-five percent are. Fifty-five percent were specification errors — failures to implement the spec. Wrong URLs, malformed queries, logic that doesn't match what the product was supposed to do. A type system can't express "this URL is correct." String errors were the second category. These are fundamentally beyond what any type system can catch.
Which is the point Daniel's making about Pydantic and Zod. The static checker stops at the boundary.
And the boundary is where the real danger is. Every byte that crosses your service boundary is hostile until proven otherwise. HTTP request bodies, external API responses, environment variables, message-queue payloads — these are all unvalidated data with no type guarantee. Static types help you write the code in between, but they don't guarantee the data is correct.
The phrase I've seen is — parse, don't pray.
Indrajeet Patil's formulation. And the reason to define the shape in Pydantic or Zod rather than in raw JSON Schema is not ergonomics. It's that the schema you send, the validator you run, and the type your editor knows about become the same object. They can't drift apart. Written by hand, those are three files that agree today. Derived from one definition, disagreement is impossible.
So you validate at the edge, and then you trust the validated data internally. The static types are a convenience layer on top of the runtime check, not the other way around.
Steven's Knowledge describes the architecture explicitly. There is exactly one place to validate untrusted input — the moment it enters your system, before any business logic touches it. Not in the database layer, not scattered through services. Pydantic's extra forbid and Zod's strict object prevent mass-assignment attacks. Size and range limits prevent denial of service through memory exhaustion. The static types are for the code you control. The runtime validators are for the data you don't.
So that's the ladder. But I want to go back to the fifteen percent, because I think that number is doing more than one thing.
It is. Because there's a counterintuitive finding that's come out of subsequent work. Bogner and Merkel looked at six hundred and four GitHub projects in twenty twenty-two. TypeScript applications have a higher bug-fix commit ratio than JavaScript applications. Zero point two zero six versus zero point one two six. And longer bug-fix time — thirty-three days versus thirty-two.
Wait. Typed codebases have more bug fixes?
More bug-fix commits. And a twenty twenty-five replication by Kuznetsova and Cherkasov confirmed it. TypeScript showed thirty-four percent fewer code smells, twenty-eight percent lower cognitive complexity — and a thirty-two percent higher bug-fix commit ratio and ten percent longer bug-fix time.
So the code is cleaner, easier to reason about, and you spend more time fixing bugs.
The plausible explanation is — TypeScript catches many defects at compile time that would otherwise go unnoticed. The type annotations make bugs easier to diagnose when they do occur. So you're seeing more bug-fix activity not because there are more bugs, but because you're actually finding and fixing bugs that were always there.
The metric is measuring detection, not defect introduction.
And that means teams adopting types should expect to see more type-related CI failures initially, not fewer. If your bug-fix commit ratio goes up after adopting types, that's the system working. You're catching things you were shipping before.
Which is a hard sell to a manager who sees the CI going red more often.
The Microsoft engineering manager quoted in the Gao study had the right framing. If you could make a change to the way we do development that would reduce the number of bugs being checked in by ten percent or more overnight, that's a no-brainer. Ten percent. Not fifty. Not ninety. Ten percent, and it's worth it.
And yet the annotation tax is real. The time cost of writing and maintaining types has not been rigorously measured at scale in the same experiment. We know what we catch. We don't know what we paid.
We don't. And the Bogner and Merkel finding that TypeScript projects have longer fix times is consistent with the idea that type checking creates more work. Whether the net effect is positive depends on how you value the bugs caught versus the time spent. There's no dollar figure for this.
So what does the person maintaining an untyped codebase, being told to adopt this, actually do on Monday morning?
Monday morning, you install the type checker in CI with a per-module quarantine list. Every module that currently has errors gets ignore errors equals true. The quarantine list is append-only shrinking. You pick one module — the leafiest, highest-fan-out module you can find — and you annotate it. You move it to the strict tier. You do this once a sprint. In six months, half your codebase is strict.
And you don't let the AI write as any to make the light green.
You run the counter. The counter is four lines. It costs you nothing and it catches the thing the reviewer will miss.
There's one more thing I want to put on the table, because it complicates the "is it worth it" question in a way that's specific to this moment. AI coding agents can generate type annotations faster than humans. That should accelerate the ladder. But the Type Ratchet creator's point is — they also reach for as any and type ignore as the fastest path to green. So you get more annotations, faster, and some fraction of them are fake.
The speed makes the cheating harder to spot.
Tomoda Hinata wrote about this earlier this year. The argument is — what makes an AI coding agent's speed safe is not human review but mechanical quality gates. The linter, the type checker, the test suite. The machine reviews the machine. And the type ratchet tools were built specifically for this world. Type Ratchet, Test Ratchet, Suppress Ratchet — they're all counters. They don't understand your code. They just count the escape hatches and fail the PR if the number goes up.
The ladder is the same ladder. The AI just means you climb it faster, and you need better guardrails.
The guardrails are simpler than the AI. That's the part I find satisfying. The counter is a bash script. The type checker is a deterministic program. The AI generates a thousand lines and four small programs check its work.
Alright. Let's land the question Daniel actually asked. What does each rung buy you?
Rung one — scattered annotations with no CI gate. Buys you better autocomplete. That's it. But that's not nothing. Multiple practitioners report the IDE experience improvement is the benefit most immediately felt by developers, even before any production bugs are caught.
Rung two — CI gate with lenient mode. The ratchet is on.
New code is typed. Old code is tolerated but can't get worse. You've stopped the bleeding. The bug detection starts here — not at fifteen percent yet, because coverage is low, but you're catching type errors in new code at review time instead of in production.
Rung three — per-module strict mode. Modules are flipping to strict one by one.
The high-fan-out modules are fully checked. Downstream consumers get better type inference. The fifteen percent number starts to become real for the modules under strict. And you get the null-checking benefits — TypeScript's strict null checks alone increased detectable bugs by fifty-eight percent. That's the single highest-return flag you can flip.
Rung four — runtime validation at the boundaries.
Pydantic, Zod. This is where you catch the eighty-five percent that static types can't touch. Malformed data, wrong URLs, missing fields, mass-assignment attacks. The static types handle the code you wrote. The runtime validators handle the data you didn't.
The top rung — the question of whether the whole thing was worth it.
You've bought fifteen percent fewer public bugs, better autocomplete, faster navigation, and a codebase where the machine can tell you when you've made a mistake before the user does. You've also bought an annotation maintenance burden that nobody has quantified in dollars. The honest answer is — it's worth it if you flip strict null checks and validate at the boundaries. If you stop at scattered annotations and never turn on the CI gate, you bought the ritual without the payoff.
The autocomplete is real but it's not why you did all this work.
The autocomplete is the gateway drug. It gets people to write annotations. The checker is what makes those annotations mean something.
Hilbert: Fifteen percent.
What?
Hilbert: The number you kept coming back to. Fifteen percent of public bugs. I spent three years testing telephone switching software, late eighties, and our target was twelve percent. Twelve percent of faults caught at the integration test stage was considered a good yield. We had a whole quality metric built around it. Fifteen would have been exceptional.
What kind of switching software?
Hilbert: Stored program control exchanges. The kind where a software fault doesn't crash a server, it takes forty thousand phone lines dark. We had a test harness that ran for eleven hours. It caught about one fault in eight. The rest we found in the field.
The fifteen percent number doesn't sound low to you.
Hilbert: It sounds familiar. The type checker is your eleven-hour test harness. It catches the category of fault it's designed to catch. The rest are specification errors, timing errors, resource exhaustion — things no static analysis was ever going to find. We didn't conclude the test harness was an expensive ritual. We concluded twelve percent was the shape of the problem.
The ones you found in the field — how bad were they?
Hilbert: We had a fault once where a memory leak in the call-routing module would degrade a switch after about six weeks of uptime. The fix was a single line. Finding it took four months and cost more than the development budget for the entire module. That's the kind of thing a type checker doesn't catch either, by the way. The code was perfectly well-typed.
Resource leaks are a different category entirely.
Hilbert: My point is — fifteen percent is not a small number. People hear fifteen percent and think failure. I hear fifteen percent and think — that's the difference between a switch that stays up for six weeks and a switch that stays up indefinitely, if the fifteen percent includes the memory leak. It doesn't, but you take the point.
The categories don't overlap perfectly between type checking and integration testing.
Hilbert: No, and that's what I'm saying. You stack them. The type checker catches its fifteen percent. The test harness catches its twelve. The runtime validators catch another chunk. None of them is the whole answer. The question isn't whether fifteen percent is enough. The question is whether you can afford to leave fifteen percent on the table.
The Microsoft manager's argument exactly. Ten percent is a no-brainer.
Hilbert: Ten percent of faults in a telephone exchange is the difference between a quiet Tuesday and a front-page story. Different stakes, same arithmetic. I'll tell you what's an expensive ritual — the four months we spent chasing that memory leak. The type annotations take a sprint. The leak took a quarter.
You can't know in advance which fifteen percent you're catching.
Hilbert: You never know. That's the whole problem with quality metrics. You only know what you found, never what you prevented. But I kept a list, for a while, of faults we caught in the harness. Every one of them would have been a customer-affecting outage. After about forty entries I stopped keeping the list. It was making me nervous.
The list was making you nervous?
Hilbert: The list was making me aware of how many ways the thing could break. I preferred not to think about it. The harness ran, the light went green, I went home.
That's the CI gate in a nutshell.
Hilbert: That's the CI gate. You don't want to know. You want the light.
We should probably wrap. The thing I keep thinking about is — the ladder Daniel described is real, it's well-understood, the patterns are documented. Eightfold, Protean, Stripe — they've all climbed it and written down what they learned. The open question isn't how to climb it. It's whether AI-generated code changes which rungs matter most.
The counter becomes more important than the checker.
Or the counter becomes the checker. The thing that enforces the ratchet isn't the type system itself, it's the meta-tool that watches the escape hatches. Which is a weird place for the industry to land — the most important tool in your type safety strategy is a script that counts hash type ignore.
The ladder ends in a bash script. That feels right, somehow.
Thanks to Hilbert Flumingtop for producing. This has been My Weird Prompts. If you want to tell us about your own type adoption war stories, email us at show at my weird prompts dot com. We'll be back soon.