Kodokon kodokon.com

OOP: classes, __init__, methods and inheritance

Structure your code with well-designed classes: initialization, instance or class attributes, inheritance and its trade-offs.

10 min · 3 questions

Open this lesson in Kodokon

You work with Python objects all the time: strings, lists, files. Writing your own classes means bundling state (the attributes) and behavior (the methods) behind a coherent interface. A vocabulary point that is often misunderstood: __init__ is not a constructor. The object already exists (created by __new__) by the time __init__ receives self; its job is to initialize the instance. And self has nothing magical about it: it is the first parameter of every instance method, which Python passes automatically at call time.

PYTHON
class Account:
    def __init__(self, owner, balance=0.0):
        self.owner = owner
        self.balance = balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("amount must be > 0")
        self.balance += amount
        return self.balance

    def __repr__(self):
        return f"Account({self.owner!r}, {self.balance})"

account = Account("Ada")
account.deposit(120.0)
print(account)
A complete class: __init__, a business method and __repr__

A crucial distinction in production: class attributes are declared in the body of the class and shared by every instance; instance attributes are created on self, usually in __init__. When reading, Python looks first on the instance, then falls back to the class, then to the parent classes. An assignment self.x = ... always creates an instance attribute, which shadows the class attribute of the same name. The classic real-world trap: a mutable class attribute.

PYTHON
class Basket:
    items = []

    def add(self, item):
        self.items.append(item)

a = Basket()
b = Basket()
a.add("apple")
print(b.items)  # ['apple']: shared list!
The mutable class attribute trap

Inheritance expresses an "is a" relationship: a Car is a Vehicle. The subclass inherits the methods, can override them, and super() lets it call the parent version - indispensable in __init__ so you don't short-circuit the parent's initialization. Python resolves calls by following the MRO (Method Resolution Order). Don't reach for inheritance systematically: to reuse code without an "is a" relationship, prefer composition (one object owns another). Deep hierarchies are fragile: each parent class becomes an implicit API for all of its children.

PYTHON
class Vehicle:
    def __init__(self, brand):
        self.brand = brand

    def describe(self):
        return self.brand

class Car(Vehicle):
    def __init__(self, brand, doors):
        super().__init__(brand)
        self.doors = doors

    def describe(self):
        base = super().describe()
        return f"{base}, {self.doors} doors"

print(Car("Aster", 5).describe())
Inheritance and super(): extend without rewriting

Knowledge check

Make sure you remember the key points of this lesson.

  1. What exactly is the role of __init__?
    • To create the object in memory
    • To initialize an already-created instance
    • To declare the class attributes
    • To be called on every attribute access
  2. A class declares items = [] in its body. After a.add(1), which runs self.items.append(1), what does another instance b see?
    • b.items is an empty list
    • b.items contains 1: the list is shared
    • Python raises an AttributeError
  3. In Car.__init__, what is the point of the call super().__init__(brand)?
    • To create a second Vehicle instance
    • To run the initialization defined in the parent class
    • To copy Vehicle's methods into Car
    • To check that Car really inherits from Vehicle