Handbooks / Python / Chapter 3

OOP in Python

7 pages · ~5 min✓ Reviewed

Builds on Functions and Closures. Next up: Errors and the Standard Library.

PythonPage 1

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
  • self is just the first parameter, passed automatically
  • d.bark() is Dog.bark(d)
python
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

output
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
  1. Instanceobj.dict
  2. ClassClass.dict
  3. Base classesvia MRO
  4. AttributeErrornot found
PythonPage 2

Instance vs Class Attributes

1. Shared vs per-object

  • Class attribute: in the class body, shared by all
  • Instance attribute: self.x in __init__, per object
  • Reading d.species falls back to the class
  • d.species = 'x' creates an instance attribute that shadows the class one
python
class Dog:
    species = 'canine'
d = Dog()
d.species = 'x'
print(Dog.species, d.species)
output
canine x

2. Method kinds

KindGetsUse
instance methodselfper-object behaviour
@classmethodclsalternative constructor, e.g. from_json
@staticmethodnothingplain 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, then self.items = items or []
Remember

Assigning through an instance never edits the class; mutating a shared list does.

PythonPage 3

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__ or D.mro()
  • class D(B, C), B and C both subclass A: A appears once, after both
  1. D
  2. B
  3. C
  4. A
  5. 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
python
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

output
B
C
A
Forgot super().init(...)

The parent __init__ never runs, so its attributes are unset: AttributeError later.

PythonPage 4

Dunder Methods and Properties

1. repr, str, eq, hash

  • __repr__ = for developers: unambiguous, ideally eval-able
  • __str__ = for users; print falls back to __repr__ if missing
  • __eq__ defines ==
  • __eq__ without __hash__ sets __hash__ to None: instances become unhashable

2. Dunders as Behaviour

DunderEnables
__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

  • @property turns a method into read-only attribute access
  • @x.setter validates on assignment
  • Call site: c.x and c.x = 5, with no ()
python
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

Setter recursion

Write self._x = v. Writing self.x = v inside the setter calls the setter again: infinite recursion.

PythonPage 5

Dataclasses and Abstract Base Classes

1. Dataclasses

  • @dataclass writes init, repr and eq from annotated fields
  • Mutable default tags: list = [] → ValueError; use field(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__
python
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

output
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
ABCPlain base
Checkedat instantiationonly when called
ErrorTypeErrorNotImplementedError
Interview Point

frozen=True? Immutable and hashable. default_factory? Fresh list per instance. ABC vs plain base? ABC fails early, at instantiation.

PythonPage 6

Python OOP Cheat Sheet

1. Create and Look Up

  1. C(args)
  2. newcreates
  3. initsets self.x
  4. obj
  • Lookup: instance → class → MRO → AttributeError
  • self.x = v sets instance state
  • Inspect with vars(obj), C.__mro__, dir(obj)

2. Methods, Properties, Dunders

KindGetsMarker
Instanceselfnone
Classcls@classmethod
Staticnothing@staticmethod
  • @property reads like an attribute; @x.setter validates, stores in self._x
  • __repr__ for developers, __str__ for users
  • __eq__ without __hash__ makes instances unhashable
  • __len__ and __iter__ make objects containers

3. Dataclass and ABC

python
@dataclass(frozen=True, order=True)
class P:
    x: int
    tags: tuple = ()

class A(ABC):
    @abstractmethod
    def run(self): ...
  • field(default_factory=list) for mutable defaults
  • frozen=True blocks reassignment (shallow); hashable only if all fields are
  • Unimplemented abstract method → TypeError at instantiation
Shared mutables

tags = [] in a class body or items=[] as a default is shared. Use None or default_factory.

PythonPage 7

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 = v calls the setter again
  • Store in self._x instead
@x.setter
def x(self, v):
    self.x = v
What does D().hi() print?
  • D B C A, one per line
  • super() 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()