Abstract Base Classes and Interfaces
Enforcing a contract across subclasses with the abc module — how larger Python codebases stay consistent.
What Happens Without Any Enforced Contract
Imagine a base class PaymentProcessor that every payment provider (Stripe, PayPal, a bank transfer integration) is meant to subclass, each implementing its own charge() method. Nothing in plain Python stops someone from writing a subclass that simply forgets to implement charge() — the mistake is only discovered at runtime, the moment the missing method is actually called, potentially in production.
class PaymentProcessor:
def charge(self, amount):
raise NotImplementedError
class StripeProcessor(PaymentProcessor):
def charge(self, amount):
return f"Charged ${amount} via Stripe"
class PayPalProcessor(PaymentProcessor):
pass # oops — forgot to override charge()! Nothing stops this from being defined.
p = PayPalProcessor()
p.charge(50) # NotImplementedError — but only discovered when this line actually runsThe raise NotImplementedError pattern communicates intent to a human reader, but it provides zero enforcement — PayPalProcessor was allowed to be defined, instantiated, and passed around the codebase perfectly normally, with the bug lying dormant until the exact line that calls charge() executes. Abstract base classes fix exactly this: they move the failure from "runtime, whenever this method happens to be called" to "the moment the incomplete subclass is instantiated at all."
ABC and @abstractmethod
Python's standard library abc module provides ABC (a base class to inherit from) and the @abstractmethod decorator, which together turn the informal NotImplementedError pattern into something the interpreter actively enforces.
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@abstractmethod
def charge(self, amount):
...
class StripeProcessor(PaymentProcessor):
def charge(self, amount):
return f"Charged ${amount} via Stripe"
class PayPalProcessor(PaymentProcessor):
pass # forgot to override charge()
s = StripeProcessor() # fine — every abstract method is implemented
p = PayPalProcessor() # TypeError, IMMEDIATELY, at instantiation:
# TypeError: Can't instantiate abstract class PayPalProcessor with abstract method chargeThe critical difference: PayPalProcessor can no longer even be constructed until every abstract method inherited from PaymentProcessor has a real implementation. The bug is caught the instant the incomplete class is used, not buried until the specific missing method happens to be called — which, for a rarely-exercised code path, could otherwise take months to surface.
An ABC itself can never be instantiated directly, even with every method defined
pp = PaymentProcessor()
# TypeError: Can't instantiate abstract class PaymentProcessor with abstract method charge
# This fails even though PaymentProcessor "defines" charge — because it's abstractAn ABC Can Require More Than Just Methods
An abstract base class can require multiple abstract methods, and even abstract properties — any subclass must satisfy every one of them before it becomes instantiable.
from abc import ABC, abstractmethod
class Shape(ABC):
@property
@abstractmethod
def area(self):
...
@abstractmethod
def perimeter(self):
...
class Rectangle(Shape):
def __init__(self, width, height):
self.width, self.height = width, height
@property
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
# Circle(Shape) that only implements area(), forgetting perimeter(),
# would still raise TypeError at instantiation — EVERY abstract member must be covered.Two Different Philosophies, Both Native to Python
Python is famous for duck typing — "if it walks like a duck and quacks like a duck, it's a duck": code that calls obj.quack() does not care what class obj actually is, only that it has a quack method. ABCs might look like they contradict this philosophy by introducing formal, enforced contracts — but they coexist deliberately, for different situations.
def make_it_quack(duck):
return duck.quack() # works on ANY object with a quack() method, no inheritance required
class RealDuck:
def quack(self): return "Quack!"
class ToyDuck:
def quack(self): return "Squeak-quack (it's a toy)"
make_it_quack(RealDuck()) # works
make_it_quack(ToyDuck()) # also works — no shared base class needed at allDuck typing is the right default for most everyday Python — it is flexible and requires no upfront ceremony. ABCs earn their place specifically when you want the interpreter to actively enforce that a family of related classes all implement a required set of methods, and to fail loudly and immediately (at instantiation) if one does not — most valuable in plugin-style architectures, or any codebase where several people implement subclasses of a shared base independently over time.
A brief look ahead — typing.Protocol
Python's typing module offers a third option, Protocol, which combines aspects of both: it lets you describe a required "shape" (structural typing) without requiring explicit inheritance from anything — an object satisfies a Protocol just by having the right methods, similar to duck typing, but the shape can still be checked by a type checker like mypy ahead of time. This is covered properly in the later Type Hints and Static Typing module.
A Plugin System at a Chicago Data Platform Company
A platform needs to support pulling data from many different sources — S3, a REST API, a local CSV drop folder — with new source types added by different engineers over time as the company grows. The team defines an ABC to guarantee every source, whoever writes it, exposes a consistent interface the rest of the pipeline can rely on.
from abc import ABC, abstractmethod
class DataSource(ABC):
@abstractmethod
def connect(self):
...
@abstractmethod
def fetch_records(self):
...
@abstractmethod
def close(self):
...
class S3Source(DataSource):
def connect(self):
self.client = build_s3_client()
def fetch_records(self):
return self.client.list_objects()
def close(self):
self.client = None
# A new engineer adds RestApiSource(DataSource) but forgets close() —
# TypeError at instantiation, caught in code review / CI, not in productionWhy this mattered as the team grew
The pipeline code that consumes any DataSource — running source.connect(), then source.fetch_records(), then always source.close() — can rely completely on every source implementing all three methods, without ever needing to check with hasattr() or wrap calls in try/except AttributeError. As more engineers added new source types over the following year, several genuinely did forget one method during initial development — and every single time, the mistake was caught immediately by a failing test that tried to instantiate the class, well before the code reached production.
Four Misconceptions About Abstract Base Classes
5 Interview Questions — With Complete Answers
ABC Mistakes Beginners Make Constantly
Errors You Will Hit With Abstract Base Classes — And Exactly Why
🎯 Key Takeaways
- ✓raise NotImplementedError alone only fails when the method is actually called; abc.ABC + @abstractmethod fails immediately at instantiation, catching incomplete subclasses far earlier.
- ✓A class inheriting from ABC cannot be instantiated until every @abstractmethod (and abstract @property) it declares has a concrete override in the subclass.
- ✓The ABC itself can never be instantiated directly, even if all its abstract methods happen to have implementations — abstractness is a property of the class, not of whether the bodies are filled in.
- ✓ABCs and duck typing coexist deliberately — duck typing for flexible everyday code, ABCs for enforced contracts across plugin-style or multi-team class families.
- ✓ABCs check WHICH methods exist, not their signatures — for real signature-level checking, pair them with type hints and mypy (covered in a later module).
- ✓typing.Protocol offers a structural-typing alternative that does not require explicit inheritance — worth knowing about even before its full coverage later in this track.
What comes next
Module 25 begins the Intermediate & Functional Python phase with a deep dive into *args and **kwargs — every way Python lets you pass arguments to a function.
Module 25 → *args, **kwargs and Function Arguments Deep DiveDiscussion
0Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.