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¶
- Write
describe(n)whose whole body is anif/elsif/elsereturning"negative","zero"or"positive"with noreturnkeyword. Then writedescribe_and_print(n)that ends with aputsof the description, call it, and print what it returned. Fix it so it both prints and returns the description. - Write
bounds(a)that doesreturn a.min, a.max, destructure the result intolo, hi, and print the class of the objectreturnbuilt. 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¶
- Parentheses are optional, but the space is not —
puts(1+2)*3fails for the reason row 6 shows:putsreturns nil - A setter returns its argument, not its body — the one kind of method whose last expression is not what the caller sees
- Everything is an expression —
if,case,beginanddefas values, measured one by one - Multiple return values — what
return a, bbuilds and how to take it apart - Destructuring assignment —
lo, hi = bounds(a)on the caller's side - Ruby docs: Methods ↗ — the reference for
return,ensureand the value of a method - Twenty-three Ruby traps for a Python programmer — The twenty-three places where a Python reflex gives the wrong answer in Ruby