Why Ruby is More Readable than Python

Why Ruby is More Readable than Python

Ruby and Python are nearly indistinguishable. If a Python programmer opens a Ruby codebase, he’ll be able to understand most of it without having to do external research. The same is true for Ruby programmers who open Python code bases.

However, it’s unlikely that a Rubyist will be able to go through a Python codebase as easily as a Pythonista would a Ruby codebase.

In this post, I’ll show you why.

Objects

Let’s create a class to represent blog posts.

In Python

class BlogPost:

    def __init__(self, title, body):
        self.title = title
        self.body = body
        self.published = False

    def publish(self):
        self.published = True

Let’s play with it:

post = BlogPost(
    title="How I built my blog using Kubernetes, Lambda, and React.",
    body="Wordpress was an insult to my intelligence and so I ...")

post.publish()

I want to be able to print the title of blog posts. There are two ways to accomplish this.

The first one is by simply printing the attribute:

print(post.title)

The second one is to add a __str__ method to our class:

class BlogPost:

    def __init__(self, title, body):
        self.title = title
        self.body = body
        self.published = False

    def publish(self):
        self.published = True

    def __str__(self):
        return self.title

Now we can print the title more easily:

print(post)

What if you want to change the title of a post?

Easy:

post.title = "How I built build my blog using modern instead of simple tools."

In Ruby

Let’s do the same in Ruby:

class BlogPost

    def initialize(title, body)
        @title = title
        @body = body
        @published = false
    end

    def publish
        @published = true
    end

end

Playing with classes is just as easy:

post = BlogPost.new("How I built my blog using Kubernetes, Lambda, and React.",
    "Wordpress was an insult to my intelligence and so I ...")

post.publish

I want to print the title of a post. Let’s try:

puts post.title

