Ruby
Python
programming languages
language comparison
software development

What does Ruby have that Python doesn't, and vice versa?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Ruby and Python are both dynamically typed, interpreted languages with a strong focus on developer happiness, yet they diverge in meaningful ways under the surface. If you already know one language and are learning the other, understanding these differences will help you avoid writing "Python in Ruby" or "Ruby in Python." This article walks through the most significant features each language offers that the other lacks or handles differently.

Ruby's Blocks, Procs, and Lambdas

Ruby treats code blocks as first-class citizens in a way Python does not. Every Ruby method can accept an implicit block, and you can capture it as a Proc or define standalone lambdas.

ruby
1# Implicit block passed to a method
2def with_logging
3  puts "Start"
4  yield
5  puts "End"
6end
7
8with_logging { puts "Doing work" }
9
10# Proc and Lambda
11square = Proc.new { |x| x * x }
12double = ->(x) { x * 2 }
13
14puts square.call(5)  # 25
15puts double.call(5)  # 10

Python has lambda expressions, but they are limited to a single expression. For multi-line logic, Python relies on named functions passed as arguments. Ruby blocks let you pass multi-line anonymous code to any method naturally, which is why Ruby DSLs (like those in Rails and RSpec) read so fluently.

Python's Generators and Comprehensions

Python has generators and list/dict/set comprehensions that Ruby does not match directly. Generators let you produce values lazily with yield inside a function, using very little memory.

python
1# Generator that produces squares lazily
2def squares(n):
3    for i in range(n):
4        yield i * i
5
6for val in squares(1_000_000):
7    if val > 100:
8        break
9    print(val)
10
11# List comprehension
12evens = [x for x in range(20) if x % 2 == 0]
13
14# Dict comprehension
15name_lengths = {name: len(name) for name in ["Alice", "Bob", "Charlie"]}

Ruby has Enumerator::Lazy for lazy evaluation, but it is more verbose. Ruby also lacks a built-in comprehension syntax; you use .map and .select chains instead, which are readable but distinct.

method_missing vs __getattr__

Both languages let you intercept calls to undefined methods, but the mechanism feels different. Ruby uses method_missing, which is central to meta-programming in frameworks like ActiveRecord.

ruby
1class DynamicGreeter
2  def method_missing(name, *args)
3    if name.to_s.start_with?("greet_")
4      language = name.to_s.sub("greet_", "")
5      "Hello from #{language}!"
6    else
7      super
8    end
9  end
10end
11
12g = DynamicGreeter.new
13puts g.greet_french  # "Hello from french!"

Python's equivalent is __getattr__, called when normal attribute lookup fails.

python
1class DynamicGreeter:
2    def __getattr__(self, name):
3        if name.startswith("greet_"):
4            language = name.replace("greet_", "")
5            return lambda: f"Hello from {language}!"
6        raise AttributeError(name)
7
8g = DynamicGreeter()
9print(g.greet_french())  # "Hello from french!"

The key difference is that Ruby's method_missing intercepts method calls directly, while Python's __getattr__ returns an attribute (often a callable). Python also has __getattribute__, which intercepts every attribute access, not just missing ones.

Mixins vs Multiple Inheritance

Ruby uses modules as mixins. A class can include multiple modules but can only inherit from one class.

ruby
1module Serializable
2  def to_json
3    # serialization logic
4  end
5end
6
7module Loggable
8  def log(msg)
9    puts "[LOG] #{msg}"
10  end
11end
12
13class User
14  include Serializable
15  include Loggable
16end

Python supports full multiple inheritance with a Method Resolution Order (MRO) determined by the C3 linearization algorithm.

python
1class Serializable:
2    def to_json(self):
3        pass  # serialization logic
4
5class Loggable:
6    def log(self, msg):
7        print(f"[LOG] {msg}")
8
9class User(Serializable, Loggable):
10    pass

Ruby's mixin approach avoids the diamond problem by design, since modules are inserted into the ancestor chain in a predictable order. Python's MRO handles the diamond problem algorithmically, which is more flexible but can be harder to reason about.

Symbols in Ruby

Ruby has a Symbol type, written as :name, which is an immutable, interned string. Symbols are used extensively as hash keys and method identifiers because they are faster to compare than strings.

ruby
status = :active
config = { host: "localhost", port: 3000 }

Python does not have a symbol type. The closest equivalent is using strings as dictionary keys, which Python interns automatically for short strings. In practice, this performance difference is negligible in Python.

GIL Differences

Both CPython and CRuby (MRI) have a Global Interpreter Lock (GIL) that prevents true parallel execution of threads. However, they differ in direction. Python's GIL has been a long-standing topic, and PEP 703 introduced a free-threaded build in Python 3.13 that allows disabling the GIL. Ruby 3.x introduced Ractors, which are actor-based concurrency units that each get their own GIL, enabling true parallelism without sharing mutable state.

Common Pitfalls

  • Assuming Ruby blocks and Python lambdas are equivalent; Ruby blocks can contain multiple statements and complex control flow, while Python lambdas cannot.
  • Using method_missing or __getattr__ without defining respond_to_missing? (Ruby) or proper hasattr behavior (Python), which breaks introspection tools.
  • Confusing Ruby's single inheritance plus mixins with Python's multiple inheritance; the method resolution strategies are fundamentally different.
  • Forgetting that Ruby symbols are not garbage collected in older Ruby versions (before 2.2), which can cause memory leaks if you dynamically create symbols from user input.
  • Writing threaded code assuming no GIL in either language; both CPython and MRI restrict true thread parallelism unless you use their newer concurrency primitives (Ractors or free-threaded builds).

Summary

  • Ruby's blocks, procs, and lambdas offer more flexible anonymous code passing than Python's single-expression lambdas.
  • Python's generators and comprehensions provide concise, memory-efficient data processing that Ruby achieves through more verbose enumerator chains.
  • Both languages support dynamic method dispatch (method_missing vs __getattr__), but with different semantics around attribute access versus method calls.
  • Ruby uses module mixins to avoid the diamond problem; Python uses C3 linearized multiple inheritance.
  • Ruby has symbols for fast, immutable identifiers; Python relies on interned strings.
  • Both languages are evolving past their GIL limitations through Ractors (Ruby) and free-threaded builds (Python).

Course illustration
Course illustration

All Rights Reserved.