Python OOP Track

Learning Objectives

Use classes, self, __init__, class/static methods, class vs instance variables and properties.

Understand inheritance, MRO and super(), duck typing, ABCs and protocols.

Use dataclasses, descriptors, dunder methods, __slots__, identity/equality/hash and context managers.

Prerequisites: Stages 1–2.

Intuition and Definition

In Python everything is an object, including classes and functions. Attributes are looked up dynamically (instance → class → bases along the MRO). Privacy is convention. Polymorphism is duck typing — "if it has read(), it's readable" — optionally checked statically with typing.Protocol.

__new__ (allocate), __init__ (initialize) Instance vs class data self.x vs class body attributes instance, @classmethod (gets cls), @staticmethod @property, setters, descriptors

abc.ABC + @abstractmethod, typing.Protocol (structural) @dataclass(frozen=True), NamedTuple dunder methods (__eq__, __lt__, __add__, __iter__, …) context managers (__enter__/__exit__, contextlib) reference counting + cycle GC; __slots__ to drop the per-instance __dict__

Why It Matters

Python is the most popular language for scripting, data, ML and tooling. Its object model is flexible but different enough from C++/Java to surprise.

Syntax and Language Notes

from typing import Protocol
class Readable(Protocol):
    def read(self) -> float: ...
def show(r: Readable) -> None: print(r.read())   # any object with read() type-checks

Complete Example

from abc import ABC, abstractmethod
from contextlib import contextmanager
from dataclasses import dataclass, field
from typing import Protocol
import sys

class Sensor(ABC):
    count = 0                                      # class variable (shared)
    def __init__(self, sid: str):
        self.sid = sid                             # instance variable
        Sensor.count += 1
    @abstractmethod
    def read(self) -> float: ...
    @classmethod
    def from_config(cls, line: str):               # alternative constructor
        return cls(line.strip())
    @staticmethod
    def c_to_f(c: float) -> float:
        return c * 9 / 5 + 32

class Coolant(Sensor):
    def __init__(self, sid, offset=0.0):
        super().__init__(sid)
        self._offset = offset
    @property
    def offset(self): return self._offset
    @offset.setter
    def offset(self, v):
        if abs(v) > 5: raise ValueError("offset out of range")
        self._offset = v
    def read(self): return 88.5 + self._offset

@dataclass(frozen=True, order=True)
class Reading:
    celsius: float
    sid: str = field(compare=False)

class Slotted:
    __slots__ = ("a", "b")
    def __init__(self): self.a, self.b = 1, 2
…
coolant 90.0 F: 194.0 count: 1
ValueError: offset out of range
equal ignoring sid: True | hash equal: True | sorted: [Reading(celsius=90.0, sid='a'), Reading(celsius=95, sid='x')]
has __dict__: True False
is vs ==: True False
  > structural typing works
enter extended
  working in extended
exit extended

Step-by-Step Walkthrough

from_config is a classmethod — it builds an instance of whatever class it's called on (Coolant), passing through __init__, which calls super().__init__.

The offset property validates on assignment; count is a class variable shared by all sensors.

The frozen, ordered dataclass compares on celsius only (sid excluded), and equal objects hash equally.

__slots__ removes the per-instance __dict__ (less memory, no ad-hoc attributes).

== compares values, is compares identity.

ConsolePrinter satisfies the Printer protocol without inheriting from it.

The context manager's finally runs on exit, even if the body raises.

Common Mistakes

Mutable default arguments (def f(items=[])) — shared between calls.

Mutable class attributes used as per-instance data.

Forgetting super().__init__() in subclasses.

Defining __eq__ without __hash__ — instances become unhashable (Python sets __hash__ = None).

Relying on __del__ for cleanup — use context managers.

Treating _private as protection.

Trade-offs and Alternatives

Python favours small classes, functions and duck typing; many patterns become simpler (strategy = function, iterator = generator, decorator = @decorator). Use type hints + a type checker (mypy, pyright) for larger codebases.

Real-World Use

Django/Flask/FastAPI applications, data pipelines, test frameworks (pytest), automation of CAN/diagnostic tools (python-can, udsoncan), ML frameworks' module classes (PyTorch nn.Module).

Practice

Q1. (Easy) What does @classmethod receive as its first argument?

Answer: The class (cls).

Q2. (Medium) Why does def add(x, items=[]) misbehave?

Answer: The default list is created once when the function is defined and shared across calls; use items=None and create a new list inside.

Q3. (Advanced) What is the MRO of class D(B, C) where B and C derive from A?

Answer: D, B, C, A, object (C3 linearization), so super() in B continues to C, not directly to A.

Interview Question

Q1. "Explain Python's attribute lookup for obj.x." Answer: Python first checks data descriptors on the type (e.g. properties) along the MRO, then the instance's __dict__, then non-data descriptors and class attributes along the MRO, and finally __getattr__ if defined. This is why a @property takes precedence over an instance attribute with the same name, and why class attributes act as defaults for instances.

Quick Quiz

Q1. What does __slots__ remove?

Answer: The per-instance __dict__ (and __weakref__ unless listed).

Q2. Structural interface type in Python?

Answer: typing.Protocol.

Q3. What runs a context manager's cleanup?

Answer: __exit__ (or the finally in a @contextmanager generator).

Summary

Python OOP is dynamic: duck typing, MRO-based lookup, properties/descriptors, dunder methods, dataclasses, protocols.

Related: Modules 11, 16, 57, 58, 82.