skip to content

In Ruby, what do nil.to_s, nil.to_a, nil.to_i and nil.inspect return, and when does relying on them hide a bug?

level: middleimportance: should knowfreq 45%

answer

  1. each converter returns an empty value
  2. one shared frozen empty string
  3. a fresh array every call
  4. inspect spells the word
  5. missing and zero look alike

basics

~20 s

nil.to_s returns "", nil.to_a returns [], nil.to_h returns {}, nil.to_i returns 0, nil.to_f returns 0.0 and nil.inspect returns "nil". They make blanks safe to print, but erase the difference between missing and empty or zero.

solid answer

~40 s

`NilClass` defines explicit converters that return each type's empty value: `to_s` gives `""`, `to_a` gives `[]`, `to_h` gives `{}`, `to_i` gives `0`, `to_f` gives `0.0` and `to_r` gives `(0/1)`, while `inspect` gives `"nil"`. On Ruby 4.0, `nil.to_s` returns one shared frozen string, so appending to it raises `FrozenError`; `to_a` and `to_h` build a new object each call. String interpolation and `Array#join` call `to_s`, so a nil middle name on an intake form prints as nothing, leaving `"Ada Lovelace"` with two spaces instead of crashing. That convenience is the risk: `nil.to_i` turns a missing weight into `0`, a valid-looking number. Convert for display; check `nil?` first wherever a missing value must be reported.

code

ruby · 8 lines
ruby
first, middle, last = "Ada", nil, "Lovelace"

"#{first} #{middle} #{last}"             # => "Ada  Lovelace" (two spaces)
[first, middle, last].join(" ")          # => "Ada  Lovelace"
[first, middle, last].compact.join(" ")  # => "Ada Lovelace"
[first, "", last].compact.join(" ")      # => "Ada  Lovelace" ("" survives compact)
[first, "", last].reject { |part| part.to_s.empty? }.join(" ")
# => "Ada Lovelace"

go deeper

for a junior

Recall the return values: "" for to_s, [] for to_a, {} for to_h, 0 for to_i and "nil" for inspect, and show interpolation printing nil as nothing.

for a middle

Explain which operations call to_s implicitly, why nil.to_s is frozen and shared while to_a is fresh, and why nil has no to_str.

for a senior

Show where conversion erases meaning: a missing measurement becoming 0 or an unasked question becoming an empty list, and insist on nil? checks in validation code.

for a principal

Set the team rule for where nil may be converted, presentation yes and domain data no, so missing values stay visible until a deliberate decision is made about them.

## What nil converts to `nil` is the only instance of `NilClass`, Ruby's object for "no value". The class documentation describes a group of its methods as **converters** that carry the idea of nullity into other classes: each returns the empty or zero value of its target type. | Call | Returns | New object each call? | |---|---|---| | `nil.to_s` | `""` | no, one shared frozen string | | `nil.to_a` | `[]` | yes | | `nil.to_h` | `{}` | yes | | `nil.to_i` | `0` | not applicable, an Integer | | `nil.to_f` | `0.0` | not applicable, a Float | | `nil.to_r` | `(0/1)` | not applicable, a Rational | | `nil.to_c` | `(0+0i)` | not applicable, a Complex | | `nil.inspect` | `"nil"` | a debugging view, not a conversion | Two things are easy to mix up here: - **`to_s` versus `inspect`.** `nil.to_s` is the empty string; `nil.inspect` is the three-letter string `"nil"`. `p nil` shows `nil` because `p` uses `inspect`. - **Explicit versus implicit conversion.** These are the *explicit* converters you call yourself. `NilClass` does not define `to_str` or `to_ary`, so methods that demand a String or an Array, such as `String#+`, do not treat `nil` as empty. `"Name: " + nil` raises `TypeError` with the message `no implicit conversion of nil into String`. ## The frozen empty string Since Ruby 2.7, `nil.to_s` returns the **same frozen `String`** every time, which saves an allocation on a very common call. On Ruby 4.0: - `nil.to_s.frozen?` returns `true`. - `nil.to_s << "x"` raises `FrozenError`, because it tries to mutate that shared string. - `nil.to_s.dup` returns an unfrozen copy you may append to. `nil.to_a` and `nil.to_h`, by contrast, allocate a fresh `Array` or `Hash` each call, so mutating the result is safe and affects nothing else. ## Where the conversions fire without being called Several everyday operations call `to_s` for you, so `nil` quietly becomes `""`: 1. **String interpolation**: `"#{nil}"` evaluates to `""`. 2. **`Array#join`**: each non-array element is converted with `to_s`, so `nil` contributes an empty string between separators. 3. **`puts`**: prints `nil` as an empty line. This is why a template that prints an optional value does not crash when the value is missing. It is also why the output can be subtly wrong. ## Worked example: a patient-intake form An intake form collects first, middle and last names, and the middle name is optional. Rendering the full name as `"#{first} #{middle} #{last}"` works, but when `middle` is `nil` the result is `"Ada Lovelace"`, with two spaces. The same happens with `[first, middle, last].join(" ")`. The usual fixes: - `[first, middle, last].compact.join(" ")` drops `nil`, but not `""`, which is what many forms actually send for a blank field. - `[first, middle, last].reject { |part| part.to_s.empty? }.join(" ")` drops both, using `nil.to_s` to fold the two cases together. ## Iterating over a value that may be nil `nil.to_a` and the splat operator make optional collections easy to loop over: - `allergies.to_a.each { ... }` runs zero times when `allergies` is `nil` and once per element when it is an `Array`, because `Array#to_a` returns the array itself. - `[*nil]` is `[]`, since splatting `nil` contributes no elements. - The trick is type-sensitive: `Hash#to_a` returns key-value pairs, and `String` has no `to_a` at all, so the value must really be an `Array` or `nil`. ## When the convenience hides a bug The converters erase one distinction: **missing** versus **empty or zero**. - A missing weight converted with `nil.to_i` becomes `0`, which looks like a measured value and passes numeric validation. - A missing allergy list converted with `nil.to_a` becomes `[]`, which reads as "no allergies" rather than "never asked". - A missing name converted with `to_s` becomes `""`, which a later presence check may or may not catch. The rule of thumb: 1. Convert `nil` freely for **display**, where an empty value is the right rendering. 2. Check `value.nil?` before converting in **data and validation** code, where "not provided" must be recorded or reported. 3. Keep `inspect` for **logs and debugging**, where seeing the word `nil` is the point. Used this way, the converters remove crashes from presentation code without silently inventing data.

  • In Ruby, why does nil.to_s << "x" raise while nil.to_a << 1 works?
    Since Ruby 2.7 `nil.to_s` returns one shared, frozen empty string, so appending to it raises `FrozenError`. `nil.to_a` allocates a new empty `Array` each call, so appending only changes that fresh array. Call `dup` on the string first if you need a mutable empty string.
  • Does nil define to_str or to_ary, and what follows from that?
    No. `NilClass` defines only explicit converters such as `to_s` and `to_a`, not the implicit `to_str` or `to_ary`. Methods that require a real String or Array therefore reject it: `"Name: " + nil` raises `TypeError` with `no implicit conversion of nil into String` instead of treating nil as empty.

saying these in an interview costs you the question

  • nil.to_s returns the string "nil"
  • Interpolating nil into a string raises an error
  • nil.to_a returns an array holding one nil
  • nil.to_s returns a new mutable string on every call
  • Converting a missing number with nil.to_i is harmless because 0 is a safe default