Object-Oriented Programming — Classes to Protocols
Why OOP? The Python Take
Object-Oriented Programming organizes code around objects — things that bundle data (attributes) and behavior (methods) together. The four pillars are:
- Encapsulation — hide internal state, expose a clean interface
- Abstraction — present only what callers need to know
- Inheritance — share behavior across related types
- Polymorphism — different objects respond to the same interface
Python's approach is more pragmatic than Java's. There are no truly private attributes (just conventions). Multiple inheritance is supported but should be used carefully. And Python adds something Java lacks: duck typing — if it walks like a duck and quacks like a duck, it is a duck. An object does not need to inherit from a class to be treated as that type; it just needs the right methods.
Class Definition and __init__
A class is a blueprint. An instance is a concrete object built from that blueprint. The __init__ method is called when a new instance is created — it initializes instance attributes.
Methods: Instance, Class, and Static
Magic / Dunder Methods
Magic methods (also called dunder methods, for double underscore) are how Python's operator overloading and protocol system works. When you write a + b, Python calls a.__add__(b). When you write len(x), Python calls x.__len__().
The Iterator Protocol
An object is iterable if it implements __iter__. An object is an iterator if it implements both __iter__ and __next__. The two are often combined.
Properties
Properties replace explicit getter/setter methods with attribute-style access while keeping validation and computation in Python. This is idiomatic Python; writing get_balance() and set_balance() is not.
Inheritance and super()
Inheritance lets a class reuse and extend another class's behavior. The subclass inherits all methods and attributes of the parent.
Multiple Inheritance and Mixins
Python supports multiple inheritance. The correct use of it is the mixin pattern: small, single-purpose classes that add specific behavior, mixed into a concrete class.
@dataclass
The @dataclass decorator auto-generates __init__, __repr__, and __eq__ from annotated class attributes. Use it for data-holding classes that don't need custom constructor logic.
Abstract Base Classes and Protocols
Abstract Base Classes (ABCs) enforce an interface at class definition time. If a subclass does not implement all abstract methods, instantiation raises TypeError.
PROJECT: Bank Account System
A full implementation demonstrating classes, inheritance, dataclasses, properties, and magic methods working together.
Challenge
-
__slots__— add__slots__to theTransactiondataclass and compare memory usage between 100,000 normal Transaction instances vs slotted ones usingsys.getsizeofandtracemalloc. -
Observable pattern — implement a
Subjectmixin andObserverABC so that any class can callself.notify("event", data)and all registered observers receive it. Apply it toBankAccountto notify on every transaction. -
Descriptor protocol — implement a
Typeddescriptor that validates attribute types on assignment:
Any assignment of the wrong type should raise TypeError.
-
Generic Stack — implement a type-safe
Stack[T]class using Python'styping.Generic. It should supportpush,pop,peek,is_empty, and__len__. Add an__iter__that yields from top to bottom. -
Protocol instead of ABC — rewrite the
Shapehierarchy usingtyping.Protocolinstead ofabc.ABC. Demonstrate that any class implementingarea()andperimeter()is structurally compatible without inheriting fromShape.
Key Takeaways
- Instance attributes live in
self.__dict__; class attributes are shared across all instances — mutating a mutable class attribute from one instance affects all instances @classmethodis ideal for alternative constructors;@staticmethodis for utility functions logically grouped with the class but not needingselforcls- Magic methods are how Python's operator and protocol system works — implementing them makes your objects work seamlessly with built-ins and the language itself
- Properties replace Java-style getters/setters with clean attribute-style access while keeping validation logic
super()in Python 3 uses the MRO (C3 linearization) to resolve which parent class's method to call — it handles multiple inheritance correctly- Use mixins for cross-cutting concerns (serialization, logging, validation) — keep them focused and stateless
@dataclasseliminates boilerplate for data-holding classes; usefrozen=Truefor immutable, hashable records- Prefer
Protocol(structural subtyping) over ABC (nominal subtyping) for interfaces between modules you don't fully control