OOP & the Data Model
Everything in Python is an object
Section titled “Everything in Python is an object”Python is a fully object-oriented language: integers, strings, functions, modules — every value is an object with an identity, a type, and a value.
A class is the blueprint; an object (or instance) is what you create from that blueprint with ClassName(...).
x = 42print(type(x)) # <class 'int'>print(id(x)) # unique memory addressThe special-method protocol
Section titled “The special-method protocol”Python exposes nearly every built-in operation through dunder methods (double-underscore methods like __init__, __repr__, __add__).
When you write a + b, Python calls a.__add__(b).
When you write len(obj), Python calls obj.__len__().
When you write repr(obj), Python calls obj.__repr__().
This protocol is called the Python Data Model — it lets your own classes integrate seamlessly with built-in syntax and standard-library functions.
A minimal class
Section titled “A minimal class”class Point: def __init__(self, x: float, y: float) -> None: self.x = x self.y = y
def __repr__(self) -> str: return f"Point({self.x}, {self.y})"__init__is the initialiser — it runs right after the object is created and sets up instance attributes.__repr__gives the object a developer-friendly string representation.
Classes are objects too: instance → class → type
Section titled “Classes are objects too: instance → class → type”The chain doesn’t stop at “an object has a class”. A class is itself an object, and it has a type too — type(Point) is <class 'type'>. type is the built-in metaclass: the type of every ordinary class, including itself (type(type) is type — the chain terminates there).
flowchart LR p1["p1 (a Point instance)"] -->|"type(p1)"| Point["Point (the class)"] Point -->|"type(Point)"| Type["type (the metaclass)"] Type -->|"type(type)"| Type
This is not a special case for one built-in name — it’s how every class in Python is built, including the ones you write. Point.__mro__ is (Point, object): even a class with no explicit base ultimately inherits from object, the root of every class hierarchy. Later lessons in this module use this chain constantly: isinstance(x, C) walks it, super() walks the MRO it produces for multiple bases, and a metaclass (an advanced, rarely-needed tool) is simply a class whose instances are themselves classes — a type subclass, one level up from where most code ever needs to go.
What this module covers
Section titled “What this module covers”| Lesson | Topic |
|---|---|
| 1. Classes | class, __init__ vs __new__, shared mutable class attributes, MRO/diamond inheritance, isinstance vs type(), duck typing vs ABC/Protocol |
| 2. Dunder methods | __repr__/__str__, __eq__/__hash__, NotImplemented, __iter__/__next__, __call__, __getattr__/__getattribute__ |
| 3. Properties | @property as a descriptor, writing your own descriptor, cached_property, @classmethod returning Self |
| 4. Dataclasses | @dataclass, field, frozen=True, slots=True, order=True, kw_only, dataclass vs NamedTuple/TypedDict/pydantic |
Runnable demo
Section titled “Runnable demo”class Point: def __init__(self, x: float, y: float) -> None: self.x = x self.y = y
def __repr__(self) -> str: return f"Point({self.x}, {self.y})"
def __add__(self, other: "Point") -> "Point": return Point(self.x + other.x, self.y + other.y)
p1 = Point(1, 2)p2 = Point(3, 4)print(p1)print(p1 + p2)print(type(p1))print(isinstance(p1, Point))print(type(Point)) # the class itself is an instance of typeprint(type(type(p1))) # same thing, spelled through the instanceprint(type(type)) # type is its own metaclass -- the chain stops hereprint(Point.__mro__)
class Base: def __init__(self) -> None: self.ready = True print("Base.__init__ ran")
class Broken(Base): def __init__(self, name: str) -> None: self.name = name # forgot super().__init__() -- Base.__init__ never runs
b = Broken("widget")print(b.name)try: print(b.ready)except AttributeError as e: print(f"AttributeError: {e}")Loading Python runtime (first run only)…
Mental Model
Section titled “Mental Model”flowchart TD Call["Point(1, 2)"] --> New["type.__call__ runs Point.__new__(Point) -- creates a bare instance"] New --> Init["Point.__init__(instance, 1, 2) -- fills in the attributes"] Init --> Ready["fully-constructed p1"]
Every mechanism this module covers — attribute lookup, MRO, descriptors, dataclass generation — is a variation on one idea: a class is a namespace of names, an instance is a thinner namespace layered on top of it, and Python always has a defined, inspectable order for resolving a name against both. Point(1, 2) isn’t magic: type.__call__ runs Point.__new__ to produce a bare object, then Point.__init__ to fill it in — the same two-step split the Classes lesson uses to explain singletons and immutable-type subclasses. Keep asking “what object, what type, what order” while reading any class-heavy code, and the rest of the Data Model stops feeling like a list of unrelated dunder methods.
Trade-offs
Section titled “Trade-offs”| What you get | Benefit | Cost |
|---|---|---|
| Everything is an object | One uniform mental model for ints, functions, classes, and modules alike | ”Object” stops meaning “instance of a class I wrote” — it includes things that feel like syntax |
| The Data Model (dunder methods) | Your classes integrate with +, len(), for, with, and every other built-in | Every dunder you implement is a contract you must honor correctly, or built-in syntax misbehaves in confusing ways |
| Deep, uniform object model (instance → class → type) | Consistent rules explain classmethods, metaclasses, and isinstance alike | The metaclass layer is real complexity that’s easy to ignore until something (an ORM, a framework) uses it on you |
Common Misconceptions
Section titled “Common Misconceptions”- Only things you create with
classare “objects”. Integers, strings, functions, modules, and classes themselves are all objects —type(42),type(print), andtype(int)all return a real class. - A class is fundamentally different from the objects it creates. A class is an object too — an instance of
type— which is whytype(Point)is a meaningful, well-defined question with a real answer. - The dunder-method system is a fixed list you either use or don’t. It’s an open protocol — any class can opt in to any subset of it, and Python only calls the methods that are actually needed for the syntax being used.
isinstance,super(), and dataclass generation are unrelated features you memorize separately. They’re all consequences of the same object model:isinstancewalks the class hierarchy,super()walks the MRO, and@dataclassjust writes ordinary methods onto the class using the same rules any hand-written method follows.
Reading AI-generated code
Section titled “Reading AI-generated code”Before trusting a generated class hierarchy at a glance, check for:
- Does a subclass’s
__init__callsuper().__init__(...)? Skipping it is one of the most common generated-code bugs — attributes the parent sets up (including ones other methods rely on) simply never exist on the subclass, and the failure surfaces later as anAttributeErrorfar from the missing call. - Does code compare types with
type(x) == SomeClasswhere anisinstancecheck (respecting subclasses) was clearly intended? - Is a mutable object assigned as a class-level attribute that a method then mutates in place? Every instance shares it.
- Does a comparison dunder (
__eq__) get defined without a matching__hash__, and does the class later get used in a set or as a dict key? - Is there a deep inheritance chain where a
has-arelationship (composition) would be simpler and less fragile than “just one more subclass”?
🐛 Spot the bug
class Connection: def __init__(self, host: str) -> None: self.host = host self.is_open = True
class LoggingConnection(Connection): def __init__(self, host: str, log_prefix: str) -> None: self.log_prefix = log_prefix # forgot super().__init__(host)
conn = LoggingConnection("db.example.com", "[DB]")print(conn.log_prefix)print(conn.is_open)Show the answer
LoggingConnection.__init__ never calls super().__init__(host), so Connection.__init__ never runs — self.host and self.is_open are never set. conn.log_prefix prints fine, but conn.is_open raises AttributeError: 'LoggingConnection' object has no attribute 'is_open', verified on 3.14. This is the single most common multiple-lesson bug in this module: it looks identical to the cooperative-super() chain from the Classes lesson, except the call is missing entirely, so the chain never starts. Fix it by adding super().__init__(host) as the first line of LoggingConnection.__init__.