Variables in Ruby
A variable in Ruby is a name that points to an object living somewhere in memory — it doesn’t have a fixed type of its own, and it doesn’t hold the object directly so much as hold a reference to it. Because Ruby is dynamically typed, the same variable name can point to a string on one line and an integer on the next; what matters is the object it currently references. Understanding how variables are created, scoped, and passed around explains everything from why some methods mutate your data unexpectedly to why every object, including numbers and nil itself, responds to methods like .class.
Overview: How Variables Work
Ruby has no var, let, or type declaration keywords. You create a variable simply by assigning a value to a name with =. The first time Ruby’s parser sees name = something in a given scope, it reserves a slot for that local variable in that scope’s variable table. From then on, that name refers to whatever object was last assigned to it. Reassigning a variable doesn’t change the object it used to point to; it just makes the name point somewhere else. The old object, if nothing else references it, becomes eligible for Ruby’s garbage collector.
This "variables are references, not boxes" model matters because in Ruby almost everything is an object, including integers, true, false, and even nil (whose class is NilClass). When you write a = "hello" and then b = a, you don’t get two independent strings — you get two names pointing at the exact same String object. Mutating that object through b with a method like << is visible through a too, because there was only ever one object. This is one of the most important ideas to internalize early, and it’s covered in detail in the Common Mistakes section below.
The Five Kinds of Variables
Ruby distinguishes variable kinds by their first character, and each kind has different scoping rules:
- Local variables (
count) — visible only within the method, block, or top-level scope where they’re first assigned. - Instance variables (
@count) — belong to one specific object; every method on that object can read and write them. - Class variables (
@@count) — shared by a class and all of its instances and subclasses. - Global variables (
$count) — visible everywhere in the program, and generally best avoided. - Constants (
COUNT) — start with an uppercase letter; Ruby lets you reassign them but prints a warning if you do.
Local variables are by far the most common, and the rest of this lesson focuses mainly on them, circling back to the others where the differences matter.
Syntax
The general form of a variable declaration is just an assignment — there is no separate declaration step:
local_variable = value
@instance_variable = value
@@class_variable = value
$global_variable = value
CONSTANT_NAME = value
The table below explains each part:
| Prefix | Kind | Naming rule | Scope |
|---|---|---|---|
| (none) | Local variable | Starts with a lowercase letter or underscore, snake_case by convention |
The method, block, or top-level context where it’s first assigned |
@ |
Instance variable | @ followed by a lowercase name |
One specific object, shared across all of that object’s methods |
@@ |
Class variable | @@ followed by a lowercase name |
Shared by the class and every instance and subclass |
$ |
Global variable | $ followed by a lowercase name |
The entire running program |
| Uppercase first letter | Constant | SCREAMING_SNAKE_CASE by convention |
The lexical scope it’s defined in (class, module, or top level) |
Ruby variable names can contain letters, digits, and underscores, but can’t start with a digit. Local variable and method names are conventionally snake_case; idiomatic Ruby doesn’t use camelCase. A name ending in ? or ! is always a method call (like empty?), never a plain variable.
Examples
Example 1: Assignment and Dynamic Typing
A variable can hold any kind of object, and reassigning it to a different kind of object later is perfectly legal:
name = "Ruby"
version = 3.2
released = true
puts name
puts version
puts released
puts name.class
puts version.class
puts released.class
name = 42
puts name.class
Output:
Ruby
3.2
true
String
Float
TrueClass
Integer
There is no type declared anywhere in this script. Each variable simply points at whatever object was last assigned to it, and .class asks that object what kind it is. When name is reassigned from the string "Ruby" to the integer 42, the variable doesn’t change type — it just points at a different object now.
Example 2: Reference Semantics (Aliasing)
Because assignment copies a reference rather than the object itself, two variables can end up sharing one object:
original = "hello"
alias_name = original
alias_name << " world"
puts original
puts alias_name
puts original.equal?(alias_name)
Output:
hello world
hello world
true
The line alias_name = original does not create a second string; it makes alias_name point at the same String object as original. The << method mutates that object in place, so the change shows up through both names. equal? confirms this by checking object identity (same object in memory) — unlike ==, which only checks that two objects have equal values.
Example 3: Instance Variables vs. Class Variables
Instance variables belong to one object; class variables are shared by the whole class and every instance of it:
class Counter
@@total_counters = 0
def initialize(start = 0)
@count = start
@@total_counters += 1
end
def increment
@count += 1
end
def value
@count
end
def self.total_counters
@@total_counters
end
end
a = Counter.new(5)
b = Counter.new(100)
a.increment
a.increment
b.increment
puts a.value
puts b.value
puts Counter.total_counters
Output:
7
101
2
Each Counter object has its own @count, so a and b track independent values (7 and 101). But @@total_counters is a single value shared by the class itself, incremented every time any instance is created — which is why it ends at 2 even though it's never accessed through an individual object in this script.
How It Works Step by Step
Here is what Ruby actually does when it evaluates x = 5:
- The parser reads the right-hand side first and evaluates it down to a single object — here, the
Integerobject5. - Ruby checks whether
xalready exists as a local variable in the current scope. If not, it creates a new binding for it in that scope's variable table. - The binding is made to point at the object from step 1 — nothing is copied, only a reference is stored.
- The assignment expression itself evaluates to the assigned value, which is why you can chain assignments like
a = b = 5or use an assignment result inside a conditional.
Scope is where local variables get interesting. A block, introduced with do...end or { }, shares the local scope of the code around it, so it can read and write variables defined outside it:
total = 0
[1, 2, 3, 4].each do |n|
total += n
end
puts total
Output:
10
The block passed to each reaches out and modifies total, which was defined before the block even started. A method definition (def...end), by contrast, always opens a brand-new, fully isolated local scope — it can never see local variables from outside it, no matter how close by they were defined. Ruby resolves this purely lexically, at parse time: if the parser has already seen an assignment to a name earlier in the same scope, a bare reference to that name is treated as a local variable; otherwise it's treated as a method call.
Multiple assignment follows the same "evaluate the right side fully first" rule:
first, second, *rest = [10, 20, 30, 40]
puts first
puts second
puts rest.inspect
Output:
10
20
[30, 40]
The array on the right is built completely first, and only then destructured, left to right, into the names on the left. The splat *rest collects whatever is left over into a new array.
Common Mistakes
Mistake 1: Expecting a Method to See an Outer Local Variable
It's tempting to assume a method defined right after a variable can see that variable, but a method's local scope is always isolated.
Wrong:
name = "Ruby"
def greet
puts "Hello, #{name}!"
end
greet
This raises NameError: undefined local variable or method 'name', because def always opens a fresh local scope that can't see variables from the surrounding code, even though the definition appears immediately below the assignment.
Corrected:
name = "Ruby"
def greet(name)
puts "Hello, #{name}!"
end
greet(name)
Output:
Hello, Ruby!
Pass the value in explicitly as an argument instead of expecting the method to reach outside its own scope.
Mistake 2: Treating Assignment as a Copy
Assigning one variable to another does not clone a mutable object — it aliases it.
Wrong:
original = ["ruby", "python", "go"]
display = original
display.push("java")
puts original.inspect
Output:
["ruby", "python", "go", "java"]
display = original didn't create a second array; both names reference the same Array object, so pushing onto display changed what original sees too, even though original was never touched directly.
Corrected:
original = ["ruby", "python", "go"]
display = original.dup
display.push("java")
puts original.inspect
puts display.inspect
Output:
["ruby", "python", "go"]
["ruby", "python", "go", "java"]
dup creates a new, independent object with the same contents, so mutating the copy leaves the original untouched.
Mistake 3: Assuming 0 or an Empty String Is Falsy
Coming from languages like C, JavaScript, or Python, it's tempting to assume 0 behaves like false in a conditional.
Wrong (surprising, not actually broken):
count = 0
if count
puts "this branch runs"
else
puts "this branch does not run"
end
Output:
this branch runs
In Ruby, only nil and false are falsy — every other value, including 0, "", and [], is truthy. Relying on a number being "falsy at zero" will silently do the wrong thing.
Corrected:
count = 0
if count.zero?
puts "count is exactly zero"
else
puts "count is not zero"
end
Output:
count is exactly zero
Use an explicit predicate like zero?, nil?, or empty? when you actually want to test for a specific value rather than relying on truthiness.
Best Practices
- Use
snake_casefor local variables and methods; reserveSCREAMING_SNAKE_CASEfor true constants. - Prefer local variables and method arguments over global variables (
$var) — globals make code hard to reason about and are rarely necessary. - Be deliberate about class variables (
@@var); because they're shared with every subclass, they can produce surprising cross-class bugs. A class-level instance variable, accessed through aself.method, is often a safer choice. - When you need an independent copy of a mutable object, call
duporclonerather than assuming=copies it. - Give variables descriptive names —
elapsed_secondscommunicates far more thant. - Use
||=to assign a default only when a variable is currentlynilorfalse, for examplecount ||= 0. - Remember that reassigning a constant is legal but prints a warning; treat constants as truly constant in your own code.
Practice Exercises
- Write a script that assigns your name, age, and a boolean for whether you're currently learning Ruby to three local variables, then prints each one along with its
.class. - Create two variables pointing at the same array, mutate one with
<<, and print both to confirm they change together. Then fix it usingdupso only one of them changes. - Write a
BankAccountclass with an instance variable@balanceand a class variable@@accounts_openedthat counts how many accounts have been created. Create three accounts and print the count. (Expected output:3.)
Summary
- A Ruby variable is a name bound to an object reference, not a typed container — the same name can point to different kinds of object over its lifetime.
- There are five kinds of variables, distinguished by prefix: local (none), instance (
@), class (@@), global ($), and constant (uppercase first letter). - Assignment never copies an object; two variables can end up referencing, and mutating, the exact same object.
defalways creates an isolated local scope; blocks share the scope of the code around them.- Only
nilandfalseare falsy in Ruby —0,"", and[]are all truthy. - Use
duporclonewhen you need an independent copy, and prefer local or instance variables over globals and class variables in most designs.
