Skip to content

puts calls to_s, p calls inspect

Level: 201 · anyone who has seen #<Foo:0x000...> and wanted a better line

One line: Every object answers two questions — to_s, "show this to a person", and inspect, "show this to a programmer" — and every printing route picks one: puts, interpolation, %s and String() use to_s, while p, %p, pp and every container's to_s use inspect; defining one never defines the other, p returns what it printed and puts returns nil, and puts nil writes an empty line.

The default to_s is #<Plain:0x…> with the object's address; the default inspect is the same plus the instance variables, which is the line you see from p. Since the address changes from run to run, the program tests the shape of those defaults instead of printing them, and prints the real strings only for classes that define their own. Both methods live in Kernel, so BasicObject has neither — a proxy built on BasicObject cannot even be p'd.

Verified output of to_s_inspect_and_p_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

1. the two defaults
   Plain.new.to_s starts with "#<Plain:0x"  true
   Plain.new.inspect == Plain.new.to_s (shape) true
   Point.new(1, 2).inspect ends " @x=1, @y=2>" true
   Point.new(1, 2).to_s includes "@x"       false
2. defining one never defines the other
   OnlyToS.new.to_s                         only to_s
   OnlyToS.new.inspect is still the default true
   OnlyInspect.new.inspect                  <OnlyInspect>
   OnlyInspect.new.to_s is still the default true
3. who calls which
   puts obj                                 to_s of 3
   p obj                                    #<Both 3>
   "#{obj}"                                 to_s of 3
   format("%s / %p", obj, obj)              to_s of 3 / #<Both 3>
   String(obj)                              to_s of 3
   obj.to_s                                 to_s of 3
   obj.inspect                              #<Both 3>
