Classes in Python
A Python class is not a blueprint compiled ahead of time; it is an object
built at runtime by executing a block of code, and everything an interviewer
asks about classes — shared attributes, self, bound methods, the mutable
class-attribute bug — follows from that. This note is the model. The
individual mechanisms (magic methods, descriptors, inheritance, metaclasses)
have their own notes, linked.
The class statement runs code
class Account:
currency = "USD" # runs now, into the class namespace
def __init__(self, owner, balance=0):
self.owner = owner # instance namespace, per object
self.balance = balance
def deposit(self, amount):
self.balance += amount
return self
print(type(Account)) # <class 'type'> - a class is an object
print(Account.__dict__.keys()) # 'currency', '__init__', 'deposit', ...The body executes top to bottom in a fresh namespace; when it finishes,
type(name, bases, namespace) turns that namespace into a class object and
binds the name. That is why decorators and metaclasses can rewrite a class —
it is just a dict until the last step
(Metaclasses).
It is also why a class body is a strange scope: it is not a closure, so a comprehension inside it cannot see class-level names — Comprehension scope.
Two namespaces, one lookup
Every instance has its own __dict__; the class has another. Reading
obj.x looks in the instance first, then the class, then each base in MRO
order. Writing obj.x = v only ever touches the instance dict.
a, b = Account("ann"), Account("bob")
a.currency = "EUR" # creates a.__dict__['currency']; class untouched
print(b.currency) # USD - still reads through to the class
class Bad:
tags = [] # one list, shared by every instance
def add(self, t):
self.tags.append(t) # reads the class list, mutates it in place
x, y = Bad(), Bad()
x.add("a")
print(y.tags) # ['a'] - the classic bugThe asymmetry is the whole story: assignment shadows, mutation shares. A
class attribute is a default; a mutable class attribute is a shared object,
and the fix is to create it in __init__. The full lookup order, including
where descriptors and __getattr__ cut in, is in
Descriptors.
Methods are functions; self is the lookup
A def in a class body is a plain function stored in the class dict. Looking
it up through an instance produces a bound method — the function with the
instance pre-filled as its first argument:
print(Account.deposit) # <function Account.deposit>
print(a.deposit) # <bound method Account.deposit of <Account>>
a.deposit(10) # == Account.deposit(a, 10)
f = a.deposit # binding happens at lookup, so this holds `a`
f(5) # a.balance is now 15self is a convention, not a keyword; it is the argument the binding
supplies. This is why Account.deposit(a, 10) works, why storing a.deposit
in a callback keeps a alive, and why @classmethod and @staticmethod
exist — they change what the binding supplies (the class, or nothing):
Difference Between staticmethod and classmethod in Python.
| Kind | First argument | Use |
|---|---|---|
| instance method | the instance | anything that reads state |
@classmethod |
the class | alternative constructors |
@staticmethod |
none | a function namespaced by the class |
Construction and identity
Account("ann") calls type.__call__, which runs __new__ to allocate and
then __init__ to populate. You override __init__; __new__ is for
immutable types and singletons, and is the thing candidates reach for too
early (Difference Between __init__() and __new__() in Python).
Two objects are the same instance only if is says so; == is whatever
__eq__ says, and defining __eq__ silently removes __hash__ —
__eq__ and __hash__.
__repr__, __eq__, ordering and the rest of the data-model hooks are in
Magic Methods and Attributes in Python.
When to write a class, and when not
The senior signal is knowing the alternatives:
- Data with no behaviour —
@dataclass(orfrozen=True,slots=True) writes__init__,__repr__and__eq__for you and documents the fields. dataclasses — boilerplate-free data classes. - A class with
__init__and one method is a function with extra steps; a closure orfunctools.partialis usually clearer. - A class holding only
@staticmethods is a module. Put the functions in a module. - A fixed set of values —
Enum. Enum vs StrEnum. - A shape other code must satisfy — a
Protocol, not a base class. Interfaces in Python: duck typing, ABCs and Protocols.
A class earns its place when state and the operations on it belong together,
change together, and have invariants worth guarding — an Account that must
never go negative, a connection that must be closed.
Where it breaks in production
- Mutable class attributes. A list or dict on the class is one object
shared by every instance and every test. Initialise it in
__init__. - Attribute typos create attributes.
self.balence = 0is not an error; it is a new field.__slots__or a dataclass withslots=Trueturns it intoAttributeError— __slots__ — when it saves memory and what it breaks. __eq__without__hash__. The instances stop working as dict keys and set members, and the failure is far from the class that caused it.- Bound methods as callbacks.
button.on_click(self.handle)keepsselfalive for as long as the button; a leak that looks like nothing. - Deep hierarchies for reuse. Four levels of base class to share a helper method. Composition or a module-level function; inheritance is for substitutability, not code sharing — Inheritance in Python.
Related
Interview angle 4
- “Class attribute versus instance attribute?” - reads fall through from
the instance dict to the class; writes only ever touch the instance. So a
class attribute is a per-class default, and a mutable one is a shared
object - the list every instance appends to. Fix: create it in
__init__. - “What is
self?” - the instance, supplied by method binding at lookup time.a.deposit(10)isAccount.deposit(a, 10); the name is convention. Follow-up: what does@classmethodchange? The binding supplies the class instead, which is what makescls(...)construct the right subclass. - “When would you not write a class?” - data with no behaviour is a dataclass; a class with one method is a function; a class of static methods is a module; a fixed set of values is an Enum; an interface is a Protocol. A class is for state and operations that share invariants.
- “Why does defining
__eq__break my set?” - Python sets__hash__toNonewhen a class defines__eq__without it, because equal objects must hash equal and it cannot guarantee that for you. Define both, or use a frozen dataclass which does.