Gastón Ramos
Personal blog
← Back to the English articles
October 20, 2026.
Python for Ruby developers: variables, classes, and objects
I'm continuing to learn Python from a Ruby developer's point of view. In this second part, I'm looking at variables, shared references, classes, objects, and methods. If you haven't read the first part, start with the basic differences between Python and Ruby.
1. Variables and assignment
In both languages, variables refer to objects.
Ruby
name = "Juan"
age = 30
active = true
Python
name = "Juan"
age = 30
active = True
Python uses True, False, and
None, while Ruby uses true,
false, and nil.
Shared references
In Python:
a = [1, 2, 3]
b = a
b.append(4)
print(a) # [1, 2, 3, 4]
In Ruby:
a = [1, 2, 3]
b = a
b << 4
puts a.inspect # [1, 2, 3, 4]
In both cases, the variables refer to the same object.
To make a shallow copy in Python:
b = a.copy()
In Ruby:
b = a.dup
Neither operation recursively copies the objects inside the collection.
2. Scope: where variables are visible
An important difference lies in how local variables work inside functions and blocks.
Ruby: blocks can change outer variables
x = 10
3.times do
x = 20
end
puts x # 20
The block changes the existing x outside it.
Python: functions have their own scope
x = 10
def change():
x = 20
change()
print(x) # 10
Assignment inside change() creates a local variable.
The global keyword
x = 10
def change():
global x
x = 20
change()
print(x) # 20
global lets us reassign a name in the module's
scope.
The nonlocal keyword
When functions are nested:
def outer():
counter = 0
def increment():
nonlocal counter
counter += 1
increment()
return counter
print(outer()) # 1
nonlocal lets us reassign a variable from an enclosing
function.
if
and for blocks do not create their own scope
if True:
message = "Hello"
print(message) # Hello
The same applies to a for loop:
for number in [1, 2, 3]:
result = number
print(result) # 3
Keep in mind: Ruby blocks can also access outer variables, but their scope rules are not identical to Python's.
3. Classes and objects
Defining a class
Ruby
class Person
def initialize(name)
@name = name
end
def greet
"Hello, #{@name}"
end
end
person = Person.new("Juan")
puts person.greet
Python
class Person:
def __init__(self, name):
self.name = name
def greet(self):
return f"Hello, {self.name}"
person = Person("Juan")
print(person.greet())
Equivalences
| Ruby | Python |
|---|---|
class Person |
class Person: |
initialize |
__init__ |
@name |
self.name |
Person.new("Juan") |
Person("Juan") |
def greet |
def greet(self): |
"Hello, #{@name}" |
f"Hello, {self.name}" |
| Implicit return value | Explicit return |
__init__ initializes an instance. Technically, the
object is created first, through __new__.
4. What self means
In both languages, self represents the current
object.
The difference is that Python requires an explicit parameter for the
instance in an instance method. By convention, we call it
self.
class Person:
def __init__(self, name):
self.name = name
def greet(self):
return f"Hello, {self.name}"
When we run:
person = Person("Juan")
person.greet()
Python automatically passes the instance as the first argument. Conceptually, this is equivalent to:
Person.greet(person)
Local variables versus attributes
This method does not change the attribute:
def change_name(self, name):
name = name
This one does:
def change_name(self, name):
self.name = name
The difference is similar to using name versus
@name in Ruby.
5. Instance attributes
Each instance can have its own values.
class Car:
def __init__(self, brand):
self.brand = brand
car1 = Car("Fiat")
car2 = Car("Ford")
print(car1.brand) # Fiat
print(car2.brand) # Ford
Changing one instance's attribute does not change the other's.
6. Class attributes
In Python, we can declare attributes directly in the class body.
class Car:
wheels = 4
def __init__(self, brand):
self.brand = brand
wheels is a class attribute; self.brand is
an instance attribute.
car1 = Car("Fiat")
car2 = Car("Ford")
print(car1.wheels) # 4
print(car2.wheels) # 4
An important detail
car1.wheels = 6
print(car1.wheels) # 6
print(car2.wheels) # 4
print(Car.wheels) # 4
Why? car1.wheels = 6 creates an instance attribute that
hides the class attribute. But if we change the class attribute
instead:
Car.wheels = 8
print(car1.wheels) # 6
print(car2.wheels) # 8
print(Car.wheels) # 8
car1 keeps using its own attribute, while
car2 finds the updated value on the class.
Comparison with Ruby
Ruby class variables (@@wheels) follow different rules
and can be shared within a class hierarchy. They are not exact
equivalents of Python class attributes.
Watch out for mutable class attributes
class Team:
members = []
team1 = Team()
team2 = Team()
team1.members.append("Juan")
print(team2.members) # ["Juan"]
Both instances access the same list on the class. If we want a
separate list for each instance, we must create it in
__init__:
class Team:
def __init__(self):
self.members = []
7. Instance methods
Instance methods receive self.
class Car:
def __init__(self, brand):
self.brand = brand
def description(self):
return f"I'm a {self.brand}"
car = Car("Fiat")
print(car.description()) # I'm a Fiat
They are useful when we need to work with an individual object's state.
8. Class methods:
@classmethod
Ruby
class Car
def self.type
"Vehicle"
end
end
Car.type
Python
class Car:
wheels = 4
@classmethod
def type(cls):
return f"Vehicle with {cls.wheels} wheels"
print(Car.type())
@classmethod is a decorator. Its first argument,
conventionally named cls, represents the class. That lets
us access class attributes:
cls.wheels
Unlike an instance method, it does not automatically receive an individual object.
9. Static methods:
@staticmethod
A static method does not automatically receive either
self or cls.
class Car:
@staticmethod
def km_to_miles(km):
return km * 0.621371
print(Car.km_to_miles(100))
# 62.1371
It makes sense when the operation needs neither instance state nor class state. In Ruby, a practical alternative is a class method:
class Car
def self.km_to_miles(km)
km * 0.621371
end
end
Method summary
| Kind | Declaration | Automatically supplied first argument |
|---|---|---|
| Instance | def method(self) |
self |
| Class | @classmethod |
cls |
| Static | @staticmethod |
None |
10. Inheritance
Ruby
class Vehicle
def start
"Starting"
end
end
class Car < Vehicle
end
Python
class Vehicle:
def start(self):
return "Starting"
class Car(Vehicle):
pass
car = Car()
print(car.start()) # Starting
In Python, inheritance is declared by putting the parent class in
parentheses. pass is a statement that does nothing. Among
other uses, it lets us leave a class or function body empty.
11. Inheritance and
initializers: super()
Ruby
class Vehicle
def initialize(brand)
@brand = brand
end
end
class Car < Vehicle
def initialize(brand, doors)
super(brand)
@doors = doors
end
end
Python
class Vehicle:
def __init__(self, brand):
self.brand = brand
class Car(Vehicle):
def __init__(self, brand, doors):
super().__init__(brand)
self.doors = doors
car = Car("Fiat", 4)
print(car.brand) # Fiat
print(car.doors) # 4
super().__init__(brand) runs the inherited initializer
according to Python's method resolution order.
What if we omit
super().__init__()?
class Car(Vehicle):
def __init__(self, brand, doors):
self.doors = doors
car = Car("Fiat", 4)
print(car.doors) # 4
print(car.brand) # AttributeError
Here brand was never initialized. The error is:
AttributeError: 'Car' object has no attribute 'brand'
The difference in Ruby
In Ruby, reading an uninitialized instance variable usually returns
nil:
class Car
def initialize(brand)
# We don't assign @brand
end
def brand
@brand
end
end
car = Car.new("Fiat")
p car.brand # nil
In Python, accessing a missing attribute usually raises
AttributeError.
What if the
subclass does not define __init__?
It inherits the parent's initializer:
class Car(Vehicle):
pass
car = Car("Fiat")
print(car.brand) # Fiat
12. Overriding methods
In Ruby:
class Vehicle
def start
"Starting"
end
end
class Car < Vehicle
def start
"#{super} the car"
end
end
In Python:
class Vehicle:
def start(self):
return "Starting"
class Car(Vehicle):
def start(self):
return f"{super().start()} the car"
car = Car()
print(car.start()) # Starting the car
In Ruby, super can call the parent method without naming
it. In Python, we use:
super().start()
Common mistake: infinite recursion
class Car(Vehicle):
def start(self):
return self.start() + " the car"
self.start() calls the same overridden method again.
Eventually, Python raises:
RecursionError: maximum recursion depth exceeded
The fix is to call the inherited method with
super().
13. Encapsulation
Ruby has method visibility controls such as public,
private, and protected. Python mainly uses
naming conventions.
Public attributes
class Person:
def __init__(self, name):
self.name = name
person = Person("Juan")
person.name = "Pedro"
Internal attributes:
_name
class Person:
def __init__(self, name):
self._name = name
person = Person("Juan")
print(person._name) # Juan
A leading underscore indicates that an attribute is intended for internal use. Python does not prohibit access to it, though.
Double underscore:
__name
class Person:
def __init__(self, name):
self.__name = name
Python applies a mechanism called name mangling. Internally,
the attribute is stored under a transformed name such as
_Person__name. This helps avoid accidental collisions,
especially with inheritance, but it is not strict privacy.
14. Properties:
@property
In Ruby we can write:
class Person
attr_reader :name
def initialize(name)
@name = name
end
end
In Python, one alternative is @property:
class Person:
def __init__(self, name):
self._name = name
@property
def name(self):
return self._name
person = Person("Juan")
print(person.name) # Juan
Although name is implemented with a method, we access it
like an attribute.
A property without a setter
person.name = "Pedro"
This raises AttributeError because the property has no
setter.
Adding a setter
class Person:
def __init__(self, name):
self._name = name
@property
def name(self):
return self._name
@name.setter
def name(self, value):
self._name = value
Now we can write:
person = Person("Juan")
person.name = "Pedro"
print(person.name) # Pedro
Validation in setters
@name.setter
def name(self, value):
if not value:
raise ValueError("Name cannot be empty")
self._name = value
This lets us control assignments made through the property.
An important detail
Even if the property is read-only, the underlying attribute may still be writable:
class Person:
def __init__(self, name):
self._name = name
@property
def name(self):
return self._name
person = Person("Juan")
try:
person.name = "Pedro"
except AttributeError:
pass # The property has no setter
person._name = "Pedro" # This works
print(person.name) # Pedro
@property without a setter provides a read-only
interface, but it does not guarantee that the underlying state is
immutable.
15. An introduction to special methods
Python has methods with double underscores at both ends of their names. They are informally called dunder methods.
__str__: similar to
to_s
In Ruby:
class Person
def initialize(name)
@name = name
end
def to_s
"Person: #{@name}"
end
end
puts Person.new("Juan")
In Python:
class Person:
def __init__(self, name):
self.name = name
def __str__(self):
return f"Person: {self.name}"
person = Person("Juan")
print(person) # Person: Juan
Python calls __str__ when we use
str(person) or print(person). Without it, we
usually get a default representation that includes the class name and a
hexadecimal address or identifier.
Other special methods worth learning later include:
__repr__: a representation aimed at developers.__eq__: comparison with==.__len__: behavior oflen(obj).__iter__: iteration support.__getitem__: bracket access.
16. Ruby/Python equivalence summary
| Concept | Ruby | Python |
|---|---|---|
| Define a class | class Person |
class Person: |
| Usual initializer | initialize |
__init__ |
| Create an instance | Person.new |
Person() |
| Instance attribute | @name |
self.name |
| Current object | Implicit self |
Explicit self parameter |
| Class method | def self.method |
@classmethod, cls |
| Static method | Class method without state | @staticmethod |
| Inheritance | Car < Vehicle |
Car(Vehicle) |
| Parent method | super |
super().method() |
| Getter | attr_reader |
@property |
| Setter | attr_writer |
@property_name.setter |
| Text representation | to_s |
__str__ |
| Missing attribute | Reading @variable returns nil |
Reading self.variable raises
AttributeError |
| Reassigned global variable | $variable |
global variable |
17. Mistakes and details to remember
- Python requires an explicit instance parameter, conventionally named
self, in instance methods. self.nameandnameare different things.- Assigning
object.attributecan hide a class attribute without changing it. - Mutable class attributes can accidentally be shared across instances.
- If a subclass defines
__init__, it needs to callsuper().__init__()when it relies on inherited initialization. - Accessing a missing attribute usually raises
AttributeError, notNone. - Calling
self.method()inside that same overridden method can causeRecursionError. - An
_internalattribute remains accessible from outside the class. @propertywithout a setter prevents assignment through the property, but not necessarily changes to the underlying state.- Python needs an explicit
returnto return a value other thanNone.
18. What to study next
This article covers the basics of variables, classes, and objects. Topics for a deeper look include:
__repr__,__eq__, and other special methods.- Equality versus identity:
==andis. - Multiple inheritance and MRO (Method Resolution Order).
- Modules, imports, and packages.
- Decorators beyond
@classmethod,@staticmethod, and@property. - An introduction to Django: models, views, URLs, and the ORM.
Conclusion
For a Ruby developer, Python's object orientation feels familiar. The
differences worth taking to heart are the explicit instance parameter,
the distinction between class and instance attributes, how
super() works, the scope rules, and what happens when an
attribute is missing.
Until next time,
Gastón Ramos
::: If you'd like to comment, email me: ramos.gaston AT gmail.com :::