Handbooks / Python / Chapter 3
OOP in Python
7 pages · ~5 min✓ Reviewed
Builds on Functions and Closures. Next up: Errors and the Standard Library.
Classes and the Object Mental Model
1. Class vs Instance
- A class is a blueprint;
Dog('Rex')creates an instance - Classes are objects too: instances of
type __new__creates the object;__init__only initialises it
2. init and self
__init__(self, ...)runs on an already-created object- It must return None
selfis just the first parameter, passed automaticallyd.bark()isDog.bark(d)
class Dog: def __init__(self, name): self.name = name def bark(self): return self.name d = Dog('Rex') print(d.bark() == Dog.bark(d), type(Dog)) print(d.__dict__)
method call = function call with the instance first
True <class 'type'> {'name': 'Rex'}
3. Where Attributes Live and Lookup
- Instance attributes:
obj.__dict__, per object - Class attributes:
Class.__dict__, shared by all instances
- Instanceobj.dict
- ClassClass.dict
- Base classesvia MRO
- AttributeErrornot found
Instance vs Class Attributes
1. Shared vs per-object
- Class attribute: in the class body, shared by all
- Instance attribute:
self.xin__init__, per object - Reading
d.speciesfalls back to the class d.species = 'x'creates an instance attribute that shadows the class one
class Dog: species = 'canine' d = Dog() d.species = 'x' print(Dog.species, d.species)
canine x
2. Method kinds
| Kind | Gets | Use |
|---|---|---|
| instance method | self | per-object behaviour |
@classmethod | cls | alternative constructor, e.g. from_json |
@staticmethod | nothing | plain function, namespaced |
3. Mutable traps
tags = []in the class body: one list shared by every instance- Fix:
self.tags = []in__init__ def __init__(self, items=[]): one list shared across calls- Fix:
items=None, thenself.items = items or []
Assigning through an instance never edits the class; mutating a shared list does.
Inheritance, MRO and super()
1. Inheriting
class Child(Parent):gets all Parent attributes and methods- Override a method by redefining it in Child
isinstance(c, Parent)is True for a Child instance
2. MRO and the diamond
- MRO = method search order, built by C3 linearisation
- View it:
D.__mro__orD.mro() class D(B, C), B and C both subclass A: A appears once, after both
- D
- B
- C
- A
- object
3. super() and mistakes
super()follows the MRO of type(self), not just the parent- So in B,
super()can reach C, the next class in line
class A: def hi(self): print("A") class B(A): def hi(self): print("B"); super().hi() class C(A): def hi(self): print("C"); super().hi() class D(B, C): pass D().hi()
B's super() calls C, not A
B C A
The parent __init__ never runs, so its attributes are unset: AttributeError later.
Dunder Methods and Properties
1. repr, str, eq, hash
__repr__= for developers: unambiguous, ideally eval-able__str__= for users;printfalls back to__repr__if missing__eq__defines==__eq__without__hash__sets__hash__to None: instances become unhashable
2. Dunders as Behaviour
| Dunder | Enables |
|---|---|
__len__ | len(x) |
__getitem__ | x[i] |
__iter__ | for a in x |
__contains__ | a in x |
__call__ | x() like a function |
__add__ | a + b |
__lt__ | a < b |
3. Properties
@propertyturns a method into read-only attribute access@x.settervalidates on assignment- Call site:
c.xandc.x = 5, with no ()
class C: @property def x(self): return self._x @x.setter def x(self, v): if v < 0: raise ValueError self._x = v
store into the private name _x
Write self._x = v. Writing self.x = v inside the setter calls the setter again: infinite recursion.
Dataclasses and Abstract Base Classes
1. Dataclasses
@dataclasswrites init, repr and eq from annotated fields- Mutable default
tags: list = []→ ValueError; usefield(default_factory=list) - A field with a default must come after those without, else TypeError
frozen=True: immutable and hashable;order=True: adds<>;__post_init__runs after__init__
from dataclasses import dataclass, field @dataclass class P: x: int y: int = 0 tags: list = field(default_factory=list) print(P(1)) print(P(1) == P(1))
each instance gets its own list
P(x=1, y=0, tags=[]) True
2. Abstract Base Classes
class Shape(ABC)with@abstractmethod: a subclass must override every abstract method- Otherwise TypeError when you instantiate it
| ABC | Plain base | |
|---|---|---|
| Checked | at instantiation | only when called |
| Error | TypeError | NotImplementedError |
frozen=True? Immutable and hashable. default_factory? Fresh list per instance. ABC vs plain base? ABC fails early, at instantiation.
Python OOP Cheat Sheet
1. Create and Look Up
- C(args)
- newcreates
- initsets self.x
- obj
- Lookup: instance → class → MRO → AttributeError
self.x = vsets instance state- Inspect with
vars(obj),C.__mro__,dir(obj)
2. Methods, Properties, Dunders
| Kind | Gets | Marker |
|---|---|---|
| Instance | self | none |
| Class | cls | @classmethod |
| Static | nothing | @staticmethod |
@propertyreads like an attribute;@x.settervalidates, stores inself._x__repr__for developers,__str__for users__eq__without__hash__makes instances unhashable__len__and__iter__make objects containers
3. Dataclass and ABC
@dataclass(frozen=True, order=True) class P: x: int tags: tuple = () class A(ABC): @abstractmethod def run(self): ...
field(default_factory=list)for mutable defaultsfrozen=Trueblocks reassignment (shallow); hashable only if all fields are- Unimplemented abstract method → TypeError at instantiation
tags = [] in a class body or items=[] as a default is shared. Use None or default_factory.
Check yourself
What does print(b.items) show?
[1]: the list lives on the class, shared by all instances- Fix:
self.items = []in__init__
class Bag: items = [] a, b = Bag(), Bag() a.items.append(1) print(b.items)
Why does assigning t.x = 5 crash?
RecursionError:self.x = vcalls the setter again- Store in
self._xinstead
@x.setter
def x(self, v):
self.x = vWhat does D().hi() print?
D B C A, one per linesuper()follows the MRO of D, not just the parent- B's
super()reaches C, not A
class A: def hi(self): print('A') class B(A): def hi(self): print('B'); super().hi() class C(A): def hi(self): print('C'); super().hi() class D(B, C): def hi(self): print('D'); super().hi() D().hi()