4. containers inspect their elements, even in to_s
   [obj].to_s                               [#<Both 3>]
   [obj].inspect                            [#<Both 3>]
   { k: obj }.to_s                          {k: #<Both 3>}
   "#{[obj]}"                               [#<Both 3>]
   puts [obj]                               to_s of 3
5. p returns its argument, puts returns nil
   p(obj) prints: #<Both 3>
     ...and returns obj itself              true
   p(1, 2) prints two lines:
1
2
     ...and returns                         [1, 2]
   p() returns                              nil
   puts("one") prints: one
     ...and returns                         nil
6. nil
   puts nil writes                          "\n"
   p nil writes                             "nil\n"
   print nil writes                         ""
   "#{nil}".inspect                         ""
   nil.to_s.inspect                         ""
   nil.inspect                              nil
   [nil].to_s                               [nil]
7. strings and symbols
   "str".to_s                               str
   "str".inspect                            "str"
   "tab\there".inspect                      "tab\there"
   "\u{e9}".inspect                         "é"
   :sym.to_s                                sym
   :sym.inspect                             :sym
8. core classes define both
   S.new(1, 2).inspect                      #<struct S x=1, y=2>
   S.new(1, 2).to_s == its inspect          true
   D.new(1).inspect                         #<data D a=1>
   RuntimeError.new("boom").to_s            boom
   RuntimeError.new("boom").inspect         #<RuntimeError: boom>
   1e20.to_s                                1.0e+20
   (1..3).inspect                           1..3
9. pp wraps a nested structure at a width
{name: "widget",
 parts:
  [{id: 1,
    tags: ["alpha", "beta", "gamma"]},
   {id: 2,
    tags: ["alpha", "beta", "gamma"]},
   {id: 3,
    tags: ["alpha", "beta", "gamma"]}]}
   obj.pretty_inspect == obj.inspect + "\n" true
10. where they live
   Object.instance_method(:to_s).owner      Kernel
   Object.instance_method(:inspect).owner   Kernel
   Kernel.private_instance_methods.include?(:p) true
   BasicObject.instance_methods.include?(:inspect) false

Reading the output

  • 1. A bare class gets a to_s of #<Plain:0x…>, and its inspect is the same string; give the object instance variables and only inspect grows to list them (@x=1, @y=2), while to_s stays as it was.
  • 2. Defining to_s leaves inspect at the default and defining inspect leaves to_s at the default. There is no fall-back in either direction: a class that wants both must write both.
  • 3. puts, "#{}", %s and String() all end up in to_s; p and %p end up in inspect.
  • 4. A container's to_s is its inspect, and it inspects its elements — [obj].to_s shows #<Both 3>, not to_s of 3 — so interpolating an array into a string gives the programmer's view of each element. puts [obj] is the exception: puts flattens an array and calls to_s on each element separately.
  • 5. p obj prints and then returns obj itself, which is why p can be dropped into the middle of an expression to look at a value; p 1, 2 prints two lines and returns the array [1, 2]; p with nothing returns nil; puts always returns nil.
  • 6. puts nil writes a bare newline, p nil writes nil, and print nil writes nothing — because nil.to_s is "" and nil.inspect is "nil". The same applies inside interpolation: "#{nil}" is the empty string.
  • 7. A string's to_s is itself; its inspect adds the quotes and escapes a tab as \t — but leaves é as is, since the program runs under -E UTF-8. A symbol's inspect keeps the colon and its to_s drops it.
  • 8. Struct, Data, exceptions, floats and ranges all define both: a Struct's to_s is its inspect, an exception's to_s is its message and its inspect wraps the class around it, and 1e20.to_s is 1.0e+20.
  • 9. PP.pp(object, $stdout, 40) is pp with an explicit width: it breaks the nested hash across lines and indents each level. Plain pp picks its width from the terminal, so the program pins it. (Not machine-checked here.) For an object whose inspect is short, pretty_inspect is just inspect plus a newline.
  • 10. to_s and inspect are owned by Kernel, not Object; p is a private instance method of Kernel — a method every object has and calls without a receiver; and BasicObject has no inspect at all.

Compared with Python

Python's two questions are __str__ and __repr__, and the same routes lead to them: print, f"{x}", %s and str() call __str__; repr(), f"{x!r}", %r and every container call __repr__. Four rows differ. Row 1: Python's default __repr__ is <__main__.Point object at 0x…> with no attributes — where Ruby's default inspect lists the instance variables, Python makes you ask vars(). Row 2: defining __repr__ alone does give you str(), because object.__str__ falls back to __repr__; Ruby's to_s never falls back to inspect. Row 4: print([obj]) prints the list's repr, elements included — the same as Ruby's [obj].to_s — but Ruby's puts [obj] flattens the array and prints each element's to_s, which Python's print never does. Row 6: print(None) writes None, where puts nil writes an empty line. Python has no p: print always returns None, so the program defines a four-line p helper that prints the repr and returns its argument. It also has no Symbol; an Enum member is the nearest thing, with a str of Sym.sym and a repr of <Sym.sym: 1>. Dataclasses and namedtuples write a __repr__ for you the way Struct and Data write an inspect, pprint.pprint(width=40) is PP.pp with a width, and pformat of a short object is its repr. The Python library's repr is not str ↗ goes further into what repr escapes and why str.isprintable is defined in terms of it.

Verified output of to_s_inspect_and_p_py.py — regenerated by tools/run_examples.py, never hand-typed.

1. the two defaults
   str(Plain()) starts with "<__main__.Plain object at 0x" True
   repr(Plain()) == str(Plain())            True
   repr(Point(1, 2)) mentions x             True
   vars(Point(1, 2))                        {'x': 1, 'y': 2}
2. __str__ never defines __repr__; __repr__ does define str
   str(OnlyStr())                           only __str__
   repr(OnlyStr()) is still the default     True
   repr(OnlyRepr())                         <OnlyRepr>
   str(OnlyRepr())  (falls back to __repr__) <OnlyRepr>
3. who calls which
   print(obj)                               str of 3
   p(obj)                                   <Both 3>
   f"{obj}"                                 str of 3
   "%s / %r" % (obj, obj)                   str of 3 / <Both 3>
   str(obj)                                 str of 3
   f"{obj!s}"                               str of 3
   f"{obj!r}"                               <Both 3>
4. containers repr their elements, even in str
   str([obj])                               [<Both 3>]
   repr([obj])                              [<Both 3>]
   str({"k": obj})                          {'k': <Both 3>}
   f"{[obj]}"                               [<Both 3>]
   print([obj])                             [<Both 3>]
5. print returns None; the p helper returns its argument
   p(obj) prints: <Both 3>
     ...and returns obj itself              True
   p(1, 2) prints two lines:
1
2
     ...and returns                         [1, 2]
   p() returns                              None
   print("one") prints: one
     ...and returns                         None
6. None
   print(None) writes                       'None\n'
   p(None) writes                           'None\n'
   print(None, end='') writes               'None'
   repr(f"{None}")                          'None'
   repr(str(None))                          'None'
   repr(None)                               None
   str([None])                              [None]
7. strings; no symbol, so an Enum member
   str("str")                               str
   repr("str")                              'str'
   repr("tab\there")                        'tab\there'
   repr("\N{LATIN SMALL LETTER E WITH ACUTE}") 'é'
   str(Sym.sym)                             Sym.sym
   repr(Sym.sym)                            <Sym.sym: 1>
8. stdlib classes define both
   repr(S(1, 2))                            S(x=1, y=2)
   str(S(1, 2)) == repr(S(1, 2))            True
   repr(NT(1))                              NT(a=1)
   str(RuntimeError("boom"))                boom
   repr(RuntimeError("boom"))               RuntimeError('boom')
   str(1e20)                                1e+20
   repr(range(1, 4))                        range(1, 4)
9. pprint wraps a nested structure at a width
{'name': 'widget',
 'parts': [{'id': 1,
            'tags': ['alpha',
                     'beta',
                     'gamma']},
           {'id': 2,
            'tags': ['alpha',
                     'beta',
                     'gamma']},
           {'id': 3,
            'tags': ['alpha',
                     'beta',
                     'gamma']}]}
   pprint.pformat(obj) == repr(obj)         True
10. where they live
   Plain.__str__ is object.__str__          True
   Plain.__repr__ is object.__repr__        True
   hasattr(builtins, 'print')               True
   hasattr(object, '__repr__')              True

Try it

  1. Write a Version class built from "1.2.3" whose to_s is the dotted string and whose inspect is #<Version 1.2.3>. Print one with puts, p, interpolation, inside an array, and through format("%s / %p"); use p's return value in an expression; then include Comparable with a <=> on the numeric parts and sort ["1.10.0", "1.2.3", "1.2.10"] correctly.
  2. Write a module Inspectable whose inspect lists the class name and every instance variable (instance_variables and instance_variable_get) without an address, and include it in two different classes. Check that p and [obj].to_s both use it and that puts obj still does not.
Solution to 1

to_s_inspect_and_p_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.

# Exercise 1: a Version with a to_s for people and an inspect for programmers.
# Print it every way a value gets printed, and show that p hands it back.

class Version
  include Comparable
  attr_reader :parts

  def initialize(text) = @parts = text.split(".").map(&:to_i)
  def <=>(other) = parts <=> other.parts
  def to_s = parts.join(".")
  def inspect = "#<Version #{self}>"
end

v = Version.new("1.2.3")

print "puts v          -> "
puts v
print "p v             -> "
p v
puts "\"\#{v}\"          -> #{v}"
puts "[v].to_s        -> #{[v]}"
puts "format %s / %p  -> #{format('%s / %p', v, v)}"
puts "p returns it    -> #{p(v).to_s}"
puts "sorted          -> #{[Version.new('1.10.0'), v, Version.new('1.2.10')].sort.inspect}"

Verified output of to_s_inspect_and_p_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

puts v          -> 1.2.3
p v             -> #<Version 1.2.3>
"#{v}"          -> 1.2.3
[v].to_s        -> [#<Version 1.2.3>]
format %s / %p  -> 1.2.3 / #<Version 1.2.3>
#<Version 1.2.3>
p returns it    -> 1.2.3
sorted          -> [#<Version 1.2.3>, #<Version 1.2.10>, #<Version 1.10.0>]

See also