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 | "\n" | Line separator string |
:genspace |
Proc | `lambda{ | n |
:output |
IO | $> |
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 |