Abstract Classes & Operator Overloading (ABC, Dunder Methods) - Python Tutorial for Beginners #22
Video: Abstract Classes & Operator Overloading (ABC, Dunder Methods) - Python Tutorial for Beginners #22 by Taught by Celeste AI - AI Coding Coach
Python Abstract Classes and Operator Overloading
from abc import ABC, abstractmethod— define a class that requires subclasses to implement specific methods. Operator overloading via dunder methods:__add__for+,__eq__for==,__lt__for<, etc. Both let you build clean APIs.
This lesson covers two object-oriented techniques: enforcing interfaces via abstract classes, and making your classes feel native via operator overloading.
Abstract base classes
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
def describe(self):
return f"area={self.area():.2f}, perimeter={self.perimeter():.2f}"
class Shape(ABC): — ABC is the abstract base class marker.
@abstractmethod on a method says "subclasses must override this." The base implementation (pass) never runs.
describe is a concrete method — subclasses inherit it without override.
Concrete subclasses
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
def perimeter(self):
return 2 * math.pi * self.radius
Both subclasses implement area and perimeter. They inherit describe from Shape for free.
Cannot instantiate abstract class
try:
s = Shape()
except TypeError as e:
print(e)
# Can't instantiate abstract class Shape with abstract methods area, perimeter
The error says exactly which methods are missing. Same error if a subclass forgets to implement one:
class Triangle(Shape):
def area(self):
return 0.5 * 3 * 4
# forgot perimeter
t = Triangle()
# TypeError: Can't instantiate abstract class Triangle with abstract method perimeter
The check happens at instantiation, not at class definition. So you find out the moment someone tries to create one.
Why use abstract classes?
In Python, duck typing lets you skip them:
# This works without inheritance:
class Square:
def area(self):
return 100
def perimeter(self):
return 40
shapes = [Rectangle(5, 3), Square()] # both have area + perimeter
So why bother with ABC?
- Documentation. Anyone reading
Shape(ABC)knows the contract. - Early failure.
TypeErrorat instantiation, not later whenarea()returnsNone. - Type hints.
def total_area(shapes: list[Shape])is enforced by type checkers. - isinstance checks.
isinstance(x, Shape)is meaningful.
For a small script, skip ABCs. For a library or large project, they're worth the boilerplate.
Abstract properties and class methods
class Animal(ABC):
@property
@abstractmethod
def species(self):
pass
@classmethod
@abstractmethod
def from_string(cls, data):
pass
@abstractmethod stacks with @property and @classmethod. The order matters: @abstractmethod always innermost.
Operator overloading
Define dunder methods to give your class native operator support.
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other):
return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(3, 4)
v2 = Vector(1, 2)
print(v1 + v2) # Vector(4, 6)
print(v1 - v2) # Vector(2, 2)
print(v1 * 3) # Vector(9, 12)
print(v1 == Vector(3, 4)) # True
Python translates operators to method calls:
| Operator | Method |
|---|---|
a + b |
a.__add__(b) |
a - b |
a.__sub__(b) |
a * b |
a.__mul__(b) |
a / b |
a.__truediv__(b) |
a // b |
a.__floordiv__(b) |
a % b |
a.__mod__(b) |
a ** b |
a.__pow__(b) |
a == b |
a.__eq__(b) |
a != b |
a.__ne__(b) |
a < b |
a.__lt__(b) |
a <= b |
a.__le__(b) |
a > b |
a.__gt__(b) |
a >= b |
a.__ge__(b) |
If a.__add__(b) returns NotImplemented (or doesn't exist), Python tries b.__radd__(a). This is how 1 + my_vector works — the int doesn't know how to add a Vector, but Vector's __radd__ does.
len, bool, iter
class Stack:
def __init__(self):
self.items = []
def __len__(self):
return len(self.items)
def __bool__(self):
return len(self.items) > 0
def __iter__(self):
return iter(self.items)
def __contains__(self, item):
return item in self.items
s = Stack()
print(len(s)) # 0
print(bool(s)) # False
if s:
print("not empty")
s.items.extend([1, 2, 3])
for x in s:
print(x) # 1 2 3
print(2 in s) # True
| Special method | Triggers |
|---|---|
__len__ |
len(obj) |
__bool__ |
if obj: (defaults to True) |
__iter__ |
for x in obj: |
__next__ |
next(obj) |
__contains__ |
x in obj |
__getitem__ |
obj[key] |
__setitem__ |
obj[key] = value |
__call__ |
obj(...) |
__hash__ |
hash(obj) (for set/dict keys) |
Implementing these makes your class feel native — it works with len, for, in, and so on.
Functools.total_ordering
Define __eq__ and one comparison (e.g., __lt__), and Python fills in the rest:
from functools import total_ordering
@total_ordering
class Money:
def __init__(self, amount):
self.amount = amount
def __eq__(self, other):
return self.amount == other.amount
def __lt__(self, other):
return self.amount < other.amount
# Now <=, >, >=, != work too
A money class with currency safety
class Money:
def __init__(self, amount, currency="USD"):
self.amount = amount
self.currency = currency
def __str__(self):
return f"${self.amount:.2f} {self.currency}"
def __add__(self, other):
if self.currency != other.currency:
raise ValueError("Cannot add different currencies")
return Money(self.amount + other.amount, self.currency)
def __gt__(self, other):
return self.amount > other.amount
def __ge__(self, other):
return self.amount >= other.amount
price = Money(29.99)
tax = Money(2.40)
total = price + tax
print(total) # $32.39 USD
Operator overloading isn't just for math — anywhere + makes sense semantically (concat strings, merge sets, combine logs), define __add__.
When NOT to overload
- The operator's meaning isn't obvious.
Customer * 5— what's that? - The operation has side effects.
pipe << input(with state mutation) — confusing. - You're being clever. Operator overloading is a tool, not a goal.
The rule: operators should "do what users expect." If you have to explain it, use a method.
Common stumbles
Forgetting __eq__ for hashable. __hash__ is auto-deleted when you define __eq__. To make instances hashable (usable as dict keys, set members), define __hash__ too.
Returning the wrong type. __add__ should return a new instance of the same class — not None, not the operands.
Mutating self in __add__. + should not mutate. Use __iadd__ for in-place (a += b).
Forgetting NotImplemented. If types don't match, return NotImplemented (the singleton) — Python will try the right-hand side's __radd__. Don't return False or raise.
Abstract method without subclass override. Instantiation fails. Make sure every concrete subclass implements every @abstractmethod.
Dunder typo. __init_(self) (one underscore) is just a regular method. Always two underscores on each side.
What's next
Lesson 23: decorators. Functions that wrap functions. The pattern behind @property, @classmethod, @abstractmethod, and lots of framework magic.
Recap
from abc import ABC, abstractmethod — define interfaces that subclasses must implement; instantiation fails if they don't. Operator overloading via dunder methods: __add__, __eq__, __lt__, __len__, __iter__, __bool__. Use functools.total_ordering to derive ordering from __eq__ + __lt__. Operators should match user intuition; if not obvious, use a method instead.
Next lesson: decorators.