Python vs Ruby

Some differences between Python vs Ruby

I’ve been coding in Python for some time now, starting with some professional work in 2011. At first it was in Python 2 but since I didn’t have any legacy code to maintain I quickly switched to Python 3, coding only in Python 2 when I needed a library that hadn’t switched yet.

I learned Python initially through a tutorial on https://openclassrooms.com/ (which was then called “le Site du Zero”) and by refactoring a bunch of old scripts for a scientific project.

I started Ruby hand in hand with Rails (like so many of us) early 2014 making web application prototypes for a fundraising early-stage startup. I learned Ruby initially using Michael Hartl’s excellent tutorial.

This is to say that most of what I know on both languages is self-learned through tutorials, trial and error, StackOverflow questions, blog posts and other resources available on the web.

I’m very fond of both Python and Ruby. Here is a list of a few differences and my preference between the two languages. It’s not meant to be complete or objective truth, the following choices come from my personal likes/dislikes. I don’t see myself as an expert so if anyone cares to teach me anything about the topics I’ll be covering, you are most welcome.

Note: There are two basic data-structure that every high-level language uses. Python calls them lists and dictionaries (dicts) while Ruby calls them arrays and hashes. I’ll be mostly using the Python terminology.

1) Mission Statements

Python ideals are mostly covered by the Zen of Python: http://docs.python-guide.org/en/latest/writing/style/#zen-of-python

Ruby ideals can be gathered from the following interview of its creator for example: http://www.artima.com/intv/ruby.html

A lot of things could be said about both of those, I’ll just oppose here two of their mission statements:

Python: “There should be one-- and preferably only one --obvious way to do it.”

Ruby: “Ruby is designed to make programmers happy.”

Both of those are good but they have different impacts in different contexts:

Scripting and working in the interpreter: I prefer the redundancy that comes with maximising programmer happiness. What’s obvious for some people might not be for others and when I’m in the interpreter I want to go as fast as I can. In Ruby the redundancy makes it easier for me to guess how I should code things.

Coding a long term project: I prefer Python’s “one obvious way”. When you’re looking on the internet for the best way to code something it’s much easier when there aren’t that many ways and one is clearly better than the others.

Winner: this is a slight win for Ruby as maximising happiness resonates with me however Python’s “one obvious way” doctrine can be pretty good and Python devs don’t follow it neurotically (for example quit() and exit() both exist).

2) Argparse vs OptionParser

When you’re used to working on Linux and start getting shell/bash skills you get used to testing new commands with --help to get a basic understanding on how you should use the command. When you write a small/simple program it’s usually simpler to make it a shell/bash command than a full designed GUI program. Making it look like classical Linux programs with it’s very own --help is pretty neat, it can also be very useful and helps making it more maintainable (auto-documented inputs).

Both languages come with a built-in argument parser library, Python has argparse (https://docs.python.org/3/library/argparse.html) and Ruby has OptionParser (http://ruby-doc.org/stdlib-2.3.0/libdoc/optparse/rdoc/OptionParser.html).

I had used argparse quite a few times in Python before needing OptionParser in Ruby. I was disappointed: argparse feels much more powerful than OptionParser. Looking in OptionParser for features that exist in argparse usually leads to SO questions like the following: http://stackoverflow.com/questions/15487628/ruby-optparse-limitations

If you look at minimal examples in both languages:

#!ruby
require 'optparse'

options = {}
OptionParser.new do |opts|
  opts.banner = "Usage: example.rb [options]"

  opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
    options[:verbose] = v
  end
end.parse!

p options
#!python3
import argparse

parser = argparse.ArgumentParser()
parser.add_argument('-v', "--verbose", action='count', help='Run verbosely')

args = parser.parse_args()
print(args)

Both examples have extra features the other doesn’t (--no-verbose in ruby and count in python) but I have a definite preference for the shorter python example. I won't give out longer examples but argparse has, in my experience, a lot more features than it's Ruby counterpart while remaining simpler.

Winner: Python, argparse feels simpler and more powerful than OptionParser.

3) Map vs List comprehension

Let’s take a contrived example of wanting to find the number of digits in the first elements of a list of lists of numbers.

#!ruby
a = [[1455,2],[53,5,23],[42455]]
a.map(&:first).map(&:to_s).map(&:length).reduce(0,:+) # map chaining
a.map{ |x| x.first.to_s.length }.reduce(0,:+) # shortened form

When I’m in the interpreter trying to process data and study it, I usually find myself iterating and chain mapping as I piece-by-piece coax my data in whatever form I need. However when it finds it way into production code I put in in it’s shortened form.

#!python3
a = [[1455,2],[53,5,23],[42455]]
sum([len(str(x[0])) for x in a])

