Overview
Ruby's inspect system provides a standardized way to generate human-readable string representations of objects for debugging and development. The modern inspect format encompasses the #inspect
method, pretty-printing capabilities, and customization techniques that allow developers to control how objects appear in debugging output, logs, and interactive sessions.
The inspect system operates through several key components. The Object#inspect
method serves as the foundation, providing a default implementation that displays the object's class name and instance variable values. The PP
(Pretty Print) module extends this with formatted output for complex data structures. Custom inspect methods allow classes to define their own representation logic.
Ruby's inspect format follows specific conventions for different object types. Basic objects display their class name with instance variables, arrays show comma-separated elements within square brackets, and hashes display key-value pairs with hash rockets or colons depending on key types.
class User
def initialize(name, email)
@name, @email = name, email
end
end
user = User.new("Alice", "alice@example.com")
user.inspect
# => "#<User:0x00007f8b8b8b8b8b @name=\"Alice\", @email=\"alice@example.com\">"
[1, 2, 3].inspect
# => "[1, 2, 3]"
{ name: "Alice", age: 30 }.inspect
# => "{:name=>\"Alice\", :age=>30}"
The inspect system integrates deeply with Ruby's debugging ecosystem. Interactive Ruby (IRB) and Pry rely on inspect methods to display evaluation results. Testing frameworks use inspect output in assertion failures and error messages. Logging systems often incorporate inspect output when displaying object states.
Basic Usage
The #inspect
method returns a string representation of any object. Ruby automatically calls inspect when objects appear in contexts requiring string conversion for debugging purposes. Most built-in classes provide meaningful inspect implementations that reveal internal structure without requiring explicit method calls.
string = "Hello World"
string.inspect
# => "\"Hello World\""
number = 42
number.inspect
# => "42"
array = [1, "two", :three, [4, 5]]
array.inspect
# => "[1, \"two\", :three, [4, 5]]"
The inspect method differs from #to_s
in purpose and output format. While to_s
generates user-facing string representations, inspect produces developer-facing output that preserves type information and internal structure. String objects demonstrate this clearly - to_s
returns the string content while inspect shows the content with quote delimiters.
text = "Hello\nWorld"
text.to_s
# => "Hello\nWorld"
text.inspect
# => "\"Hello\\nWorld\""
Hash and array inspect methods handle nested structures by recursively calling inspect on contained elements. This creates nested representations that maintain readability while preserving structural information.
complex_data = {
users: [
{ name: "Alice", roles: ["admin", "user"] },
{ name: "Bob", roles: ["user"] }
],
settings: { theme: "dark", notifications: true }
}
complex_data.inspect
# => "{:users=>[{:name=>\"Alice\", :roles=>[\"admin\", \"user\"]}, {:name=>\"Bob\", :roles=>[\"user\"]}], :settings=>{:theme=>\"dark\", :notifications=>true}}"
Custom classes inherit the default inspect behavior from Object
, which displays the class name, object ID, and instance variables. This provides immediate visibility into object state during development and debugging sessions.
class Product
def initialize(name, price, category)
@name = name
@price = price
@category = category
end
end
product = Product.new("Laptop", 999.99, "Electronics")
product.inspect
# => "#<Product:0x00007f8b8c1a2b3c @name=\"Laptop\", @price=999.99, @category=\"Electronics\">"
Advanced Usage
Custom inspect methods allow classes to control their debugging representation by overriding the default #inspect
implementation. These methods should return strings that clearly identify the object type and relevant internal state while maintaining consistency with Ruby's inspect conventions.
class BankAccount
def initialize(account_number, balance, account_type)
@account_number = account_number
@balance = balance
@account_type = account_type
end
def inspect
"#<#{self.class.name}:#{account_number_masked} " \
"balance=#{@balance} type=#{@account_type}>"
end
private
def account_number_masked
@account_number.to_s.gsub(/.(?=.{4})/, '*')
end
end
account = BankAccount.new("123456789", 1500.0, "checking")
account.inspect
# => "#<BankAccount:*****6789 balance=1500.0 type=checking>"
Complex domain objects benefit from inspect methods that highlight the most relevant attributes while omitting internal implementation details. This approach reduces noise in debugging output and focuses attention on business-relevant information.
class Order
def initialize(id, customer, items, status = 'pending')
@id = id
@customer = customer
@items = items
@status = status
@created_at = Time.now
@internal_tracking_data = generate_tracking_data
end
def inspect
item_summary = @items.length == 1 ? "1 item" : "#{@items.length} items"
"#<Order##{@id} customer=#{@customer.name} #{item_summary} status=#{@status}>"
end
private
def generate_tracking_data
{ workflow_stage: 'initial', processing_flags: [] }
end
end
customer = Struct.new(:name).new("Alice Johnson")
items = ["Widget A", "Widget B", "Widget C"]
order = Order.new(12345, customer, items)
order.inspect
# => "#<Order#12345 customer=Alice Johnson 3 items status=pending>"
The PP
(Pretty Print) module provides enhanced formatting for complex data structures that become unreadable when displayed on single lines. Pretty printing automatically handles indentation, line breaks, and structural organization.
require 'pp'
nested_data = {
configuration: {
database: {
host: "localhost",
port: 5432,
credentials: { username: "app", password: "[FILTERED]" }
},
cache: {
provider: "redis",
settings: { ttl: 3600, max_connections: 10 }
}
},
features: ["authentication", "authorization", "logging", "monitoring"]
}
# Standard inspect - single line, hard to read
puts nested_data.inspect
# Pretty print - formatted with indentation
pp nested_data
# => {:configuration=>
# {:database=>
# {:host=>"localhost",
# :port=>5432,
# :credentials=>{:username=>"app", :password=>"[FILTERED]"}},
# :cache=>
# {:provider=>"redis", :settings=>{:ttl=>3600, :max_connections=>10}}},
# :features=>["authentication", "authorization", "logging", "monitoring"]}
Metaprogramming techniques enable dynamic inspect behavior based on runtime conditions or configuration settings. Classes can modify their inspect output based on application state, security contexts, or debugging modes.
class ApiResponse
attr_reader :status, :headers, :body, :debug_mode
def initialize(status, headers, body, debug_mode: false)
@status = status
@headers = headers
@body = body
@debug_mode = debug_mode
end
def inspect
base_info = "#<ApiResponse status=#{@status}"
if @debug_mode
"#{base_info} headers=#{@headers.inspect} body=#{body_summary}>"
else
"#{base_info} size=#{@body.length}>"
end
end
private
def body_summary
@body.length > 100 ? "#{@body[0..97]}..." : @body
end
end
response = ApiResponse.new(200, {"Content-Type" => "application/json"},
'{"users": [{"name": "Alice"}, {"name": "Bob"}]}')
response.inspect
# => "#<ApiResponse status=200 size=43>"
debug_response = ApiResponse.new(200, {"Content-Type" => "application/json"},
'{"users": [{"name": "Alice"}, {"name": "Bob"}]}',
debug_mode: true)
debug_response.inspect
# => "#<ApiResponse status=200 headers={\"Content-Type\"=>\"application/json\"} body={\"users\": [{\"name\": \"Alice\"}, {\"name\": \"Bob\"}]}>"
Error Handling & Debugging
Inspect methods must handle edge cases gracefully to avoid generating exceptions during debugging sessions. Defensive programming techniques prevent inspect calls from failing when objects contain nil values, circular references, or invalid states.
class SafeContainer
def initialize(data = nil)
@data = data
@metadata = {}
end
def inspect
data_repr = @data.nil? ? "nil" : @data.inspect
"#<#{self.class.name} data=#{data_repr} metadata=#{@metadata.inspect}>"
rescue => e
"#<#{self.class.name} [inspect error: #{e.class.name}]>"
end
end
# Test with various problematic states
container1 = SafeContainer.new(nil)
container1.inspect
# => "#<SafeContainer data=nil metadata={}>"
container2 = SafeContainer.new("normal data")
container2.inspect
# => "#<SafeContainer data=\"normal data\" metadata={}>"
Circular reference detection prevents infinite recursion when objects contain references to themselves or participate in reference cycles. Ruby's default inspect implementation handles simple circular references, but custom inspect methods require explicit protection.
class Node
attr_accessor :value, :children, :parent
def initialize(value)
@value = value
@children = []
@parent = nil
end
def inspect
# Track objects being inspected to detect cycles
@inspecting ||= false
return "#<Node:#{object_id} [circular]>" if @inspecting
@inspecting = true
result = "#<Node value=#{@value.inspect} " \
"children=[#{@children.map(&:inspect).join(', ')}]>"
@inspecting = false
result
rescue => e
@inspecting = false
"#<Node [inspect error: #{e.message}]>"
end
end
# Create circular reference
root = Node.new("root")
child = Node.new("child")
root.children << child
child.parent = root
root.inspect
# => "#<Node value=\"root\" children=[#<Node value=\"child\" children=[]>]>"
Exception handling within inspect methods prevents debugging sessions from crashing when objects enter invalid states. Recovery strategies include fallback representations and error reporting that maintains debugging workflow continuity.
class DatabaseConnection
def initialize(host, port, database)
@host = host
@port = port
@database = database
@connection = nil
@last_error = nil
end
def connect
# Simulate connection logic that might fail
@connection = @host.nil? ? nil : "connected"
rescue => e
@last_error = e
@connection = nil
end
def inspect
connection_status = case
when @connection
"connected"
when @last_error
"error: #{@last_error.message}"
else
"disconnected"
end
"#<DatabaseConnection #{@host}:#{@port}/#{@database} [#{connection_status}]>"
rescue => e
# Fallback when even basic attribute access fails
"#<DatabaseConnection [inspect failed: #{e.class.name}]>"
end
end
# Test error scenarios
db1 = DatabaseConnection.new("localhost", 5432, "myapp")
db1.inspect
# => "#<DatabaseConnection localhost:5432/myapp [disconnected]>"
db2 = DatabaseConnection.new(nil, 5432, "myapp")
db2.connect
db2.inspect
# => "#<DatabaseConnection :5432/myapp [error: undefined method `nil?' for nil:NilClass]>"
Common Pitfalls
String encoding issues frequently cause inspect output to display incorrectly or raise exceptions. Ruby's inspect method attempts to represent strings safely by escaping non-printable characters, but encoding mismatches can create unexpected results.
# UTF-8 string with special characters
utf8_string = "Héllö Wörld! 🌍"
utf8_string.inspect
# => "\"Héllö Wörld! 🌍\""
# Binary data incorrectly treated as text
binary_data = "\x89PNG\r\n\x1a\n".force_encoding('UTF-8')
binary_data.inspect
# => "\"\x89PNG\r\n\x1A\n\""
# Proper binary handling
binary_data.force_encoding('ASCII-8BIT')
binary_data.inspect
# => "\"\x89PNG\r\n\x1A\n\""
Performance implications emerge when inspect methods perform expensive operations or access external resources. Inspect calls occur frequently during debugging and development, making performance-conscious implementation crucial.
class ExpensiveResource
def initialize(id)
@id = id
end
# Problematic - performs expensive operation during inspect
def bad_inspect
data = fetch_from_database(@id) # Expensive database call
"#<ExpensiveResource id=#{@id} data=#{data.inspect}>"
end
# Better - uses cached or minimal representation
def inspect
"#<ExpensiveResource id=#{@id} [use #details for full data]>"
end
def details
fetch_from_database(@id)
end
private
def fetch_from_database(id)
sleep(0.1) # Simulate database delay
{ name: "Resource #{id}", status: "active" }
end
end
resource = ExpensiveResource.new(123)
resource.inspect # Fast
# => "#<ExpensiveResource id=123 [use #details for full data]>"
Mutating object state within inspect methods creates subtle bugs that manifest during debugging sessions. Inspect methods should be read-only operations that do not modify object state or trigger side effects.
class Counter
def initialize
@count = 0
@inspect_calls = 0
end
# Problematic - modifies state during inspection
def bad_inspect
@inspect_calls += 1 # Side effect!
"#<Counter count=#{@count} inspected=#{@inspect_calls} times>"
end
# Correct - read-only inspection
def inspect
"#<Counter count=#{@count}>"
end
def increment
@count += 1
end
end
counter = Counter.new
counter.increment
# Multiple inspect calls shouldn't change object behavior
3.times { counter.inspect }
counter.inspect
# => "#<Counter count=1>"
Security vulnerabilities arise when inspect methods inadvertently expose sensitive information in logs, error messages, or debugging output. Careful filtering prevents credential leakage while maintaining debugging utility.
class UserSession
def initialize(user_id, token, permissions)
@user_id = user_id
@token = token
@permissions = permissions
@created_at = Time.now
end
# Problematic - exposes sensitive token
def bad_inspect
"#<UserSession user=#{@user_id} token=#{@token} permissions=#{@permissions}>"
end
# Secure - filters sensitive data
def inspect
token_preview = @token ? "#{@token[0..3]}***" : "nil"
"#<UserSession user=#{@user_id} token=#{token_preview} " \
"permissions=#{@permissions.length} created=#{@created_at}>"
end
end
session = UserSession.new(12345, "abc123def456ghi789", ["read", "write"])
session.inspect
# => "#<UserSession user=12345 token=abc1*** permissions=2 created=2025-09-01 10:30:45 -0500>"
Reference
Core Methods
Method | Parameters | Returns | Description |
---|---|---|---|
Object#inspect |
None | String |
Returns string representation for debugging |
Kernel#p(*objects) |
Variable arguments | Array |
Prints inspect output and returns objects |
PP.pp(object, output=$>, width=79) |
Object, IO, Integer | nil |
Pretty prints object to output stream |
Object#pretty_inspect |
None | String |
Returns pretty printed string representation |
Pretty Print Methods
Method | Parameters | Returns | Description |
---|---|---|---|
PP.pp(obj, out, width) |
Object, IO, Integer | nil |
Pretty print object with width limit |
PP.singleline_pp(obj, out) |
Object, IO | nil |
Single line pretty print |
PP.sharing_detection |
None | Boolean |
Current sharing detection setting |
PP.sharing_detection=(val) |
Boolean | Boolean |
Enable/disable sharing detection |
Inspect Behavior by Type
Type | Inspect Format | Example |
---|---|---|
String | Quoted with escapes | "Hello\nWorld" |
Symbol | Colon prefix | :symbol_name |
Integer | Numeric value | 42 |
Float | Decimal notation | 3.14159 |
Array | Square brackets | [1, 2, 3] |
Hash | Curly braces | {:key => "value"} |
Nil | Literal nil | nil |
Boolean | true/false | true |
Range | Dot notation | 1..10 |
Regexp | Forward slashes | /pattern/flags |
Custom Inspect Guidelines
Guideline | Format | Example |
---|---|---|
Class identification | #<ClassName:object_id ...> |
#<User:0x007f8b...> |
Attribute display | attr=value |
name="Alice" |
State indication | [status] |
[connected] |
Collection summary | count items |
5 items |
Sensitive filtering | prefix*** |
abc*** |
Performance Considerations
Scenario | Impact | Recommendation |
---|---|---|
Database access | High latency | Cache or summarize data |
Large collections | Memory/CPU | Limit displayed elements |
Complex calculations | CPU overhead | Pre-compute or approximate |
Network calls | High latency + failure risk | Avoid external dependencies |
Recursive structures | Stack overflow risk | Implement cycle detection |
Security Checklist
Risk Category | Mitigation Strategy |
---|---|
Credential exposure | Filter tokens, passwords, keys |
Personal information | Mask or truncate sensitive fields |
Internal structure | Hide implementation details |
System information | Avoid exposing paths, hostnames |
Debug information | Disable verbose output in production |
Common Patterns
# Standard object inspect
def inspect
"#<#{self.class.name}:0x#{object_id.to_s(16)} @attr=#{@attr.inspect}>"
end
# Filtered sensitive data
def inspect
safe_attrs = instance_variables.map do |var|
value = instance_variable_get(var)
filtered_value = sensitive?(var) ? "[FILTERED]" : value.inspect
"#{var}=#{filtered_value}"
end.join(" ")
"#<#{self.class.name}:0x#{object_id.to_s(16)} #{safe_attrs}>"
end
# Collection summary
def inspect
count = @items.length
summary = count.zero? ? "empty" : "#{count} items"
"#<#{self.class.name} #{summary}>"
end
# State-based representation
def inspect
status = connected? ? "connected" : "disconnected"
"#<#{self.class.name} #{@host}:#{@port} [#{status}]>"
end