Variables, Constants and Scope

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Ruby 4.0 (4.0.7, checked on 2026-10-18). It covers the five kinds of names a Ruby program can bind a value to: local variables, instance variables, class variables, global variables and constants. For each it explains where the name is visible, what an unset name evaluates to, how to inspect it at run time, and which mistakes show up in real code. The running scenario is the shelf library-catalogue gem used throughout this section. Features newer than Ruby 3.0 carry their version in the text, and the version table near the end collects them.

Every example was syntax-checked with ruby -wc, run with Ruby 4.0.7, and linted with RuboCop 1.92 using its default cops and TargetRubyVersion: 4.0. The output under each example is the real output of that run. A # rubocop:disable comment marks the few places where an example breaks a cop on purpose because the cop is what the page is explaining. Error messages use the Ruby 3.4 format ('name' with straight quotes and for main or for an instance of X).

The neighbouring pages build on this one: Assignment and Multiple Assignment covers the = operator itself, Blocks, Procs and Lambdas covers closures in depth, and Modules and Mixins covers what a module contributes to a lookup chain.

The five kinds of names at a glance

A name’s first characters decide its kind, and its kind decides everything else: how far it is visible, what it holds before the first assignment, and how it can be listed.

Kind Looks like Visible in Unset value Listing and access

Local variable

title, _unused

the current scope only: a method body, a class body, a file, or a block and the scopes around it

a method call; a NameError if no such method exists (nil only after the parser has seen an assignment)

local_variables, binding.local_variable_get

Instance variable

@name

every method of one object

nil, without a warning (since 3.0)

instance_variables, instance_variable_get

Class variable

@@opened

a class, its subclasses and their instances

NameError

class_variables, class_variable_get

Global variable

$stdout, $level

everywhere in the process

nil (a warning under -w)

global_variables, trace_var

Constant

Policy, MAX_LOANS

the lexical scopes around the reference, then the ancestors of the innermost one

NameError (after const_missing)

constants, const_get, const_source_location

A bare lower-case name is either a local variable or a method call, and the next section explains how Ruby decides. Names that begin with an upper-case letter are constants: class and module names are ordinary constants that happen to hold a Class or Module.

Source: Assignment

Local variables

A local variable name starts with a lower-case letter, an underscore or a non-ASCII character, and continues with letters, digits and underscores. By convention it is snake_case. A local variable is created by assigning to it; there is no declaration, no let, and no type. It stays visible for the rest of its scope and nothing outside that scope can see it.

The parse-time rule

Ruby decides whether a bare lower-case word is a variable or a method call when it parses the file, not when it runs the code. The first time the parser sees an assignment to a name, every later occurrence of that name in the same scope is a local-variable read, whether or not the assignment ever executes. Before that point, the word is a method call with no arguments and no receiver.

parse_time_local.rb
# frozen_string_literal: true

# A bare name is a method call until the parser has seen an assignment to it.
def title = 'from the method'

puts title # the method

on_loan = false
title = 'from the local' if on_loan # never assigned, but the parser saw it
p title # the local variable, which is nil

title = 'now assigned'
puts title
puts self.title # rubocop:disable Style/RedundantSelf
p local_variables
from the method
nil
now assigned
from the method
[:on_loan, :title]

Three consequences follow from the output:

  • title = '...' if on_loan never ran its assignment, yet title is now a local variable holding nil. The method title is shadowed for the rest of the scope. Call it with empty parentheses (title()) or with an explicit receiver (self.title) to reach the method again.

  • A modifier condition is evaluated first, but it is written second. p copies if (copies = 3).positive? fails with a NameError because the parser reaches p copies before it has seen the assignment (the non_gates.rb example later on this page prints the real message).

  • The classic trap big_calculation = big_calculation() is legal: the right-hand side is a method call because of the parentheses, and from then on the name is a local variable.

x ||= 10 is the same rule used on purpose: the parser sees the assignment, so x is a local variable (with value nil) by the time ||= reads it.

Scope gates

A scope gate is a construct that starts a new local-variable scope with no access to the enclosing one. Ruby has three keyword gates (def, class and module), plus the top level of each file. Everything else, blocks included, leaves the surrounding variables visible.

Constructs that open a new, isolated scope (a gate):

  • def: the method body sees none of the variables around the def;

  • class, module and class << obj: the body starts a fresh scope;

  • the top level of each file, including files loaded with require and load.

Constructs that do not isolate anything:

  • if, unless, case, while, until, for, begin, rescue and ensure open no scope at all;

  • a block ({ ... } or do ... end), a lambda written with ->, and the blocks given to define_method, Class.new, Module.new, Struct.new and Data.define open a nested scope: they can read and write the outer variables, and their own variables die with them;

  • a string passed to eval, instance_eval or class_eval is a nested scope too: it sees the caller’s locals, and the variables it creates stay private to it.

A gate cuts the scope off completely, and a block is the way to cross a gate on purpose.

scope_gates.rb
# frozen_string_literal: true

limit = 14

# def, class and module open a new scope: nothing outside is visible inside.
def days_allowed
  limit
rescue NameError => e
  e.message
end
puts days_allowed

# A block is a closure: it sees, and can change, the surrounding locals.
renewals = 0
3.times { renewals += 1 }
p renewals

# define_method takes a block, so it can capture a local from the class body.
class Reader
  max_books = 3
  define_method(:max_books) { max_books }
end
puts Reader.new.max_books
puts limit
undefined local variable or method 'limit' for main
3
3
14

The def in days_allowed cannot see limit, which belongs to the top-level scope, so the NameError message reports undefined local variable or method 'limit' for main. The block passed to 3.times is a closure: it reads and writes renewals itself, and define_method takes a block, so the method it creates can read max_books from the class body, which a def max_books could not.

The if, while and begin family never opens a scope, so a variable first assigned inside them is visible after them. This is different from JavaScript’s let and from most C-family languages.

non_gates.rb
# frozen_string_literal: true

# if, unless, while, case and begin do not open a scope.
if ARGV.empty?
  mode = :interactive
  prompt = '> '
end
p mode, prompt

while (attempt ||= 0) < 2
  attempt += 1
  last_try = attempt
end
p last_try

# The modifier condition runs first, so the parser has not yet seen the assignment.
begin
  p copies if (copies = 3).positive? # rubocop:disable Lint/UselessAssignment
rescue NameError => e
  puts e.message
end

# A name with no assignment seen yet is a method call; ||= makes it a local.
count ||= 10
p count

# Struct.new and Data.define blocks do not open a constant scope.
Point = Struct.new(:x, :y) do
  ORIGIN_X = 0 # rubocop:disable Lint/ConstantDefinitionInBlock
end
p Point.const_defined?(:ORIGIN_X, false)
p Object.const_defined?(:ORIGIN_X)
:interactive
"> "
2
undefined local variable or method 'copies' for main
10
false
true

The last two lines show a related surprise: a constant assigned inside the block of Struct.new or Data.define is defined in the enclosing lexical scope, here Object, and not on the new class. The constants section returns to that.

Blocks, closures and shadowing

A block sees the variables of the scope it was written in, and those variables live as long as any block still refers to them. Variables first assigned inside the block do not survive it.

blocks_and_shadowing.rb
# frozen_string_literal: true

# Variables first assigned inside a block disappear when it ends.
[1, 2].each { |n| puts n * 2 }
p defined?(n)

# for does not open a scope, so its variable leaks.
for i in 1..3 do end # rubocop:disable Style/For
p i

# A block-local variable (after the semicolon) shadows an outer one on purpose.
total = 100
[1, 2, 3].each do |n; total|
  total = n * 10
  puts total
end
p total

# A plain block parameter with the same name also shadows the outer variable.
name = :outer
[:inner].each { |name| puts name }
p name

