Skip to content

There is more than one way, and one obvious way

Level: 101 · a Python programmer's first Ruby afternoon, and the other way round

One line: Ruby's idioms — each, blocks, ? and ! methods, unless, attr_accessor, nil, Symbol, mixins, Comparable, Enumerable, method_missing, Struct, Data, case/in, Rational — each have a Python twin, printed here on one line each; Ruby keeps several names for one operation where Python keeps one, and import this has no Ruby counterpart at all.

Perl's motto was "there is more than one way to do it", and Ruby inherited the attitude: map and collect give the same result, so do select and filter, inject and reduce, size and length, then and yield_self. Python's answer, in the Zen it ships as a module, is that "there should be one — and preferably only one — obvious way to do it", and the language mostly keeps that promise: a list has sort and nothing else spelled like it. Neither attitude makes the other language's idioms unavailable; it decides which spelling the community reads as native. The table below is the crosswalk this library keeps coming back to, one lesson per row; the program prints one measured line per row, then checks the aliases, then asks Ruby for its Zen (there is none — require "this" raises), and ends with a five-line word-frequency program whose output the Python twin reproduces to the byte.

# The Ruby way The Pythonic way Lesson
1 each with a block for for loops do not scope
2 select/map blocks a comprehension Blocks are not objects
3 empty? — a ? predicate not xs, is_/isdigit-style names Bang and question methods
4 sort! returns the receiver sort() returns None Bang and question methods
5 unless if not unless, until and modifiers
6 attr_accessor a plain attribute, or @property Instance variables are private
7 nil None nil, false and truthiness
8 puts print puts, print, p and pp
9 require import require, require_relative and load
10 Hash dict Hashes and default values
11 Symbol str, interned, or an Enum Symbols are names
12 module as a mixin a mixin base class Mixins: include, extend, prepend
13 Comparable + <=> functools.total_ordering + __lt__/__eq__ Comparable and the spaceship
14 Enumerable + each the iterator protocol, __iter__ Enumerable is a mixin
15 method_missing __getattr__ method_missing and respond_to_missing?
16 Struct namedtuple Struct and Data
17 Data @dataclass(frozen=True) Struct and Data
18 case … in match … case case/in pattern matching
19 Rational, 1/3r fractions.Fraction Rational and BigDecimal

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

The idiom pairs
 1. [1, 2, 3].each { |x| out << x.to_s }     "123"
 2. xs.select(&:even?).map { it * it }       [4, 16]
 3. [].empty? / [1].empty?                   true / false
 4. a = [3, 1]; a.sort! returns              [1, 3]  (the receiver, now sorted)
 5. unless [].any? then "empty" end          "empty"
 6. attr_accessor :name; person.name = "ann" "ann"
 7. nil.class / nil.inspect / nil.to_s       NilClass / nil / ""
 8. r = puts "hi"  (printed "hi\n")          r = nil
 9. require "json" twice                     true / false  (loaded once)
10. {a: 1}[:a] / [:z] / fetch(:z, 0)         1 / nil / 0
11. :a.equal?(:a) / "a".equal?("a")          true / false
12. include Greeting; Host.new.hello         "hello from the mixin"  ancestors [Host, Greeting, Object]
13. include Comparable, <=> only: V(1) < V(2) true  between? true
14. include Enumerable, each only: sort / map [1, 2] / [20, 10]
15. method_missing: Finder.new.find_by_name("x") "name=x"
16. Struct.new(:x, :y).new(1, 2)             #<struct Point x=1, y=2>
17. Data.define(:x, :y): with(y: 5) / set @x #<data Coord x=1, y=5> / FrozenError
18. case/in: shape([1, 2]) / shape({name: "ann"}) "pair 1,2" / "named ann"
19. 1/3r + 1/6r                              (1/2)

More than one way: two names, one result
    collect==map true, filter==select true, reduce==inject true, size==length true, yield_self==then true
    Method#== (one definition under two names): then/yield_self true, map/collect false  (two definitions of one behaviour)

The Zen: Ruby ships no equivalent of `import this`
    require "this" -> LoadError: cannot load such file -- this

Word frequency, the same program in both languages
    the      4
    and      2
    barks    2
    dog      2
    fox      2

Reading the output

  • Rows 1–5 are the syntax habits: iteration with a block, filtering and mapping with blocks, a ? predicate, a ! method that mutates and hands back its receiver, and unless as an expression with a value.
  • Rows 6–11 are the vocabulary: generated accessors, nil and its two string forms, puts returning nil after printing, require returning true the first time and false the second (the file loads once), Hash lookups with nil for a missing key, and the identity that makes a Symbol different from a String.
  • Rows 12–15 are the object model: a module mixed in appears in ancestors; Comparable derives five operators and between? from <=>; Enumerable derives sort and map from each; method_missing answers a name that was never defined.
  • Rows 16–19: the value types — a mutable Struct, an immutable Data (with returns a copy; setting an ivar is a FrozenError), pattern matching on an array and a hash, and exact rational arithmetic.
  • Two names, one result: five pairs of spellings, five equal results. The second line is a finer point: Method#== says then and yield_self are one definition under two names (a true alias), while map and collect are two separate definitions of the same behaviour — Ruby offers the second name either way.
  • The Zen: require "this" is a LoadError. Ruby's design philosophy lives in Matz's talks and books, not in a module. (Not machine-checked here.)
  • Word frequency: scan, tally, sort_by with a two-element key, first(5), format — five lines.

