CrackedRuby logo

CrackedRuby

Modern Inspect Format

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