# Implicit parameters: it (3.4) and _1 to _9 (2.7).
tens = [1, 2, 3].map { it * 10 }
sums = [[1, 2], [3, 4]].map { _1 + _2 }
p tens, sums
2
4
nil
3
10
20
30
100
inner
:outer
[10, 20, 30]
[3, 7]
  • defined?(n) is nil after the first loop because the block parameter n exists only inside the block.

  • for i in 1..3 does not open a scope. i is still readable afterwards and holds 3. This is the main reason idiomatic Ruby prefers each; RuboCop’s Style/For flags it.

  • A block-local variable, declared after a semicolon in the parameter list (|n; total|), is a fresh variable that shadows the outer total and leaves it untouched. Use it when a block needs a scratch variable and the same name already exists outside.

  • A plain block parameter with the name of an outer variable also shadows it, and Ruby prints no warning for it any more (the old "shadowing outer local variable" warning was removed in 2.6). The outer name is unchanged afterwards, and the block’s name is a different variable.

  • it (Ruby 3.4) and _1 to _9 (Ruby 2.7) are implicit block parameters. it is the name of the only parameter; _1 and _2 are the first and second parameters of a block that takes several. Assigning to a numbered parameter is a SyntaxError since 3.0, and a block cannot mix numbered parameters with an ordinary parameter list. Ruby 4.0 stops Binding#local_variables from listing implicit parameters; Binding#implicit_parameters lists them instead.

See Blocks, Procs and Lambdas for the it and numbered-parameter rules in full.

Flat scopes and closures

Because blocks cross scope gates, Ruby code that needs to hand a local variable to a method body uses a flat scope: it replaces def with define_method, and class Name with Class.new do ... end. The block closes over the surrounding locals, and the closure holds the variable, not a copy of its value.

flat_scope.rb
# frozen_string_literal: true

prefix = '978'

# Class.new, Module.new and Data.define take blocks, so they can close over locals
# (the "flat scope" technique). A def inside would open a new scope and lose them.
Catalogued = Class.new do
  define_method(:catalogue_prefix) { prefix }

  def broken_prefix
    prefix
  rescue NameError => e
    e.message.lines.first.chomp
  end
end

item = Catalogued.new
p item.catalogue_prefix
p item.broken_prefix

prefix = '979' # the closure shares the variable, it does not copy it
p item.catalogue_prefix

# A lambda keeps its variables alive after the defining scope has ended.
def counter
  count = 0
  -> { count += 1 }
end

next_id = counter
next_id.call
p next_id.call
p next_id.binding.local_variables
"978"
"undefined local variable or method 'prefix' for an instance of Catalogued"
"979"
2
[:count]

broken_prefix is written with def, so it opens a new scope; prefix there is a method call, and since the class has no method of that name the error message names the receiver as an instance of Catalogued. After prefix = '979' the define_method body returns '979': the block shares the variable, so a later assignment is visible inside it. The lambda returned by counter keeps its own count alive after counter has returned, which is the usual way to build private mutable state without an object.

Unused variables and the underscore

Under -w, Ruby warns when a local variable is assigned and never read. The message names the variable, for example warning: assigned but unused variable - copies. A leading underscore (_copies, or just _) silences the warning, and the convention tells readers the value is deliberately ignored. A name that starts with an underscore is an ordinary variable otherwise: it can still be read, and it still appears in local_variables.

Inspecting scope: local_variables, Binding and eval

Kernel#local_variables lists the names visible in the current scope, including names the parser has seen but the program has not yet reached. Kernel#binding captures the current scope as a Binding object, which can read, write and evaluate code in it.

binding_locals.rb
# frozen_string_literal: true

# Binding exposes the locals of a scope as data.
def describe_scope(isbn)
  _copies = 2
  binding.local_variables.to_h { |name| [name, binding.local_variable_get(name)] }
end
p describe_scope('9780596516178')

# local_variable_set on a name the parser has not seen lives only in the binding.
scope = binding
scope.local_variable_set(:late, 1)
p scope.local_variable_get(:late)
p scope.local_variable_defined?(:late)
p defined?(late)
p scope.eval('late')
{isbn: "9780596516178", _copies: 2}
1
true
nil
1
  • binding.local_variables lists names; local_variable_get reads a value, and local_variable_set writes one.

  • local_variable_set on a name the parser has not seen creates the variable only inside that Binding. The scope itself still treats late as a method call, so defined?(late) is nil; code evaluated through the binding (scope.eval('late')) does see it.

  • eval with a string is a nested scope of its own: assignments inside the string are not visible to the code around it, and two eval calls in the same scope do not see each other’s new variables. The eval documentation in the Ruby guide has a worked example. Prefer a Binding or a block to eval for anything that is not a REPL or a template engine; see Hooks, eval and DSLs.

  • The top-level binding is available as the constant TOPLEVEL_BINDING.

Instance variables

An instance variable begins with a single @ and belongs to one object. Any method of that object reads and writes it, including methods inherited from a superclass: there is no separate namespace per class, so a subclass and its superclass see the same @name. No other object sees it. Unlike Java or C#, Ruby has no declaration: the variable comes into existence the first time something assigns it, and different instances of one class may end up with different sets.

instance_variables.rb
# frozen_string_literal: true

# A library member who borrows titles.
class Reader
  attr_reader :name

  def initialize(name)
    @name = name
  end

  def borrow(title)
    @borrowed ||= []
    @borrowed << title
    self
  end

  def borrowed? = !@borrowed.nil?
end

ana = Reader.new('Ana')
p ana.instance_variables
p ana.borrowed? # reading an unset ivar gives nil, with no warning
p ana.instance_variable_get(:@borrowed)
ana.borrow('Dune').borrow('Emma')
p ana.instance_variables
p ana.instance_variable_get(:@borrowed)
p ana.instance_variable_defined?(:@name)

ana.instance_variable_set(:@vip, true) # any object accepts any ivar
p ana.instance_variables
p ana.remove_instance_variable(:@vip)
p ana.instance_variables

begin
  ana.instance_variable_get('borrowed')
rescue NameError => e
  puts e.message
end

p self
p @top_level_ivar
[:@name]
false
nil
[:@name, :@borrowed]
["Dune", "Emma"]
true
[:@name, :@borrowed, :@vip]
true
[:@name, :@borrowed]
'borrowed' is not allowed as an instance variable name
main
nil

Points worth knowing:

  • An unset instance variable reads as nil. Before Ruby 3.0 that read printed a warning under -w; the warning was removed in 3.0, so code that relies on @x ||= [] or on @x.nil? is warning-free. It also means a typo such as @nmae is silently nil: tests catch it, the interpreter does not.

  • instance_variable_get(:@name), instance_variable_set, instance_variable_defined? and remove_instance_variable are the reflective API. The name must start with @: the string 'borrowed' raises NameError with 'borrowed' is not allowed as an instance variable name.

  • The default inspect (and so p) prints every instance variable. Ruby 4.0 lets a class narrow that output by defining a private instance_variables_to_inspect method that returns an array of names (see the example below).

  • Instance variables are private to the object: the only way another object reads them is through a method. attr_reader, attr_writer and attr_accessor generate those methods. See Classes and Objects and Visibility and Encapsulation.

  • Everything is an object, so self at the top level (main) can have instance variables, and so can a class or a module. @top_level_ivar above is nil because nothing assigned it. These are the "class-level instance variables" discussed in the next section.

  • Since Ruby 3.2 the VM represents the instance-variable layout of an object as a shape, which makes ivar access faster. Objects whose variables are assigned in a consistent order share a shape, so assign every variable in initialize, always in the same order, instead of adding new ones from random methods.

inspect_ivars.rb
# frozen_string_literal: true

# A reader with a private token that should stay out of inspect output.
class Account
  def initialize(name, token)
    @name = name
    @token = token
  end

  private

  def instance_variables_to_inspect = [:@name]
end

puts Account.new('ana', 'demo-token').inspect.sub(/0x\h+/, '0x0')
#<Account:0x0 @name="ana">

Source: Object#inspect

An object that has been frozen (freeze) refuses new assignments to its instance variables with a FrozenError; see Object Lifecycle, Freezing and Copying.

The setter pitfall

Inside a method, count = 5 always creates a local variable, even when the class has a count= writer. To call the writer, write self.count = 5. RuboCop’s Style/RedundantSelf leaves such a self. alone for that reason.

