Python OOP Advanced
Tutorial 65 of 65 · pythondeck.com Python course
Advanced OOP includes properties, slots, descriptors, metaclasses and abstract base classes. Use them sparingly - prefer simple classes and composition - but they are powerful when you need them.
Advanced OOP in Python means protocols, descriptors, dataclasses, ABCs, and composition over inheritance—using the language’s dynamic features without Java ceremony.
These tools model domain concepts clearly and integrate with type checkers and frameworks that introspect classes.
Python is not Java: patterns that fight the language—deep inheritance trees, excessive getters—usually lose to plain functions and well-named modules.
dataclasses — boilerplate-free data carriers with optional frozen/slots.
ABC / Protocol — structural subtyping (typing.Protocol) vs nominal ABCs.
descriptors — __get__/__set__ powering properties and ORM fields.
__slots__ — memory savings when millions of instances exist.
context managers — __enter__/__exit__ for resource safety.
metaclasses — rare; prefer class decorators or __init_subclass__.
Python favors duck typing: if it quacks (has the right methods), use it—Protocols document that contract for mypy. Descriptors explain how @property and SQLAlchemy columns work. Multiple inheritance uses MRO (C3 linearization); mixins should be small and document required methods.
Favor composition: pass collaborators in __init__ instead of deep hierarchies. Use enums and NamedTuple for closed sets of constants. When ORMs need magic, isolate it behind repository interfaces testable without a database.
Single dispatch (functools.singledispatch) offers function-based polymorphism without subclass explosion for variant types.
Match protocols to real usage: Iterable for loops, SupportsIndex for custom sequences.
Overusing inheritance where a simple function or dict would suffice.
Metaclass magic that confuses readers and static analyzers.
Mutable default dataclass fields without default_factory.
Calling super() incorrectly in diamond inheritance hierarchies.
Reach for @dataclass(frozen=True, slots=True) for value objects.
Define Protocols for plug-in points; implement explicitly in tests with fakes.
Keep __repr__ and __eq__ meaningful for debugging and collections.
Reserve metaclasses for frameworks; application code rarely needs them.
Run mypy or pyright on public modules; Protocols document extension points for contributors.
Re-read the examples below with these ideas in mind; change variable names and inputs to match your own project.
The program below demonstrates @property. Read the comments on each line, run the code, then change names or values to see how the output shifts.
# Example: @property
# Run in the REPL or save as a .py file and execute with python.
class Circle:
def __init__(self, r):
self._r = r
@property
def area(self):
import math
return math.pi * self._r ** 2
print(Circle(3).area)
This sample walks through __slots__ in a small, runnable script. Paste it into the REPL or save it as a .py file before you continue to the next block.
# Example: __slots__
# Run in the REPL or save as a .py file and execute with python.
class Point:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x, self.y = x, y
p = Point(1, 2)
print(p.x, p.y)
# p.z = 3 # AttributeError
Here is a hands-on illustration of metaclass. Follow the inline comments first; only then execute the snippet and compare the result with what you expected.
# Example: Metaclass
# Run in the REPL or save as a .py file and execute with python.
class Auto(type):
def __new__(mcs, name, bases, ns):
ns["created_by"] = "meta"
return super().__new__(mcs, name, bases, ns)
class C(metaclass=Auto): ...
print(C.created_by)
The program below demonstrates property setter. Read the comments on each line, run the code, then change names or values to see how the output shifts.
# @property adds controlled attribute access
class Temperature: # model
def __init__(self, c): # store Celsius internally
self._c = c # private field
@property # getter
def celsius(self): # read Celsius
return self._c # return stored
@celsius.setter # setter
def celsius(self, value): # validate on write
if value < -273.15: # absolute zero guard
raise ValueError("too cold") # reject
self._c = value # assign
t = Temperature(20) # construct
print(t.celsius) # 20
This sample walks through abstract base in a small, runnable script. Paste it into the REPL or save it as a .py file before you continue to the next block.
# abc.ABC marks interfaces with required methods
from abc import ABC, abstractmethod # abstract base helpers
class Repo(ABC): # interface
@abstractmethod # must override
def get(self, key: str): # contract
... # no implementation
class MemoryRepo(Repo): # concrete impl
def __init__(self): # storage
self.data = {} # dict backend
def get(self, key): # fulfill contract
return self.data.get(key) # lookup
r = MemoryRepo(); r.data["x"] = 1 # populate
print(r.get("x")) # 1
Continue with these focused follow-up lessons on Python OOP Advanced: