I recently worked on a project where I wrote the unit tests first, then had Claude Code generate the implementation that passed those tests. The experience crystallized something I’ve been thinking about: natural language is fundamentally too ambiguous to be an effective specification language for software, no matter how smart our LLMs become.
This isn’t a hot take about current AI capabilities. This is a statement about linguistics.
The Fundamental Problem
Human language is inherently ambiguous. This is not a limitation of GPT-4, GPT-5, or whatever GPT-24 eventually arrives. This is a fundamental property of natural language itself. No amount of model sophistication can fix the ambiguity baked into how humans communicate.
Consider: “I need a recipe class where the id is case insensitive.”
You prompt an LLM with this. It goes off and codes. How do you know it actually did what you wanted? How do you know where it put the class? Did it use id or ID or recipe_id? Did it make equality case-insensitive, or just comparison? What about hashing?
You don’t know. You have to review it. And reviewing code is expensive cognitive labor, because you’re now verifying that the LLM correctly interpreted your inherently ambiguous natural language specification.
Why Formalism Won
There’s a reason science made its greatest leaps forward over the past few centuries. Chemistry embraced $H_2O$. Physics embraced $E = \frac{mc^2}{\sqrt{1-v^2/c^2}}$. Mathematics embraced symbols and formal notation.
Why? Because natural language wasn’t precise enough to move these fields forward.
When you write $H_2O$, there is no ambiguity. Two hydrogen atoms, one oxygen atom, specific bonding structure. When you write that relativistic energy equation, there is no ambiguity about the relationship between energy, mass, velocity, and the speed of light-or that energy diverges to infinity as $v \to c$, which is why you can’t actually reach the speed of light.
Actually, speaking of limits: consider how much hand-waving is required to explain the conceptual understanding of a limit in natural language. “As x gets closer and closer to a, the function approaches L.” What does “closer and closer” mean? How close is close enough?
Contrast that with the formal epsilon-delta definition: for every $\epsilon > 0$, there exists a $\delta > 0$ such that if $0 < |x-a| < \delta$, then $|f(x)-L| < \epsilon$.
It’s the same concept. One is ambiguous, hand-wavy, and imprecise. The other is formal, unambiguous, and executable. Natural language would require paragraphs to express what these symbols capture precisely. And those paragraphs would still leave room for misinterpretation.
Programming languages exist for the same reason. They are formal systems that eliminate ambiguity. This is not a bug-it’s the entire point.
Tests As Formal Specification
Here’s what TDD advocates have been saying for years, but which becomes crucial in the age of AI: unit tests are not primarily about testing code. They are formal specifications for how that code must behave.
Consider this test:
from Recipe import Recipe
def test_recipe_equality_case_insensitive():
assert Recipe(id="MyRecipe") == Recipe(id="myrecipe")
Look at what I’ve specified in two lines:
- There must exist a class called
Recipe - It must be in a module named
Recipeavailable for import - It takes an
idparameter in its constructor - Equality comparison must be case-insensitive (because “MyRecipe” == “myrecipe”)
Four precise, unambiguous specifications encoded in two lines of code. Try writing the equivalent in natural language without introducing ambiguity. You can’t. Or rather, you can try, but you’ll write three paragraphs and still leave gaps.
This is the power of formal specification: you leverage the precision of a programming language to eliminate the ambiguity of natural language. You’re not escaping formalism-you’re embracing it, but only for the specification, not the implementation.
The Inversion
Before AI, TDD was a hard sell because it required double labor. You had to write the test and write the implementation. The implementation was usually the hard part, requiring careful design decisions, performance optimization, error handling, and so on.
AI inverts this equation.
There’s perhaps a deeper reason we’ve been avoiding tests: they force us to be precise, and that precision is uncomfortable. Writing tests means confronting ambiguity in our own thinking. Natural language lets us handwave past details; formal specifications don’t. This discomfort-this cognitive load of thinking clearly about what we actually want-is exactly what made tests feel burdensome.
In the age of AI, that discomfort is now the most valuable part of the development process. The implementation (the formerly hard part) is now handled by AI. The cognitive load has shifted entirely to specification. What used to be the “extra” work is now the only work that matters.
Implementation is now cheap. It’s scalable. It can be outsourced to an LLM that can generate multiple implementations in seconds. Want to try three different approaches? Fine, generate three implementations and benchmark them.
What’s no longer cheap is knowing what you actually want. And the only way to specify that unambiguously is through formal specification-which in practice means tests.
Some developers are using AI to write tests from implementation after the fact. Some are using AI to write both. I’m saying these approaches are backwards. You should write the tests-perhaps with AI assistance-and not care about the implementation. Focus on what matters: that the software does what you want it to do.
Why This Matters
When you write a test, the space for ambiguity radically shrinks. The test is the specification, written in a language both humans and machines can execute and verify.
When you write natural language prompts, even incredibly detailed ones, you’re asking the LLM to interpret ambiguous human language and translate it into precise code. You then have to review that code to ensure the translation was correct. This is backwards.
Write the test. Let the LLM generate code that passes the test. Now you know-not believe, not hope, but know-that the implementation satisfies your specification. Because your specification was formal, not ambiguous.
The Actionable Takeaway
Developers already know TDD. It’s not a new practice. What’s new is that AI has removed the main objection: the “double labor” of writing both tests and implementation.
Focus on your tests. Treat them as the specification. Make them precise, comprehensive, and unambiguous. Then let AI handle the implementation. Verify it passes your tests. Done.
This isn’t about trusting AI or not trusting AI. It’s about the fundamental reality that natural language is ambiguous and formal languages are not. We’ve known this for 400 years across every scientific discipline. It’s time to apply that lesson to AI-assisted software development.
The future of software development in the age of AI isn’t about writing better prompts. It’s about writing better tests.