Overview
Ruby provides several built-in methods for object inspection that convert objects into readable string representations. The core inspection methods include inspect, to_s, p, pp, and display, each serving different purposes in object examination and debugging workflows.
The inspect method returns a string representation of an object that aims to be unambiguous and suitable for debugging. Ruby calls inspect internally when displaying objects in interactive sessions and when objects appear in arrays or hashes during output.
arr = [1, "hello", :symbol, nil]
arr.inspect
# => "[1, \"hello\", :symbol, nil]"
hash = { name: "Alice", age: 30 }
hash.inspect
# => "{:name=>\"Alice\", :age=>30}"
The to_s method provides a string representation intended for end-user display rather than debugging. Many Ruby classes override to_s to provide human-readable output while leaving inspect for technical representation.
time = Time.new(2024, 3, 15, 14, 30)
time.to_s # => "2024-03-15 14:30:00 -0500"
time.inspect # => "2024-03-15 14:30:00.000000000 -0500"
Ruby also provides convenience methods like p for quick debugging output and pp for pretty-printed formatted output of complex data structures. The display method writes the object's string representation directly to an output stream without adding newlines.
Object inspection forms the foundation of Ruby's debugging and development experience, enabling developers to examine program state and verify object behavior during execution.
Basic Usage
The p method combines object inspection with immediate output, making it invaluable for debugging. It calls inspect on each argument and prints the result to standard output, returning the original object.
def calculate_total(items)
total = 0
items.each do |item|
p item # Debug output
total += item[:price] * item[:quantity]
end
total
end
items = [
{ name: "Book", price: 15.99, quantity: 2 },
{ name: "Pen", price: 1.50, quantity: 5 }
]
result = calculate_total(items)
# Output: {:name=>"Book", :price=>15.99, :quantity=>2}
# {:name=>"Pen", :price=>1.50, :quantity=>5}
The inspect method provides detailed object representation that includes class information and internal state. Different Ruby classes implement inspect to show relevant details for debugging purposes.
str = "Hello\nWorld"
str.inspect # => "\"Hello\\nWorld\""
regexp = /[a-z]+/i
regexp.inspect # => "/[a-z]+/i"
range = (1..10)
range.inspect # => "1..10"
For complex data structures, the pp method from the PP library provides formatted output with proper indentation and line breaks. This method proves especially valuable when examining nested structures.
require 'pp'
data = {
users: [
{ id: 1, name: "Alice", preferences: { theme: "dark", lang: "en" } },
{ id: 2, name: "Bob", preferences: { theme: "light", lang: "es" } }
],
settings: { timeout: 300, retry_limit: 3 }
}
pp data
# Output:
# {:users=>
# [{:id=>1, :name=>"Alice", :preferences=>{:theme=>"dark", :lang=>"en"}},
# {:id=>2, :name=>"Bob", :preferences=>{:theme=>"light", :lang=>"es"}}],
# :settings=>{:timeout=>300, :retry_limit=>3}}
The display method writes object representations directly to output streams without automatic newlines, providing precise control over output formatting.
numbers = [1, 2, 3, 4, 5]
numbers.each { |n| n.display; print " " }
# Output: 1 2 3 4 5
File.open("debug.log", "w") do |file|
data = { timestamp: Time.now, event: "user_login" }
data.display(file)
end
Advanced Usage
Custom classes can override inspect and to_s to provide meaningful representations tailored to specific use cases. The inspect method should return a string that helps developers understand the object's state and identity.
class BankAccount
def initialize(account_number, balance)
@account_number = account_number
@balance = balance
end
def inspect
"#<BankAccount:#{object_id} @account_number=#{@account_number.inspect} @balance=#{@balance.inspect}>"
end
def to_s
"Account #{@account_number[-4..-1].rjust(4, '*')}: $#{@balance}"
end
end
account = BankAccount.new("1234567890", 1500.75)
account.inspect # => "#<BankAccount:70245851180460 @account_number=\"1234567890\" @balance=1500.75>"
account.to_s # => "Account 7890: $1500.75"
p account # Uses inspect method
puts account # Uses to_s method
Metaprogramming techniques enable dynamic inspection capabilities that adapt based on object state or configuration. Classes can implement conditional inspection behavior or generate inspection output programmatically.
class ConfigurableInspection
attr_accessor :debug_mode
def initialize(data)
@data = data
@debug_mode = false
end
def inspect
if @debug_mode
instance_variables.map do |var|
"#{var}=#{instance_variable_get(var).inspect}"
end.join(" ")
else
"#<#{self.class.name}:#{object_id}>"
end
end
end
obj = ConfigurableInspection.new({ key: "value", count: 42 })
obj.inspect # => "#<ConfigurableInspection:70245851180460>"
obj.debug_mode = true
obj.inspect # => "@data={:key=>\"value\", :count=>42} @debug_mode=true"
Ruby's inspection methods integrate with custom formatting systems for specialized output requirements. Classes can implement inspection methods that generate different formats based on context or output destination.
class APIResponse
def initialize(status, data, headers = {})
@status = status
@data = data
@headers = headers
@timestamp = Time.now
end
def inspect
"#<APIResponse status=#{@status} data_size=#{@data.to_s.length} headers=#{@headers.length}>"
end
def detailed_inspect
{
status: @status,
data: @data,
headers: @headers,
timestamp: @timestamp,
object_id: object_id
}.inspect
end
def json_inspect
require 'json'
{
status: @status,
data: @data,
timestamp: @timestamp.iso8601
}.to_json
end
end
response = APIResponse.new(200, { users: ["Alice", "Bob"] }, { "Content-Type" => "application/json" })
response.inspect # => "#<APIResponse status=200 data_size=23 headers=1>"
response.detailed_inspect # => "{:status=>200, :data=>{:users=>[\"Alice\", \"Bob\"]}, ...}"
response.json_inspect # => "{\"status\":200,\"data\":{\"users\":[\"Alice\",\"Bob\"]},\"timestamp\":\"2024-03-15T19:30:00Z\"}"
Advanced inspection techniques include conditional output, sensitive data masking, and integration with logging systems. These patterns enable production-safe inspection methods that protect sensitive information while providing useful debugging output.
class User
attr_reader :id, :username, :email
def initialize(id, username, email, password)
@id = id
@username = username
@email = email
@password = password
end
def inspect
if ENV['RAILS_ENV'] == 'production'
"#<User id=#{@id} username=#{@username.inspect}>"
else
"#<User id=#{@id} username=#{@username.inspect} email=#{@email.inspect}>"
end
end
def safe_inspect
attrs = instance_variables.reject { |var| var.to_s.include?('password') }
attrs.map { |var| "#{var}=#{instance_variable_get(var).inspect}" }.join(" ")
end
end
Error Handling & Debugging
Object inspection methods can raise exceptions when dealing with problematic objects or circular references. The most common issue occurs with objects that contain circular references, causing infinite recursion during string conversion.
class Node
attr_accessor :value, :next_node
def initialize(value)
@value = value
@next_node = nil
end
end
# Create circular reference
node1 = Node.new("first")
node2 = Node.new("second")
node1.next_node = node2
node2.next_node = node1
# This will raise SystemStackError due to infinite recursion
begin
node1.inspect
rescue SystemStackError => e
puts "Circular reference detected: #{e.message}"
end
Ruby provides mechanisms to detect and handle circular references during inspection. The pp library includes built-in circular reference detection that prevents infinite loops during pretty printing.
require 'pp'
# Create circular structure
hash1 = { name: "hash1" }
hash2 = { name: "hash2" }
hash1[:ref] = hash2
hash2[:ref] = hash1
# pp handles circular references automatically
pp hash1
# Output: {:name=>"hash1", :ref=>{:name=>"hash2", :ref=>#<Object:0x... @name="hash1", @ref=...>}}
Custom inspection methods should implement safeguards against problematic scenarios such as missing instance variables, nil values, or objects in invalid states. Defensive programming techniques prevent inspection failures during debugging sessions.
class SafeInspection
def initialize(data)
@data = data
@processed = false
end
def inspect
begin
state = @processed ? "processed" : "unprocessed"
data_info = @data.nil? ? "no data" : "#{@data.class}(#{@data.size rescue 'unknown size'})"
"#<#{self.class.name}:#{object_id} state=#{state} data=#{data_info}>"
rescue => e
"#<#{self.class.name}:#{object_id} inspection_error=#{e.class.name}>"
end
end
end
# Test with various problematic scenarios
obj1 = SafeInspection.new(nil)
obj1.inspect # => "#<SafeInspection:70245851180460 state=unprocessed data=no data>"
obj2 = SafeInspection.new("not enumerable")
obj2.inspect # => "#<SafeInspection:70245851180461 state=unprocessed data=String(unknown size)>"
Debugging complex inspection issues often requires examining the method resolution order and understanding which classes provide specific inspection implementations. Ruby's introspection methods help identify inspection method sources.
class DebugInspection
def self.trace_inspection_methods(obj)
methods = [:inspect, :to_s, :display]
methods.each do |method|
method_obj = obj.method(method)
puts "#{method}: defined in #{method_obj.owner}"
puts " Source location: #{method_obj.source_location}"
puts " Parameters: #{method_obj.parameters}"
end
end
end
# Trace inspection methods for different object types
DebugInspection.trace_inspection_methods("string")
DebugInspection.trace_inspection_methods([1, 2, 3])
DebugInspection.trace_inspection_methods({ key: :value })
Common Pitfalls
The distinction between inspect and to_s frequently causes confusion among Ruby developers. Many assume these methods produce identical output, but they serve different purposes and often return different strings.
class Product
def initialize(name, price)
@name = name
@price = price
end
def to_s
"#{@name} - $#{@price}"
end
# No custom inspect method - uses default
end
product = Product.new("Laptop", 999.99)
product.to_s # => "Laptop - $999.99"
product.inspect # => "#<Product:0x00007f8b1c0a1234 @name=\"Laptop\", @price=999.99>"
puts product # Uses to_s: "Laptop - $999.99"
p product # Uses inspect: #<Product:0x00007f8b1c0a1234 @name="Laptop", @price=999.99>
String escaping in inspect output catches developers off guard when dealing with special characters, newlines, and quotes. The inspect method produces Ruby literal representations that include necessary escape sequences.
text_with_escapes = "Line 1\nLine 2\tTabbed\nQuote: \"Hello\""
puts text_with_escapes
# Output: Line 1
# Line 2 Tabbed
# Quote: "Hello"
text_with_escapes.inspect
# => "Line 1\\nLine 2\\tTabbed\\nQuote: \\\"Hello\\\""
# Common mistake: expecting inspect output to match display output
filename = "report\n2024.txt"
puts "Processing: #{filename.inspect}" # Correct: shows escaped newline
puts "Processing: #{filename}" # Incorrect: literal newline in output
Infinite recursion occurs when objects contain circular references and custom inspection methods lack proper safeguards. This issue commonly appears in linked data structures, graph representations, and objects with bidirectional relationships.
class LinkedNode
attr_accessor :value, :parent, :children
def initialize(value)
@value = value
@parent = nil
@children = []
end
# Dangerous: can cause infinite recursion
def inspect_unsafe
"#<LinkedNode value=#{@value.inspect} parent=#{@parent.inspect} children=#{@children.inspect}>"
end
# Safe implementation with recursion detection
def inspect
return "#<LinkedNode:#{object_id} value=#{@value.inspect} (inspecting...)>" if @inspecting
@inspecting = true
result = "#<LinkedNode:#{object_id} value=#{@value.inspect} parent=#{@parent ? @parent.object_id : 'nil'} children=#{@children.length}>"
@inspecting = false
result
rescue
@inspecting = false
"#<LinkedNode:#{object_id} (inspection failed)>"
end
end
# Create circular reference
parent = LinkedNode.new("parent")
child = LinkedNode.new("child")
parent.children << child
child.parent = parent
parent.inspect # Safe: shows object_id instead of full parent inspection
Performance issues arise when inspection methods perform expensive operations or process large data sets. Developers often overlook the performance implications of complex inspection logic, especially in logging and debugging scenarios.
class ExpensiveInspection
def initialize(data)
@data = data
end
# Problematic: expensive operation in inspect
def inspect_slow
processed_data = @data.map { |item| expensive_calculation(item) }
"#<ExpensiveInspection processed=#{processed_data.inspect}>"
end
# Better: defer expensive operations
def inspect
"#<ExpensiveInspection data_size=#{@data.size} sample=#{@data.first(3).inspect}>"
end
def detailed_inspect
# Expensive version available when explicitly requested
processed_data = @data.map { |item| expensive_calculation(item) }
"#<ExpensiveInspection processed=#{processed_data.inspect}>"
end
private
def expensive_calculation(item)
# Simulate expensive operation
sleep(0.01)
item * 2
end
end
Memory leaks can occur when inspection methods retain references to large objects or create unnecessary object allocations. This particularly affects long-running applications with frequent debugging output.
class MemoryEfficientInspection
def initialize(large_dataset)
@large_dataset = large_dataset
end
# Memory-inefficient: creates large string allocations
def inspect_wasteful
"#<DataProcessor dataset=#{@large_dataset.inspect}>"
end
# Memory-efficient: provides summary information
def inspect
"#<DataProcessor dataset_size=#{@large_dataset.size} memory=#{@large_dataset.size * 8} bytes>"
end
end
Reference
Core Inspection Methods
| Method | Parameters | Returns | Description |
|---|---|---|---|
#inspect |
None | String |
Returns debugging representation of object |
#to_s |
None | String |
Returns string representation for display |
#p(*objs) |
Zero or more objects | Last object | Prints inspect output and returns object |
#pp(obj, out=$>, width=79) |
Object, output stream, width | nil |
Pretty prints object with formatting |
#display(port=$>) |
Output stream | nil |
Writes string representation to stream |
Object Identity Methods
| Method | Parameters | Returns | Description |
|---|---|---|---|
#object_id |
None | Integer |
Returns unique identifier for object |
#class |
None | Class |
Returns class of object |
#instance_variables |
None | Array<Symbol> |
Returns array of instance variable names |
#instance_variable_get(symbol) |
Symbol | Object | Returns value of instance variable |
Debugging Helper Methods
| Method | Parameters | Returns | Description |
|---|---|---|---|
#method(name) |
Symbol or String | Method |
Returns Method object for given name |
#methods(regular=true) |
Boolean | Array<Symbol> |
Returns array of method names |
#respond_to?(method, private=false) |
Symbol, Boolean | Boolean |
Tests if object responds to method |
#kind_of?(class) |
Class | Boolean |
Tests if object is instance of class |
Pretty Print Options
| Option | Type | Default | Description |
|---|---|---|---|
| width | Integer | 79 | Maximum line width for formatting |
| newline | String | newline character | Line separator string |
| genspace | Proc | space generator function | Space generation function |
| output | IO | standard output | Output stream for printing |
Common Escape Sequences in Inspect Output
| Character | Inspect Output | Description |
|---|---|---|
\n |
\\n |
Newline character |
\t |
\\t |
Tab character |
\" |
\\\" |
Double quote |
\\ |
\\\\ |
Backslash |
\r |
\\r |
Carriage return |
Standard Object Inspection Formats
| Object Type | Inspect Format Example |
|---|---|
| String | "content with \"quotes\"" |
| Symbol | :symbol_name |
| Array | [1, "two", :three] |
| Hash | {:key => "value", "string_key" => 42} |
| Range | 1..10 or 1...10 |
| Regexp | /pattern/flags |
| Class | #<Class:0x007f8b1c0a1234> |
| Module | #<Module:0x007f8b1c0a1234> |
Error Types in Inspection
| Exception | Common Cause | Prevention Strategy |
|---|---|---|
SystemStackError |
Circular references in inspect | Implement recursion detection |
NoMethodError |
Missing to_s or inspect method | Define default implementations |
ArgumentError |
Invalid parameters to pp or display | Validate parameters before calling |
Encoding::CompatibilityError |
Mixed encodings in string output | Ensure consistent encoding |
StandardError |
General exceptions in custom inspect | Use rescue blocks in custom methods |
Thread Safety Considerations
| Scenario | Risk Level | Mitigation |
|---|---|---|
| Shared mutable state in inspect | High | Use local variables or synchronization |
| Global variables in inspection | Medium | Avoid or protect with mutexes |
| Instance variable modification | Low | Make inspection methods read-only |
| Concurrent pp output | Low | Use separate output streams |