Abstract Base Classes let you define a required interface that Python enforces at instantiation time, not just at type-check time. This lesson covers abc.ABC and @abstractmethod, and the specific situations where that runtime enforcement is worth choosing over a Protocol.
1What ABC + @abstractmethod Actually Enforces
class PaymentGateway(ABC): with methods decorated @abstractmethod defines a class that is deliberately impossible to instantiate on its own ā attempting PaymentGateway() raises TypeError: Can't instantiate abstract class PaymentGateway with abstract methods charge, refund immediately, before any of your own code even runs. This isn't a convention or a lint warning; it's enforced by ABCMeta (the metaclass behind abc.ABC, tying directly back into the Metaclasses lesson), which tracks every method decorated @abstractmethod across the class hierarchy and checks, at the exact moment of instantiation, whether every one of them has been overridden by a concrete implementation.
This is the key distinguishing feature relative to a Protocol: a Protocol violation is caught by mypy or pyright, a *static* check that runs before your code executes and can, in principle, be skipped, suppressed, or simply not run if the checker isn't wired into CI. An ABC violation is caught by the Python interpreter itself, at runtime, unconditionally ā even in a codebase with no type checker configured at all, StripeGateway() missing its refund implementation fails loudly the instant something tries to construct it.
This makes ABC genuinely stronger in one specific, important sense: it guarantees the contract holds in *any* execution of the code, not just in code paths a type checker happened to analyze ā valuable for library authors who can't assume every consumer runs a type checker, and for catching an incomplete implementation the moment it's used, not just when a specific missing method finally gets called.
from abc import ABC, abstractmethod
class PaymentGateway(ABC):
@abstractmethod
def charge(self, amount: float) -> bool: ...
@abstractmethod
def refund(self, amount: float) -> bool: ...
gateway = PaymentGateway() # TypeError: Can't instantiate abstract classTypeError at instantiation ā before any code runs
2Beyond @abstractmethod: Shared Concrete Behavior Too
An ABC isn't limited to pure interface declarations with no implementation ā abstract methods can coexist with fully-implemented concrete methods on the same base class, letting you provide shared behavior alongside an enforced contract. A PaymentGateway base class could implement a concrete log_transaction(self, amount) method used by every subclass, while charge and refund remain abstract and subclass-specific ā combining 'here's behavior you get for free' with 'here's behavior you must provide yourself' in one hierarchy, something a pure Protocol (which carries no implementation at all) cannot do.
This is precisely the shape of many real standard-library and framework base classes: collections.abc.MutableSequence provides concrete implementations of append, extend, pop, and several other methods *derived from* a smaller set of abstract methods (__getitem__, __setitem__, __delitem__, __len__, insert) that a subclass must actually implement ā write five methods, get a dozen more for free, all while ABCMeta guarantees the five required ones are genuinely present before any instance can exist.
This 'minimal required interface, maximal free behavior' pattern is a significant part of why collections.abc is structured the way it is, and it's a design technique worth deliberately reaching for: identify the smallest set of primitive operations a concept genuinely requires, make those abstract, and implement everything else in terms of them as concrete methods on the same base class.
class StripeGateway(PaymentGateway):
def charge(self, amount: float) -> bool:
return True
# forgot to implement refund()!
StripeGateway() # TypeError: Can't instantiate abstract class StripeGateway with abstract method refundEnforced contract + shared implementation, together
3ABC vs Protocol: Choosing Nominal or Structural Typing
The deeper distinction between ABC and Protocol, beyond 'runtime-enforced vs statically-checked', is nominal versus structural typing (introduced in the Type Hints lesson). Subclassing an ABC is an explicit, deliberate act ā class StripeGateway(PaymentGateway): declares a real, permanent relationship in the class hierarchy, discoverable via isinstance() and issubclass() checks that reflect actual inheritance. A Protocol requires no such declaration; any object with matching methods satisfies it, whether or not its author ever knew the Protocol existed.
This makes ABC the better fit when you genuinely want to *own* and control the hierarchy of implementations ā a plugin system where every plugin must explicitly register by subclassing your Plugin base class, or a payment gateway abstraction internal to your own codebase where every implementation is written by your own team and benefits from shared concrete methods. Protocol fits better when you're describing 'anything with this shape' across code you don't control ā third-party classes, standard library types, or test doubles that shouldn't need to import and inherit from your interface just to satisfy it.
Neither is universally 'better' ā they solve different problems. A useful rule of thumb: reach for ABC when you want runtime enforcement and are fine requiring explicit inheritance; reach for Protocol when you want static-only checking that works across code you don't own, including code that predates your interface entirely.
class StripeGateway(PaymentGateway):
def charge(self, amount: float) -> bool:
print(f"Charging ${amount} via Stripe")
return True
def refund(self, amount: float) -> bool:
print(f"Refunding ${amount} via Stripe")
return True
gateway = StripeGateway() # works ā every abstract method is implementedProtocol: implicit, structural, works across foreign code
4Step-by-Step Breakdown
Protocols are checked only by your type checker. ABCs are enforced by the interpreter itself, at the moment you try to instantiate an incomplete subclass ā let's see that guarantee in action.
abc.ABC + @abstractmethod defines a class that CANNOT be instantiated directly, and forces subclasses to implement every abstract method.
Checkpoint: What happens if you try to instantiate PaymentGateway() directly, without subclassing it?
- āTypeError, immediately, because it has unimplemented abstract methods
- āIt works fine, since abstractmethod is just documentation
A subclass that forgets to implement even ONE abstract method still can't be instantiated ā Python catches the gap for you.
Checkpoint: If StripeGateway implements charge() but forgets refund(), when does Python catch that mistake?
- āImmediately, the moment you try to instantiate StripeGateway()
- āOnly when something actually tries to call the missing refund() method
A complete subclass instantiates normally and works as expected ā the ABC guarantees every required method exists BEFORE any code tries to call it.
ABCs enforce a contract through inheritance; Protocols achieve a similar goal structurally ā comparing them directly closes out this section.
Enforce a Real Abstract Class. Finish can_instantiate(): an ABC with an unimplemented abstractmethod can never be instantiated.
Level Up š
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Best Practices
Use ABC when you want runtime-enforced completeness and control the whole hierarchy
A plugin system or an internal interface where every implementation is written in your own codebase benefits from ABCMeta's guarantee that no incomplete implementation can ever be instantiated, by anyone, even without a type checker in the loop.
Combine abstract methods with concrete shared methods to avoid repeating logic across implementations
Following collections.abc's pattern ā a small required interface plus a larger set of methods implemented in terms of it ā lets every subclass get substantial behavior for free while still guaranteeing the required primitives are present.
Frequent Bugs
Defining a base class intended as an interface using plain inheritance and NotImplementedError inside method bodies, instead of ABC + @abstractmethod, so an incomplete subclass only fails when the missing method is actually called ā potentially much later, in production.
Use abc.ABC and @abstractmethod for any interface-like base class ā it moves the failure from "whenever the missing method happens to be called" to "the moment an incomplete subclass is instantiated", catching the bug far earlier.
Real-World Examples
A Plugin Base Class Enforcing a Complete Implementation
A data-export plugin system needs every plugin to implement both export() and validate(), and wants incomplete plugins to fail immediately at load time rather than causing a confusing failure deep in a pipeline run.
from abc import ABC, abstractmethod
class ExportPlugin(ABC):
@abstractmethod
def validate(self, data: dict) -> bool: ...
@abstractmethod
def export(self, data: dict) -> bytes: ...
def run(self, data: dict) -> bytes:
if not self.validate(data):
raise ValueError("Invalid data")
return self.export(data)