setter_pitfall.rb
# frozen_string_literal: true

# Setters need an explicit receiver: a bare assignment creates a local variable.
class Counter
  attr_accessor :count

  def initialize = @count = 0

  def wrong
    count = 5
    [count, @count]
  end

  def right
    self.count = 5
    [count, @count]
  end
end

counter = Counter.new
p counter.wrong
p counter.right
[5, 0]
[5, 5]

wrong returns [5, 0]: the local variable is 5 and the instance variable never changed. right goes through the attr_accessor setter. The same applies to compound assignment: pass:c[self.count += 1] is correct and pass:c[count += 1] creates a new local.

Class variables

A class variable begins with @@. It is shared by the class that defines it, by every subclass and by every instance of any of them. Reading one that has not been set raises NameError (unlike instance variables, which are nil).

The example also sets up the preferred alternative: a class-level instance variable, written @count inside the class body. A class is an object, so it has its own instance variables, and the subclass Annex has its own @count.

class_variables.rb
# frozen_string_literal: true

# A branch library; @@opened is shared, @count is per class.
class Branch
  @@opened = 0 # rubocop:disable Style/ClassVars
  @count = 0

  class << self
    attr_accessor :count
  end

  def self.opened = @@opened

  def initialize
    @@opened += 1 # rubocop:disable Style/ClassVars
    self.class.count += 1
  end
end

# A smaller branch with its own counter.
class Annex < Branch
  @count = 0
end

Branch.new
Annex.new
Annex.new
p Branch.opened, Annex.opened
p Branch.count, Annex.count
p Branch.class_variables, Annex.class_variables
p Annex.class_variable_get(:@@opened)
p Annex.class_variable_defined?(:@@missing)
p Branch.instance_variables
begin
  Branch.class_variable_get(:@@missing)
rescue NameError => e
  puts e.message
end
3
3
1
2
[:@@opened]
[:@@opened]
3
false
[:@count]
uninitialized class variable @@missing in Branch

Reading the output:

  • @@opened is one variable: Branch.opened and Annex.opened both report 3 after one Branch and two Annex instances were created.

  • @count is two variables: Branch.count is 1 and Annex.count is 2, because each class object stores its own and pass:c[self.class.count += 1] updates the class of the instance being created.

  • class_variables lists names including inherited ones. Branch.instance_variables lists the class-level instance variable [:@count].

  • Annex needed its own @count = 0: a subclass does not inherit class-level instance variables. When subclasses should start from the parent’s value, copy it in the inherited hook; see Hooks, eval and DSLs and Singleton Classes and Class-Level Behavior.

Class variable @@x Class-level instance variable @x in the class body

Shared with subclasses

yes, one variable for the whole tree

no, each class object has its own

Read from instance methods

directly (@@x)

through a reader on the class (self.class.x)

Unset value

NameError

nil

RuboCop

Style/ClassVars complains about assignments

no complaint

The advice in most style guides is to prefer the class-level instance variable. A class variable is shared mutable state across an inheritance tree, which makes tests order-dependent and means that a library’s subclass can change the behaviour of its superclass.

Errors specific to class variables

Two rules were tightened in Ruby 3.0:

overtaken_class_variable.rb
# frozen_string_literal: true

# A library whose subclass defines the same class variable.
class Library
end

# A branch of the library.
class Branch < Library
  @@seats = 10 # rubocop:disable Style/ClassVars

  def self.seats = @@seats
end

# Reopening the superclass and defining the same name overtakes the subclass.
class Library
  @@seats = 99 # rubocop:disable Style/ClassVars
end

begin
  Branch.seats
rescue RuntimeError => e
  puts "#{e.class}: #{e.message}"
end
RuntimeError: class variable @@seats of Branch is overtaken by Library

If a class and an ancestor both define the same class variable, the one in the subclass is overtaken when the ancestor is (re)defined. Before 3.0 this only printed a warning under -w; now any access raises a RuntimeError.

toplevel_class_variable.rb
# frozen_string_literal: true

# Class variables cannot be used at the top level (RuntimeError since 3.0).
begin
  @@shelf_count = 1 # rubocop:disable Style/ClassVars
rescue RuntimeError => e
  puts "#{e.class}: #{e.message}"
end
RuntimeError: class variable access from toplevel

Accessing a class variable from the top level (outside any class or module body) is a RuntimeError for the same reason: the variable would be defined on Object and shared with every class in the program.

Class variables are looked up through the lexical class (the innermost class or module keyword in the source), not through the receiver. Methods defined with define_method in a block, and code in class_eval blocks, therefore see the variables of the class where the block was written. class_variable_get and class_variable_set are the reflective way to reach a specific class’s variable.

Global variables

A global variable begins with $ and is visible everywhere in the process. An unset global reads as nil, and under -w Ruby prints a warning that the global was not initialized:

uninitialized_global.rb
# frozen_string_literal: true

p $never_set # rubocop:disable Style/GlobalVars
uninitialized_global.rb:3: warning: global variable '$never_set' not initialized
nil

(This one file is run as ruby -w.) defined?($never_set) avoids the warning, and global_variables lists every name.

User-defined globals are rare in well-structured code and RuboCop’s Style/GlobalVars discourages them. They are not thread-safe, they make tests depend on each other, and any gem can overwrite them. The next example defines some on purpose to show the machinery.

user_globals.rb
# frozen_string_literal: true

require 'stringio'

# Style/GlobalVars discourages defining globals; this page shows them on purpose.
# rubocop:disable Style/GlobalVars

# Globals are visible everywhere.
$shelf_log_level = :info
p $shelf_log_level
p global_variables.include?(:$shelf_log_level)
p defined?($never_set)

# alias gives a global a second name that always agrees with the first.
alias $level $shelf_log_level
$level = :debug
p $shelf_log_level

# trace_var runs a block each time the global is assigned.
trace_var(:$shelf_log_level) { |value| puts "log level is now #{value}" }
$shelf_log_level = :warn
untrace_var(:$shelf_log_level)

# rubocop:enable Style/GlobalVars

# $stdout accepts any object that responds to write.
captured = StringIO.new
$stdout = captured
puts 'hidden'
$stdout = STDOUT
p captured.string

begin
  $stdout = Object.new
rescue TypeError => e
  puts e.message
