skip to content

Variables & Constants

Ruby marks scope with sigils: locals, @instance, @@class and $global variables, plus capitalised constants. Interviewers probe where each is visible and why def cannot see an outer local.

on this pageshow

explore

questions

6

In Ruby, what do the sigils in total, @total, @@total and $total tell you about where each variable is visible?

level: juniorimportance: must knowfreq 72%

answer

  1. the first character is the declaration
  2. a bare name stops at def
  3. @ belongs to self
  4. unset @@ raises, unset @ does not
  5. $~ is not really global

basics

~20 s

The prefix is the scope: a bare name is a local limited to its current scope, @name belongs to the current object, @@name is shared by a class, its subclasses and instances, and $name is visible to the whole process.

solid answer

~40 s

The prefix is the scope. A bare `total` is a **local**, visible only in the current `def`, class body, block or script from its assignment on; `def`, `class` and `module` hide outer locals. `@total` is an **instance variable** of whatever object is `self`, shared by all of that object's methods, and reads as `nil` when unset. `@@total` is a **class variable** shared by the class, its subclasses and their instances; reading an unset one raises `NameError`, and since Ruby 3.0 any class-variable access from the top-level script raises `RuntimeError`. `$total` is a **global** visible everywhere in the process and reads as `nil` when unset, which is why teams avoid defining their own. A few predefined globals, such as `$~`, are actually thread-local and frame-local.

code

ruby · 21 lines
ruby
$runs = 0

class Tally
  def initialize
    @total = 0
  end

  def add(n)
    step = n * 2   # local: gone when add returns
    @total += step # instance: kept on this Tally
    $runs += 1     # global: shared by every Tally
    @total
  end
end

a = Tally.new
b = Tally.new
a.add(1)
b.add(5)
p a.add(1) # => 4
p $runs    # => 3

go deeper

for a junior

Recall the four prefixes and one sentence each on where the variable is visible, plus what happens when you read one that was never set.

for a middle

Explain why a bare unassigned name raises NameError (it is parsed as a method call) while @ and $ read nil, and that def, class and module hide outer locals.

for a senior

Show judgement: why custom globals and @@ make state hard to trace, how silent nil from a misspelt @ variable hides bugs, and which predefined globals are thread- or frame-local.

for a principal

Frame the sigils as a readability contract: the prefix tells a reviewer the blast radius of a write, which is a reason to keep wide-scope state rare and explicit.

