Structure your code with well-designed classes: initialization, instance or class attributes, inheritance and its trade-offs.
Open this lesson in KodokonYou 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.
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 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.
class Basket:
items = []
def add(self, item):
self.items.append(item)
a = Basket()
b = Basket()
a.add("apple")
print(b.items) # ['apple']: shared list!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.
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())