Admittedly the Python example is actually shorter than the Ruby example, but it’s a bit harder to follow. It becomes even harder to understand when you’re dealing with complicated structures with multi-level nested lists.

For example I have needed many times to flatten a list of lists.

Here’s how in Python:
http://stackoverflow.com/questions/952914/making-a-flat-list-out-of-list-of-lists-in-python
The top two answers (list comprehension and itertools.chain) are pretty verbose and the third answer (overloading sum) is simple and short but not that readable to someone not knowing the trick.

Ruby has it better with flatten which can also take an argument if you don’t want to flatten all the way down. When you add to that flat_map (a.flat_map(&b) works exactly like a.map(&b).flatten!(1)) you get a lot of power to unpack and process nested structures in very little code.

Winner: Ruby, though this is mostly for when you’re in “scripting and working in the interpreter” mode.

4) Array/list access

This a pretty simple and basic item, how do you access elements in an array/list ? How do you get a sublist?

#!ruby
a[0] # gets first element, also a.first works which helps when using map
a[-1] # gets last element, also a.last works which helps when using map
a[0...2] # gets first to second element
a[0..2] # gets first to third element
a[1..-2] # gets second to before-last element
a[1..-1] # gets second to last element
#!python3
a[0] # gets first element
a[-1] # gets last element
a[0:3] # gets first to third element
a[:2] # gets first to second element (by default start at 0)
a[1:-1] # gets second to before-last element
a[1:] # gets second to last element (by default end at -1)

Ruby has something nice with the difference between .. and ... which let’s you avoid having to write things like n-1 when you’d rather just write n. However I much prefer avoiding having to write -1 when I want to get to the end of a list.

Winner: Python, however the difference are mostly cosmetic so it comes down to pretty much my personal preferences rather than a real usability issue.

5) Numbers

In Ruby you can add underscores in a number to get better readability:
1_000_000 == 1000000

It’s been that way since at least Ruby 2.0.0 released in 2013. This can really help your code be more readable which is a big plus in long term production code.

This feature has just arrived in Python 3.6 released on 2016-12-23 (https://www.python.org/downloads/release/python-360/)

Winner: Ruby, but only because they implemented the idea first: both languages are equal on that score now.

6) A dict/hash with a default value

Having a default value for a dictionary can be useful in a lot of instances. My basic example is counting the frequency of words in a list.

#!ruby
a = [5, 12, 1, 4, 5, 2, 4, 6, 3, 4, 4, 13, 12, 7]
d = Hash.new { |h,k| h[k] = 0 }
a.each { |x| d[x] += 1 }
d
#!python3
from collections import defaultdict
a = [5, 12, 1, 4, 5, 2, 4, 6, 3, 4, 4, 13, 12, 7]
d = defaultdict(lambda : 0)
for x in a:
    d[x] += 1
d

Imagine that you only want the defaultdict behaviour for the initialisation of your object but don’t want that behaviour when you later use the object, both languages have solutions for this.

#!ruby
d = Hash.new # or just {}
a.each { |x| d[x] = (d[x] || 0) + 1 }
#!python3
d = defaultdict(lambda : 0)
for x in a:
    d[x] += 1
d = dict(d)

Winner: Undecided, the Python method is more readable but requires an extra import, the Ruby method feels a bit more hackish but it’s also built-in which is a plus.

7) Bonus problem: non-breaking space or “espace insécable”

So this is probably only a problem for French Ruby coders using French keyboards.

Ever got the following errors?

> [4,5,6].map { |x| x + 1 }
NoMethodError: undefined method `map ' for [4, 5, 6]:Array

or

> [4,5,6].map { |x| x + 1 }
NameError: undefined local variable or method ` ' for main:Object

See the problem?

Do you notice that space after map in “undefined method `map '” ? It’s not your average, run-of-the-mill space.

On French keyboards you need to press Alt-Gr + 4 in order to get { and Alt-Gr + 6 in order to get |, the problem comes of trying to add spaces in your code at the same time: Alt-Gr + Space makes a non-breaking space (or “espace insécable” in French). It’s easy to make this mistake when you’re trying to code fast. That’s how you get error like “undefined method ‘map-non-breakable-space’ for array”.

After a while you debug it very quickly but the first few times are extremely confusing. My examples were pretty basic but imagine debugging a long line of complicated code working in structures you don’t fully comprehend: you have to unpack a lot of it before you actually get to the real problem.

Funnily enough this can be used to obfuscate code or play pranks:

# Just make sure that the first space is a non-breaking space
 4 = 5
 4 + 4 == 10

Winner: Python

Afterword

I probably have material for another one or two posts like this. However both languages are actually very alike so most of the differences between them is cosmetic or complicated under-the-hood stuff. The key thing is that they are both very productive and fun languages.