A literal value can be perfectly clear when it describes the mechanics of a calculation. index + 1 usually needs no explanation. But the same syntax becomes harder to understand when a value carries a business rule or engineering decision: if retries > 3, price * 0.85, or timeout = 30.

The problem is not that numbers or strings appear in code. The problem is that some literals have meaning that the code does not name. These are often called magic values.

Replacing a magic value is a small refactoring, but doing it well requires more than moving the literal into a constant. This article develops a practical mental model for deciding which values deserve names, where those names should live, and when a richer abstraction is more useful than a constant.

Start by asking what the value means

Consider a simplified shipping rule:

if order.total >= 50:
    shipping_fee = 0
else:
    shipping_fee = 5

The arithmetic is easy. The policy is not.

A reader must infer that 50 is the minimum order total for free shipping and that 5 is the standard shipping fee. If the rule changes later, a developer must rediscover those meanings before changing the code safely.

Naming the concepts makes that knowledge explicit:

FREE_SHIPPING_MINIMUM = 50
STANDARD_SHIPPING_FEE = 5

if order.total >= FREE_SHIPPING_MINIMUM:
    shipping_fee = 0
else:
    shipping_fee = STANDARD_SHIPPING_FEE

The behavior has not changed. What changed is the amount of interpretation required from the reader.

A useful test is:

If I changed this literal, would I be changing a named rule, policy, protocol detail, or domain concept?

If the answer is yes, the value probably deserves an explicit name or abstraction.

Do not remove literals mechanically

Not every literal is magic.

for index from 0 to items.length - 1:
    process(items[index])

The 0 and 1 express ordinary indexing mechanics. Replacing them with names such as FIRST_INDEX and ONE_POSITION would add indirection without adding useful meaning.

Compare that with:

if failed_attempts >= 5:
    lock_account()

Here 5 is not merely arithmetic. It represents a policy: the number of failed attempts allowed before locking an account. A name such as MAX_FAILED_ATTEMPTS tells the reader what decision the number represents.

The distinction is semantic, not syntactic. The question is not “Is this a literal?” but “Does understanding this literal require knowledge that is missing from the expression?”

Name the rule, not the representation

A weak name often preserves the mystery:

LIMIT = 5
TIME_VALUE = 30
DISCOUNT_NUMBER = 0.15

These names say what kind of value exists, but not why it exists.

Prefer names that expose the decision:

MAX_FAILED_ATTEMPTS = 5
PAYMENT_TIMEOUT_SECONDS = 30
LOYALTY_DISCOUNT_RATE = 0.15

The unit matters when the representation is otherwise ambiguous. PAYMENT_TIMEOUT = 30 leaves a reader asking whether the value means seconds, milliseconds, or minutes. PAYMENT_TIMEOUT_SECONDS makes the assumption visible at the use site.

Names should also describe the current meaning rather than an accidental history. NEW_TIMEOUT becomes misleading as soon as it is no longer new. PAYMENT_TIMEOUT_SECONDS can remain accurate for years.

Put the name near the knowledge it represents

Once a value has a name, the next question is where that name belongs.

Suppose several functions use the free-shipping threshold. A global constants file may seem convenient:

constants:
    FREE_SHIPPING_MINIMUM = 50

But convenience is not the same as ownership. If the threshold belongs specifically to shipping policy, placing it with that policy keeps related knowledge together:

ShippingPolicy:
    FREE_SHIPPING_MINIMUM = 50

    fee_for(order):
        if order.total >= FREE_SHIPPING_MINIMUM:
            return 0
        return STANDARD_SHIPPING_FEE

Now a developer looking for shipping rules can find the threshold where those rules are implemented.

A shared constants module is appropriate when the value genuinely has shared ownership. It becomes a problem when unrelated decisions are collected there simply because they are constants. That structure hides which part of the system is responsible for each value.

A named constant is sometimes only the first step

A constant improves a value whose meaning is stable and whose behavior is simple. Some concepts need more structure.

Imagine code that repeatedly compares order totals with a free-shipping threshold:

if order.total >= FREE_SHIPPING_MINIMUM:
    ...

If the policy later depends on destination, customer tier, or delivery method, the threshold is no longer the whole concept. Continuing to add constants can scatter the rule across callers:

if customer.is_member:
    minimum = MEMBER_FREE_SHIPPING_MINIMUM
