Polyglot
Languages Ruby scope
Ruby § scope

Scope and visibility

Ruby has five kinds of variables, each with distinct scoping rules: local (per method/block), instance (per object, prefixed with @), class (per class, prefixed with @@), global (program-wide, prefixed with $), and constants (visible from the lexical scope outward, named with an uppercase initial). The principal scoping unit is the scope gatedef, class, and module create new local-variable scopes; blocks do not (they share the enclosing scope). The self reference indicates the current object — its meaning depends on the method’s receiver. The combination — five variable kinds, scope-gating constructs, the conventional self semantics, lexical constant lookup — is the substance of Ruby’s scope model.

Local variables

The conventional default — visible within the current method, block, or top-level scope:

def example
  x = 5                                           # local to example
  y = 10
  x + y
end

# x and y are not visible here

Local variables are not declared — assignment creates them. The first assignment to a name introduces it.

A subtle pitfall: a method with the same name as a local variable is shadowed:

foo = 5
def foo                                           # method definition (different scope)
  10
end

puts foo                                          # 5 (local variable wins)

The conventional discipline uses distinct naming.

Block scoping

Blocks share the enclosing local scope:

x = 10
[1, 2, 3].each do |n|
  x += n                                          # modifies outer x
end
puts x                                            # 16

# Block parameters and block-local variables don't leak:
[1, 2, 3].each do |n|
  inner = n * 2                                   # local to block (since 2.0)
end
# n and inner not visible here

Block parameters are local to the block. The semicolon syntax admits explicit block-local variables:

n = 100
[1, 2, 3].each do |item; n|                       # n is block-local; doesn't shadow outer
  n = item * 2
end
puts n                                            # 100 (unchanged)

The form is rare; the conventional discipline uses distinct names.

Instance variables @

Each object has its own instance variables:

class Person
  def initialize(name)
    @name = name                                  # instance variable
  end

  def greet
    puts "Hello, #{@name}"
  end
end

p1 = Person.new("Alice")
p2 = Person.new("Bob")
p1.greet                                          # "Hello, Alice"
p2.greet                                          # "Hello, Bob"

Instance variables:

  • Are local to the object.
  • Default to nil if read before assignment (no error).
  • Are not visible from outside without an explicit accessor (attr_reader, etc.).
p1.instance_variable_get(:@name)                  # "Alice"
p1.instance_variable_set(:@name, "Charlie")
p1.instance_variables                              # [:@name]

Treated in Classes and OOP.

Class variables @@

Shared across instances and across the inheritance hierarchy:

class Counter
  @@count = 0

  def increment
    @@count += 1
  end

  def self.total
    @@count
  end
end

c1 = Counter.new
c1.increment
c2 = Counter.new
c2.increment
puts Counter.total                                # 2 (shared)

A pitfall: class variables are shared with subclasses — modification in a subclass affects the parent:

class A
  @@x = "from A"
end

class B < A
  @@x = "from B"                                  # mutates the SAME @@x
end

class A
  puts @@x                                        # "from B"
end

The conventional contemporary discipline avoids @@class instance variables (@x on the class, not on an instance) are the conventional alternative:

class Counter
  @count = 0                                      # class instance variable

  class << self                                   # singleton class
    attr_accessor :count
  end

  def increment
    self.class.count += 1
  end
end

Global variables $

Visible from anywhere:

$debug = true

def log(msg)
  puts msg if $debug
end

Global variables are strongly discouraged in idiomatic Ruby. The standard library uses several built-in globals:

$stdin                                            # alias STDIN
$stdout                                           # alias STDOUT
$stderr                                           # alias STDERR
$0                                                # program name
$:                                                # load path (alias $LOAD_PATH)
$LOAD_PATH                                        # load path
$!                                                # last raised exception
$~                                                # last regex match data

For configuration, constants in modules are conventionally preferred:

module Config
  DEBUG = true
end

if Config::DEBUG then ... end

Constants

Identifiers with an uppercase initial are constants:

PI = 3.14159
MAX_RETRIES = 3

class Calculator
  ROUND_DIGITS = 4

  def calculate(x)
    x.round(ROUND_DIGITS)                         # accessed lexically
  end
end

# Outside the class:
Calculator::ROUND_DIGITS                          # 4

Constants:

  • Have lexical lookup — they’re visible from the scope where they’re defined and inwards.
  • Produce a warning (not an error) when reassigned.
  • Are accessed across scopes via ::.
PI = 3.14
PI = 3.14159                                      # warning, but admitted

The convention treats constants as immutable; the language does not enforce strict immutability.

The :: admits accessing nested constants:

module Outer
  class Inner
    Value = 42
  end
end

Outer::Inner::Value                                # 42

Constant lookup

Ruby’s constant lookup is lexical (based on where the code is defined), then traverses the ancestor chain:

class Animal
  KIND = "Animal"
end

class Dog < Animal
  def kind
    KIND                                          # "Animal" (looks up in ancestor chain)
  end
end

Dog.new.kind                                      # "Animal"

The conventional discipline:

  • Lexical scope: the surrounding module or class blocks.
  • Inheritance chain: the receiver’s class and its ancestors.

self

The self reference indicates the current object:

