Ruby and Python: a quick comparison

Python and Ruby seem similar. They’re dynamic, flexible and expressive. But we don’t always use them the same way. After 6 years with Ruby and 2 months with Python, I’ve tried to find ‘pivot points’ that define how different they are. Here’s a brief look, sans analysis, at those pivot points. (I go more in-depth in another essay.)

The hook

Learning Python, the first thing I winced at was the hook.

Ruby and Python both let us code hooks, methods that the interpreter calls at important parts of an object’s life cycle.

When I design an object, for example, I want to describe its initial state, but I don’t want to manage memory. To separate those two things, Ruby and Python create objects by allocating resources, then calling my initializing hook, and finally by returning my object. The only part of that I need to specify is the part that’s unique to my code. From Ruby’s object.c:

VALUE rb_class_new_instance(int argc, VALUE *argv, VALUE klass) {
  VALUE obj = rb_obj_alloc(klass);

  /* Call my initialize hook */
  rb_obj_call_init(obj, argc, argv);

  return obj;
}

The initialize hook is nice when I don’t need to explicitly manage memory. All I have to do is define the correct hook, and it’ll be called when the time’s right. And I even get return obj; for free, just because of how the hook is designed.

Ruby and Python both have hooks, though, so why is this a pivot point? The difference I’m getting at is how each language names hooks.

In Ruby, we find hooks that are clean and simple: initializeto_s<=>. Python names the same hooks, well, more cryptically: __init__()__str__()__cmp__().

These seem ugly, at first, and I definitely don’t like typing them. What if a language can have ugly features without itself being ugly? What if ugliness acts as a guide toward solid code and consistent style?

(I wrote more on that in ‘The Ugliness of Python’.)

Function passing

Ruby and Python let us pass functions as first-class objects. In Python, I define functions and pass them around, uncalled, without much effort. It’s beautiful. I feed one into another as a normal variable. No special syntax required. And then when I’m ready to call a function, all I need to do is add parentheses, just like in math. It’s really a breath of fresh air coming from Ruby:

f = lambda x, y: x + y ** 3 - y
f
# => <function <lambda> at 0x100481050>

f(2, 3)
# => 26

reduce(f, [2, 3, 4], 0)
# => 90

In Ruby, things are more complicated. I need an ampersand to pass a function and brackets to call it:

f = lambda {|x, y| x + y ** 3 - y }
f
# => #<Proc:0x000001012a8fc8@(irb):32 (lambda)>

f.call(2, 3)
# => 26

# Brackets are syntactic sugar for #call:
f[2, 3]
# => 26

[2, 3, 4].reduce(0, &f)
# => 90

More importantly (and annoyingly), if I define a top-level method with def–let’s call those top-level def methods (TLDMs)–Ruby won’t let me pass it as a block to any other method. (TLDMs actually belong to an object, so strictly speaking, this makes sense. It’s still annoying.) In Python, we can pass lambdas and TLDMs like they’re identical.

So Ruby makes function-passing doable. Python makes it absolutely painless.

Functions, methods, objects

Out of the box, Python gives us great tools for functional programming goodness:

  • len(coll)
  • map(f, coll)
  • reduce(f, coll, i)
  • filter(f, coll)
  • str(obj)

Not all the Lisp functions are functions in Python, though–and this was a shocker for me. Some are instance methods. (Examples: capitalize()reverse().) Actually, it’s hard to guess when Python will go one way or the other. It seems to follow convenience and tradition more than any kind of rationale, and that makes coding a little confusing. Sometimes it’s even hard to guess what the right receiver is. To join a list, for example, you have to pass the list to a string! (''.join(['my', 'list', 'here']).) This trips me up every time.

In Ruby, every function has a receiver and is really a method. (Lambdas come close to being functions.) And for Ruby libraries, there isn’t usually a question about whether to manipulate a data structure with a method or a function. The Ruby equivalents of the above, for example, are all methods:

  • coll.length
  • coll.map &f
  • coll.reduce i, &f
  • coll.select &f
  • obj.to_s

Ruby lets us use these methods on other methods and on lambdas, so I can do this without a problem:

str = lambda {|x| x.to_s }

numbers = [1, 2, 3]
strings = ['a', 'b', 'c']

numbers.map &str
# => ['1', '2', '3']

strings.map &:capitalize
# => ['A', 'B', 'C']

(Note: methods need a colon after the ampersand because you’re naming a method to call, not passing an actual method object.)

Blocks

In Ruby, blocks unify anonymous functions, variable capture (closure), and iteration. On top of that, they make chaining really simple. Python gives us the tools to do all of the same things as Ruby, but they’re not quite as unified. (I’ll have a detailed example for you in the next article.)

Explicitness

Python holds explicitness as a virtue, and Ruby doesn’t. I find this topic fascinating and want to dive deeper in an upcoming article.

Miscellaneous

The differences I just listed are most important to me. Other people might include a couple of other biggies:

  • Ruby lacks list comprehensions
  • Python doesn’t give us literals for regular expressions
  • Python distinguishes attributes and methods; Ruby doesn’t