Gastón Ramos
Personal blog
← Back to the English articles
October 20, 2026.
Python for Ruby developers: differences, concepts, and common mistakes
I'm learning Python, and Ruby is the language I know best. So I decided to start by comparing the two. They have a lot in common, but a few differences in syntax and behavior can catch me out. Here are the ones I want to keep handy, with examples to compare them.
1. Indentation and code blocks
One of the first differences is how blocks are delimited.
In Ruby, we use end. In Python, indentation is part of
the syntax.
Ruby:
if x > 5
puts "Greater"
end
Python:
if x > 5:
print("Greater")
In Python:
- Blocks start after
:. - Four spaces are the usual indentation.
- Indentation is required and determines which statements belong to each block.
2. None, True, and False
Python uses different names for some basic values.
| Ruby | Python |
|---|---|
nil |
None |
true |
True |
false |
False |
To check whether a variable is None, you normally use
is:
if x is None:
print("No value")
Using is None instead of == None checks
identity and does not depend on a custom equality implementation.
Falsy values
In Ruby, only nil and false are falsy in a
condition.
Python has more falsy values, including:
NoneFalse0""[]{}()
For example:
items = []
if items:
print("Has items")
else:
print("It's empty")
This prints "It's empty". In Ruby, an empty array is
truthy. This matters when working with collections.
3. Function return values
In Ruby, the value of the last evaluated expression is returned automatically.
def add(a, b)
a + b
end
Python requires an explicit return:
def add(a, b):
return a + b
If a Python function finishes without executing a
return, it returns None.
def add(a, b):
a + b
print(add(2, 3)) # None
It's a small difference, but an easy one to miss when moving between languages.
4. Handling exceptions: try and except
Ruby uses begin and rescue to handle
exceptions.
begin
result = 10 / 0
rescue ZeroDivisionError
puts "Error"
end
Python uses try and except:
try:
result = 10 / 0
except ZeroDivisionError:
print("Error")
The idea is much the same, but the syntax changes. In both languages, it's better to catch specific exceptions than every error indiscriminately.
5. Lists and tuples
Python has two data structures that may look like Ruby arrays: lists and tuples.
Lists
Lists are mutable and use square brackets.
numbers = [1, 2, 3]
numbers.append(4)
They are comparable to Ruby arrays.
Tuples
Tuples are immutable sequences.
numbers = (1, 2, 3)
Once a tuple is created, we cannot replace, add, or remove its elements. But there's a detail: if a tuple contains a mutable object, that object can still change.
data = ([1, 2], "example")
data[0].append(3)
print(data) # ([1, 2, 3], 'example')
The tuple itself is still immutable; the list inside it isn't.
6. Multiple assignment and unpacking
Python lets us assign several variables in one statement.
a, b = 10, 20
We can also unpack values returned by a function:
def coordinates():
return 10, 20
x, y = coordinates()
Here Python packs the two return values into a tuple, then unpacks it. Ruby has a similar feature:
a, b = 10, 20
7. Dictionaries: Python's equivalent of hashes
Python dictionaries are comparable to Ruby hashes.
Ruby:
person = { name: "Juan", age: 30 }
Python:
person = {"name": "Juan", "age": 30}
One difference is that Python has no direct equivalent of Ruby's
Symbol type. Dictionary keys must be hashable; strings,
numbers, and tuples whose elements are hashable are common choices.
Accessing missing keys
This difference matters. In Ruby:
person = { name: "Juan" }
person[:age] # nil
In Python:
person = {"name": "Juan"}
person["age"] # KeyError
If the key does not exist, Python raises KeyError. For
behavior closer to Ruby's Hash#[], use
get:
person.get("age") # None
We can also supply a default value:
person.get("age", 0) # 0
In Ruby, fetch behaves more like Python's bracket access
when a key is missing.
8. Mutable default arguments
This is one of the most surprising Python behaviors for someone coming from Ruby. Consider this function:
def add_item(item, items=[]):
items.append(item)
return items
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2]
print(add_item(3)) # [1, 2, 3]
Why? Default arguments are evaluated when the function is defined, not each time it is called. The empty list is created once and reused by later calls that omit the argument.
The usual solution
Use None as the default and create the list inside the
function:
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
Now:
print(add_item(1)) # [1]
print(add_item(2)) # [2]
print(add_item(3)) # [3]
In Ruby, default arguments are evaluated on each call, so this problem does not occur in the same way.
9. References, assignment, and copies
Both Ruby and Python work with references to objects. Assigning an object to another variable does not automatically copy it.
Python:
a = [1, 2]
b = a
b.append(3)
print(a) # [1, 2, 3]
Ruby:
a = [1, 2]
b = a
b << 3
puts a.inspect # [1, 2, 3]
In both cases, a and b refer to the same
object.
Making a copy
In Python:
b = a.copy()
In Ruby:
b = a.dup
Both operations make shallow copies, so nested objects may still be shared.
Reassignment is different from mutation
a = [1, 2]
b = a
b = [3, 4]
print(a) # [1, 2]
Here we have not changed the original object. We have simply assigned
a new list to b. Ruby works similarly.
10. Common Python exceptions
Both languages have similar exception systems, but names and circumstances can differ.
| Python | Meaning | Ruby comparison |
|---|---|---|
AttributeError |
Missing attribute or method | Similar to NoMethodError |
TypeError |
Incompatible operation or incorrect arguments | May correspond to TypeError or
ArgumentError |
ValueError |
Invalid value for an operation | Similar to some ArgumentError cases |
KeyError |
Missing dictionary key | Hash#[] usually returns nil |
IndexError |
Index out of range | Array#[] usually returns nil |
NameError |
Undefined name | Also exists in Ruby |
ZeroDivisionError |
Division by zero | Also exists in Ruby |
AttributeError
Raised when we access a missing attribute or method.
x = None
x.upper()
TypeError
Raised when we try an operation that is incompatible with the types involved.
result = "10" + 5
ValueError
Raised when the argument's type is acceptable but its value is not.
number = int("hello")
KeyError
Raised when we access a missing dictionary key with brackets.
data = {"name": "Juan"}
data["age"]
IndexError
Raised when we access a list position that does not exist.
numbers = [1, 2, 3]
numbers[10]
In Ruby, the equivalent operation usually returns
nil.
NameError
Raised when we use a name that has not been defined.
print(undefined_variable)
ZeroDivisionError
Raised when we try to divide by zero.
result = 10 / 0
11. Reading tracebacks
When Python encounters an uncaught exception, it prints a traceback. For example:
Traceback (most recent call last):
File "app.py", line 5, in <module>
user.name
AttributeError: 'NoneType' object has no attribute 'name'
The traceback tells us where the error happened and what kind of exception it was:
AttributeErrortells us we tried to access a missing attribute.NoneTypetells us the object wasNone.nameis the attribute we tried to access.
When I investigate an error like this, I identify the exception, read its message, find the relevant line, and trace how the program reached that state.
Conclusion
Learning Python from Ruby feels fairly natural. The languages share many concepts, but their differences in behavior are worth learning. The ones I especially want to remember are:
- Indentation as part of the syntax.
- Explicit
returnstatements. - Differences in truthy and falsy values.
- Lists and tuples.
- What happens when a dictionary key is missing.
- How mutable default arguments are evaluated.
- Differences in exceptions.
Knowing these differences lets me carry much of what I learned in Ruby over to Python, avoid common mistakes, and better understand how Python code is usually written.
Part two: variables, classes, and objects →
Until next time,
Gastón Ramos
::: If you'd like to comment, email me: ramos.gaston AT gmail.com :::