Hard Coding
Embedding values directly in source code that should live somewhere a reader, an operator, or a future agent can change them.
Also known as: Magic Numbers, Magic Strings, Inline Constants
Understand This First
- Configuration — the pattern that gives environment- and deployment-varying values a proper home.
- Naming — what changes when a bare literal becomes a named symbol other readers can find.
- Source of Truth — why a value that means one thing should have exactly one place it is defined.
Symptoms
- A function returns 25, 1024, or 3600 and nobody can say what the number means without reading the surrounding code.
- The same literal — a timeout, a page size, a rate limit, a base URL — appears in several files and tests, with no shared definition.
- Environment-specific values (database URLs, API endpoints, bucket names, account IDs) live in source instead of in configuration.
- A behavior change requires editing code rather than flipping a setting; the diff for “raise the upload limit to 50 MB” touches half a dozen files.
- An agent answers a small request by inlining a fresh literal next to one that already exists, slightly different, two lines away.
- A string like
"prod","admin", or"v2"decides control flow and is repeated wherever the decision is made.
Why It Happens
Hard coding is easy because it works on the first try. The number that makes the test pass is right there. Typing 25 is faster than naming MAX_UPLOAD_MB, finding a home for it, and importing it. A literal feels concrete; a constant feels like overhead, and when you’re moving fast, overhead is what you cut first.
It also tends to ride along with other shortcuts. A developer copies a working block from another module and the literal travels with it. A reviewer recognizes the block, approves the change, and the duplicate lands. Two weeks later the rule changes, and only one of the copies gets updated.
Agents are unusually good at producing hard-coded values. A model trained on millions of code snippets has seen reasonable defaults for nearly everything: page sizes, timeouts, retry counts, content-type strings. It will happily emit them whenever a function needs a number. The literals look plausible because they are plausible. They’re also unconfigured, undocumented, and unsourced. The agent doesn’t know whether your system already has a canonical place for that value, so it makes a new one.
The deepest cause is missing ownership. If the codebase does not have a clear answer to “where does the upload limit live?” or “where do feature flags get read?”, every contributor invents a local answer. The literal in the function body is just the visible end of a missing decision about where shared values belong.
The Harm
Hard coded values make a system unsafe to change. The literal 25 looks innocent until you discover that three services rely on it as the megabyte cap, one CLI tool encodes it as a count, and one migration script uses it for an entirely unrelated table size. Lifting the cap to 50 looks like a one-line edit and turns into a multi-day investigation.
Hard coded environment values make a system unsafe to deploy. A staging build that connects to the production database because the URL was inlined two years ago is a real incident, not a hypothetical one. The same shape (secrets, keys, account identifiers in source) is one of the most common ways credentials end up in version control history.
In agentic coding, the harm scales with the agent’s reach. An agent fixing a bug may add a new literal beside the broken one rather than touching the surrounding structure. An agent writing a new feature may invent its own conventions: a 30-second timeout in one file, 60 in another, 45 in a third. The system accumulates a quiet sediment of numbers and strings that no one chose deliberately and no one can change confidently.
Hard coding also hides intent. Future readers, human or agent, see a number with no name, no source, and no link to the requirement that motivated it. Even the original author may not remember in six months whether 0.85 was a fudge factor, a regulatory threshold, or a guess.
The Way Out
Decide where each value belongs before writing it down. The choice is small but the discipline matters.
Use three checks:
Ask whether the value names knowledge. If the literal stands for a concept the system has opinions about — a limit, a threshold, a window, a magic phrase — it deserves a name. MAX_UPLOAD_MB, RETRY_BUDGET, LEGACY_TENANT_PREFIX make the next reader’s job easier even when the value never changes.
Ask whether the value varies. If the value might differ between dev, staging, and prod, or between customers, or between tenants, it belongs in Configuration, not in code. Connection strings, API endpoints, credentials, feature flags, rate limits, and quotas almost always vary; treat them as configuration by default and prove a special case before inlining.
Ask whether the value has one home. If the system already has a canonical location for similar values — a config module, a settings table, an environment-variable schema — put the new value there too. If it does not, create one and make the new value the first inhabitant. The point is not that every literal must be extracted; the point is that the system should have an obvious place for shared values, and contributors should use it.
A literal that survives all three checks is fine in place. A one-off constant local to a function, a clear loop index, a bit pattern that names itself: these don’t need extracting. Common, neutral values like 0, 1, -1, and small enumerations rarely earn a constant. Reach for naming and configuration when the value carries meaning, varies by context, or appears more than once.
When you’re working with an agent, state the convention explicitly. “Put all environment-dependent values in config/settings.py. Reference them by name. Don’t inline new literals for limits, timeouts, or external URLs.” Without that direction the agent will follow the locally visible convention, and whichever convention it sees first becomes the one it propagates.
Before accepting an agent’s patch, search the diff for new numeric and string literals. For each one, ask whether it names knowledge, whether it varies, and whether the codebase already has a home for it. Most regrettable literals are caught in the seconds after the diff appears, not in the months after it ships.
How It Plays Out
A team ships a file-upload feature with a 25 MB cap. The number lives as 25 in the validation function, 25 * 1024 * 1024 in the storage service, "max 25 MB" in the user-facing error string, and 25000000 in a metrics label. Six months later, a sales request raises the cap to 100 MB. The validation function gets bumped. Storage rejects the file because nobody touched the second copy. The error string still says 25. Metrics roll up under the old label. The fix becomes a hunt across services for a value that should have lived in a single configuration entry and been read by every consumer.
A founder asks an agent to wire up a Stripe integration. The agent inlines the test API key directly in the payments module so the smoke test will pass. The change ships through a fast review and lands in version control. A week later the key rotates, the integration breaks in three environments at once, and a credential-scanner alert lands in inbox because the key was readable in the public repo’s history. The fix isn’t just “rotate again.” It’s moving every credential to a secrets store and rewriting the section of the codebase that assumed they were source-level constants.
A developer asks an agent to add retry logic to an outbound webhook. The agent writes a backoff loop with MAX_RETRIES = 5 and a 30-second base. Two weeks later the team asks an agent to add retries to a payment-processor callback. The agent writes a fresh backoff loop with RETRY_COUNT = 3 and a 10-second base. Neither agent saw the other’s code. Production now has two notions of “how patient we are with downstream failures,” disagreeing by a factor of two, and any future engineer who wants to make the system uniform has to read all the call sites to discover the inconsistency. A retry policy belongs in one place: a configured policy with named defaults that every caller imports.
A migration job inlines tenant_id = 47 because that’s the customer being repaired. The job ships, runs, and works. Six months later, an agent is asked to rerun the same migration against a new tenant. It opens the script, sees the literal, and “fixes” it by editing 47 to the new tenant’s ID. The change passes review because the diff is small. Two days later, the original tenant’s records are corrupted because the script’s reverse path still assumed 47 in a string-formatted log query that the reviewer didn’t look at. A tenant identifier is configuration, not source.
Related Patterns
Sources
- Steve McConnell’s Code Complete (Microsoft Press, 2nd ed. 2004) is the canonical treatment of magic numbers and named constants in production code; it gives the operational rules (“use named constants for any literal that means something”) that this article codifies.
- Martin Fowler and Kent Beck’s Refactoring (Addison-Wesley, 2nd ed. 2018) names “Magic Number” as a smell and “Replace Magic Number with Symbolic Constant” as the behavior-preserving step that removes it; the chapter on Mysterious Name generalizes the same idea to strings and identifiers.
- William J. Brown, Raphael C. Malveau, Thomas J. Mowbray, and Hays W. “Skip” McCormick III’s AntiPatterns (Wiley, 1998) is the source for the antipattern form and frames hard-coding as an instance of avoiding the design decision about where shared values live.
- The “Twelve-Factor App” methodology (12factor.net, 2011) crystallized the rule that environment-specific configuration belongs outside the code, in environment variables or equivalent stores, never inlined into the build artifact.
- The OWASP “Hardcoded Credentials” weakness (CWE-798) records the security-specific failure mode in which credentials and keys end up in source — the single most common form of hard-coding that has shipped to production at scale.