GadaaLabs
Python Mastery — From Zero to AI Engineering
Lesson 5

Object-Oriented Programming — Classes to Protocols

30 min

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.

Class definition and instance attributes
Click Run to execute — Python runs in your browser via WebAssembly

Methods: Instance, Class, and Static

Instance, class, and static methods
Click Run to execute — Python runs in your browser via WebAssembly

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__().

Magic methods: __repr__, arithmetic, comparison, iteration
Click Run to execute — Python runs in your browser via WebAssembly

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.

Iterator protocol: __iter__ and __next__
Click Run to execute — Python runs in your browser via WebAssembly

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.

Properties with @property, @setter, @deleter
Click Run to execute — Python runs in your browser via WebAssembly

Inheritance and super()

Inheritance lets a class reuse and extend another class's behavior. The subclass inherits all methods and attributes of the parent.

Inheritance and super()
Click Run to execute — Python runs in your browser via WebAssembly

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.

Mixins and multiple inheritance
Click Run to execute — Python runs in your browser via WebAssembly

@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.

@dataclass decorator
Click Run to execute — Python runs in your browser via WebAssembly

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.

Abstract base classes
Click Run to execute — Python runs in your browser via WebAssembly

PROJECT: Bank Account System

A full implementation demonstrating classes, inheritance, dataclasses, properties, and magic methods working together.

PROJECT: Bank Account System
Click Run to execute — Python runs in your browser via WebAssembly

Challenge

  1. __slots__ — add __slots__ to the Transaction dataclass and compare memory usage between 100,000 normal Transaction instances vs slotted ones using sys.getsizeof and tracemalloc.

  2. Observable pattern — implement a Subject mixin and Observer ABC so that any class can call self.notify("event", data) and all registered observers receive it. Apply it to BankAccount to notify on every transaction.

  3. Descriptor protocol — implement a Typed descriptor that validates attribute types on assignment:

python
class Person:
    name = Typed(str)
    age  = Typed(int)

Any assignment of the wrong type should raise TypeError.

  1. Generic Stack — implement a type-safe Stack[T] class using Python's typing.Generic. It should support push, pop, peek, is_empty, and __len__. Add an __iter__ that yields from top to bottom.

  2. Protocol instead of ABC — rewrite the Shape hierarchy using typing.Protocol instead of abc.ABC. Demonstrate that any class implementing area() and perimeter() is structurally compatible without inheriting from Shape.

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
  • @classmethod is ideal for alternative constructors; @staticmethod is for utility functions logically grouped with the class but not needing self or cls
  • 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
  • @dataclass eliminates boilerplate for data-holding classes; use frozen=True for immutable, hashable records
  • Prefer Protocol (structural subtyping) over ABC (nominal subtyping) for interfaces between modules you don't fully control