end
:info
true
nil
:debug
log level is now warn
"hidden\n"
$stdout must have write method, Object given
  • alias $level $shelf_log_level makes two names for one variable. It is a language keyword, not a method, and it works only for globals.

  • trace_var(:$name) { |value| ... } runs the block each time the global is assigned; untrace_var removes it.

  • The stream variables $stdout, $stderr and $stdin accept any object that responds to the right method (write for the output ones). Reassigning $stdout is how libraries capture output in tests, and the constant STDOUT keeps pointing at the original stream so it can be restored. $> is another name for $stdout. Assigning an object that has no write raises TypeError with $stdout must have write method, Object given.

  • Many specials cannot be assigned. Assigning $PID (the same variable as $$) raises NameError ($PID is a read-only variable), and $1 = 2 is a SyntaxError (Can't set variable $1); the table below says which is which.

Pre-defined global variables

Ruby defines a fixed set of special globals. Some are only names for something else, some are filled in by the interpreter, and some change the behaviour of core methods when assigned. The table groups them as the official guide does. Variables with an English name become available under that long name after require 'English'.

Variable English name Contains Writable

$!

$ERROR_INFO

the exception being handled, or nil

no (NameError)

$@

$ERROR_POSITION

$!.backtrace

only while an exception is being handled

$~

$LAST_MATCH_INFO

the last MatchData (frame-local and thread-local)

yes

$&

$MATCH

the matched substring

no (SyntaxError)

$` (backtick)

$PREMATCH

the text before the match

no (SyntaxError)

$'

$POSTMATCH

the text after the match

no (SyntaxError)

pass:c[$+]

$LAST_PAREN_MATCH

the last group matched

no (SyntaxError)

$1 to $9, $n

none

the nth group of the last match

no (SyntaxError)

$/, $-0

$INPUT_RECORD_SEPARATOR, $RS

the input record separator, newline by default

yes

$\

$OUTPUT_RECORD_SEPARATOR, $ORS

the output record separator, nil by default

yes

$stdin, $stdout, $stderr

none

the current standard streams

yes

$<

$DEFAULT_INPUT

ARGF

no (NameError)

$>

$DEFAULT_OUTPUT

the same variable as $stdout

yes (needs write)

$.

$INPUT_LINE_NUMBER, $NR

the line number of the last read

yes (an Integer)

$_

$LAST_READ_LINE

the last line read by gets and friends (frame-local)

yes

$0, $PROGRAM_NAME

$PROGRAM_NAME

the script name, which can be reassigned

yes (a String)

$*

$ARGV

the ARGV array

no (NameError)

$$

$PROCESS_ID, $PID

the process id

no (NameError)

$?

$CHILD_STATUS

the Process::Status of the last child (thread-local)

no (NameError)

$LOAD_PATH, $:, $-I

$LOAD_PATH

the directories searched by require and load

no (mutate the array)

$LOADED_FEATURES, $"

$LOADED_FEATURES

the files already loaded

no (mutate the array)

$FILENAME

none

ARGF.filename

no (NameError)

$DEBUG, $-d

none

true under -d or --debug; prints each raised exception

yes

$VERBOSE, $-v, $-w

none

true, false or nil; controls warnings

yes

$;, $-F

$FIELD_SEPARATOR, $FS

the default String#split separator; setting it is deprecated

yes (deprecated)

$,

$OUTPUT_FIELD_SEPARATOR, $OFS

the default Array#join separator; setting it is deprecated

yes (deprecated)

$-a, $-i, $-l, $-p, $F

none

state set by the command-line options -a, -i, -l, -p

$-i and $F yes; the others no (NameError)

Exceptions, match data and the English module

$! and $@ are set while an exception is being handled and are reset when the rescue clause ends. The match variables $~, $&, $1 and the others belong to the current method frame and thread: a regular-expression match in a called method does not clobber the caller’s $1, and another thread has its own. That is also why they exist at all in a language that prefers explicit objects; Regexp.last_match and MatchData are the explicit forms, and RuboCop’s Style/PerlBackrefs suggests them. The English library (a default gem you load with require 'English') gives every cryptic name a readable alias.

special_globals.rb
# frozen_string_literal: true

require 'English'

# $! and $@ are set while an exception is handled and reset afterwards.
begin
  raise ArgumentError, 'bad ISBN'
rescue ArgumentError
  p $ERROR_INFO
  p $ERROR_POSITION == $ERROR_INFO.backtrace
end
p $ERROR_INFO

# The match globals belong to the current method frame and thread.
# Style/PerlBackrefs prefers Regexp.last_match; the globals are shown on purpose.
# rubocop:disable Style/PerlBackrefs
if 'ISBN 9780596516178' =~ /(\d{3})(\d+)/
  p $LAST_MATCH_INFO[0], $LAST_MATCH_INFO.pre_match
  p $PREMATCH, $POSTMATCH, $LAST_PAREN_MATCH
  p $1, $2
end

def match_in_another_frame = 'X-1' =~ /(\d)/

match_in_another_frame
p $1
# rubocop:enable Style/PerlBackrefs

p $PROGRAM_NAME == $0 # rubocop:disable Style/SpecialGlobalVars
p $PID == Process.pid
p __FILE__ == $PROGRAM_NAME
p $LOAD_PATH.equal?($:) # rubocop:disable Style/SpecialGlobalVars
p $ARGV.equal?(ARGV)

begin
  eval('$PID = 1', binding, __FILE__, __LINE__)
rescue NameError => e
  puts "#{e.class}: #{e.message}"
end
#<ArgumentError: bad ISBN>
true
nil
"9780596516178"
"ISBN "
"ISBN "
""
"0596516178"
"978"
"0596516178"
"978"
true
true
true
true
true
NameError: $PID is a read-only variable
  • $ERROR_POSITION == $ERROR_INFO.backtrace is true: $@ is just the backtrace of $!.

  • After the begin block, $ERROR_INFO is nil again.

  • $1 after calling match_in_another_frame is still "978": the match in the other method did not change this frame’s match data.

  • $PROGRAM_NAME and $0 are the same variable, so __FILE__ == $PROGRAM_NAME is the usual "was this file run directly" test. Assigning to it changes the process title that ps shows on most platforms.

  • $LOAD_PATH.equal?($:) is true: the short form is an alias, as $> is for $stdout.

See Regular Expressions and Exceptions and Error Handling for the objects behind $~ and $!.

Source: English

Warnings, $VERBOSE and the deprecated separators

$VERBOSE has three levels. false (the default) prints ordinary warnings; true (ruby -w, -W2 or -v) adds the extra warnings; nil (-W0) suppresses every warning, including those raised by Kernel#warn.

verbose_levels.rb
# frozen_string_literal: true

# Warnings go through Kernel#warn; $VERBOSE = nil silences them all.
def old_api
  warn 'old_api is deprecated', uplevel: 1
end

old_api
$VERBOSE = nil
old_api
warn 'also silent'
$VERBOSE = false
warn 'silent no longer'
verbose_levels.rb:8: warning: old_api is deprecated
silent no longer

Source: Kernel#warn

The first call to old_api prints its warn message with the caller’s location (uplevel: 1). Once $VERBOSE is nil, neither old_api nor warn prints anything. Setting it back to false re-enables output. Assign $VERBOSE = nil in a narrow, restored scope only: it also hides the "already initialized constant" warning and warnings from your dependencies.

Deprecation warnings (category :deprecated) are off by default since Ruby 2.7.2 and are turned on by -w, -W:deprecated or Warning[:deprecated] = true. Two old record-separator globals belong to that category:

deprecated_separators.rb
# frozen_string_literal: true

# The old separator globals still work, but setting them is deprecated.
$, = ','
$; = ' '
p 'a b'.split
puts [1, 2].join
deprecated_separators.rb:4: warning: non-nil '$,' is deprecated
deprecated_separators.rb:5: warning: non-nil '$;' is deprecated
deprecated_separators.rb:6: warning: $; is set to non-nil value
["a", "b"]
deprecated_separators.rb:7: warning: $, is set to non-nil value
1,2

The output is for ruby -W:deprecated deprecated_separators.rb. Setting $, or $; to anything but nil is deprecated since Ruby 2.7, and while set it changes the behaviour of every Array#join and String#split call in the process, so the "default separators" are best left alone and passed explicitly (join(','), split(' ')). The input and output record separators $/ and $\ carry a similar deprecation note for non-nil assignment other than by the command line.

Pre-defined constants

Some constants are always defined. They hold the standard streams, the environment, and information about the running interpreter.

Constant Holds

STDIN, STDOUT, STDERR

the original standard streams (the $stdin, $stdout and $stderr globals may point elsewhere)

ENV

the environment variables (see fetch, key? and the Hash-like interface)

ARGV, ARGF

the command-line arguments, and the concatenated contents of the files they name (or standard input)

TOPLEVEL_BINDING

the Binding of the top-level scope

RUBY_VERSION, RUBY_PATCH_LEVEL, RUBY_RELEASE_DATE, RUBY_REVISION

version details of this interpreter

RUBY_ENGINE, RUBY_ENGINE_VERSION, RUBY_PLATFORM, RUBY_DESCRIPTION, RUBY_COPYRIGHT

the implementation, platform and banner

DATA

a File positioned just after __END__, defined only in the main script when it contains __END__

Ruby (4.0)

a module that groups the RUBY_* constants as Ruby::VERSION, Ruby::ENGINE and so on; the name was reserved in 3.4

predefined_constants.rb
# frozen_string_literal: true

p RUBY_VERSION, RUBY_ENGINE
p ARGV
p ENV.fetch('SHELF_ENV', 'development')
p TOPLEVEL_BINDING.local_variable_defined?(:titles)
p DATA.class
titles = DATA.each_line(chomp: true).to_a
p titles
p TOPLEVEL_BINDING.local_variable_get(:titles)

__END__
Dune
Emma
"4.0.7"
"ruby"
[]
"development"
true
File
["Dune", "Emma"]
["Dune", "Emma"]

TOPLEVEL_BINDING.local_variable_defined?(:titles) is already true on its first use because of the parse-time rule described earlier. ENV.fetch('SHELF_ENV', 'development') returns the default when the variable is unset. The ENV and ARGV details live on Ruby Command Line and Environment.

Thread-local and fiber-local state

When the real need is "state that belongs to one request, thread or fiber", reach for the storage Ruby provides for that instead of a global.

thread_fiber_locals.rb
# frozen_string_literal: true

# Per-thread and per-fiber state, instead of a plain global.
Thread.current[:request_id] = 'r-1' # fiber-local, despite the name
Thread.current.thread_variable_set(:user, 'ana') # thread-local

Fiber.new do
  p Thread.current[:request_id]
  p Thread.current.thread_variable_get(:user)
end.resume

Thread.new do
  p Thread.current[:request_id]
  p Thread.current.thread_variable_get(:user)
end.join
p Thread.current[:request_id]
nil
"ana"
nil
nil
"r-1"

Despite the name, Thread.current[:key] is fiber-local: the new Fiber and the new Thread both see nil. The value set with thread_variable_set is truly thread-local: the fiber (which runs on the same thread) sees it and the new thread does not. The last line shows that the main fiber still has its own 'r-1'. See Threads and the GVL and Fibers and the Fiber Scheduler.

Source: Thread

Constants

A constant is a name that begins with an upper-case letter, usually written SCREAMING_SNAKE_CASE for values and CamelCase for classes and modules. Constants live inside a module (the top level is Object) and are looked up by rules that differ from the ones for variables. This section covers assigning and reassigning them, what a constant protects (very little), and how a reference is resolved.

Assigning, listing and reassigning

constants_api.rb
# frozen_string_literal: true

# Lending rules for the shelf catalogue.
module Policy
  MAX_LOANS = 5
  MAX_LOANS = 7
end
p Policy::MAX_LOANS

# Lending rules, reopened.
module Policy
  p constants.sort
  p const_get(:MAX_LOANS)
  p const_defined?(:LOST_FEE)
  p const_source_location(:MAX_LOANS)
end

Policy.const_set(:LOST_FEE, 20)
p Policy::LOST_FEE
p Policy.constants.sort
p Policy.send(:remove_const, :LOST_FEE)
p Policy.const_defined?(:LOST_FEE)

begin
  Policy::LOST_FEE
rescue NameError => e
  puts e.message
  p e.name, e.receiver
end

begin
  Policy.const_get(:lost_fee)
rescue NameError => e
  puts e.message
end
constants_api.rb:6: warning: already initialized constant Policy::MAX_LOANS
constants_api.rb:5: warning: previous definition of MAX_LOANS was here
7
[:MAX_LOANS]
7
false
["constants_api.rb", 6]
20
[:LOST_FEE, :MAX_LOANS]
20
false
uninitialized constant Policy::LOST_FEE
:LOST_FEE
Policy
wrong constant name lost_fee
  • Assigning a constant a second time is allowed. Ruby prints two warnings on standard error, already initialized constant Policy::MAX_LOANS and then previous definition of MAX_LOANS was here, and continues with the new value. The warnings appear without -w, and $VERBOSE = nil hides them. RuboCop’s Lint/ConstantReassignment cop reports the case statically; it is a newer cop that is pending, so it is not enabled by default.

  • constants returns the constants of the module and, for a module that includes others or a class with a superclass, the inherited ones; constants(false) returns only the module’s own.

  • const_get accepts a symbol or a string and raises NameError: wrong constant name lost_fee for a name that is not a valid constant name. const_defined? is the non-raising check. Both take an inherit argument: true (the default) follows the ancestors, and for a module also checks Object; false looks only in the receiver.

  • const_set creates or replaces a constant dynamically, and remove_const (a private method, so called with send here) deletes one and returns its value.

  • A failed lookup raises NameError with uninitialized constant Policy::LOST_FEE; the exception exposes name and receiver.

  • const_source_location (Ruby 2.7) returns the file and line of the last assignment, so it points at line 6, not 5.

Constant definition rules

A constant cannot be assigned inside a method body. The parser rejects it:

dynamic_constant.rb
# frozen_string_literal: true

source = <<~RUBY
  def configure
    DEFAULT_DAYS = 14
  end
RUBY

begin
  eval(source, binding, 'config.rb', 1) # rubocop:disable Security/Eval
rescue SyntaxError => e
  puts e.message
end

# Constants are fine in a class body, and const_set works inside a method.
class Config
  def self.setup = const_set(:DEFAULT_DAYS, 14)
end
p Config.setup
p Config::DEFAULT_DAYS
config.rb:2: syntax error found
  1 | def configure
> 2 |   DEFAULT_DAYS = 14
    |   ^~~~~~~~~~~~~~~~~ dynamic constant assignment
  3 | end
14
14

The SyntaxError explains dynamic constant assignment. Inside a method, use const_set (a method call, evaluated when it runs) or restructure so the constant is defined once, in the class body. Defining a constant from a method is rarely the right design; a memoized value in an instance variable of the class or module is usually what is meant. Constants may be defined in class, module and Class.new { } bodies, at the top level, and through const_set.

Blocks are a source of confusion: a block keeps the lexical scope where it was written, so a constant assigned inside Struct.new(...) do ... end or Desk.class_eval do ... end lands in the enclosing scope, usually Object. A string passed to class_eval is compiled with the receiver as its scope, so a constant assigned in it lands on the receiver.

class_eval_constants.rb
# frozen_string_literal: true

# A class whose constants are defined from outside.
class Desk
end

# A block keeps the lexical scope of where it was written (top level).
# rubocop:disable Lint/ConstantDefinitionInBlock
Desk.class_eval do
  FROM_BLOCK = 1
end
# rubocop:enable Lint/ConstantDefinitionInBlock

# A string is compiled with the receiver as the lexical scope.
Desk.class_eval('FROM_STRING = 2', __FILE__, __LINE__)

p Object.const_defined?(:FROM_BLOCK)
p Desk.const_defined?(:FROM_BLOCK, false)
p Desk.constants
true
false
[:FROM_STRING]

FROM_BLOCK ended up on Object, and FROM_STRING on Desk. RuboCop’s Lint/ConstantDefinitionInBlock is the cop for this mistake; to define a constant on another class, use const_set on that class.

Constants do not freeze values

A constant is a name that should not be rebound. It says nothing about the object it points to, which can still be mutated through any reference. The convention for collections is to freeze them, and to freeze what they contain.

constant_collections.rb
# frozen_string_literal: true

# Lending states and rules; a constant name does not make its value immutable.
module Rules
  STATES = %i[available lent overdue returned] # rubocop:disable Style/MutableConstant
  FEES = { late: [1, 2], lost: [20] }.freeze
  NAMES = %w[Dune Emma].map(&:freeze).freeze
  CALENDAR = Ractor.make_shareable({ open: %w[mon tue], closed: %w[sun] })
end

Rules::STATES << :lost
p Rules::STATES

Rules::FEES[:late] << 3 # the hash is frozen, the array inside it is not
p Rules::FEES
begin
  Rules::FEES[:extra] = []
rescue FrozenError => e
  puts e.message
end

begin
  Rules::NAMES << 'Ulysses'
rescue FrozenError => e
  puts e.message
end

begin
  Rules::CALENDAR[:open] << 'wed'
rescue FrozenError => e
  puts e.message
end

p Rules::CALENDAR.frozen?, Rules::CALENDAR[:open].frozen?
p Ractor.shareable?(Rules::FEES), Ractor.shareable?(Rules::CALENDAR)

editable = Rules::CALENDAR.dup # shallow copy: unfrozen hash, same frozen arrays
editable[:holiday] = %w[dec25]
p editable.frozen?, editable[:open].frozen?
[:available, :lent, :overdue, :returned, :lost]
{late: [1, 2, 3], lost: [20]}
can't modify frozen Hash: {late: [1, 2, 3], lost: [20]}
can't modify frozen Array: ["Dune", "Emma"]
can't modify frozen Array: ["mon", "tue"]
true
true
false
true
false
true
  • STATES was not frozen, so << changed a "constant" shared by the whole program. The Style/MutableConstant cop reports this assignment.

  • FEES is frozen, so adding a key raises FrozenError (can't modify frozen Hash: {...}). Freezing is shallow: the array stored under :late was not frozen and accepted << 3.

  • NAMES freezes each string and the array, which makes the whole structure immutable.

  • Ractor.make_shareable freezes an object graph deeply (and returns it), which is the portable way to make a constant immutable all the way down. Ractor.shareable? tests whether a value qualifies. Only shareable constants can be read from a non-main Ractor; see Ractors.

  • dup returns an unfrozen shallow copy: the hash can change, and the arrays inside are the same frozen objects.

With # frozen_string_literal: true (the standard first line of a library file), string literals are frozen. Without it, Ruby 3.4 and 4.0 treat them as chilled: mutating one works but can emit a deprecation warning under -W:deprecated. See Literals and Object Lifecycle, Freezing and Copying.

The shareable_constant_value magic comment

Ruby 3.0 added a magic comment that freezes constants for you. It applies to the constants assigned after it, in the current lexical scope, and has four modes:

Mode Effect

none (default)

no change; constants are assigned as written

literal

literals assigned to constants are deeply frozen; any other value must already be Ractor-shareable, or Ractor::IsolationError is raised

experimental_everything

every assigned value is made shareable with Ractor.make_shareable, which freezes it (and what it references) in place

experimental_copy

every assigned value is deep-copied and the copy made shareable, leaving the original untouched

The experimental_* modes are documented as experimental since 3.0; this page does not recommend them in production code. The literal mode is the safe one.

shareable_literal.rb
# frozen_string_literal: true

# shareable_constant_value: literal

# Fees in cents; with the directive, literal values are deeply frozen.
module Fees
  LATE = { per_day: [10, 25], cap: 500 }
  CODES = %w[L M]
end

p Fees::LATE.frozen?, Fees::LATE[:per_day].frozen?
p Ractor.shareable?(Fees::CODES)

begin
  Fees::CODES << 'X'
rescue FrozenError => e
  puts e.message
end

# const_set is not affected by the directive.
Fees.const_set(:LIST, [])
p Fees::LIST.frozen?
true
true
true
can't modify frozen Array: ["L", "M"]
false

Both LATE and its inner array are frozen without any .freeze call, and CODES << 'X' raises FrozenError. The directive does not touch const_set, so Fees::LIST stays unfrozen. A value that is not a literal is rejected:

shareable_rejected.rb
# frozen_string_literal: true

# shareable_constant_value: literal

# A value built at load time is not a literal, so it must already be shareable.
reader = Struct.new(:name).new('Ana')
begin
  CURRENT_READER = reader
rescue Ractor::IsolationError => e
  puts "#{e.class}: #{e.message}"
end
Ractor::IsolationError: cannot assign unshareable object to CURRENT_READER

Ractor::IsolationError says cannot assign unshareable object to CURRENT_READER. To fix it, build the object from a literal, freeze it, or call Ractor.make_shareable(reader) before assigning. Because the directive is read per scope, a file can switch modes by repeating the comment, and it covers only the module or class body where it appears.

Lookup: lexical scope, then ancestors

A reference to a bare constant NAME is resolved in this order:

  1. The lexical scopes in Module.nesting, from the innermost outward, each searched for its own constants only (not its ancestors). The top-level Object is not part of this step.

  2. The ancestors of the innermost lexical scope (Module.nesting.first), in method-resolution order, including included modules and the superclass chain. For a class this reaches Object.

  3. If the innermost scope is a module (which has no Object among its ancestors), Object is searched too, so top-level constants stay visible.

  4. If nothing was found, Ruby calls const_missing(:NAME) on the innermost scope. The default raises NameError.

flowchart TD A["Bare constant NAME is referenced"] --> B{"Defined directly in a scope of
Module.nesting, innermost first?"} B -->|yes| Z["Use that constant"] B -->|no| C{"Defined in the ancestors of
Module.nesting.first?"} C -->|yes| Z C -->|no| D{"Innermost scope is a module?
Then check Object as well"} D -->|found| Z D -->|not found| E["Call const_missing with :NAME
on the innermost scope"] E -->|returns a value| Z E -->|default: raises| F["NameError: uninitialized constant"]

The point is that the text of the program decides which scopes count, not the class of the object running the method.

lexical_lookup.rb
# frozen_string_literal: true

DAYS = 'top level'

# A kind of lendable item.
class Media
  KIND = 'media'
  DAYS = 'media'
end

# Lexical scope is searched before ancestors.
module Lending
  KIND = 'lending'
  p Module.nesting

  # A book that can be lent.
  class Book < Media
    p Module.nesting

    # Lending::KIND is found lexically, before Media::KIND.
    def kind = KIND

    # Not in Lending, so Media::DAYS is found through the ancestors.
    def days = DAYS

    # Nothing nearer, so Object (through the ancestors) provides it.
    def comparable = Comparable
  end
end

# The compact form: Lending is not in Module.nesting, so its constants are not visible.
# rubocop:disable Style/ClassAndModuleChildren
class Lending::Dvd < Media
  p Module.nesting

  # Media::KIND, not Lending::KIND.
  def kind = KIND
end
# rubocop:enable Style/ClassAndModuleChildren

p Lending::Book.new.kind
p Lending::Book.new.days
p Lending::Book.new.comparable
p Lending::Dvd.new.kind
[Lending]
[Lending::Book, Lending]
[Lending::Dvd]
"lending"
"media"
Comparable
"media"

Source: Module.nesting

  • Module.nesting is [Lending] at the module level and [Lending::Book, Lending] inside Book. It lists the lexical scopes, innermost first.

  • Book#kind finds KIND in Lending (step 1) before it ever reaches Media::KIND (step 2, through the superclass). A nearer lexical definition beats an inherited one.

  • Book#days has no DAYS in Lending, so step 2 finds Media::DAYS through the ancestors, not the top-level DAYS.

  • Book#comparable finds Comparable through Object, which is in `Book’s ancestors.

  • With the compact class Lending::Dvd < Media, Lending is not in Module.nesting ([Lending::Dvd]), so Dvd#kind skips Lending::KIND and returns the inherited 'media'. The nested form (module Lending; class Dvd) and the compact form are not interchangeable when constants are involved. RuboCop’s Style/ClassAndModuleChildren prefers the nested form.

A constant reached through a mixin is an ancestor constant, so it is not seen from every lexical position:

mixin_lookup.rb
# frozen_string_literal: true

# Constants come from the lexical scope, then the ancestors, including mixins.
module Greeting
  WORD = 'hello'
end

# A class that mixes the module in and defines one constant of its own.
class Desk
  include Greeting

  OWN = 'own'

  def word = WORD

  class << self
    # Desk is in the lexical scope of the singleton class body.
    def own_word = OWN

    # Greeting is only an ancestor of Desk, and lexical lookup ignores ancestors.
    def mixin_word = WORD
  end
end

p Desk.new.word, Desk.own_word, Desk::WORD
begin
  Desk.mixin_word
rescue NameError => e
  puts e.message
end
p Desk.const_defined?(:WORD, false)
p Desk.constants.sort
"hello"
"own"
"hello"
uninitialized constant #<Class:Desk>::WORD
false
[:OWN, :WORD]

Inside class << self, the lexical scope includes Desk (so OWN, defined directly in Desk, is found), but the ancestors searched are those of the singleton class, which does not include Greeting. Desk::WORD and the instance method word both work, since they search the ancestors of Desk. Desk.constants lists WORD because the inherited ones count, while const_defined?(:WORD, false) is false. When a mixin’s constant must be visible from class methods, qualify it (Greeting::WORD).

Qualified lookup and the top level

The :: operator looks a constant up in one specific module and its ancestors, skipping the lexical scopes. A leading :: starts at Object.

scoped_lookup.rb
# frozen_string_literal: true

DAYS = 'top level'

# A kind of lendable item.
class Media
  KIND = 'media'
end

# Items that can be lent.
module Lending
  # A book that can be lent.
  class Book < Media
  end
end

p Lending::Book::KIND # scoped lookup: Book, then its ancestors
p ::DAYS              # an explicit top-level constant
p Lending.const_get(:DAYS)
begin
  Lending.const_get(:DAYS, false) # inherit = false: only Lending itself
rescue NameError => e
  puts e.message
end

begin
  Lending::Book::String # scoped lookup does not fall back to Object
rescue NameError => e
  puts e.message
end
p Lending::Book.const_get(:String) # const_get does
p Lending::Book.const_get('Media::KIND')
"media"
"top level"
"top level"
uninitialized constant Lending::DAYS
uninitialized constant Lending::Book::String
String
"media"
  • Lending::Book::KIND finds Media::KIND through `Book’s ancestors.

  • ::DAYS names the top-level constant explicitly, which is useful when a nearer DAYS would otherwise win.

  • Lending.const_get(:DAYS) returns the top-level value, because const_get with inherit true checks Object for a module. With false it raises uninitialized constant Lending::DAYS.

  • A scoped reference such as Lending::Book::String does not fall back to Object; it raises NameError. Ruby 2.5 removed the old behaviour that returned top-level constants through a class path with a warning. Book.const_get(:String) still reaches it, because const_get follows Object by design.

  • const_get accepts a path as a string, 'Media::KIND', and walks it one segment at a time.

Top-level constants are defined on Object. Every class and every module (through step 3) can therefore see them, and a top-level class Policy is simply the constant Object::Policy. Keep application constants in a namespace so they do not collide with a gem’s constants.

Hiding and retiring constants

private_constant makes a constant unreachable with the Module::NAME syntax while leaving it usable by name inside the module. deprecate_constant keeps a constant working but makes references to it print a deprecation warning.

private_deprecated_constants.rb
# frozen_string_literal: true

# Item kinds known to the catalogue.
module Kinds
  BOOK = :book
  LEGACY_MAGAZINE = :magazine
  INTERNAL_CODES = %w[b m].freeze

  private_constant :INTERNAL_CODES
  deprecate_constant :LEGACY_MAGAZINE

  # Inside the module the private constant is an ordinary name.
  def self.codes = INTERNAL_CODES
end

p Kinds.codes
p Kinds::LEGACY_MAGAZINE

begin
  Kinds::INTERNAL_CODES
rescue NameError => e
  puts e.message
end
p Kinds.constants
p Kinds.const_defined?(:INTERNAL_CODES)
p Kinds.const_get(:INTERNAL_CODES) # const_get ignores private_constant
["b", "m"]
private_deprecated_constants.rb:17: warning: constant Kinds::LEGACY_MAGAZINE is deprecated
:magazine
private constant Kinds::INTERNAL_CODES referenced
[:BOOK, :LEGACY_MAGAZINE]
true
["b", "m"]

The output is for ruby -W:deprecated private_deprecated_constants.rb; deprecation warnings are off in a plain run.

  • Inside the module, INTERNAL_CODES is an ordinary name, so Kinds.codes works.

  • Kinds::INTERNAL_CODES raises NameError: private constant Kinds::INTERNAL_CODES referenced.

  • constants omits private constants, but const_defined? still says true, and const_get ignores the privacy and returns the value. Private constants are an API signal, not a security mechanism.

  • public_constant reverses private_constant.

  • If the module defines const_missing, a reference to one of its private constants calls const_missing instead of raising the privacy error:

private_const_missing.rb
# frozen_string_literal: true

# A module that hides one constant and also defines const_missing.
module Vault
  SECRET = 'demo'
  private_constant :SECRET

  def self.const_missing(name)
    "const_missing called for #{name}"
  end
end

puts Vault::SECRET # the private constant reaches const_missing
const_missing called for SECRET

That interaction can hide a mistake. If you define const_missing and also use private_constant, check that the hook re-raises with super for names it does not recognise.

const_missing, const_added and autoload

When lookup fails, const_missing(name) is called on the innermost scope. Its result becomes the value of the reference. Code loaders use it to load a file on demand and gems use it to generate constants.

const_missing_hook.rb
# frozen_string_literal: true

# Shelf codes resolved on demand: Codes::FICTION gives 'F', Codes::HISTORY gives 'H'.
module Codes
  # Called when a constant is not found anywhere in the lookup path.
  def self.const_missing(name)
    code = name.to_s[0]
    return super if code.nil? || name.to_s.length < 4

    const_set(name, code) # define it so const_missing runs only once
  end
end

p Codes::FICTION
p Codes.constants
begin
  Codes::X
rescue NameError => e
  puts e.message
end
"F"
[:FICTION]
uninitialized constant Codes::X

const_set inside the hook defines the constant so the hook runs only once per name. return super for names the hook does not handle keeps the standard NameError. A const_missing that returns a value without defining the constant runs again for every reference, and one that hides typos by returning a default makes failures harder to find; use it with care. The standard, declarative alternative is Module#autoload (and, for whole applications, a code loader such as Zeitwerk), covered in Code Loading: require and autoload.

Module#const_added (Ruby 3.2) is the opposite hook: it runs each time a constant is defined, including constants that name nested classes and modules.

const_added_hook.rb
# frozen_string_literal: true

# Module#const_added (3.2) runs every time a constant is defined.
class Registry
  def self.const_added(name)
    super
    puts "defined #{self}::#{name}"
  end

  LIMIT = 3

  # A nested class is a constant too.
  class Entry; end
end

p Ruby::VERSION == RUBY_VERSION
defined Registry::LIMIT
defined Registry::Entry
true

Define const_added before the constants it should observe, and call super. The hook gives registries, DSLs and documentation tools a supported alternative to polling constants.

Naming anonymous classes

A class created with Class.new has no name until it is assigned to a constant. The first constant that receives it names it, and later constants are aliases that leave the name alone.

naming_classes.rb
# frozen_string_literal: true

# An anonymous class gets its name the first time it is assigned to a constant.
branch = Class.new
p branch.name
Annex = branch # rubocop:disable Naming/ConstantName
p branch.name, branch
Other = branch # rubocop:disable Naming/ConstantName
p Other.name # the first constant wins

temp = Module.new.set_temporary_name('fake<shelf>') # 3.3
p temp
Permanent = temp # rubocop:disable Naming/ConstantName
p temp.name
nil
"Annex"
Annex
"Annex"
fake<shelf>
"Permanent"

Module#set_temporary_name (Ruby 3.3) gives an anonymous class or module a name for debugging, and the name must not be a valid constant path (fake<shelf> is fine, Fake is rejected). Assigning the module to a real constant later replaces the temporary name with the permanent one.

Checking existence with defined?

defined?(expression) returns a short description of the expression or nil, without evaluating the expression. It is a keyword, not a method, and it never raises NameError for the name it checks.

defined_operator.rb
# frozen_string_literal: true

LIMIT = 3
title = 'Dune'
@reader = 'Ana'

checks = {
  local: defined?(title),
  ivar: defined?(@reader),
  missing_ivar: defined?(@other),
  constant: defined?(LIMIT),
  method: defined?(puts),
  call: defined?(title.upcase),
  bad_call: defined?(title.nothing),
  assign: defined?(_copy = title),
  self: defined?(self),
  literal: defined?(42),
  nil: defined?(nil),
  operator: defined?(1 + 1),
  global: defined?($stdout),
  undefined: defined?(nothing_here)
}
checks.each { |key, value| puts "#{key}: #{value.inspect}" }

def with_block = defined?(yield)

p with_block
p(with_block { nil })

# A base type and a subtype that asks whether super exists.
class Base
  def greet = 'hi'
end

# The subtype.
class Child < Base
  def greet = defined?(super)
  def other = defined?(super)
end

p Child.new.greet
p Child.new.other
local: "local-variable"
ivar: "instance-variable"
missing_ivar: nil
constant: "constant"
method: "method"
call: "method"
bad_call: nil
assign: "assignment"
self: "self"
literal: "expression"
nil: "nil"
operator: "method"
global: "global-variable"
undefined: nil
nil
"yield"
"super"
nil
Expression Result

local variable

"local-variable"

@ivar

"instance-variable"; nil when not set

@@cvar

"class variable"; nil when not defined

$global

"global-variable"; nil when never assigned

constant

"constant", without calling const_missing

method call

"method", only when the method exists and is callable from here

yield, super

"yield" or "super" when a block was given or a super method exists

assignment

"assignment"; the assignment is not executed

self, nil, true, false

"self", "nil", "true", "false"

other literals

"expression"

$1 and friends

"global-variable" when the group matched, otherwise nil

Use it for optional integration (defined?(Rails)), to guard yield, and to check a variable without reading it. Prefer respond_to? when a method on an object matters, since defined?(obj.meth) only reports whether the call could be made, and prefer Module#const_defined? or const_get when the name arrives as a string.

Variables and constants in the shelf gem

The shelf gem keeps its mutable state in one place and its fixed data in constants. Shelf.config is a module-level instance variable created lazily with ||=, and Shelf::Loan::STATES is a frozen constant array.

shelf_state.rb
# frozen_string_literal: true

require 'shelf'

p Shelf.instance_variables # no state until first use
p Shelf.config.loan_days
p Shelf.instance_variables # @config was created lazily by ||=
p Shelf.constants.sort
p Shelf::Loan::STATES, Shelf::Loan::STATES.frozen?
p Shelf::Loan.constants
p Shelf::Loan.name, Shelf::Loan.const_source_location(:STATES).last

Shelf.configure { |config| config.loan_days = 21 }
p Shelf.config.loan_days
p Shelf::Config.members
[]
14
[:@config]
[:Author, :Book, :Catalog, :Config, :Error, :Isbn, :Loan, :Refinements, :Store, :VERSION]
[:available, :lent, :overdue, :returned]
true
[:STATES]
"Shelf::Loan"
6
21
[:store, :loan_days]
  • Shelf.instance_variables is empty until the first call to Shelf.config, which creates @config and caches it. The module itself is the object holding the variable.

  • Loan::STATES is frozen, listed by Shelf::Loan.constants, and const_source_location(:STATES) points at line 6 of the file that defined it.

  • Shelf.configure yields the configuration object, so the same instance is updated and read back as 21 afterwards.

  • The @config ||= ... idiom is not thread-safe if two threads race to initialize it; initialize eagerly at load time, or guard with a Mutex as Catalog does.

Naming conventions

Name Convention Example and RuboCop cop

Local variable, instance variable, method

snake_case

loan_days, @loan_days (Naming/VariableName)

Class, module

CamelCase; acronyms stay upper-case

Catalog, Store::Memory, HTTPClient (Naming/ClassAndModuleCamelCase)

Constant

SCREAMING_SNAKE_CASE

MAX_LOANS (Naming/ConstantName)

Unused value

a leading underscore, or _ alone

_copies, _

Predicate method

ends with ?; it returns a truthy or falsy value, not necessarily true or false

borrowed?

Dangerous or in-place method

ends with ! and has a safer twin without it

sort!

Global variable

$snake_case, and rarely

$shelf_log_level (Style/GlobalVars)

The ? and ! suffixes belong to method names, so a variable never ends with them. The Ruby style guide, linked from the references below, is the community source for these rules, and Idioms, Style and Clean Code collects the rest.

Coming from other languages

Language Familiar idea In Ruby

JavaScript

let/const have block scope; const forbids rebinding

if and while bodies share the method’s scope, and only blocks nest; a constant can be rebound with a warning, and the object stays mutable unless frozen

Python

global, nonlocal; module-level names

blocks close over outer variables and assign to them with no keyword; a bare name inside def never reaches the enclosing scope; $globals are the only process-wide variables

Java, C#

static fields and final

a class-level instance variable or a constant replaces a static field; a class variable @@x is shared with subclasses, unlike a Java static in an unrelated subclass

Go

package-level var and const; capitalisation decides export

module constants are public unless private_constant is used, and $ globals are process-wide

Rust

immutable by default, shadowing with let

everything is mutable unless frozen; reassigning a local simply replaces the binding

Where older code and books differ

Code and books written for Ruby 2.x or early 3.x show behaviour that has changed. The table lists the differences for the topics on this page.

Older text or code Current behaviour (Ruby 4.0)

An error such as +undefined local variable or method `limit' for main:Object+

undefined local variable or method 'limit' for main: the receiver is shown without :Object (3.3) and quotes are straight (3.4)

Reading an unset @ivar warns under -w

no warning since 3.0

An overtaken class variable only warns, and class variables are accepted at the top level

RuntimeError for both since 3.0

Foo::String resolves to the top-level String with a warning

NameError since 2.5; use ::String or const_get

$SAFE, taint, trust

the taint methods have had no effect since 2.7 and $SAFE became an ordinary global in 3.0; remove them

Setting $, and $; is a normal way to change join and split

deprecated since 2.7; pass the separator as an argument

Hash#inspect prints {:name=>"Ico"}

{name: "Ico"} since 3.4, which is also what p shows in the outputs above

Block parameters named like an outer local warn about shadowing

no warning since 2.6

_1 is the only implicit parameter

it also works since 3.4

Constants are frozen "by being constants"

they are not; freeze them, or use shareable_constant_value: literal (3.0)

A constant written inside Struct.new do ... end belongs to the struct

it belongs to the enclosing scope; use const_set on the struct, or a normal class body

'Foo'.safe_constantize shown as plain Ruby

it is an ActiveSupport method and not part of core Ruby; core Ruby has Object.const_get('Foo'), which raises NameError for an unknown name, and building constants from untrusted input is a security risk

Memoized `@instance

= new` singletons

not thread-safe; see Singleton Classes and Class-Level Behavior for the Singleton module

Common mistakes

Mistake Fix

count = 5 in a method meant to call count=

write self.count = 5

A variable assigned in an if branch that did not run is nil, not undefined

initialize it before the branch, or test it with nil?

Expecting a def to see a local of the surrounding file or class

pass it as an argument, store it in a constant or instance variable, or use define_method

@@setting shared by a whole class hierarchy

use a class-level @setting with a reader, and copy it in inherited

Using $globals for configuration or request state

use an object, a constant, Thread.current or thread_variable_get

Treating pass:c[NAME = [...\]] as immutable

freeze it (deeply), or use shareable_constant_value: literal

Redefining a constant in a gem or in tests

reopen the module and use const_set only intentionally; the "already initialized constant" warning is the signal

Kinds::NAME raises after private_constant

read it inside the module, or expose a method

Relying on a constant being visible inside class << self

qualify it with its module

Defining constants inside a method or via Struct.new do

use const_set, or a class body

Metaprogramming with eval strings for names

use const_get, instance_variable_get and define_method

See also Common Mistakes and Gotchas for the rest of the section.

Version notes

Release Change

Ruby 2.5

Foo::String no longer finds top-level constants through a class

Ruby 2.6

no more "shadowing outer local variable" warning for block parameters

Ruby 2.7

Module#const_source_location; _1 to _9 implicit parameters; setting $, and $; to non-nil values is deprecated; $SAFE warns on access

Ruby 3.0

uninitialized instance variable reads do not warn; overtaken class variables and toplevel class-variable access raise RuntimeError; $SAFE is an ordinary global; shareable_constant_value magic comment; assigning to _1 is a SyntaxError

Ruby 3.2

Module#const_added; object shapes for instance variables

Ruby 3.3

Module#set_temporary_name; error messages show for an instance of X and for main

Ruby 3.4

error messages use ' instead of a backtick; it implicit parameter; Hash#inspect uses key: value; the top-level name ::Ruby is reserved

Ruby 4.0

Ruby module defined; Object#inspect honours instance_variables_to_inspect; Binding#local_variables omits implicit parameters and Binding#implicit_parameters was added; Set is a core class

Ruby 4.1 (preview)

not released at the time of writing; this page makes no claims about it

References