Gastón Ramos

Personal blog

← Back to the English articles

Leer este artículo en español

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 of len(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

  1. Python requires an explicit instance parameter, conventionally named self, in instance methods.
  2. self.name and name are different things.
  3. Assigning object.attribute can hide a class attribute without changing it.
  4. Mutable class attributes can accidentally be shared across instances.
  5. If a subclass defines __init__, it needs to call super().__init__() when it relies on inherited initialization.
  6. Accessing a missing attribute usually raises AttributeError, not None.
  7. Calling self.method() inside that same overridden method can cause RecursionError.
  8. An _internal attribute remains accessible from outside the class.
  9. @property without a setter prevents assignment through the property, but not necessarily changes to the underlying state.
  10. Python needs an explicit return to return a value other than None.

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: == and is.
  • 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 :::