The shape
__init__ runs on an object that has already been created — its job is to set it up, not to make it. There is no new, and there is no declaration of which attributes exist: assigning self.x creates it.
class Cart:
currency = "USD" # CLASS attribute — one, shared by all carts
def __init__(self, owner): # not a constructor: the object already exists
self.owner = owner # INSTANCE attributes — one set per cart
self.items = []
def add(self, name, price):
self.items.append((name, price))
return self # returning self allows chaining
def total(self):
return sum(price for _, price in self.items)
cart = Cart("ada")
cart.add("door", 1200).add("handle", 90)
cart.total() # 1290
self is not magic
cart.total() looks the method up on the class and calls it with cart as the first argument. self is just the name everybody gives that parameter — the language does not require it, and using any other name will confuse every reader you have.
That also explains the rule people find strange: every method must declare self, because the call always passes it.
cart.total() # what you write
Cart.total(cart) # what actually happens
The shared-attribute trap
A class attribute is created once, when the class statement runs — the same rule as a default argument, and the same bug. Mutable state belongs in __init__; class attributes are for constants and defaults that never change.
Reading self.currency finds the class attribute; writing self.currency = "EUR" creates an instance attribute that shadows it, leaving every other object untouched.
class Cart:
items = [] # ONE list, shared by every Cart ever made
a, b = Cart(), Cart()
a.items.append("door")
b.items # ['door'] ← the same list
class Cart:
def __init__(self):
self.items = [] # a new list per cart
Three kinds of method
@classmethod receives the class, which is how you write alternative constructors that still work in a subclass. @property turns a method into an attribute access, which is why Python has no getters and setters: start with a plain attribute, and turn it into a property the day it needs logic, without changing a single caller.
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
def describe(self): # instance method — needs an object
return f"{self.celsius}°C"
@classmethod
def from_fahrenheit(cls, f): # alternative constructor
return cls((f - 32) * 5 / 9)
@staticmethod
def is_freezing(celsius): # related, but needs nothing
return celsius <= 0
@property
def fahrenheit(self): # computed, used like an attribute
return self.celsius * 9 / 5 + 32
t = Temperature.from_fahrenheit(212)
t.fahrenheit # 212.0 — no parentheses
Inheritance, sparingly
Python has multiple inheritance and a defined order (the MRO, visible as Cls.__mro__), but deep hierarchies are almost always the wrong answer. Prefer composition — an object that has a pricing strategy is easier to change than one that is a subclass of three things.
And because Python is duck-typed, you do not need a common base class to treat two objects the same way. They only need the same methods.
class Door:
def price(self):
return 1000
class ArmouredDoor(Door):
def price(self):
return super().price() + 800 # super() finds the next class along