Overview
Instance variables store object state and exist independently for each instance of a class. Ruby prefixes instance variables with @
and creates them dynamically upon first assignment. Instance methods define object behavior and operate within the context of a specific object instance, accessing that object's instance variables and other methods.
Ruby creates instance variables when first assigned, regardless of whether they appear in initialize
or other methods. The variable exists only within that specific object instance and remains accessible to all instance methods of that object.
class User
def initialize(name)
@name = name
@created_at = Time.now
end
def profile
"#{@name} joined #{@created_at.strftime('%Y-%m-%d')}"
end
end
user = User.new("Alice")
user.profile
# => "Alice joined 2025-08-26"
Instance methods execute within the object's context, accessing instance variables and other methods through implicit self
. Ruby resolves method calls by searching the object's class, then included modules, then the superclass chain.
class Account
def initialize(balance)
@balance = balance
end
def deposit(amount)
@balance += amount
log_transaction("deposit", amount)
end
private
def log_transaction(type, amount)
puts "#{type.capitalize}: #{amount}"
end
end
account = Account.new(100)
account.deposit(50)
# Deposit: 50
Ruby distinguishes between instance variables (object state) and instance methods (object behavior). Instance variables remain private to the object, while instance methods can have different visibility levels through private
, protected
, and public
modifiers.
Basic Usage
Instance variables store data specific to each object instance. Ruby creates them upon first assignment and initializes unassigned instance variables to nil
. Instance variables persist throughout the object's lifetime and remain accessible to all instance methods.
class Product
def initialize(name, price)
@name = name
@price = price
@discount = 0
end
def apply_discount(percentage)
@discount = percentage
@discounted_price = @price * (1 - percentage / 100.0)
end
def final_price
@discounted_price || @price
end
def description
base = "#{@name}: $#{@price}"
@discount > 0 ? "#{base} (#{@discount}% off: $#{final_price})" : base
end
end
product = Product.new("Laptop", 1000)
puts product.description
# => "Laptop: $1000"
product.apply_discount(15)
puts product.description
# => "Laptop: $1000 (15% off: $850.0)"
Instance methods define object behavior and access instance variables through the @
prefix. Methods can modify instance variables, call other instance methods, and return values based on the object's current state.
class Temperature
def initialize(celsius)
@celsius = celsius
end
def celsius
@celsius
end
def fahrenheit
(@celsius * 9.0 / 5.0) + 32
end
def kelvin
@celsius + 273.15
end
def update(new_celsius)
@celsius = new_celsius
log_update
end
private
def log_update
puts "Temperature updated to #{@celsius}°C"
end
end
temp = Temperature.new(25)
puts temp.fahrenheit # => 77.0
puts temp.kelvin # => 298.15
temp.update(30)
# Temperature updated to 30°C
Accessor methods provide controlled access to instance variables. Ruby generates getter and setter methods through attr_reader
, attr_writer
, and attr_accessor
declarations, which create methods that read or modify instance variables.
class Book
attr_reader :title, :author
attr_accessor :available
def initialize(title, author)
@title = title
@author = author
@available = true
@checkout_count = 0
end
def checkout
return false unless @available
@available = false
@checkout_count += 1
@last_checkout = Time.now
true
end
def return_book
@available = true
@last_return = Time.now
end
def checkout_history
"Checked out #{@checkout_count} times"
end
end
book = Book.new("1984", "George Orwell")
puts book.title # => "1984"
puts book.available # => true
book.checkout
puts book.available # => false
book.return_book
puts book.available # => true
Ruby allows dynamic instance variable assignment within any instance method. Variables created this way become available to all other instance methods of that object, but unassigned variables return nil
rather than raising errors.
class DynamicData
def store_value(key, value)
instance_variable_set("@#{key}", value)
end
def retrieve_value(key)
instance_variable_get("@#{key}")
end
def has_value?(key)
instance_variable_defined?("@#{key}")
end
def all_variables
instance_variables.map { |var| var.to_s.delete('@') }
end
end
data = DynamicData.new
data.store_value('name', 'Ruby')
data.store_value('version', 3.0)
puts data.retrieve_value('name') # => "Ruby"
puts data.has_value?('version') # => true
puts data.all_variables # => ["name", "version"]
Advanced Usage
Instance variables support complex object composition and state management patterns. Objects can contain other objects as instance variables, creating nested data structures and enabling sophisticated object relationships.
class Address
attr_reader :street, :city, :zipcode
def initialize(street, city, zipcode)
@street = street
@city = city
@zipcode = zipcode
end
def full_address
"#{@street}, #{@city} #{@zipcode}"
end
end
class Person
attr_reader :name
def initialize(name)
@name = name
@addresses = {}
@phone_numbers = []
@metadata = {}
end
def add_address(type, address)
@addresses[type] = address
@metadata[:last_address_update] = Time.now
end
def add_phone(number, type = :mobile)
@phone_numbers << { number: number, type: type, added: Time.now }
end
def primary_address
@addresses[:home] || @addresses[:work] || @addresses.values.first
end
def contact_summary
addr = primary_address&.full_address || "No address"
phones = @phone_numbers.map { |p| "#{p[:number]} (#{p[:type]})" }.join(", ")
"#{@name}\n#{addr}\n#{phones.empty? ? "No phone" : phones}"
end
def recent_activity
updates = []
updates << "Address: #{@metadata[:last_address_update]}" if @metadata[:last_address_update]
updates << "Phone: #{@phone_numbers.last[:added]}" if @phone_numbers.any?
updates.empty? ? "No recent updates" : updates.join(", ")
end
end
person = Person.new("Jane Doe")
person.add_address(:home, Address.new("123 Oak St", "Portland", "97201"))
person.add_address(:work, Address.new("456 Pine Ave", "Portland", "97202"))
person.add_phone("555-0123", :mobile)
person.add_phone("555-0456", :work)
puts person.contact_summary
# Jane Doe
# 123 Oak St, Portland 97201
# 555-0123 (mobile), 555-0456 (work)
Instance methods can implement complex business logic by coordinating multiple instance variables and calling other methods. This enables sophisticated object behavior while maintaining encapsulation.
class BankAccount
attr_reader :account_number, :balance
def initialize(account_number, initial_balance = 0)
@account_number = account_number
@balance = initial_balance
@transaction_history = []
@daily_withdrawal_limit = 500
@daily_withdrawn = 0
@last_withdrawal_date = nil
@frozen = false
end
def deposit(amount)
return false if @frozen || amount <= 0
@balance += amount
record_transaction(:deposit, amount, @balance)
reset_daily_limits_if_new_day
true
end
def withdraw(amount)
return false if @frozen || amount <= 0 || insufficient_funds?(amount) || exceeds_daily_limit?(amount)
@balance -= amount
update_daily_withdrawal(amount)
record_transaction(:withdrawal, amount, @balance)
check_low_balance_warning
true
end
def transfer_to(other_account, amount)
return false unless withdraw(amount)
if other_account.deposit(amount)
record_transaction(:transfer_out, amount, @balance, other_account.account_number)
true
else
# Rollback the withdrawal
@balance += amount
@transaction_history.pop
false
end
end
def freeze_account
@frozen = true
record_transaction(:freeze, 0, @balance)
end
def account_status
status = @frozen ? "FROZEN" : "ACTIVE"
daily_remaining = @daily_withdrawal_limit - @daily_withdrawn
{
status: status,
balance: @balance,
daily_withdrawal_remaining: daily_remaining,
transaction_count: @transaction_history.size,
last_transaction: @transaction_history.last&.dig(:timestamp)
}
end
private
def insufficient_funds?(amount)
@balance < amount
end
def exceeds_daily_limit?(amount)
reset_daily_limits_if_new_day
(@daily_withdrawn + amount) > @daily_withdrawal_limit
end
def update_daily_withdrawal(amount)
reset_daily_limits_if_new_day
@daily_withdrawn += amount
@last_withdrawal_date = Date.today
end
def reset_daily_limits_if_new_day
if @last_withdrawal_date != Date.today
@daily_withdrawn = 0
@last_withdrawal_date = nil
end
end
def record_transaction(type, amount, balance_after, reference = nil)
@transaction_history << {
type: type,
amount: amount,
balance_after: balance_after,
timestamp: Time.now,
reference: reference
}
end
def check_low_balance_warning
puts "WARNING: Low balance ($#{@balance})" if @balance < 100
end
end
account1 = BankAccount.new("ACC-001", 1000)
account2 = BankAccount.new("ACC-002", 500)
account1.withdraw(200)
account1.transfer_to(account2, 150)
puts account1.account_status
# {:status=>"ACTIVE", :balance=>650, :daily_withdrawal_remaining=>150, :transaction_count=>3, :last_transaction=>...}
Metaprogramming with instance variables enables dynamic behavior creation. Objects can define methods at runtime, modify their own structure, and respond to method calls that don't exist through method_missing
.
class ConfigurableObject
def initialize
@config = {}
@computed_values = {}
@value_dependencies = {}
end
def configure(key, value = nil, &block)
if block_given?
@config[key] = block
define_singleton_method(key) { compute_value(key) }
else
@config[key] = value
define_singleton_method(key) { @config[key] }
end
define_singleton_method("#{key}=") do |new_value|
@config[key] = new_value
invalidate_dependents(key)
end
end
def depends_on(key, *dependencies)
@value_dependencies[key] = dependencies
end
def method_missing(method_name, *args)
key = method_name.to_s.gsub(/[=?]$/, '').to_sym
if method_name.to_s.end_with?('?')
@config.key?(key)
elsif method_name.to_s.end_with?('=')
configure(key, args.first)
elsif @config.key?(key)
compute_value(key)
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
key = method_name.to_s.gsub(/[=?]$/, '').to_sym
@config.key?(key) || super
end
private
def compute_value(key)
return @computed_values[key] if @computed_values.key?(key)
if @config[key].is_a?(Proc)
@computed_values[key] = instance_eval(&@config[key])
else
@computed_values[key] = @config[key]
end
end
def invalidate_dependents(changed_key)
@value_dependencies.each do |dependent_key, dependencies|
if dependencies.include?(changed_key)
@computed_values.delete(dependent_key)
invalidate_dependents(dependent_key)
end
end
@computed_values.delete(changed_key)
end
end
obj = ConfigurableObject.new
obj.configure(:base_price, 100)
obj.configure(:tax_rate, 0.08)
obj.configure(:total_price) { base_price * (1 + tax_rate) }
obj.depends_on(:total_price, :base_price, :tax_rate)
puts obj.total_price # => 108.0
obj.base_price = 200
puts obj.total_price # => 216.0
puts obj.tax_rate? # => true
Common Pitfalls
Instance variable initialization timing creates common confusion. Ruby creates instance variables only when assigned, not when declared, and uninitialized instance variables return nil
instead of raising errors.
class ProblematicCounter
def initialize
# @count is not initialized here
end
def increment
@count += 1 # Error: nil can't be coerced into Integer
end
def current_count
@count # Returns nil, not 0
end
end
# Fix: Initialize all instance variables
class CorrectCounter
def initialize
@count = 0
end
def increment
@count += 1
end
def current_count
@count
end
end
counter = ProblematicCounter.new
puts counter.current_count # => nil
# counter.increment # TypeError
fixed_counter = CorrectCounter.new
puts fixed_counter.current_count # => 0
fixed_counter.increment
puts fixed_counter.current_count # => 1
Instance variables lack automatic accessor generation, requiring explicit method definitions for external access. Forgetting to create accessors while expecting external access leads to NoMethodError.
class HiddenData
def initialize(value)
@secret_value = value
end
# No accessor methods defined
end
data = HiddenData.new(42)
# puts data.secret_value # NoMethodError: undefined method
# Fix: Add accessor methods as needed
class AccessibleData
def initialize(value)
@secret_value = value
end
def secret_value
@secret_value
end
def secret_value=(new_value)
@secret_value = new_value
end
end
accessible = AccessibleData.new(42)
puts accessible.secret_value # => 42
accessible.secret_value = 24
puts accessible.secret_value # => 24
Method visibility affects instance variable access patterns. Private methods can access instance variables, but calling private methods externally raises NoMethodError, even when the method exists.
class ConfusingVisibility
def initialize(data)
@data = data
end
def process
validate_data
transform_data
end
private
def validate_data
raise "Invalid data" if @data.nil?
end
def transform_data
@data = @data.upcase
end
end
processor = ConfusingVisibility.new("hello")
processor.process
# processor.validate_data # NoMethodError: private method called
# Access instance variables through public methods only
class ClearVisibility
def initialize(data)
@data = data
end
def process
validate_data
transform_data
@data
end
def valid?
!@data.nil? && !@data.empty?
end
private
def validate_data
raise "Invalid data" unless valid?
end
def transform_data
@data = @data.upcase
end
end
Instance variable scope differs from local variable scope, creating confusion when variables shadow each other or when expecting automatic initialization behaviors.
class ScopeConfusion
def initialize(name)
@name = name
end
def problematic_method(name)
# Local variable 'name' shadows parameter access
name = @name.upcase
puts name # Prints the uppercased @name
puts @name # Still the original @name
# Later code expects 'name' parameter but gets modified local variable
process_name(name) # Gets uppercased version, not original parameter
end
def process_name(input_name)
puts "Processing: #{input_name}"
end
def clear_method(input_name)
# Use different variable names to avoid confusion
processed_name = @name.upcase
puts "Instance variable: #{@name}"
puts "Processed: #{processed_name}"
puts "Parameter: #{input_name}"
process_name(input_name) # Clear which value gets passed
end
end
confusing = ScopeConfusion.new("ruby")
confusing.problematic_method("parameter")
# RUBY
# ruby
# Processing: RUBY
confusing.clear_method("parameter")
# Instance variable: ruby
# Processed: RUBY
# Parameter: parameter
# Processing: parameter
Reference
Instance Variable Operations
Operation | Syntax | Returns | Description |
---|---|---|---|
instance_variable_get(symbol) |
:"@name" |
Value or nil |
Retrieves instance variable value |
instance_variable_set(symbol, value) |
:"@name" , any value |
Set value | Assigns instance variable value |
instance_variable_defined?(symbol) |
:"@name" |
Boolean | Checks if instance variable exists |
instance_variables |
None | Array of symbols | Lists all instance variables |
remove_instance_variable(symbol) |
:"@name" |
Removed value | Removes instance variable |
Method Definition and Access
Method Type | Syntax | Visibility | Access Pattern |
---|---|---|---|
public |
def method_name |
External and internal | object.method_name |
protected |
protected; def method_name |
Same class/subclass | object.method_name within class |
private |
private; def method_name |
Internal only | method_name (no receiver) |
attr_reader |
attr_reader :name |
Public getter | object.name |
attr_writer |
attr_writer :name |
Public setter | object.name = value |
attr_accessor |
attr_accessor :name |
Public getter/setter | Both patterns above |
Common Instance Variable Patterns
Pattern | Example | Use Case |
---|---|---|
Lazy initialization | @value ||= expensive_computation |
Computed values |
Boolean flags | @active = true |
State tracking |
Collections | @items = [] |
Aggregated data |
Nested objects | @address = Address.new |
Composition |
Caching | @cached_result = nil |
Performance optimization |
Method Definition Modifiers
Modifier | Effect | Example |
---|---|---|
alias_method :new, :old |
Creates method alias | Access original method |
undef_method :method |
Removes method definition | Prevent method calls |
define_method(:name) { block } |
Dynamic method creation | Runtime method definition |
method_defined?(:name) |
Checks method existence | Conditional method calls |
private_method_defined?(:name) |
Checks private method | Visibility verification |
Initialization Patterns
Pattern | Code Example | Behavior |
---|---|---|
Required parameters | def initialize(name) |
Must provide value |
Optional parameters | def initialize(name = "Default") |
Uses default if omitted |
Keyword arguments | def initialize(name:, age: 18) |
Named parameters |
Variable arguments | def initialize(*args) |
Accepts any number |
Hash options | def initialize(**opts) |
Named option hash |
Error Conditions
Error Type | Cause | Solution |
---|---|---|
TypeError |
nil + value operations |
Initialize variables properly |
NoMethodError |
Missing accessor methods | Add attr_reader /attr_accessor |
NoMethodError |
Private method external call | Use public interface |
ArgumentError |
Wrong parameter count | Check method signature |
NameError |
Uninitialized constant/variable | Define before use |
Visibility Rules
Context | Public Methods | Protected Methods | Private Methods |
---|---|---|---|
External objects | ✓ Accessible | ✗ NoMethodError | ✗ NoMethodError |
Same class instance | ✓ Accessible | ✓ Accessible | ✗ NoMethodError with receiver |
Subclass instance | ✓ Accessible | ✓ Accessible | ✗ NoMethodError with receiver |
Internal method calls | ✓ Accessible | ✓ Accessible | ✓ Accessible without receiver |
Performance Characteristics
Operation | Time Complexity | Notes |
---|---|---|
Instance variable access | O(1) | Direct memory reference |
Method call | O(1) | Method lookup cached |
instance_variables |
O(n) | Scans all instance variables |
Dynamic method definition | O(1) | Adds to method table |
Method visibility change | O(1) | Updates method metadata |