Twenty-three Ruby traps for a Python programmer¶
Level: 201 · someone who writes Python and has just started reading Ruby
One line: The twenty-three places where a Python reflex gives the wrong answer in Ruby — 0 is truthy, a[10] is nil, 7 / 2 is 3, x = 5 if false still defines x, {"a": 1} has a symbol key, puts [1].map do … end prints an Enumerator — each measured on one line, with the lesson that explains it.
Most of these are not bugs in either language; they are two consistent designs meeting. Ruby has two false values and everything else is true; a missing index or key is nil rather than an error; integer division floors; strings are mutable objects; and/or are control-flow operators that bind below =; a do … end block binds to the outermost method call on its line; a leading 0 in Integer() means octal; for shares the enclosing scope and a block does not; private restricts the receiver, not the caller. The program prints one line per row, in the table's order, and the Python twin prints what Python does in the same spot.
| # | The trap | Ruby says | Python would say | Lesson |
|---|---|---|---|---|
| 1 | 0 and "" are truthy |
only nil and false are false |
bool(0) and bool("") are False |
nil, false and truthiness |
| 2 | a[10] is nil |
no error for a missing index | IndexError |
Arrays and negative indexes · slicing is not indexing ↗ |
| 3 | 7 / 2 is 3 |
integer division floors; fdiv or 2.0 for a float |
7 / 2 is 3.5, 7 // 2 is 3 |
Integer division floors |
| 4 | strings are mutable | s << "d" changes the object in place |
s += "d" makes a new string; s[0] = "x" raises |
Strings are mutable · bytearray is mutable ↗ |
| 5 | "a" == :a is false |
a String and a Symbol never compare equal |
there is one string type | Symbols are names |
| 6 | x = false or true leaves x false |
or binds below = |
or has ordinary precedence |
and/or bind below = |
| 7 | puts returns nil |
a method ending in puts returns nil |
print returns None too |
The last expression is the value |
| 8 | x = 5 if false defines x as nil |
the parser creates the local; the body never runs | if False: x = 5 leaves x undefined; x = 5 if False will not parse |
unless, until and modifiers |
| 9 | elsif |
elif is a syntax error |
elif |
unless, until and modifiers |
| 10 | end closes every block |
a missing end is a syntax error |
a dedent closes the block | Indentation is convention in Ruby, syntax in Python |
| 11 | 2.5.round is 3 |
half away from zero; half: :even for banker's |
round(2.5) is 2 |
Floats and rounding |
| 12 | uniq! returns nil when nothing changed |
the bang methods signal "no change" with nil |
sort() returns None always |
Bang and question methods |
| 13 | Hash.new([]) shares one default array |
h[:a] << 1 mutates the default, and stores no key |
defaultdict(list) makes a fresh list per key |
Hashes and default values |
| 14 | {"a": 1} has a symbol key |
quotes then a colon make a Symbol |
{"a": 1} has a str key |
A string key and a symbol key are two keys |
| 15 | puts [1].map do … end prints an Enumerator |
do … end binds to puts, { } to map |
a lambda sits inside the parentheses | Blocks are not objects |
| 16 | ?a is the string "a" |
a character literal is a one-character String |
"a" is a str; there is no character type |
String literals ↗ |
| 17 | Integer("08") raises |
a leading 0 means octal; "08".to_i is 8 |
int("08") is 8; the literal 08 will not parse |
to_i never fails; Integer() does ↗ |
| 18 | %w[a b c] |
a word list literal | "a b c".split() |
Text lives in the text library |
| 19 | a for loop leaks its variable |
for shares the scope; a block does not |
for leaks too; a comprehension does not |
for loops do not scope |
| 20 | 1.equal?(1) is true |
equal? is identity, == is value |
256 is 256 on variables is True, [] is [] is False |
Four kinds of equality |
| 21 | private means "no explicit receiver" |
obj.secret raises; send bypasses |
_secret is a convention, nothing is enforced |
Private means no receiver |
| 22 | attr_accessor |
reader and writer methods, generated | attributes are public by default | Instance variables are private |
| 23 | require, not import |
returns true, then false |
import returns a module |
require, require_relative and load |
Verified output of gotchas_for_python_programmers_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. 0 and "" in a condition truthy / truthy
2. [1, 2, 3][10] nil
3. 7 / 2 / 7.fdiv(2) / 7 / 2.0 3 / 3.5 / 3.5
4. s = "abc"; s << "d" "abcd", same object? true
5. "a" == :a false
6. x1 = false or true / x2 = false || true x1=false / x2=true
7. puts "hi" returns (printed "hi\n") nil
8. y = 5 if false; y / defined?(y) nil / "local-variable"
9. elsif / elif compiles / SyntaxError
10. an if without end / with end SyntaxError / compiles
11. 2.5.round / (-2.5).round / 2.5.round(half: :even) 3 / -3 / 2
12. [1, 2].uniq! / [1, 1].uniq! nil / [1]
13. h = Hash.new([]); h[:a] << 1; h[:b] / h.size [1] / 0 (one array, no keys)
14. {"a": 1}.keys.first.class Symbol
15. show [1].map do |v| v * 2 end / with { } #<Enumerator: [1]:map> / [2]
16. ?a / ?a.class "a" / String
17. Integer("08") / "08".to_i / Integer("010") ArgumentError / 8 / 8 (a leading 0 means octal)
18. %w[a b c] ["a", "b", "c"]
19. for i in 1..3: i after / each { |elem| }: defined?(elem) 3 / nil
20. 1.equal?(1) / "a".equal?("a") / "a" == "a" true / false / true
21. private: obj.secret / obj.reveal / obj.send(:secret) NoMethodError / "s3cret" / "s3cret"
22. attr_accessor :name defines [:name, :name=]
23. require "json" / import json true / NameError
Reading the output¶
- Rows 1–2:
0and""take the truthy branch;[1, 2, 3][10]isnil, no exception. - Rows 3–5:
7 / 2is3and onlyfdivor a float operand gives3.5;<<appended to the same string object;"a" == :aisfalse. - Rows 6–7:
x1isfalsebecause the line parsed as(x1 = false) or true;putsprintedhiand returnednil. - Row 8:
yisnilanddefined?(y)reports a local variable — the parser declared it when it read the assignment, even though the assignment never ran. - Rows 9–10:
elsifcompiles,elifdoes not; anifwithoutenddoes not compile. - Rows 11–12:
2.5.roundis3and-2.5rounds to-3(away from zero);[1, 2].uniq!returnednilbecause nothing changed. - Row 13: the default
[]was shared:h[:b]already holds[1]and the hash has no keys at all. - Rows 14–15:
{"a": 1}made aSymbolkey;show [1].map do … endhanded the block toshow, somapgot no block and returned anEnumerator, while the{ }form gave[2]. - Rows 16–18:
?ais a one-characterString;Integer("08")is anArgumentErrorbecause0starts an octal literal and8is not an octal digit, while"08".to_iis8andInteger("010")is8;%w[a b c]is three strings. - Rows 19–20:
isurvives theforloop whileelemfrom the block does not exist afterwards (defined?isnil);1.equal?(1)istruebecause small integers are one object, and two"a"literals are two objects that are==. - Rows 21–23:
obj.secretis aNoMethodError, a method inside the class may callsecretwith no receiver, andsendreaches it from outside;attr_accessor :namedefinednameandname=;requirereturnedtrue, andimport jsonis aNameErrorbecause Ruby has no such keyword —jsonis read as a variable.
Compared with Python¶
The twin prints the Python behaviour behind each expectation. Rows 1–3 are the three most common first-day bugs: bool(0) is False, a bad index raises IndexError, and / is true division. Row 4 shows that += on a str rebinds the name (is says the object changed) and that item assignment raises. Rows 6 and 8 are opposite corners: or has ordinary precedence, and an assignment inside a false if leaves the name undefined (NameError) — while Ruby's modifier form does not even parse in Python. Row 10 measures the missing end: the ast shows the dedent closed the if after one statement. Rows 11–13 are banker's rounding, sort() returning None whether or not anything moved, and defaultdict making a fresh list per key — and storing it on first access, which is why the dict grew to two keys where Ruby's Hash.new([]) stored none. Row 15 has no block syntax to bind wrongly; row 17 accepts "08" as decimal but rejects the literal 08 at compile time; row 19 shows Python's for leaks the same way and a comprehension does not; row 20 is CPython's small-integer cache, which this page keys only for 256 (the cache's documented upper bound), because 257 is 257 on variables is not a promise. (Not machine-checked here.) Rows 21–22: _secret and __hidden are conventions the interpreter does not enforce, and attributes need no declaration. Row 23: require is a NameError in Python, just as import is in Ruby.
Verified output of gotchas_for_python_programmers_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. bool(0) / bool("") False / False
2. [1, 2, 3][10] IndexError
3. 7 / 2 / 7 // 2 / 7 / 2.0 3.5 / 3 / 3.5
4. s = "abc"; s += "d" / s[0] = "x" 'abcd', same object? False / TypeError
5. "a" == "a" (no symbol type) True
6. x1 = False or True x1=True (or has ordinary precedence)
7. print("hi") returns (printed 'hi\n') None
8. if False: y = 5; y / x = 5 if False NameError / SyntaxError
9. elif / elsif compiles / SyntaxError
10. no end: a dedent closes the block module has 2 statements, the if holds 1
11. round(2.5) / round(-2.5) / round(3.5) 2 / -2 / 4
12. [1, 2].sort() / [2, 1].sort() (always None) None / None
13. defaultdict(list): dd["a"].append(1); dd["b"] [] / len 2 (a fresh list per key, stored on access)
14. {"a": 1}: type of the key str
15. list(map(lambda v: v * 2, [1])) (no blocks) [2]
16. "a" / type("a").__name__ / ord("a") 'a' / str / 97 (no character type)
17. int("08") / int("010") / the literal 08 8 / 10 / SyntaxError
18. "a b c".split() ['a', 'b', 'c']
19. for i in range(1, 4): i after / [j for j in ...]: 'j' in dir() 3 / False
20. m = 256; n = 256; m is n / [] is [] / [] == [] True / False / True
21. obj._secret() / obj.reveal() / obj._Safe__hidden() 's3cret' / 's3cret' / 'mangled' (nothing enforced)
22. self.name = ...: attributes are public vars(Person()) = {'name': 'ann'}
23. import json / require("json") module / NameError
Try it¶
- Write
py_truthy?(value)in Ruby that answers as Python'sbool()would —nil,false, numeric zero and every emptyString,ArrayorHashare false — and print a table of ten values with Ruby's!!valuebeside it, marking the rows where the two disagree. - Write
safe_div(a, b)that mimics Python's/(always aFloat) and//(floor) in Ruby, and print both for7, 2,-7, 2and7, 0— rescuing what Ruby raises for the last one and printing its class.
Solution to 1
gotchas_for_python_programmers_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# Kata: Python's truthiness, implemented in Ruby, beside Ruby's own.
# Python treats None, False, zero of any numeric type and every empty
# container or string as false; Ruby treats only nil and false as false.
def py_truthy?(value)
case value
when nil, false then false
when Numeric then !value.zero?
when String, Array, Hash then !value.empty?
else true
end
end
values = [nil, false, 0, 0.0, "", " ", [], {}, :a, [0]]
puts format("%-8s %-12s %s", "value", "Ruby !!v", "py_truthy?(v)")
values.each do |v|
mark = !!v == py_truthy?(v) ? "" : " <- differ"
puts format("%-8s %-12s %s%s", v.inspect, !!v, py_truthy?(v), mark)
end
Verified output of gotchas_for_python_programmers_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
See also¶
- Twenty-three Python traps for a Ruby programmer — the same crossing, walked the other way
- There is more than one way, and one obvious way — the idioms behind these traps, one measured line each
x ||= vassigns whenxis nil or false — what row 1's truthiness does to the memoization idiom- Common error messages — the
NoMethodError,NameErrorandArgumentErrorof rows 17, 21 and 23, with their full messages - Parentheses are optional — why row 15's block went to the wrong method