skip to content

In Ruby, what is the difference between to_s and inspect, and which of them do puts, p, interpolation and Array#to_s use?

level: middleimportance: must knowfreq 64%

answer

  1. human text vs developer view
  2. puts and "#{}" call to_s
  3. p calls inspect
  4. Array#to_s is an alias of inspect
  5. Kernel#inspect ignores to_s since 2.0

basics

~10 s

to_s is an object's plain, human-facing text; inspect is the developer-facing form showing type and contents. puts and string interpolation call to_s, p calls inspect, and Array#to_s and Hash#to_s are aliases of inspect.

solid answer

~40 s

`to_s` is the text meant for people: `"Leeds".to_s` is `Leeds`, `nil.to_s` is empty. `inspect` shows what the value is: quotes around strings, `nil` spelled out, a colon on symbols, and for an ordinary object `#<Stop:0x... @name="Leeds">` with its instance variables. `puts`, `print` and `"#{obj}"` call `to_s`; `p` and `pp` call `inspect`. Containers are the twist: `Array#to_s` and `Hash#to_s` are aliases of `inspect`, and they call `inspect` on their elements, so an element's custom `to_s` never shows inside `p [obj]` or `"#{[obj]}"`. Since Ruby 2.0 the default `inspect` no longer calls `to_s`, so overriding `to_s` alone does not change what `p` shows. And if `to_s` returns a non-String, interpolation and `puts` quietly fall back to the default `#<Stop:0x...>`.

code

ruby · 20 lines
ruby
class Stop
  def initialize(name)
    @name = name
  end

  def to_s = @name
end

stop = Stop.new("Leeds")
puts stop              # Leeds
puts "Next: #{stop}"   # Next: Leeds
p stop                 # #<Stop:0x... @name="Leeds">
puts "All: #{[stop]}"  # All: [#<Stop:0x... @name="Leeds">]

class Broken
  def to_s = nil
end
puts "Got #{Broken.new}"  # Got #<Broken:0x...>

p({platform: 4, "line" => "R2"})  # {platform: 4, "line" => "R2"}

go deeper

for a junior

Recall the split: puts and interpolation use to_s, p uses inspect, and inspect shows quotes, nil and symbols that to_s hides.

for a middle

Explain that collections render elements with inspect, that Kernel#inspect no longer calls to_s, and that a non-String to_s silently falls back to the default representation.

for a senior

Design both methods deliberately: to_s for user-facing text, inspect for diagnostics, and never let specs or log parsers depend on inspect formats that changed in 3.4 and 4.0.

for a principal

Treat inspect as an unstable diagnostic view across upgrades; anything that must stay parseable needs an explicit serialisation owned by the team.