class Person
  def initialize(name)
    @name = name
  end

  def name
    @name                                         # equivalent to self.name (but no method call)
  end

  def name=(value)
    @name = value                                 # NOT a method call — assigns local
                                                  # Use self.name = value to call setter
  end

  def rename(new_name)
    self.name = new_name                          # OK; explicit method call
    @name = new_name                              # also OK; direct instance var
  end
end

The conventional pitfall: name = value inside a method assigns a local variable, not the setter. The defence: self.name = value.

In different contexts, self refers to different objects:

class Foo
  # self is Foo (the class object)

  def instance_method
    # self is the instance
  end

  def self.class_method
    # self is Foo (the class)
  end
end

Scope gates

Three constructs introduce new scopes — they don’t share local variables with the enclosing context:

x = 5

class Foo
  # x is NOT visible here
  y = 10
end
# y is NOT visible here

module Bar
  # new scope
end

def foo
  # new scope
end

Blocks (curly braces or do/end) do not introduce new scopes for local variables — they share with the enclosing method.

The mechanism admits methods being defined without polluting outer scopes; blocks admit substantial closure-style flexibility.

nil and unbound variables

Reading an unbound local variable raises NameError:

puts undefined_var                                # NameError

Reading an unbound instance variable returns nil:

class Foo
  def get
    @undefined_ivar                               # returns nil (with warning)
  end
end

The asymmetry is conventional Ruby behaviour; the conventional discipline initialises instance variables in initialize.

defined?

The defined? operator returns a string describing the kind, or nil:

defined?(x)                                       # "local-variable" if defined, else nil
defined?(@x)                                      # "instance-variable" or nil
defined?(@@x)                                     # "class variable" or nil
defined?(PI)                                      # "constant" or nil
defined?(self)                                    # "self"
defined?(puts)                                    # "method"

Conventional uses are rare; nil? and respond_to? are conventional alternatives.

Visibility (private, protected, public)

Methods admit visibility modifiers:

class Account
  def deposit(amount)                             # public (default)
    @balance += amount
    log_transaction("deposit", amount)
  end

  private                                          # everything below is private

  def log_transaction(kind, amount)
    # only callable from within instances of Account
  end
end

Three visibility modifiers:

  • public — callable from anywhere (default).
  • private — callable only on the implicit self.
  • protected — callable on any instance of the class (or subclass).
class A
  def public_method; end

  private
  def private_method; end

  protected
  def protected_method; end
end

a = A.new
a.public_method                                   # OK
a.private_method                                  # NoMethodError
a.protected_method                                # NoMethodError (from outside)

Ruby 3.0+ admits private def method_name:

class A
  private def helper
    # ...
  end
end

Treated in Classes and OOP.

Closures and scope

Blocks, procs, and lambdas close over the enclosing scope:

def make_counter
  count = 0
  -> { count += 1 }                               # captures count
end

counter = make_counter
counter.call                                      # 1
counter.call                                      # 2

Each call to make_counter produces a new counter with its own count.

Treated in Blocks and procs.

Common patterns

Memoization with ||=

def expensive
  @expensive ||= compute_expensive
end

The ||= is the conventional Ruby memoization idiom; the @expensive is initialised only on the first call.

Module-scoped configuration

module Config
  HOST = "localhost"
  PORT = 8080
  DEBUG = ENV["DEBUG"] == "1"
end

# Usage:
Config::HOST
Config::DEBUG

Private helpers

class Service
  def process(input)
    validated = validate(input)
    transformed = transform(validated)
    save(transformed)
  end

  private

  def validate(input)
    # ...
  end

  def transform(data)
    # ...
  end

  def save(data)
    # ...
  end
end

The conventional discipline keeps the public surface small; helpers are private.

Singleton class constants

class HttpClient
  DEFAULT_TIMEOUT = 30

  def fetch(url, timeout: DEFAULT_TIMEOUT)
    # ...
  end
end

Class instance variables instead of @@

class Counter
  @count = 0

  class << self
    attr_accessor :count
  end

  def initialize
    self.class.count += 1
  end
end

Counter.new
Counter.new
Counter.count                                     # 2

Lexical constants

module Geometry
  PI = 3.14159

  class Circle
    def area(r)
      PI * r ** 2                                 # found via lexical scope
    end
  end
end

Global escape with $

For genuinely program-wide state (rare):

$start_time = Time.now

at_exit { puts "elapsed: #{Time.now - $start_time}" }

A note on the conventional discipline

The contemporary Ruby scope advice:

  • Use local variables freely; they’re cheap and clear.
  • Use instance variables (@) for object state.
  • Avoid class variables (@@) — use class instance variables (@) on the class instead.
  • Avoid global variables ($) — use module constants.
  • Use constants for genuinely constant values.
  • Use attr_accessor and friends to expose instance variables.
  • Use private to keep the public API small.
  • Use ||= for memoization.
  • Use self.method_name = value in setters (not bare method_name = value).
  • Avoid setter pitfalls — the receiver-less form creates a local.

The combination — five variable kinds, scope-gating constructs (def, class, module), block-shared scopes, lexical constant lookup, the self reference — is the substance of Ruby’s scope model. The discipline produces predictable scope behaviour with a few well-known pitfalls.