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 gate — def, 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
nilif 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
moduleorclassblocks. - 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 implicitself.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_accessorand friends to expose instance variables. - Use
privateto keep the public API small. - Use
||=for memoization. - Use
self.method_name = valuein setters (not baremethod_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.