else:
    minimum = STANDARD_FREE_SHIPPING_MINIMUM

if order.total >= minimum and destination.is_domestic:
    ...

At that point, name the behavior rather than only its ingredients:

if shipping_policy.qualifies_for_free_shipping(order, customer, destination):
    ...

The important progression is:

unexplained literal
    -> named value
    -> named behavior when the rule becomes richer

Do not jump to a class or service for every number. But do not let a constant become a substitute for modeling a rule that has developed real behavior.

Repeated literals are evidence, not proof

Seeing the same literal in several places is a useful signal, but identical text does not guarantee identical meaning.

Suppose a system contains three occurrences of 30:

payment_timeout_seconds = 30
trial_period_days = 30
page_size = 30

They happen to share a numeric representation, but they represent three independent decisions. Replacing them with a single THIRTY = 30 constant would create false coupling. If the trial period changes to 14 days, the payment timeout should not change with it.

The reverse can also happen: one concept may appear with different syntax. A retry policy might be expressed as attempts < 4 in one place and failures <= 3 in another. Searching only for identical literals can miss that semantic duplication.

Refactor by meaning. Shared representation is secondary.

Strings can be magic values too

Magic values are not limited to numbers.

if order.status == "ready":
    dispatch(order)

The string "ready" may be part of a defined set of order states. Repeating it throughout the code creates several risks: spelling mistakes, inconsistent terminology, and hidden knowledge about which values are valid.

A named constant can help in a small system:

READY_STATUS = "ready"

If the set of states is closed and meaningful, an enum-like type or domain type may communicate the model more clearly:

OrderStatus.READY

The exact mechanism depends on the language. The engineering principle is broader: when a primitive literal represents a constrained domain concept, make that concept explicit enough that callers do not have to reconstruct it from raw text.

Be careful with values that change at runtime

A named constant is appropriate for a decision that changes through code deployment. It is not automatically appropriate for operational configuration.

For example, a request timeout may need to vary between environments or be tuned without rebuilding the application. In that case:

REQUEST_TIMEOUT_SECONDS = 30

may be the wrong ownership model even though the name is good. The value may belong in validated configuration instead:

request_timeout = config.request_timeout_seconds

Naming and configurability solve different problems. Naming explains what a value means. Configuration determines when and how the value can change.

Moving every magic value into configuration can make a system harder to operate because it creates knobs that nobody needs. Keep stable design choices in code; expose configuration when there is a real runtime or deployment need.

Watch for common mistakes

The most common mistake is replacing every literal indiscriminately. This produces names that merely restate obvious mechanics and makes simple code harder to read.

Another mistake is creating a large constants module with no clear ownership. It may remove duplicated literals while preserving, or even worsening, scattered domain knowledge.

A third mistake is giving one constant several meanings because the underlying values happen to match. Two rules should share a name only when they are the same decision and should change together.

Finally, avoid treating a named constant as the final abstraction when callers still need to understand a complicated rule. If several places combine the same constants in the same way, the missing abstraction may be behavior rather than data.

Use the smallest abstraction that exposes the meaning

When reviewing a literal, choose the smallest representation that makes the relevant knowledge clear:

  • Leave the literal in place when its meaning is obvious from local mechanics.
  • Use a named constant when the value represents a stable rule or decision and a name provides enough context.
  • Include units in the name when the representation could be ambiguous.
  • Keep the value near the module or policy that owns the decision.
  • Use a domain type or enum-like representation when a primitive value belongs to a constrained conceptual set.
  • Move toward named behavior when the concept develops rules that callers should not reconstruct themselves.
  • Use configuration when the value has a genuine need to vary at deployment or runtime.

This is not a ladder that every value must climb. Each step is justified only when it removes knowledge that would otherwise be duplicated or inferred.

Conclusion

Magic values are a maintainability problem when their representation is visible but their meaning is hidden. The first useful move is often simple: give the decision a precise name and place that name near the code that owns it.

The deeper lesson is to refactor by meaning rather than by syntax. Two identical numbers may represent unrelated decisions, while several different expressions may encode the same rule. Name stable values when a name is enough, introduce richer abstractions when behavior grows, and leave ordinary mechanical literals alone.

A good result is not code with no literals. It is code where a developer can tell which values are mere mechanics and which ones carry decisions that matter.