Twenty-three Python traps for a Ruby programmer¶
Level: 201 · someone who writes Ruby and has just started reading Python
One line: The twenty-three places where a Ruby reflex gives the wrong answer in Python — a [] default argument is shared across calls, a closure in a loop sees the last value, round(2.5) is 2, self must be declared, indentation is grammar, x = 5 if False will not parse and if False: x = 5 leaves x undefined, d[key] raises — each measured on one line in Python, with Ruby's answer beside it.
This is the mirror of the previous page, and here the Python twin is the primary program: it measures the trap, and the Ruby program prints what Ruby does in the same spot so the two outputs read side by side. The pattern behind most rows: Python evaluates a default argument once at def time where Ruby evaluates it per call; Python closures capture variables where Ruby block parameters are fresh per iteration; Python has a larger set of false values; Python's is is Ruby's equal?; Python's functions and methods are the same thing, so self is a parameter; and Python's grammar owns the indentation, the elif, the for … else and the chained comparison that Ruby leaves to conventions or does not have.
| # | The trap | Python says | Ruby says | Lesson |
|---|---|---|---|---|
| 1 | a mutable default argument is shared | f() then f() gives [1, 1] |
a fresh [] per call |
Default arguments are evaluated each call |
| 2 | closures bind late | [lambda: i for i in range(3)] all give 2 |
block parameters are fresh: 1, 2, 3 |
Closures capture variables |
| 3 | is is identity, == is value |
[] is [] is False |
equal? versus == |
Four kinds of equality · comparing int and float ↗ |
| 4 | / is true division, // floors |
7 / 2 is 3.5 |
7 / 2 is 3 |
Integer division floors |
| 5 | round is banker's |
round(2.5) is 2 |
2.5.round is 3 |
Floats and rounding |
| 6 | self is explicit |
a method without self cannot be called on an instance |
self is implicit |
self is implicit |
| 7 | indentation is syntax | IndentationError at compile time |
mis-indented code runs | Indentation is convention in Ruby, syntax in Python |
| 8 | print is a function |
print 'x' is a SyntaxError |
puts "x" needs no parentheses |
Parentheses are optional · repr is not str ↗ |
| 9 | elif |
elsif is a SyntaxError |
elsif |
unless, until and modifiers |
| 10 | for … else |
else runs when the loop did not break |
no such clause; find returns nil |
for loops do not scope |
| 11 | list.sort() returns None |
sorted() returns the new list |
sort! returns the receiver |
Bang and question methods |
| 12 | strings are immutable | s[0] = "x" is a TypeError |
s[0] = "x" works |
Strings are mutable · bytearray is mutable ↗ |
| 13 | no ? or ! in names |
def empty?() is a SyntaxError |
empty? and save! are ordinary names |
Bang and question methods |
| 14 | range is half-open |
range(1, 4) is 1, 2, 3 |
1..3 is closed, 1...3 half-open |
Ranges: two dots and three · slicing is not indexing ↗ |
| 15 | d[key] raises KeyError |
d.get(key) is the nil-returning form |
h[:k] is nil; fetch raises |
Hashes and default values |
| 16 | x = 5 if False will not parse |
and if False: x = 5 leaves x undefined |
x = 5 if false defines x as nil |
unless, until and modifiers |
| 17 | __init__, not initialize |
a def initialize is never called |
new calls initialize |
new, allocate and initialize |
| 18 | global and nonlocal |
assigning in an inner def makes a new local |
a block assigns the outer local; a def is a new scope |
Closures capture variables |
| 19 | lambda is one expression |
lambda: x = 1 is a SyntaxError |
a lambda body holds statements | Procs and lambdas differ |
| 20 | [] and 0 are falsy |
bool([]) is False |
both are truthy | nil, false and truthiness |
| 21 | len(x) is a function |
"abc".len is an AttributeError |
"abc".length |
Everything is an object |
| 22 | (1,) is a tuple |
(1) is just 1 |
no tuple; a frozen Array | return a, b returns one Array |
| 23 | 1 < x < 3 chains |
1 < 3 < 2 is False |
1 < 2 < 3 is a NoMethodError on true |
and/or bind below = |
Verified output of gotchas_for_ruby_programmers_in_python_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. def f(a=[]): a.append(1); f() / f() [1] / [1, 1] (one list, shared)
2. [lambda: i for i in range(3)] / lambda i=i: i [2, 2, 2] / [0, 1, 2]
3. m = 256; n = 256; m is n / [] is [] / [] == [] True / False / True
4. 7 / 2 / 7 // 2 / -7 // 2 3.5 / 3 / -4
5. round(2.5) / round(3.5) / round(-2.5) 2 / 4 / -2
6. class NoSelf: def m(): ...; NoSelf().m() TypeError (self is a real parameter)
7. compile('if True:\nprint(1)') IndentationError
8. print 'x' / print('a', 'b', sep='-') SyntaxError / printed 'a-b\n'
9. elif / elsif compiles / SyntaxError
10. for ... else, without and with a break ['else ran (no break)']
11. a = [3, 1]; a.sort() / sorted([3, 1]) None / [1, 3]
12. s = "abc"; s[0] = "x" TypeError
13. def empty?(): ... / def save!(): ... SyntaxError / SyntaxError
14. list(range(1, 4)) / list(range(1, 3)) [1, 2, 3] / [1, 2] (half-open)
15. d = {}; d["k"] / d.get("k") KeyError / None
16. if False: y = 5; y / x = 5 if False NameError / SyntaxError
17. def initialize(self) is never called: Account().balance AttributeError
18. count += 1 in an inner def / with nonlocal UnboundLocalError / 1
19. lambda: x = 1 / lambda v: v * 2 SyntaxError / 4
20. bool([]) / bool(0) / bool(0.0) False / False / False
21. len("abc") / "abc".len 3 / AttributeError
22. type((1,)) / type((1)) / type([1]) tuple / int / list
23. 1 < 2 < 3 / 1 < 3 < 2 True / False (chained comparison)
Reading the output¶
- Rows 1–2 are the two classic Python questions: the default list is one object created at
deftime, so the second call sees the first call's append; the lambdas in the comprehension all readiafter the loop ended, and thei=idefault freezes each value. - Rows 3–5:
isis identity —256is cached so two variables holding it are one object, while two empty lists are two objects;/always gives a float and//floors toward negative infinity;roundgoes to the even neighbour on a half. - Rows 6–8: a method defined without
selfcannot take the instance it is called on (TypeError); a body at the wrong column is anIndentationErrorbefore anything runs;printwithout parentheses is aSyntaxErrorand with them takessep=. - Rows 9–10:
elsifdoes not parse; thefor … elseclause ran only in the loop that did notbreak. - Rows 11–13:
sort()returnsNone,sorted()a new list; item assignment on astrraises; a?or!in a name is aSyntaxError. - Rows 14–16:
range(1, 4)stops before4;d["k"]raises whered.get("k")returnsNone; the modifierifdoes not parse and the blockifleavesyundefined — aNameError. - Rows 17–18: a
def initializeis an ordinary method nobody calls, sobalancewas never set (AttributeError);count += 1in an inner function makescountlocal to it and reads it before assignment (UnboundLocalError), andnonlocalfixes it. - Rows 19–23: a
lambdaholds one expression;[],0and0.0are allFalse;lenis a function and.lenanAttributeError; the comma makes the tuple, not the parentheses; and1 < 3 < 2isFalsebecause the comparison chained as1 < 3 and 3 < 2.
Compared with Python¶
Here the Python output is the measurement, so this section reads Ruby's program instead. Rows 1–2: Ruby evaluates a = [] on every call and gives each block invocation a fresh i, so neither trap exists. Row 3 is the same distinction under other names, equal? and ==. Rows 4–5 flip: 7 / 2 is 3, 2.5.round is 3, and half: :even is the option that reproduces Python. Rows 6–8 are the language shape: self is implicit, a def with every line at column 0 runs, and puts "x" is a call. Row 10 has no for … else — find returns nil when nothing matched and each returns its receiver. Row 11: sort! returns the receiver, and the bang family returns nil only to say "nothing changed". Rows 12–14: the string was changed in place, empty? and save! compile, and Ruby has both range forms. Rows 15–16 are the two nils where Python raises — a missing key and the never-run modifier assignment — and fetch is the raising form. Row 17: def __init__ is ignored by new, so @balance is nil. Row 18: a block assigned the outer count, and a def cannot see it at all (NameError), so Ruby needs neither nonlocal nor global. Rows 19–23: a lambda body holds statements, [] and 0 are truthy, length is a method, there is no tuple (a frozen Array stands in), and 1 < 2 < 3 is a NoMethodError because true < 3 has no meaning.
Verified output of gotchas_for_ruby_programmers_in_python_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. def f(a = []); a << 1; end; f / f [1] / [1] (a fresh array per call)
2. (1..3).map { |i| -> { i } }.map(&:call) [1, 2, 3]
3. 256.equal?(256) / "a".equal?("a") / "a" == "a" true / false / true
4. 7 / 2 / 7.fdiv(2) / -7 / 2 3 / 3.5 / -4
5. 2.5.round / 3.5.round / 2.5.round(half: :even) 3 / 4 / 2
6. def m = self.class.name; WithSelf.new.m "WithSelf" (self is implicit)
7. eval of a def with every line at column 0 1 (ran)
8. puts "x" is a method call without parentheses compiles / compiles
9. elsif / elif compiles / SyntaxError
10. no for-else: [1, 2].find { it == 9 } / each returns nil / [1, 2]
11. a = [3, 1]; a.sort! / [1, 2].uniq! [1, 3] / nil (bang returns self, or nil for no change)
12. s = "abc"; s[0] = "x"; s "xbc"
13. def empty?; end / def save!; end compiles / compiles
14. (1..3).to_a / (1...3).to_a [1, 2, 3] / [1, 2] (.. is closed, ... half-open)
15. h = {}; h[:k] / h.fetch(:k) nil / KeyError
16. y = 5 if false; y / defined?(y) nil / "local-variable"
17. def __init__ is never called: @balance after new nil
18. a block assigns the outer local / a def sees it? 1 / NameError (no nonlocal; a def is a new scope)
19. ->(v) { w = v * 2; w + 1 }.call(2) 5 (a lambda holds statements)
20. [] and 0 in a condition truthy / truthy
21. "abc".length / length("abc") 3 / NoMethodError
22. [1].class / (1).class / no tuple: [1].freeze.frozen? Array / Integer / true
23. 1 < 2 < 3 NoMethodError (no chained comparison: (1 < 2) < 3 asks true < 3)
Try it¶
- Reproduce Python's arithmetic in Ruby: write
py_floor_div(a, b)withInteger#divandpy_round(x)withround(half: :even), and print a table for7, 2,-7, 2,7, -2,-7, -2and for0.5,1.5,2.5,3.5,-2.5, beside Ruby's own/andround. - Write a
make_countersmethod in Ruby that returns three lambdas from a loop, one per index, and show each returns its own index; then write the Python version twice, once with the late-binding bug and once with thei=ifix, and run it underpython3 -I.
Solution to 1
gotchas_for_ruby_programmers_in_python_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# Kata: Python's `//` and `round()` reproduced in Ruby, so the numbers a Ruby
# programmer sees in Python stop being surprising.
def py_floor_div(a, b) = a.div(b) # Integer#div floors, like Python's //
def py_round(x) = x.round(half: :even) # Python rounds half to even
puts format("%-10s %-8s %s", "a, b", "a / b", "a // b")
[[7, 2], [-7, 2], [7, -2], [-7, -2]].each do |a, b|
puts format("%-10s %-8s %s", "#{a}, #{b}", a.fdiv(b), py_floor_div(a, b))
end
puts
puts format("%-6s %-12s %s", "x", "x.round", "py_round(x)")
[0.5, 1.5, 2.5, 3.5, -2.5].each do |x|
puts format("%-6s %-12s %s", x, x.round, py_round(x))
end
Verified output of gotchas_for_ruby_programmers_in_python_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
See also¶
- Twenty-three Ruby traps for a Python programmer — the same crossing, walked the other way
- There is more than one way, and one obvious way — the idioms these traps sit inside
x ||= vassigns whenxis nil or false — how row 20's falsy0and""breakx = x or 5return a, breturns one Array — the tuple of row 22, from the Ruby side- Python FAQ: programming ↗ — the maintainers' own answers to rows 1, 2 and 18