Variables in Ruby

Published by Programming Line in Ruby. Last updated .

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:

  1. The parser reads the right-hand side first and evaluates it down to a single object — here, the Integer object 5.
  2. Ruby checks whether x already exists as a local variable in the current scope. If not, it creates a new binding for it in that scope's variable table.
  3. The binding is made to point at the object from step 1 — nothing is copied, only a reference is stored.
  4. The assignment expression itself evaluates to the assigned value, which is why you can chain assignments like a = b = 5 or 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_case for local variables and methods; reserve SCREAMING_SNAKE_CASE for 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 a self. method, is often a safer choice.
  • When you need an independent copy of a mutable object, call dup or clone rather than assuming = copies it.
  • Give variables descriptive names — elapsed_seconds communicates far more than t.
  • Use ||= to assign a default only when a variable is currently nil or false, for example count ||= 0.
  • Remember that reassigning a constant is legal but prints a warning; treat constants as truly constant in your own code.

Practice Exercises

  1. 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.
  2. Create two variables pointing at the same array, mutate one with <<, and print both to confirm they change together. Then fix it using dup so only one of them changes.
  3. Write a BankAccount class with an instance variable @balance and a class variable @@accounts_opened that 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.
  • def always creates an isolated local scope; blocks share the scope of the code around them.
  • Only nil and false are falsy in Ruby — 0, "", and [] are all truthy.
  • Use dup or clone when you need an independent copy, and prefer local or instance variables over globals and class variables in most designs.