Skip to content

The last expression is the return value

Level: 101 · a first-week reader who writes Python

One line: A Ruby method returns the value of the last expression it evaluated, so return is only for leaving early — and a method that ends in puts returns nil.

There is no "fall off the end and get nothing" in Ruby: every construct is an expression with a value, and a method hands back whichever value its last evaluated expression produced. return exists, but its job is to leave early (or to return several values at once, which builds an Array). The corollaries are the ones that surprise Python readers: an if whose condition was false and has no else evaluates to nil; def itself is an expression whose value is the new method's name as a Symbol; an assignment as the last line returns the assigned value; and puts returns nil, so a method whose last line is puts returns nil — the classic bug where a method "works" in the terminal and returns nothing to its caller.

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

1. implicit return:           add(1, 2)               -> 3
2. early return:              early(-1), early(1)     -> "early", "late"
3. if with no branch taken:   negative_or_nothing(5)  -> nil
4. def is an expression:      r = def named; end      -> :named (a Symbol)
   so private can take a def:  private def helper; end -> private received :helper
5. a method ending in puts:   ends_with_puts          -> prints:
   (printing from inside)
   then returns nil
6. puts itself:               v = (puts "hi")         -> prints hi
   and v is nil
7. return with two values:    pair                    -> [1, 2] (an Array)
8. assignment as last line:   assigns                 -> 5
9. an empty body:             empty_body              -> nil
10. ensure does not replace:  ensured                 -> "the body"

Reading the output

Row 1 is the rule: add has no return and still gives 3. Row 2 shows return doing its actual job, leaving early. Row 3 is the first corollary: negative_or_nothing(5) takes no branch, and an if that takes no branch is nil. Row 4 is the second: def named; end is an expression, its value is :named, a Symbol — which is why private def helper; end works: private receives the name, as the second line of row 4 shows.

Rows 5 and 6 are the bug to remember. ends_with_puts prints its line and returns nil, because puts returns nil (row 6 measures that directly: v = (puts "hi") leaves v as nil). Row 7: return 1, 2 builds an Array. Rows 8 to 10 close the remaining corners: an assignment as the last line returns what was assigned (5), an empty body returns nil, and an ensure clause runs but never replaces the body's value — ensured still returns "the body".

Compared with Python

Python's rule is the opposite one: a function returns None unless a return statement runs. Row 1 shows it — the same a + b body returns None because nothing returned it. Rows 3 and 4 show two things that are expressions in Ruby but statements in Python: falling off the end of an if gives None rather than the value of a branch, and def cannot appear on the right of = at all (a SyntaxError at compile time). Rows 5 and 6 are the shared bug: print returns None just as puts returns nil, so a function that ends in print returns None. Row 7 differs in type only — return 1, 2 builds a tuple, not a list — and rows 8 to 10 agree: assignment is a statement (so assigns() is None), an empty body needs pass, and finally never replaces a returned value.

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

1. no return statement:       add(1, 2)               -> None
2. early return:              early(-1), early(1)     -> 'early', 'late'
3. if with no branch taken:   negative_or_nothing(5)  -> None (fell off the end)
4. def is a statement:        r = def named(): pass   -> SyntaxError
5. a function ending in print: ends_with_print        -> prints:
   (printing from inside)
   then returns None
6. print itself:              v = print("hi")         -> prints hi
   and v is None
7. return with two values:    pair                    -> (1, 2) (a tuple)
8. assignment as last line:   assigns                 -> None
9. a body that is only pass:  empty_body              -> None
10. finally does not replace: ensured                 -> 'the body'

Try it

  1. Write describe(n) whose whole body is an if/elsif/else returning "negative", "zero" or "positive" with no return keyword. Then write describe_and_print(n) that ends with a puts of the description, call it, and print what it returned. Fix it so it both prints and returns the description.
  2. Write bounds(a) that does return a.min, a.max, destructure the result into lo, hi, and print the class of the object return built. Then rewrite the method with [a.min, a.max] as its last expression and show the two versions return equal values.
Solution to 1

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

# Kata: a method whose body is an if/elsif/else needs no `return`; a method
# that ends in `puts` returns nil, and the fix is to end with the value.
def describe(n)
  if n < 0
    "negative"
  elsif n.zero?
    "zero"
  else
    "positive"
  end
end

def describe_and_print(n)
  puts "   #{n} is #{describe(n)}"
end

def describe_and_print_fixed(n)
  puts "   #{n} is #{describe(n)}"
  describe(n)
end

puts "1. describe(-3)                 -> #{describe(-3).inspect}"
puts "2. describe(0)                  -> #{describe(0).inspect}"
puts "3. describe(7)                  -> #{describe(7).inspect}"
puts "4. describe_and_print(7)        -> prints:"
v = describe_and_print(7)
puts "   and returns #{v.inspect}, because puts returns nil"
puts "5. describe_and_print_fixed(7)  -> prints:"
v = describe_and_print_fixed(7)
puts "   and returns #{v.inspect}, because the value is now the last expression"

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

1. describe(-3)                 -> "negative"
2. describe(0)                  -> "zero"
3. describe(7)                  -> "positive"
4. describe_and_print(7)        -> prints:
   7 is positive
   and returns nil, because puts returns nil
5. describe_and_print_fixed(7)  -> prints:
   7 is positive
   and returns "positive", because the value is now the last expression

See also