Overview
Variables serve as named storage locations in memory that hold data during program execution. A variable provides a symbolic name for a memory address, allowing programs to store, retrieve, and modify values without managing memory addresses directly. The data type associated with a variable determines what kind of value it can contain, how much memory it occupies, and which operations are valid.
Ruby implements dynamic typing, where variables do not have fixed types declared at compile time. Instead, variables hold references to objects, and each object carries its own type information. This differs from statically-typed languages where variable types must be declared and remain fixed. Ruby's approach provides flexibility but requires different discipline around type management.
# Variable holds reference to an integer object
count = 42
# => 42
# Same variable now references a string object
count = "forty-two"
# => "forty-two"
Variables in Ruby exist within specific scopes determined by naming conventions and declaration context. The scope defines where a variable can be accessed and how long it persists. Ruby uses prefixes and naming patterns to distinguish between local variables, instance variables, class variables, and global variables, each with different visibility and lifetime characteristics.
Data types in Ruby are represented by classes in the object hierarchy. Every value is an object, including primitives like numbers and booleans that in other languages might be stored as raw values. This object-oriented approach means all values respond to methods and carry their type information at runtime. The type system includes numeric types, strings, symbols, booleans, arrays, hashes, and custom classes, each with specific behaviors and method interfaces.
Key Principles
Object References and Assignment
Variables in Ruby store references to objects rather than the objects themselves. When you assign a value to a variable, Ruby creates an object in memory and stores a reference to that object in the variable. Multiple variables can reference the same object, and modifying a mutable object through one reference affects all references to that object.
original = [1, 2, 3]
copy = original
copy << 4
original
# => [1, 2, 3, 4]
# Both variables reference the same array object
original.object_id == copy.object_id
# => true
Assignment creates a new reference, not a new object. The distinction matters when working with mutable objects. Immutable objects like numbers, symbols, and frozen strings cannot be modified in place, so reassignment always creates a new object reference.
Dynamic Typing and Duck Typing
Ruby determines types at runtime rather than compile time. A variable can reference any type of object, and the type can change through reassignment. Ruby uses duck typing: if an object responds to the required methods, it can be used regardless of its actual class. This principle emphasizes behavior over type hierarchy.
def process(item)
item.upcase
end
process("hello")
# => "HELLO"
process(:symbol)
# => :SYMBOL
# Works with any object implementing upcase
The dynamic type system requires defensive programming and testing to catch type-related errors that static type systems would catch at compile time. Ruby 3 introduced RBS type signatures and type checking tools, but the language remains dynamically typed at its core.
Scope and Lifetime
Variable scope determines where a variable can be accessed. Ruby defines scope through lexical boundaries: method definitions, class definitions, and blocks. Variables declared in an inner scope cannot be accessed from outer scopes, but inner scopes can access variables from enclosing scopes under certain conditions.
Local variables exist only within their defining scope and cease to exist when that scope exits. Instance variables persist for the lifetime of the object they belong to. Class variables are shared across all instances of a class and its subclasses. Global variables persist for the entire program execution.
Mutability and Immutability
Objects in Ruby are mutable by default, meaning their internal state can change. Strings, arrays, and hashes can be modified in place through destructive methods. Numbers, symbols, true, false, and nil are immutable and cannot change once created.
str = "hello"
str.upcase!
str
# => "HELLO"
num = 5
# No method can change the value 5 itself
# Assignment creates a new reference
num = num + 1
# => 6
Freezing an object prevents further modifications, making a mutable object effectively immutable. Frozen objects raise exceptions when code attempts to modify them.
Type Coercion and Conversion
Ruby performs implicit type coercion in certain contexts, particularly with numeric operations. The language also provides explicit conversion methods following two patterns: to_* methods for lenient conversion and to_* class methods for strict conversion that raise errors for invalid inputs.
# Implicit coercion in arithmetic
5 + 2.5
# => 7.5
# Explicit conversion
"42".to_i
# => 42
Integer("42")
# => 42
Integer("invalid")
# => ArgumentError: invalid value for Integer(): "invalid"
Ruby Implementation
Variable Types and Naming Conventions
Ruby distinguishes variable types through naming prefixes and patterns. Local variables begin with a lowercase letter or underscore. Instance variables begin with @. Class variables begin with @@. Global variables begin with $. Constants begin with an uppercase letter, though Ruby constants can be reassigned with a warning.
local_var = "accessible only in current scope"
class Example
@instance_var = "belongs to this class object"
def initialize
@instance_var = "belongs to this instance"
end
@@class_var = "shared across all instances"
CONSTANT = "conventionally immutable"
end
$global_var = "accessible everywhere"
Local variables require initialization before use. Referencing an uninitialized local variable raises a NameError. Instance variables default to nil if not explicitly initialized, which can hide bugs. Class variables must be initialized when declared.
Core Data Types
Ruby's type system centers on several fundamental classes. Numeric types include Integer for whole numbers and Float for decimal numbers. Ruby automatically handles arbitrary-precision integers without overflow. Complex and Rational types support mathematical operations requiring exact precision.
# Integer type handles arbitrary precision
big_number = 10**100
# => 10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
# Float type for decimal numbers
decimal = 3.14159
# => 3.14159
# Rational for exact fractions
fraction = Rational(1, 3)
# => (1/3)
fraction * 3
# => (1/1)
String objects hold sequences of bytes with an associated encoding. Ruby strings are mutable and provide extensive methods for manipulation, searching, and transformation. Symbols are immutable string-like objects used as identifiers, with the same symbol name always referencing the same object in memory.
# Strings are mutable objects
str = "hello"
str.object_id
# => 260
another_str = "hello"
another_str.object_id
# => 280
# Symbols reference the same object
sym = :hello
sym.object_id
# => 1234568
another_sym = :hello
another_sym.object_id
# => 1234568
Arrays store ordered collections of objects accessible by integer index. Hashes store key-value pairs where keys can be any object. Range objects represent sequences with start and end values. Boolean values true and false, along with nil, complete the core types.
Variable Assignment and Multiple Assignment
Ruby supports parallel assignment, allowing multiple variables to be assigned in a single statement. This feature enables swapping values without temporary variables and unpacking collections into individual variables.
# Parallel assignment
x, y = 1, 2
# x => 1, y => 2
# Swapping without temporary variable
x, y = y, x
# x => 2, y => 1
# Unpacking arrays
first, second, *rest = [1, 2, 3, 4, 5]
# first => 1, second => 2, rest => [3, 4, 5]
Assignment returns the assigned value, enabling chained assignments. However, the return value of assignment as an expression can cause confusion when used in conditionals, as the intent may not be clear.
Type Checking and Introspection
Ruby provides several methods for runtime type inspection. The class method returns an object's class. The is_a? and kind_of? methods check if an object is an instance of a class or its subclasses. The instance_of? method checks for exact class match without considering inheritance.
value = "hello"
value.class
# => String
value.is_a?(String)
# => true
value.is_a?(Object)
# => true
value.instance_of?(String)
# => true
value.instance_of?(Object)
# => false
The respond_to? method checks if an object implements a specific method, supporting duck typing by focusing on capabilities rather than types. This approach aligns with Ruby's philosophy of behavior-based programming.
Type Conversion Methods
Ruby defines conversion methods following naming conventions. Methods starting with to_ perform lenient conversions, returning nil or a default value for invalid inputs. Methods using the type name as a method call perform strict conversions, raising exceptions for invalid inputs.
# Lenient conversion
"42".to_i
# => 42
"invalid".to_i
# => 0
# Strict conversion
Integer("42")
# => 42
Integer("invalid")
# => ArgumentError
# String conversion
42.to_s
# => "42"
[1, 2, 3].to_s
# => "[1, 2, 3]"
Practical Examples
Managing State in Objects
Instance variables store object state, persisting across method calls on the same object. The state remains private unless explicitly exposed through accessor methods.
class BankAccount
def initialize(balance)
@balance = balance
@transactions = []
end
def deposit(amount)
@balance += amount
@transactions << { type: :deposit, amount: amount, balance: @balance }
end
def withdraw(amount)
return false if amount > @balance
@balance -= amount
@transactions << { type: :withdrawal, amount: amount, balance: @balance }
true
end
def balance
@balance
end
end
account = BankAccount.new(1000)
account.deposit(500)
account.withdraw(200)
account.balance
# => 1300
Instance variables provide encapsulation, hiding internal state from external code. Accessor methods control how external code interacts with state, enabling validation, transformation, or controlled access patterns.
Type-Safe Operations with Duck Typing
Duck typing enables writing methods that work with any object implementing required behavior, without coupling to specific types. This flexibility requires careful documentation and defensive checks for production code.
class TextFormatter
def format(content)
return "" unless content.respond_to?(:to_s)
text = content.to_s
return text if text.empty?
text.strip.capitalize
end
end
formatter = TextFormatter.new
formatter.format(" hello world ")
# => "Hello world"
formatter.format(42)
# => "42"
formatter.format([:a, :b, :c])
# => "[:a, :b, :c]"
formatter.format(nil)
# => ""
This pattern accepts any object implementing to_s, providing flexibility while maintaining predictable behavior. The method handles edge cases like nil values and empty strings defensively.
Working with Mutable and Immutable Objects
Understanding mutability prevents unexpected side effects when objects are shared across different parts of a program. Defensive copying protects against unintended modifications.
class Configuration
attr_reader :settings
def initialize(settings)
# Store a frozen duplicate to prevent external modifications
@settings = settings.dup.freeze
end
def get(key)
@settings[key]
end
def with_override(key, value)
# Return new configuration without modifying original
Configuration.new(@settings.merge(key => value))
end
end
original = Configuration.new(timeout: 30, retries: 3)
modified = original.with_override(:timeout, 60)
original.get(:timeout)
# => 30
modified.get(:timeout)
# => 60
This immutable configuration pattern prevents accidental state changes, making the code more predictable and thread-safe. Each modification produces a new object rather than changing existing state.
Type Coercion in Numeric Operations
Ruby performs automatic type coercion when mixing numeric types, promoting integers to floats when necessary to preserve precision.
def calculate_average(numbers)
return 0 if numbers.empty?
sum = numbers.reduce(0) { |total, n| total + n }
sum / numbers.size.to_f
end
calculate_average([10, 20, 30])
# => 20.0
calculate_average([1, 2, 3, 4])
# => 2.5
# Without .to_f, integer division truncates
numbers = [1, 2, 3, 4]
numbers.reduce(0, :+) / numbers.size
# => 2
Explicit conversion to float prevents integer division truncation. The coercion rules determine result types based on operand types, with float operations producing float results even when the mathematical result is an integer.
Common Patterns
Nil Handling and Default Values
Ruby's nil object represents absence of value. Safe navigation and default value patterns prevent nil-related errors.
# Using || for default values
def fetch_config(key)
@config[key] || "default_value"
end
# Using fetch with default
def get_setting(key)
settings.fetch(key, "default")
end
# Safe navigation operator
user&.profile&.email
# Returns nil if any step is nil, without raising error
# Nil coalescing pattern
result = potentially_nil_value || calculate_default_value
# Conditional assignment
@cache ||= expensive_computation
The || operator works well for boolean false values but can produce unexpected results since false and nil are both falsy. The fetch method with a default argument handles this more explicitly.
Type Guards and Validation
Validating types and values at method boundaries catches errors early and provides clear error messages.
class DataProcessor
def process(data)
validate_input(data)
transform(data)
end
private
def validate_input(data)
raise ArgumentError, "Data must be an Array" unless data.is_a?(Array)
raise ArgumentError, "Data cannot be empty" if data.empty?
raise ArgumentError, "All elements must be numeric" unless data.all? { |item| item.is_a?(Numeric) }
end
def transform(data)
data.map { |n| n * 2 }
end
end
processor = DataProcessor.new
processor.process([1, 2, 3])
# => [2, 4, 6]
processor.process("invalid")
# => ArgumentError: Data must be an Array
Explicit validation makes method contracts clear and fails fast with descriptive errors. This pattern particularly matters in dynamically-typed languages where type errors only surface at runtime.
Memoization Pattern
Caching computed values in instance variables avoids redundant calculations while preserving lazy evaluation.
class DataAnalyzer
def initialize(data)
@data = data
end
def mean
@mean ||= calculate_mean
end
def median
@median ||= calculate_median
end
def standard_deviation
@std_dev ||= calculate_standard_deviation
end
private
def calculate_mean
@data.reduce(0.0, :+) / @data.size
end
def calculate_median
sorted = @data.sort
mid = sorted.size / 2
sorted.size.odd? ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2.0
end
def calculate_standard_deviation
avg = mean
variance = @data.map { |n| (n - avg) ** 2 }.reduce(:+) / @data.size
Math.sqrt(variance)
end
end
analyzer = DataAnalyzer.new([1, 2, 3, 4, 5])
analyzer.mean
# => 3.0
analyzer.mean # Retrieved from cache
# => 3.0
The memoization pattern using ||= stores the result after first computation. Subsequent calls return the cached value without recalculation. This fails for boolean false values, requiring alternative patterns for those cases.
Value Object Pattern
Immutable objects representing values simplify reasoning about state and prevent accidental modifications.
class Money
attr_reader :amount, :currency
def initialize(amount, currency)
@amount = amount
@currency = currency
freeze
end
def +(other)
raise ArgumentError, "Currency mismatch" unless currency == other.currency
Money.new(amount + other.amount, currency)
end
def *(multiplier)
Money.new(amount * multiplier, currency)
end
def to_s
"#{amount} #{currency}"
end
end
price = Money.new(100, :USD)
total = price * 3
discount = Money.new(50, :USD)
final = total + discount
# => 350 USD
Freezing the object after initialization prevents modifications. Arithmetic operations return new objects rather than modifying existing ones, following immutable value semantics.
Common Pitfalls
Unintended Variable Scope
Block scope and method scope differ in how they capture variables. Blocks can access and modify variables from enclosing scopes, but method definitions create new scopes.
counter = 0
# Block can modify outer variable
5.times do
counter += 1
end
counter
# => 5
def increment_counter
counter += 1 # NameError - counter not defined in method scope
end
# Correct approach passes variable as parameter
def increment(value)
value + 1
end
counter = increment(counter)
# => 6
Block variable shadowing occurs when a block parameter has the same name as an outer variable, hiding the outer variable within the block. This can mask intended variable access.
Instance Variable Typos
Misspelled instance variable names create new variables rather than raising errors. Ruby returns nil for undefined instance variables instead of raising NameError like it does for undefined local variables.
class Person
def initialize(name)
@name = name
end
def greet
"Hello, #{@nane}" # Typo: @nane instead of @name
end
end
person = Person.new("Alice")
person.greet
# => "Hello, "
Using attr_accessor, attr_reader, or explicit accessor methods catches these errors earlier by raising NoMethodError when the wrong name is used. Code review and testing remain essential for catching instance variable typos.
Mutating Frozen Objects
Attempting to modify a frozen object raises FrozenError. Freezing an object does not recursively freeze nested objects, so care is needed with nested structures.
array = [1, 2, 3].freeze
array << 4
# => FrozenError: can't modify frozen Array
# Shallow freeze doesn't protect nested objects
nested = [[1, 2], [3, 4]].freeze
nested[0] << 5 # Modifies inner array
nested
# => [[1, 2, 5], [3, 4]]
Deep freezing requires recursively freezing all nested objects. The ice_nine gem provides deep freeze functionality, or implement recursive freezing manually.
String Mutability Surprises
String methods with exclamation marks modify strings in place. Without the exclamation mark, most string methods return new strings.
original = "hello"
modified = original.upcase
original
# => "hello"
modified
# => "HELLO"
original = "hello"
original.upcase!
original
# => "HELLO"
Accidentally using the mutating version when multiple references to the string exist causes unexpected changes. Using frozen strings or immutable patterns avoids this category of bugs.
Integer Division Truncation
Dividing two integers performs integer division, discarding the fractional part. This commonly causes precision loss in calculations.
result = 5 / 2
# => 2
result = 5 / 2.0
# => 2.5
result = 5.to_f / 2
# => 2.5
# Percentage calculation pitfall
correct_answers = 47
total_questions = 50
# Wrong - integer division
percentage = correct_answers / total_questions * 100
# => 0
# Correct - convert to float first
percentage = correct_answers.to_f / total_questions * 100
# => 94.0
Converting at least one operand to float before division preserves decimal precision. Order of operations matters: multiplying before dividing can preserve some precision in integer arithmetic but still truncates intermediate results.
Hash Default Value Sharing
Setting a hash default value with an object causes all missing keys to reference the same object, leading to unintended sharing.
hash = Hash.new([])
hash[:a] << 1
hash[:b] << 2
hash[:a]
# => [1, 2]
hash[:b]
# => [1, 2]
# Correct approach uses block form
hash = Hash.new { |h, k| h[k] = [] }
hash[:a] << 1
hash[:b] << 2
hash[:a]
# => [1]
hash[:b]
# => [2]
The block form creates a new object for each missing key access, preventing sharing. This distinction matters for any mutable default value including hashes, arrays, and custom objects.
Global Variable Side Effects
Global variables persist across the entire program and can be modified anywhere, making debugging difficult and creating hidden dependencies.
$config = { timeout: 30 }
def process_request
# Unexpected modification affects entire program
$config[:timeout] = 60
# process...
end
# Other code sees modified value
$config[:timeout]
# => 60
Avoiding global variables in favor of passing parameters or using dependency injection makes dependencies explicit and code more testable. Global variables create coupling that makes code fragile and hard to understand.
Reference
Variable Scope Types
| Scope Type | Prefix | Lifetime | Visibility |
|---|---|---|---|
| Local | lowercase or _ | Current scope only | Current scope |
| Instance | @ | Object lifetime | Within object |
| Class | @@ | Class lifetime | Class and subclasses |
| Global | $ | Program lifetime | Entire program |
| Constant | UPPERCASE | Program lifetime | Class/module plus descendants |
Core Data Type Classes
| Type | Class | Mutability | Common Use Cases |
|---|---|---|---|
| Integer | Integer | Immutable | Counting, indexing, math |
| Float | Float | Immutable | Decimal calculations |
| String | String | Mutable | Text processing, output |
| Symbol | Symbol | Immutable | Hash keys, identifiers |
| Array | Array | Mutable | Ordered collections |
| Hash | Hash | Mutable | Key-value mappings |
| Range | Range | Immutable | Sequences, iteration |
| True/False | TrueClass/FalseClass | Immutable | Boolean logic |
| Nil | NilClass | Immutable | Absence of value |
Type Conversion Methods
| Method | Behavior | Return on Invalid |
|---|---|---|
| to_i | Convert to integer | 0 |
| to_f | Convert to float | 0.0 |
| to_s | Convert to string | String representation |
| to_a | Convert to array | Array or error |
| to_h | Convert to hash | Hash or error |
| Integer() | Strict integer conversion | Raises ArgumentError |
| Float() | Strict float conversion | Raises ArgumentError |
| String() | Strict string conversion | Raises TypeError |
| Array() | Strict array conversion | Single-element array or array |
Type Checking Methods
| Method | Purpose | Considers Inheritance |
|---|---|---|
| class | Return object's class | No |
| is_a? | Check if instance of class | Yes |
| kind_of? | Alias for is_a? | Yes |
| instance_of? | Check exact class | No |
| respond_to? | Check method availability | Yes |
Assignment Operators
| Operator | Description | Example |
|---|---|---|
| = | Simple assignment | x = 5 |
| += | Add and assign | x += 3 |
| -= | Subtract and assign | x -= 2 |
| *= | Multiply and assign | x *= 4 |
| /= | Divide and assign | x /= 2 |
| %= | Modulo and assign | x %= 3 |
| **= | Exponentiation and assign | x **= 2 |
| ||= | Assign if nil or false | x ||= default |
| &&= | Assign if truthy | x &&= value |
Object Mutability Methods
| Method | Purpose | Effect |
|---|---|---|
| freeze | Prevent modifications | Object becomes immutable |
| frozen? | Check if frozen | Returns true if frozen |
| dup | Create shallow copy | Returns mutable duplicate |
| clone | Create shallow copy | Preserves frozen state |
Truthy and Falsy Values
| Value | Truthiness | Note |
|---|---|---|
| false | Falsy | Only false boolean |
| nil | Falsy | Represents absence |
| 0 | Truthy | Unlike some languages |
| "" | Truthy | Empty string is truthy |
| [] | Truthy | Empty array is truthy |
| {} | Truthy | Empty hash is truthy |
| Everything else | Truthy | All other values |
Variable Naming Conventions
| Pattern | Convention | Example |
|---|---|---|
| Local/parameter | snake_case | user_name, total_count |
| Instance variable | @snake_case | @balance, @created_at |
| Class variable | @@snake_case | @@instance_count |
| Global variable | $snake_case | $debug_mode |
| Constant | SCREAMING_SNAKE_CASE | MAX_SIZE, API_KEY |
| Class/Module | PascalCase | UserAccount, DataProcessor |