Oh no! undefined method `title'

In Ruby, accessing instance variables like in Python isn’t possible. You need a getter:

class BlogPost

    def initialize(title, body)
        @title = title
        @body = body
        @published = false
    end

    def publish
        @published = true
    end

    def title
        @title
    end

end

You can’t set attributes directly either — you need a setter:

class BlogPost

    def initialize(title, body)
        @title = title
        @body = body
        @published = false
    end

    def publish
        @published = true
    end

    def title
        @title
    end

    def title=(new_title)
        @title = new_title
    end

end

Now we can play:

puts post.title
post.title = "How I built build my blog using modern instead of simple tools."

What about the __str__ trick we used in Python?

class BlogPost

    def initialize(title, body)
        @title = title
        @body = body
        @published = false
    end

    def publish
        @published = true
    end

    def title
        @title
    end

    def title=(new_title)
        @title = new_title
    end

    def to_s
        @title
    end

end
puts post

Ruby vs Python Objects

Wait so, it looks like both are readable enough. True, but check this out:

class BlogPost:
    count = 0

    def __init__(self, title, body):
        self.title = title
        self.body = body
        self.published = False
        BlogPost.count += 1

    def publish(self):
        self.published = True

    def __str__(self):
        return self.title

We can access the number of posts with BlogPost.count or post.count.

In Ruby:

class BlogPost
    @@count = 0

    def initialize(title, body)
        @title = title
        @body = body
        @published = false
        @@count += 1
    end

    def publish
        @published = true
    end

    def title
        @title
    end

    def title=(new_title)
        @title = new_title
    end

    def to_s
        @title
    end

    def count
        @@count
    end

end

Now we can access post.count but we can’t access BlogPost.count like in Python. Since this is a class variable, we need to be able to access it from the class itself.

class BlogPost
    @@count = 0

    def initialize(title, body)
        @title = title
        @body = body
        @published = false
        @@count += 1
    end

    def publish
        @published = true
    end

    def title
        @title
    end

    def title=(new_title)
        @title = new_title
    end

    def to_s
        @title
    end

    def count
        @@count
    end

    def self.count
        @@count
    end

end

Now we can do BlogPost.count. But we’d rather not be able to do post.count because it could be confused with a regular instance variable.

class BlogPost
    @@count = 0

    def initialize(title, body)
        @title = title
        @body = body
        @published = false
        @@count += 1
    end

    def publish
        @published = true
    end

    def title
        @title
    end

    def title=(new_title)
        @title = new_title
    end

    def to_s
        @title
    end

    def self.count
        @@count
    end

end

Now we can only access count from the BlogPost class. Can we set the class variable though?

Let’s try:

BlogPost.count = 0

Of course not! We never defined a setter for this variable.

What about in Python?

post.count = 0

It works. We can even do it from the class:

BlogPost.count = 0

Ruby objects are more straighforward?

I don’t know about you but I think that it’s easier to see the difference between class and instance attributes in Ruby.

Setters and getters allow you to clearly specify which attributes are readable and writable. You can protect your class attributes by not implementing a setter. In Python, it’s easy to accidentally write to the count attribute — which can break your program.

By default, both post.count and BlogPost.count returns the value of the attribute but it would be easier to notice that it’s a class attribute if it was only accessible from the class.

One reason for that is Ruby’s objects only respond to method calls while Python allows access to both attributes and methods.

Look at this Python code:

class Person
    def __init__(self):
        self.age = 0

person = Person()
person.age = -1

A programming error could cause a person to end up with a negative age. To solve the problem we can do this:

class Person:
    def __init__(self):
        self.__age = 0

    @property
    def age(self):
        return self.__age

    def set_age(self, val):
        if val < 0:
            raise ValueError("Age must be 0 or more")
        self.__age = val
        return val

person = Person()
print(person.age)
person.age = 12
>>> AttributeError
person.set_age(12)

You can use a @property but then you need to implement a separate API to perform write actions on this property. You can no longer rely on the familiar = operator. Unless you use __setattr__ as below:

class Person:
    ...
    def __setattr__(self, att, val):
        if att == 'age':
            if val < 0:
                raise ValueError("Age must be >= 0")
            self.__age = val
            return val
        else:
            return super().__setattr__(att, val)

person.age = 10
# Bithday
person.age += 1
print(person.age)
>>> 11
person.age = -1
>>> ValueError
person.age = 0
person.age -= 1
>>> ValueError

EDIT 23/07/2022:

A reader pointed out that it’s also possibe to use the setter method once a method is decorated. This is definitely nicer than explicitly intercepting __setattr__:

class Person:
    def __init__(self):
        self.__age = 0

    @property
    def age(self):
        return self.__age

    @age.setter
    def age(self, val):
        if val < 0:
            raise ValueError("Age must be 0 or more")
        self.__age = val
        return val

person = Person()
print(person.age)
>>> 0
person.age = 12
print(person.age)
>>> 12

Now for the same in Ruby:

class Person
    attr_reader :age

    def initialize
        @age = 0
    end

    def age=(val)
        raise ArgumentError, "Age must be >= 0" unless age >= 0
        @age = val
    end
end

person = Person.new
person.age -= 1
>>> ArgumentError

How much simpler is that?

What do you think about dunder methods (__str__)? Personally, I find to_s to be clearer. It clearly means that calling this method will return a string representation of this object. In Python, you will never do post.__str__ for example. Instead, you’ll do str(post). The str method will call the __str__ method for you and return the result. Sure, all roads lead to Rome but in terms of straighforwardness, they’re not all equal.

More on dunders

Check this out:

class Matrix:

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, to):
        return Matrix(self.x + to.x, self.y + to.y)

    def __str__(self):
        return f"({self.x}, {self.y})"

Let’s play:

a = Matrix(1, 1)
b = Matrix(2, 2)
print(a + b)

You should get (3, 3) back.

The same thing in Ruby:

class Matrix
    attr_reader :x, :y

    def initialize(x, y)
        @x = x
        @y = y
    end

    def +(to)
      Matrix.new(@x + to.x, @y + to.y)
    end

    def to_s
      "(#{@x}, #{@y})"
    end
end

Play with it:

a = Matrix.new(1, 1)
b = Matrix.new(2, 2)
puts a + b

As you can see, there’s no need to know about special dunder methods in Ruby. If you want your classes to respond to the + operator, simply implement a + method in your class.

I find this to be more readable than Python’s __add__. Behind the scenes, calling + with your objects will call add that will call your object’s __add__ method.

Neo needs to dodge bullets fast and implementing his matrices in Ruby will provide for more straighforward calculations.

Class inheritance

Let’s implement a view to create objects in the DRYest way possible:

class View:
    ...

class ProcessFormView(View):
    ...

class ContextMixin:
    ...

class SingleObjectMixin:
    ...

class FormMixin:
    ...

class ModelFormMixin:
    ...

class BaseCreateView(ContextMixin,  SingleObjectMixin, FormMixin, ModelFormMixin, ProcessFormView):
    ...

class TemplateResponseMixin:
    ...

class SingleObjectTemplateResponseMixin:
    ...

class CreateView(TemplateResponseMixin, SingleObjectTemplateResponseMixin, BaseCreateView):

    def form_valid(self, form):
        ...

Which one of these classes has the form_valid method that I’m overriding? In other words, if I call super().form_valid(form), which class is super() referring to? To figure that out, Python uses an algorithm called C3 to implement something called Method Resolution Order (MRO).

Unless you have Pycharm installed, it’s almost impossible to figure that manually. As a matter of fact, a whole website was created just for helping people figure out the method resolution order of Django’s class based views. Python has full support for multiple inheritance and developers use it lavishly.

In Ruby, multiple inheritance is impossible:

class View:
    ...
end

class ProcessFormView < View:
    ...
end

module ContextMixin
    ...
end

module SingleObjectMixin
    ...
end

module FormMixin
    ...
end

module ModelFormMixin
    ...
end

class BaseCreateView < ProcessFormView
    include ContextMixin
    include SingleObjectMixin
    include FormMixin
    include ModelFormMixin
end

module TemplateResponseMixin
    ...
end

module SingleObjectTemplateResponseMixin
    ...
end

class CreateView < BaseCreateView
    include TemplateResponseMixin
    include SingleObjectTemplateResponseMixin

    def form_valid(form)
        ...
    end
end

A wise man once said “Favor inheritance over composition”. He was executed for heresy.

As you can see, in Ruby, you’re forced to use composition instead of inheritance when you need to share behavior. Method resolution is so simple that a caveman could understand it:

  1. First Ruby looks in the class to see if the method is defined.
  2. If not, it looks at the most recently included module.
  3. If not found, repeat the same for the superclass.

This process continues for subsequent superclasses until the method is found. If it’s not mound, raise an exception.

In Python, mixins are simply classes. You can instantiate them and they can hold state. In Ruby, mixins are modules and can’t be instantiated. You won’t have to worry about MRO like in Python because:

  1. You’re inheriting from a single super class (no need for MRO).
  2. The last mixin will override anything with a similar name from the previous one.
  3. No constructors are being called.

In Python, you have to pretend like you’re doing composition when you’re actually just doing multiple inheritance. In Ruby, composition is baked into the language. Since Ruby modules aren’t classes, they’re likely to be simpler as well — a big win for readability.

Conclusion

Look at these instructions:

  1. Turn to your right and take a step.
  2. Then turn to your left and take a step.
  3. And then, turn to your left again and take a step.
  4. Finally, turn right.

And this one:

  1. Take a step forward.

While both languages are much easier to read than say, PHP or Java, Ruby takes it a step further by allowing you to write code that can be understood at a single glance.

I merely scratched the surface here and if you’re new to Ruby, I recommend you go through this book to learn more about the various ways Ruby can help you write human readable code.

If this post made you happy then email me your thanks. If it made you angry, then come duel me via email.