Every Ruby object answers two "turn yourself into text" methods, and knowing which one each printing path calls explains most confusing output in logs, consoles and tests. ## Two methods, two audiences - **`to_s`** - the **plain text** of an object, meant for end users or for building other strings. The default from `Kernel#to_s` is the class name and an encoding of the object's address, such as `#<Stop:0x000000010a3b1c28>`. - **`inspect`** - the **developer-facing representation**, meant to show what a value is. The default `Kernel#inspect` shows the class name, the address and every instance variable with its value (each rendered with `inspect` in turn), such as `#<Stop:0x000000010a3b1c28 @name="Leeds", @platform=4>`. With no instance variables it looks like the default `to_s`. Built-in classes override both. A few side by side: | Value | `to_s` | `inspect` | |---|---|---| | `"a\tb"` | `a` + tab + `b` | `"a\tb"` (quoted, escaped) | | `nil` | `""` (empty) | `nil` | | `:leeds` | `leeds` | `:leeds` | | `[1, "x"]` | `[1, "x"]` | `[1, "x"]` | | `{platform: 4}` | `{platform: 4}` | `{platform: 4}` | | `Set[1, 2]` | `Set[1, 2]` | `Set[1, 2]` | ## Who calls which 1. **`puts` and `print`** call `to_s` on anything that is not already a String. 2. **String interpolation** `"#{obj}"` calls `to_s` (a String is used as it is). 3. **`p` and `pp`** call `inspect`. 4. **`Array#to_s` and `Hash#to_s`** are aliases of their `inspect`, so interpolating or `puts`-ing a whole hash, or `print`-ing an array, shows the inspect form - and each element is rendered with its own `inspect`, not its `to_s`. That last point is the frequent surprise. Give a `Stop` class a friendly `to_s` returning `"Leeds"`: `puts stop` and `"Next: #{stop}"` show `Leeds`, but `p stop`, `p [stop]` and `"Stops: #{[stop]}"` all show `#<Stop:0x...>`, because those paths go through `inspect`. ## inspect does not call to_s Before Ruby 2.0, the default `inspect` used to call a redefined `to_s`. Since 2.0, `Kernel#inspect` never calls `to_s`. The consequence for class design: - Override **`to_s`** for how the object appears in user-facing text and interpolation. - Override **`inspect`** (or, in Ruby 4.0, filter it with `instance_variables_to_inspect`) for how it appears in `p`, `pp`, a console and inside collections. - If both should match, define `inspect` explicitly; there is no automatic link. ## When to_s returns something that is not a String Interpolation and `puts` expect a String back. If an override returns `nil`, a number or anything else, Ruby does not raise: it silently substitutes the default `Kernel#to_s` output, `#<Stop:0x...>`. A buggy `to_s` therefore shows up as an unexpected address in output, not as an exception - worth remembering when a log line suddenly contains `#<...:0x...>`. ## Version notes that change what you see The inspect format of core classes is not a stable contract, and it has moved recently: - **Ruby 3.4** changed `Hash#inspect`: symbol keys print as `{platform: 4}` instead of `{:platform=>4}`, and other keys get spaces, `{"line" => "R2"}` instead of `{"line"=>"R2"}`. - **Ruby 4.0** made `Set` a core class and changed `Set#inspect` to `Set[1, 2]` from `#<Set: {1, 2}>`. - **Ruby 4.0** added `instance_variables_to_inspect` to control which instance variables the default `inspect` shows. Tests that compared `p` output or `inspect` strings of hashes broke on the 3.4 upgrade for exactly this reason. Assert on the data - the hash itself, or a deliberate serialisation such as JSON - not on its `inspect` text. ## Designing both for your own classes A value class such as a departure record usually wants both methods, for different readers: - `to_s` returns what a passenger display would show, e.g. `08:15 Leeds (platform 4)`, so interpolation into messages reads naturally. - `inspect` returns something a developer can recognise and ideally paste back into code, e.g. `#<Departure 08:15 "Leeds" platform=4>`, keeping quotes so a trailing space in the destination stays visible. - Keep `inspect` short and cheap. It runs whenever the object is shown in a console or inside a logged collection, so it should not trigger database queries or walk large graphs. `Data.define` and `Struct` classes already provide a readable `inspect` listing their members, which is one reason they are the default choice for small value objects. ## A short checklist - Readable output for people: `to_s`, via `puts` or interpolation. - Unambiguous output for developers: `inspect`, via `p`, `pp` or a console. - Collections always show their elements' `inspect`. - Never parse or assert on `inspect` output across Ruby versions.

  • In Ruby, what happens when a class's to_s returns nil and an instance is interpolated into a string?
    No exception is raised. Interpolation expects a String from `to_s`; when it gets anything else, Ruby substitutes the default `Kernel#to_s` output, `#<ClassName:0x...>`. The same fallback applies to `puts`. The symptom of a broken `to_s` is therefore an object address in the output, not a `TypeError`.
  • Why did specs that compared p or inspect output of hashes start failing on Ruby 3.4?
    Ruby 3.4 changed `Hash#inspect`: symbol keys now render as `{platform: 4}` and other keys as `{"line" => "R2"}` with spaces around `=>`. Any expectation written against the old `{:platform=>4}` text broke. `inspect` is a human-readable view, not a serialisation format, so specs should compare the hash itself.
  • In Ruby, if you override only to_s, why does p still show #<Stop:0x... @name="Leeds">?
    `p` calls `inspect`, and since Ruby 2.0 the default `Kernel#inspect` builds its string from the class name, address and instance variables without calling `to_s`. To change what `p`, `pp` and collections show, override `inspect` itself or, in Ruby 4.0, limit its instance variables with `instance_variables_to_inspect`.

saying these in an interview costs you the question

  • p calls to_s and just wraps the result in quotes.
  • Overriding to_s is enough to change what p and collections display.
  • Array#to_s joins the elements' to_s values into one string.
  • A to_s that returns nil makes interpolation raise a TypeError.
  • inspect output is a stable format that is safe to assert on in specs.