skip to content

In Ruby, what happens when code reassigns a constant such as MAX_RETRIES, and why is assigning one inside a method a SyntaxError?

level: seniorimportance: should knowfreq 38%

answer

  1. capital first letter makes a constant
  2. reassignment works but warns
  3. already initialized constant
  4. dynamic constant assignment
  5. the warning guards the name, not the object

basics

~20 s

Reassigning a constant at top level or in a class body succeeds but prints 'already initialized constant' plus the previous location. Inside a method it is rejected at parse time as 'dynamic constant assignment', because a method runs many times.

solid answer

~50 s

A name starting with a capital letter is a **constant**. Reassigning it at the top level or in a class body **works**: the constant now refers to the new object, and Ruby prints `warning: already initialized constant MAX_RETRIES` followed by `warning: previous definition of MAX_RETRIES was here`. The warning is on by default and disappears only when warnings are silenced, for example with `-W0`. Code that already holds the old object keeps it. Writing `MAX_RETRIES = 5` **inside a `def`** is rejected when the file is parsed, with a `SyntaxError` (`dynamic constant assignment`), because a method body can run many times and a constant is meant to be set once when its scope is defined. `Module#const_set` can still set one at run time, and it warns the same way. Mutating the object a constant refers to, such as `TAGS << "sale"`, triggers no warning at all.

code

ruby · 15 lines
ruby
class Client
  TIMEOUT = 5
end

class Client
  TIMEOUT = 10
end
# warning: already initialized constant Client::TIMEOUT
# warning: previous definition of TIMEOUT was here

p Client::TIMEOUT # => 10

TAGS = ["new"]
TAGS << "sale"    # no warning: same array, new contents
p TAGS           # => ["new", "sale"]

go deeper

for a junior

Know that a capitalised name is a constant and that reassigning it prints a warning rather than raising.

for a middle

Explain the dynamic constant assignment SyntaxError, the const_set escape hatch, and why mutating a constant's object is silent.

for a senior

Diagnose double-definition warnings from load, reopened classes or tests that reassign constants, and fix the cause rather than silencing output.

for a principal

Set a policy that constant warnings fail CI, since they signal double loading or shared mutable configuration that can make behaviour order-dependent.

## What makes a name a constant In Ruby any identifier that starts with an **uppercase letter** is a constant: `MAX_RETRIES`, `Config`, `HTTPClient`. Class and module names are constants too. A constant is a **name bound to an object** in a class, module or the top level (top-level constants live on `Object`). Ruby treats constants as *intended* to be set once, but it does not strictly enforce that at run time. ## Reassignment: allowed, with a warning At the top level or directly inside a `class`/`module` body, assigning to an existing constant succeeds: ```ruby MAX_RETRIES = 3 MAX_RETRIES = 5 # warning: already initialized constant MAX_RETRIES # warning: previous definition of MAX_RETRIES was here p MAX_RETRIES # => 5 ``` - The constant now refers to the new object. - Inside a class the warning names the owner, for example `already initialized constant Client::TIMEOUT`. - The warning is emitted by default, not only under `ruby -w`. Silencing warnings, for example with `-W0` or by setting `$VERBOSE` to `nil`, hides it. - Any object that captured the **old value**, such as a local assigned earlier or an object built with it, keeps the old object. ## Inside a method: a SyntaxError ```ruby def reset_limits MAX_RETRIES = 3 # SyntaxError: dynamic constant assignment end ``` The whole file fails to parse, even if the method is never called. The reasoning is that a method body can run any number of times, so an assignment there would make the constant's value depend on call history. The parser rejects it outright. - **`Module#const_set`** sets a constant at run time and is allowed inside methods: `self.class.const_set(:MAX_RETRIES, 3)`. It goes through the same code path as a normal assignment, so replacing an existing constant still prints the "already initialized constant" warning. - If a value genuinely changes at run time, it is not a constant: store it in an instance variable, a configuration object or an argument instead. ## The warning covers the binding, not the object | Operation | Warning? | Result | |---|---|---| | `MAX_RETRIES = 5` again at top level | yes, "already initialized constant" | name now points to 5 | | `MAX_RETRIES = 5` inside `def` | file does not parse | `SyntaxError` | | `Object.const_set(:MAX_RETRIES, 5)` | yes, same warning | name now points to 5 | | `TAGS << "sale"` where `TAGS = ["new"]` | none | the array itself changes | The last row surprises people: the constant still points to the same array, so Ruby has nothing to warn about. Protecting the object's contents is a separate topic (making the value immutable). ## Reading the warning The two lines Ruby prints are a ready-made trail: - the first line carries the file and line of the **new** assignment and names the constant, qualified by its class when it is not top-level; - the second line, `previous definition of TIMEOUT was here`, carries the file and line of the **earlier** assignment. Comparing the two locations usually reveals the cause at once: the same file twice (double loading), two files defining the same name (a naming collision), or a test file overriding production code. ## Where senior engineers meet this warning 1. **A file loaded twice.** `Kernel#load` re-executes a file every time, so its constants are reassigned and the warnings appear in logs. The same happens when a class body that defines a constant is reopened and run again. 2. **Tests that reassign constants.** A test that sets `Billing::RATE = 0.5` to exercise a branch changes it for every later test in the process, and produces warnings. Inject the value, or use a helper such as RSpec's `stub_const`, which restores the original. 3. **Guarded definitions.** `MAX_RETRIES = 3 unless defined?(MAX_RETRIES)` avoids the warning when a file may be evaluated twice, but hides the design problem behind it. 4. **Warnings as signals.** Treat "already initialized constant" in CI output as a bug report: something is being defined twice. ## Interview summary Say that reassignment is allowed but warned, that assignment inside a method is a parse-time `SyntaxError` (`dynamic constant assignment`), that `const_set` is the run-time escape hatch, and that the warning guards the name, not the contents of the object.

  • How can a method legitimately set a constant, and does that avoid the warning?
    A method can call `Module#const_set`, for example `self.class.const_set(:TIMEOUT, 10)`, which is allowed at run time because it is an ordinary method call rather than constant-assignment syntax. It does not avoid the warning: replacing an existing constant through `const_set` prints the same 'already initialized constant' message.
  • Why do 'already initialized constant' warnings appear when a file is loaded with load?
    `Kernel#load` executes a file every time it is called, unlike `require`, which runs a file once. Each run re-executes the constant assignments at the top of the file or in its class bodies, so every constant after the first run is a reassignment and warns.
  • Is it safe for a test to reassign a production constant to exercise a branch?
    Not directly. The reassignment lasts for the rest of the process, so later tests see the changed value and results depend on test order; it also floods output with warnings. Inject the value into the code under test, or use a stubbing helper such as RSpec's `stub_const`, which restores the original after each example.

saying these in an interview costs you the question

  • Reassigning a constant raises an error, like final or const in other languages
  • The reassignment warning appears only when running ruby -w
  • MAX_RETRIES = 3 inside a method just resets the constant each call
  • The constant warning also fires when you push into an array held by a constant
  • const_set bypasses the already initialized constant warning