## The first character is the declaration Ruby has no `var`, `let` or `global` keyword. The kind of a variable, and therefore where it can be seen, is spelled by its first character or two. The parser reads that prefix, so `total`, `@total`, `@@total` and `$total` are four unrelated variables that can exist side by side in one program. | Written as | Kind | Visible from | Read before any assignment | |---|---|---|---| | `total` | local variable | the current `def`, `class`/`module` body, block or top-level script, from the assignment onward | treated as a method call, so `NameError` unless such a method exists | | `@total` | instance variable | every method running with that object as `self` | `nil`, silently (no warning since Ruby 3.0) | | `@@total` | class variable | the class body, its methods, its subclasses and their instances | `NameError` (uninitialized class variable) | | `$total` | global variable | everywhere in the process | `nil`; `ruby -w` adds a "not initialized" warning | A name that starts with a capital letter, such as `TOTAL`, is a fifth kind of name: a **constant**, with lookup rules of its own. ## Locals: the narrowest scope - A local is created by assignment and lives until its scope ends. - `def`, `class` and `module` each start a **fresh local table**, so a method cannot read a local assigned outside it. - Blocks differ: code inside a block can read and write the locals around it, and locals first assigned inside the block disappear when it ends. - Until a name has been assigned, Ruby treats a bare `total` as a call to a method named `total`. That is why the failure reads `undefined local variable or method 'total'` rather than naming a missing variable. ## Instance variables: the state of one object `@total` belongs to whatever object is `self` when the line runs. Every method called on that object shares it, and a different object has its own copy. Instance variables are never declared: the first assignment creates one, and reading one that no method has set returns `nil`. Ruby 3.0 removed the verbose-mode warning for that read. A typo such as `@totl` therefore yields `nil` with no message at all, which is one reason code often reads state through methods: a misspelt method name raises `NameError`, a misspelt instance variable does not. ## Class variables and globals: the wide scopes - **`@@total`** is shared by a class, its subclasses and all their instances. Reading one that was never assigned raises `NameError`. - Since **Ruby 3.0**, reading or writing a class variable from the top-level script raises `RuntimeError` (`class variable access from toplevel`). - **`$total`** is one slot for the whole process: any file, class or method can read and replace it. An unset global reads as `nil`; only with warnings on (`ruby -w`) does Ruby mention that it was never initialized. - Ruby's **predefined** `$` variables are not all truly global. `$~` (the last `MatchData`) and the match variables derived from it are thread-local and frame-local, so a regexp match inside one method does not overwrite `$~` in its caller; `$?`, the status of the last child process, is thread-local. ## Choosing between them 1. Default to a **local**, and pass values into methods as arguments. 2. Use an **instance variable** for state that belongs to one object across its methods. 3. Use **`@@`** sparingly, because every subclass writes to the same slot. 4. Avoid defining your own **`$` globals**: any line in any file can change them, so a wrong value is hard to trace. A constant or an explicit configuration object is easier to follow. ## Traps interviewers probe - **A misspelt instance variable** such as `@totl` returns `nil` instead of failing, so the bug surfaces later as `NoMethodError` on `nil`. - **An instance variable at the top level** belongs to `main`, the top-level object, not to any class; methods running on other objects never see it. - **A top-level local is not a global**: a method defined further down the same file cannot read it, because `def` starts a fresh local table. - **A local with the same name as a method** hides that method for the rest of the scope; `self.name` or `name()` still calls it. - **A global is one slot for every thread**, so two threads writing `$total` race with each other. ## Reading the sigils in one class In the example below `step` vanishes when `add` returns, `@total` survives on each `Tally` object separately, and `$runs` counts calls across every `Tally` in the process. An interviewer who asks "where is each visible?" wants that three-way answer plus the unset behaviour from the table: `nil` for `@` and `$`, `NameError` for a bare name and for `@@`.

  • Why does reading an unset @total return nil while an unset bare total raises NameError?
    An instance variable is looked up on `self` at run time, and a missing one simply reads as `nil`. A bare lowercase name that the parser has not seen assigned in the current scope is not a variable at all: Ruby compiles it as a method call with no receiver and no arguments, so when no such method exists it raises `NameError` with `undefined local variable or method`.
  • Which predefined $ variables are not truly process-wide?
    `$~`, the `MatchData` of the last successful regexp match, is thread-local and frame-local, and so are the match variables derived from it such as `$1` and `$&`. A match inside a helper method does not change `$~` in the caller. `$?`, the `Process::Status` of the last child process, is thread-local. Most other predefined globals, such as `$stdout` or `$PROGRAM_NAME`, are genuinely global.

saying these in an interview costs you the question

  • An @ variable is private to the method that assigned it
  • Reading an unset instance variable raises NameError, just like an unassigned local
  • Custom $ globals are the idiomatic way to share settings between classes
  • Every $ variable, including $~ after a regexp match, is shared by all threads and methods
  • A class variable can be read from the top-level script like any global
open as a page

In a Ruby temperature-conversion script, why can't def to_fahrenheit(celsius) read a factor variable assigned at the top level?

level: middleimportance: must knowfreq 62%

basics

~20 s

def, class and module are scope gates: each starts a new, empty set of local variables, so a top-level local is invisible inside the method and the bare name raises NameError. Pass the value as an argument or make it a constant.

open as a page

In Ruby, how does parallel assignment such as a, b = b, a swap two values without a temporary variable?

level: juniorimportance: should knowfreq 52%

basics

~10 s

Ruby evaluates every value on the right-hand side first, then assigns them to the left-hand targets in order, so a, b = b, a reads both old values before writing either one.

open as a page

In Ruby, what does the defined? keyword return for a local, an instance variable, a constant, a method and an undefined name?

level: middleimportance: should knowfreq 40%

basics

~20 s

defined? is a keyword that returns a string naming what its argument is, such as "local-variable", "instance-variable", "constant" or "method", or nil when the name is not defined, without running the method or assignment it inspects.

open as a page

In Ruby, why does x = 10 if false followed by p x print nil instead of raising NameError?

level: middleimportance: should knowfreq 34%

basics

~20 s

Ruby creates a local variable when the parser sees an assignment to it, not when the assignment runs. After x = 10 if false, x exists as a local for the rest of the scope and holds nil.

open as a page

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%

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.

open as a page