Variables #
Ruby is a dynamically typed language — you don’t need to declare a data type before using a variable. But that doesn’t mean Ruby has no rules about variables. Quite the opposite: Ruby is very opinionated about where a variable can be accessed. Ruby’s variable scope system is marked visually through the variable name’s prefix — so just by looking at a variable name, you already know its scope of access. Understanding this system well is the foundation for writing correct Ruby code, especially once you start working with classes and objects.
Variable Scope and Prefix System #
In Ruby, a variable’s type is determined by the first character of its name. This isn’t just a convention — the Ruby interpreter reads this prefix to determine how the variable is stored and where it can be accessed from.
| Prefix | Type | Example | Scope |
|---|---|---|---|
Lowercase / _ | Local variable | name, _temp | Within the current method or block |
@ | Instance variable | @name | Within the entire object instance |
@@ | Class variable | @@count | Within the class and all its instances |
$ | Global variable | $mode | Throughout the program |
| Uppercase | Constant | MAX, PI | Within the class or module (covered in the Constants article) |
flowchart TD
A[What does the variable name start with?] --> B["Lowercase / _"]
A --> C["@"]
A --> D["@@"]
A --> E["$"]
A --> F["UPPERCASE"]
B --> B1["Local Variable\nOnly within this method/block"]
C --> C1["Instance Variable\nWhole object, per-instance"]
D --> D1["Class Variable\nShared across all class instances"]
E --> E1["Global Variable\nAnywhere in the program"]
F --> F1["Constant\nCovered in the next article"]Local Variables #
Local variables are the type you’ll use most in day-to-day work. They can only be accessed within the method, block, or proc where they’re defined — no more, no less.
def calculate_installment(price, down_payment_pct, tenure_months)
down_payment = price * down_payment_pct # local variable
remaining = price - down_payment # local variable
installment = remaining / tenure_months # local variable
installment.round(2)
end
puts calculate_installment(120_000_000, 0.3, 36) # => 2333333.33
# None of the local variables above can be accessed from here:
puts down_payment # => NameError: undefined local variable or method 'down_payment'
puts remaining # => NameError: undefined local variable or method 'remaining'
Local Variable Scope Inside Blocks #
A block creates a new scope for local variables defined inside it. But local variables that already exist outside the block remain accessible from within it — this is what distinguishes it from scope in languages like Java or C.
total = 0
products = ["laptop", "mouse", "keyboard"]
prices = [12_000_000, 350_000, 450_000]
products.each_with_index do |name, i|
subtotal = prices[i] # new local variable inside the block
total += subtotal # accessing 'total' from the outer scope — valid
puts "#{name}: #{subtotal}"
end
puts "Total: #{total}" # => Total: 12800000
puts subtotal # => NameError: 'subtotal' only lives inside the block
Block-Local Variables #
Ruby provides an explicit way to ensure a variable inside a block won’t modify a variable with the same name in the outer scope — using a semicolon (;) in the block parameters.
value = 100
# ANTI-PATTERN: block without a block-local variable
# the 'value' variable inside the block refers to the same 'value' outside
[1, 2, 3].each do |n|
value = n * 10 # ← this modifies 'value' in the outer scope!
end
puts value # => 30 (not 100 as you might expect)
# CORRECT: declare it as a block-local variable with ;
value = 100
[1, 2, 3].each do |n; value| # 'value' after ; is a block-local variable
value = n * 10 # this does NOT modify the outer 'value'
puts value
end
puts value # => 100 (unchanged)
Multiple Assignment #
Ruby supports assigning several variables at once in a single line — a feature called parallel assignment or destructuring.
# Simple simultaneous assignment
a, b, c = 1, 2, 3
puts "#{a}, #{b}, #{c}" # => 1, 2, 3
# Swapping two variables — the idiomatic Ruby way, no temp variable
x, y = 10, 20
x, y = y, x
puts "x=#{x}, y=#{y}" # => x=20, y=10
# Destructuring from an array
coordinates = [3.14, 2.71, 1.41]
pi, e, sqrt2 = coordinates
puts pi # => 3.14
puts e # => 2.71
# Splat operator to capture the "rest"
first, *rest = [1, 2, 3, 4, 5]
puts first.inspect # => 1
puts rest.inspect # => [2, 3, 4, 5]
*start, last = [1, 2, 3, 4, 5]
puts start.inspect # => [1, 2, 3, 4]
puts last # => 5
# From a hash (Ruby 3.1+)
data = {name: "Rina", city: "Surabaya"}
name, city = data.values_at(:name, :city)
puts "#{name} from #{city}" # => Rina from Surabaya
Instance Variables #
Instance variables are variables attached to a particular object. They live as long as the object lives, and each object of the same class has its own completely separate copy of its instance variables.
class Account
def initialize(number, owner, initial_balance = 0)
@number = number # instance variable
@owner = owner # instance variable
@balance = initial_balance # instance variable
@history = [] # instance variable — empty array per object
end
def deposit(amount)
@balance += amount
@history << {type: :deposit, amount: amount, time: Time.now}
self # return the object for method chaining
end
def withdraw(amount)
raise "Insufficient balance" if amount > @balance
@balance -= amount
@history << {type: :withdraw, amount: amount, time: Time.now}
self
end
def info
"Account #{@number} | Owner: #{@owner} | Balance: #{@balance}"
end
end
acc1 = Account.new("001", "Andi", 500_000)
acc2 = Account.new("002", "Budi", 1_000_000)
acc1.deposit(200_000).deposit(100_000) # method chaining thanks to return self
acc2.withdraw(300_000)
puts acc1.info # => Account 001 | Owner: Andi | Balance: 800000
puts acc2.info # => Account 002 | Owner: Budi | Balance: 700000
# @balance of acc1 and acc2 are completely separate
Default Values of Instance Variables #
An uninitialized instance variable evaluates to nil — not an error. This can be a source of subtle bugs if you don’t anticipate it.
class User
def show_name
# @name may not have been set yet — could be nil
puts @name
end
def set_name(name)
@name = name
end
end
u = User.new
u.show_name # => nil (no error, but probably not what you want)
u.set_name("Citra")
u.show_name # => Citra
# CORRECT: initialize all instance variables in initialize
class User
def initialize(name = nil)
@name = name
@email = nil
@active = false
@logged_in = nil
end
def show_name
@name || "Anonymous User" # fallback if nil
end
end
Class-Level Instance Variables #
Instance variables aren’t limited to the object level — the class itself is also an object in Ruby, so it can have its own instance variables. This is different from class variables (@@).
class Config
# @timeout is an instance variable belonging to the CLASS (not instances)
@timeout = 30
@max_retry = 3
class << self
attr_accessor :timeout, :max_retry
end
end
puts Config.timeout # => 30
Config.timeout = 60
puts Config.timeout # => 60
# Subclasses have their own instance variables — no sharing with the parent
class PremiumConfig < Config
@timeout = 120 # separate from Config.timeout
end
puts Config.timeout # => 60 (unchanged)
puts PremiumConfig.timeout # => 120
This is the advantage of class-level instance variables over @@ class variables: they don’t leak into subclasses.
Class Variables #
Class variables (@@) share their value across all instances of the same class, including its subclasses. This characteristic makes them both useful and dangerous.
class Connection
@@active_count = 0
@@max_limit = 10
def initialize(host)
raise "Connection limit reached!" if @@active_count >= @@max_limit
@host = host
@@active_count += 1
puts "Connection to #{@host} opened. Active: #{@@active_count}"
end
def close
@@active_count -= 1
puts "Connection to #{@host} closed. Active: #{@@active_count}"
end
def self.active_count
@@active_count
end
def self.max_limit
@@max_limit
end
end
c1 = Connection.new("db-primary") # => Active: 1
c2 = Connection.new("db-replica") # => Active: 2
c3 = Connection.new("cache-server") # => Active: 3
puts Connection.active_count # => 3
c1.close # => Active: 2
puts Connection.active_count # => 2
The Danger of Class Variables with Inheritance #
The behavior of class variables that most often causes bugs is that they’re inherited — and shared — with subclasses.
# ANTI-PATTERN: class variables leak into subclasses
class Animal
@@count = 0
def initialize
@@count += 1
end
def self.count
@@count
end
end
class Cat < Animal; end
class Dog < Animal; end
Cat.new
Cat.new
Dog.new
puts Animal.count # => 3
puts Cat.count # => 3 (not 2 as you might expect!)
puts Dog.count # => 3 (not 1 as you might expect!)
# All subclasses share the same @@count with the parent class
# CORRECT: use class-level instance variables for per-class state
class Animal
@count = 0
def self.count
@count
end
def self.inherited(subclass)
subclass.instance_variable_set(:@count, 0)
end
def initialize
self.class.instance_variable_set(
:@count,
self.class.instance_variable_get(:@count) + 1
)
end
end
class Cat < Animal; end
class Dog < Animal; end
Cat.new
Cat.new
Dog.new
puts Animal.count # => 0 (only animals created directly)
puts Cat.count # => 2
puts Dog.count # => 1
Global Variables #
Global variables start with $ and can be accessed from anywhere in the program — inside methods, classes, modules, even from different files after being required.
$app_version = "2.1.0"
$debug_mode = false
def system_info
puts "Version: #{$app_version}"
puts "Debug: #{$debug_mode}"
end
class Logger
def log(message)
puts "[#{$app_version}] #{message}" if $debug_mode
end
end
system_info
Ruby’s Built-in Global Variables #
Ruby itself uses global variables to store important system information. These are the global variables that are allowed to be used because they were designed for that purpose.
# $0 — the name of the currently running program file
puts $0 # => variables.rb (or the name of the file being run)
# $PROGRAM_NAME — a more expressive alias for $0
puts $PROGRAM_NAME
# $LOAD_PATH ($:) — the library search directory list
puts $LOAD_PATH.first(3).inspect
# $stdout, $stderr, $stdin — standard I/O streams
$stdout.puts "Written to stdout"
$stderr.puts "Written to stderr"
# $/ — line separator (default: "\n")
# $\ — output separator (default: nil)
# $, — separator between puts arguments (default: nil)
# $! — the last exception caught
begin
raise "Example error"
rescue => e
puts $!.message # => Example error (same as e.message)
end
# $~ — the last regex match result
"Ruby 3.3" =~ /(\d+\.\d+)/
puts $~[0] # => 3.3
puts $1 # => 3.3 ($1, $2, etc. are capture groups)
Frequently used built-in global variables:
$0 / $PROGRAM_NAME → the file being run
$LOAD_PATH / $: → library search path
$stdout / $stderr → standard output streams
$stdin → standard input stream
$! → the currently active exception
$@ → the current exception backtrace
$~ → MatchData of the last regex match
$1 .. $9 → regex capture groups
$/ → input record separator
✗ Don't create your own global variables unless absolutely necessary
Your own global variables (as opposed to Ruby’s built-ins) can almost always be replaced with a better solution: constants for values that don’t change, class-level instance variables for per-class state, or method parameters to pass values between parts of the code. Global variables make code hard to test because their state is spread across the whole program.
Variable Shadowing #
Shadowing happens when a new variable with the same name as a variable in an outer scope is defined in a deeper scope, hiding access to the original variable.
value = 100
# Shadowing inside a block
[1, 2, 3].each do |value| # the 'value' parameter shadows the outer variable
puts value # prints 1, 2, 3 — not 100
end
puts value # => 100 (the outer variable is unchanged)
Shadowing can be intentional or unintentional. The dangerous kind is unintentional shadowing — when you use a name that’s already taken in the outer scope without realizing it.
# ANTI-PATTERN: unintentional shadowing
def process_order(order)
total = 0
order.each do |item|
price = item[:price]
qty = item[:quantity]
total = price * qty # ← this OVERWRITES total in the outer scope!
# the developer probably meant: total += price * qty
end
total # only holds the last item's calculation
end
# CORRECT: different names for different scopes
def process_order(order)
total = 0
order.each do |item|
price = item[:price]
qty = item[:quantity]
subtotal = price * qty # different name, no conflict
total += subtotal
end
total
end
Ruby will emit a warning if you enable warning mode (ruby -w) and shadowing is detected:
ruby -w program.rb
# => warning: shadowing outer local variable - value
Naming Rules and Conventions #
Ruby has strict variable naming rules (syntactic) as well as strong conventions (community).
Syntactic Rules #
# VALID — allowed variable names:
name = "Rani"
full_name = "Rani Putri"
_temp = 42
__internal = true
name2 = "alias"
# INVALID — will cause a SyntaxError:
# 2name = "must not start with a digit"
# my-name = "hyphens aren't underscores"
# my name = "no spaces allowed"
Community Conventions #
# snake_case for variables and methods — REQUIRED in Ruby
total_price = 150_000
username = "Budi"
is_active = true # or use active? for booleans (in methods)
# ANTI-PATTERN: styles from other languages that aren't idiomatic in Ruby
totalPrice = 150_000 # ✗ camelCase
Username = "Budi" # ✗ PascalCase (reserved for Classes/Modules)
TOTAL_PRICE = 150_000 # ✗ SCREAMING_SNAKE_CASE (reserved for Constants)
# Underscore prefix for intentionally unused variables
result_array.each do |value, _index| # _index is ignored, but explicit
puts value
end
# Or a single underscore for "throw-away"
array.each_with_index do |element, _|
puts element
end
Descriptive Naming #
# ANTI-PATTERN: names too short or meaningless
d = 0.1
p = 150_000
t = d * p
r = p - t
# CORRECT: names that explain their content and purpose
discount_pct = 0.1
base_price = 150_000
discount_value = discount_pct * base_price
final_price = base_price - discount_value
# ANTI-PATTERN: names too long and redundant
value_of_the_discount_pct_variable_to_be_multiplied = 0.1
# CORRECT: short but meaningful — enough to understand the context
discount = 0.1
Variable name length guidelines:
✓ 1-2 words for short iteration variables (i, n, key, val)
✓ 2-4 words for general variables (total_price, username)
✓ Longer if the context demands clarity
✗ Single letters except in truly trivial loops
✗ Ambiguous abbreviations (tmp, dat, cnt — use temp, data, count)
✗ Misleading names (a list that's actually a Hash, etc.)
Freeze — Immutable Variables #
In Ruby, you can “freeze” an object’s value using the freeze method. Once frozen, the object can’t be modified.
city = "Jakarta".freeze
city.upcase # => "JAKARTA" — safe, creates a new object
city << " Selatan" # => FrozenError: can't modify frozen String
# Useful for strings used as keys or identifiers:
STATUS_ACTIVE = "active".freeze
STATUS_INACTIVE = "inactive".freeze
# With the frozen_string_literal magic comment:
# frozen_string_literal: true
# All string literals in the file are automatically frozen
sequenceDiagram
participant K as Code
participant O as Object
participant M as Memory
K->>M: name = "Andi"
M-->>O: Create String "Andi" (mutable)
K->>O: name << " Wijaya"
O-->>M: In-place modification → "Andi Wijaya"
K->>M: name = "Andi".freeze
M-->>O: Create String "Andi" (frozen)
K->>O: name << " Wijaya"
O-->>K: FrozenError! Cannot be modifiedSummary #
- The prefix determines scope — lowercase (local),
@(instance),@@(class),$(global). Just look at the variable name to know where it can be accessed.- Local variables are limited to the method/block — they can’t be accessed from an outer scope, but they can access variables from an outer scope.
- Block-local variables with
;— use|param; local|to prevent a block variable from unintentionally shadowing an outer variable.- Multiple assignment and splat — Ruby supports
a, b = 1, 2andfirst, *rest = arrayfor expressive destructuring.- Instance variables default to
nil— always initialize all instance variables ininitializeto avoid subtle bugs.- Class variables
@@leak into subclasses — use class-level instance variables (@varinsideclass << self) for isolated per-class state.- Avoid your own global variables — there’s almost always a better solution: constants, parameters, or class instance variables.
- snake_case is mandatory — local and instance variable names are always snake_case. camelCase and PascalCase are reserved for other contexts.
- Descriptive names beat short ones —
total_priceis far better thant, unless the context is truly trivial.freezefor immutable values — use it on constant strings and enable# frozen_string_literal: truefor better performance.