Compared with Python

The twin prints the same nineteen rows. Rows 4 and 8 are where a Python reader most often stumbles in Ruby: Python's sort() returns None and mutates, while Ruby's sort! returns the sorted receiver so it can be chained; print and puts at least agree on returning nothing. Row 6 needs no declaration — attributes are public by default. Row 9 shows import returning a module object and caching it in sys.modules, the counterpart of require's true/false. Row 11 has no symbol type: sys.intern gives the identity, an Enum gives the fixed vocabulary. Rows 12–15 are the same four ideas under Python's names — a base class in the MRO, total_ordering, __iter__, __getattr__. Rows 16–19 use namedtuple, a frozen dataclass (FrozenInstanceError on assignment), match, and Fraction. The one-way check finds none of Ruby's alias names on a list, the Zen prints its twenty-one lines, and the word-frequency program — Counter, re.findall, sorted with a tuple key — prints the same five lines the Ruby program did.

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

The idiom pairs
 1. for x in [1, 2, 3]: out += str(x)        '123'
 2. [x * x for x in xs if x % 2 == 0]        [4, 16]
 3. not [] / not [1]                         True / False
 4. a = [3, 1]; a.sort() returns             None  (a is now [1, 3])
 5. if not any([]): "empty"                  'empty'
 6. person.name = "ann"  (no declaration)    'ann'
 7. type(None).__name__ / repr(None) / str(None) NoneType / None / 'None'
 8. r = print("hi")  (printed 'hi\n')        r = None
 9. import json twice                        module / True  (loaded once, cached in sys.modules)
10. {"a": 1}["a"] / .get("z") / .get("z", 0) 1 / None / 0
11. sys.intern("a") is sys.intern("a") / Key.A True / <Key.A: 'a'>
12. class Host(Greeting): Host().hello()     'hello from the mixin'  mro ['Host', 'Greeting', 'object']
13. @total_ordering, __lt__ and __eq__: V(1) < V(2) True  <= True
14. __iter__ only: sorted(bag) / map         [1, 2] / [20, 10]
15. __getattr__: Finder().find_by_name("x")  'name=x'
16. namedtuple("Point", "x y")(1, 2)         Point(x=1, y=2)
17. frozen dataclass: replace(y=5) / set .x  Coord(x=1, y=5) / FrozenInstanceError
18. match: shape([1, 2]) / shape({"name": "ann"}) 'pair 1,2' / 'named ann'
19. Fraction(1, 3) + Fraction(1, 6)          Fraction(1, 2)

One obvious way: the aliases Ruby has do not exist
    hasattr([], 'collect') False, hasattr([], 'filter') False, hasattr([], 'length') False, hasattr([], 'size') False, hasattr(5, 'then') False

The Zen, as `import this` prints it
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

Word frequency, the same program in both languages
    the      4
    and      2
    barks    2
    dog      2
    fox      2

The Python library's crosswalk ↗ and glossary ↗ are the same table seen from the other side.

Try it

  1. Prove "more than one way" on the word-frequency program: write it three ways in Ruby — with tally, with each_with_object(Hash.new(0)), and with group_by(&:itself) — print all three top-five lists, and print whether the three results are equal.
  2. Pick five rows of the table, write the Python side from memory into a file, run it under python3 -I, and diff your lines against the twin's output above.
Solution to 1

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

# Kata: "more than one way" made concrete - the word-frequency program written
# three ways, with a check that all three agree.

TEXT = "the quick brown fox jumps over the lazy dog the fox sleeps and the dog barks and barks"

def top_five_with_tally(text)
  text.scan(/[a-z']+/).tally.sort_by { |w, c| [-c, w] }.first(5)
end

def top_five_with_each_with_object(text)
  counts = text.scan(/[a-z']+/).each_with_object(Hash.new(0)) { |w, h| h[w] += 1 }
  counts.sort_by { |w, c| [-c, w] }.first(5)
end

def top_five_with_group_by(text)
  text.scan(/[a-z']+/).group_by(&:itself).map { |w, ws| [w, ws.size] }.sort_by { |w, c| [-c, w] }.first(5)
end

results = {
  "tally" => top_five_with_tally(TEXT),
  "each_with_object" => top_five_with_each_with_object(TEXT),
  "group_by" => top_five_with_group_by(TEXT),
}

results.each do |name, pairs|
  puts "#{name}:"
  pairs.each { |w, c| puts format("  %-8s %d", w, c) }
end
puts "all three equal? #{results.values.uniq.size == 1}"

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

tally:
  the      4
  and      2
  barks    2
  dog      2
  fox      2
each_with_object:
  the      4
  and      2
  barks    2
  dog      2
  fox      2
group_by:
  the      4
  and      2
  barks    2
  dog      2
  fox      2
all three equal? true

See also