What are Abstract Base Classes in Python, how do you define them, and how do they relate to duck typing?
Abstract Base Classes (ABCs) from the abc module let you declare interfaces with @abstractmethod — any concrete subclass that does not implement all abstract methods raises TypeError at instantiation. ABCs coexist with duck typing: you can register unrelated classes as virtual subclasses without inheritance, and isinstance checks will pass.
How to think about it
At bottom this question asks: how do you enforce a contract in Python without switching to a statically-typed language? Plain duck typing is flexible but silent — you find out a method is missing at runtime, usually at the worst possible moment. Abstract Base Classes move that discovery forward: a class that doesn’t implement the full interface refuses to be instantiated at all.
The machinery is small. ABC is a base class wired to the ABCMeta metaclass; @abstractmethod flags the methods a subclass must provide. The metaclass tracks which abstract methods are still unfilled, and when you try to construct an instance it checks that set first — so the error fires at ClassName(), not three frames deep inside some later method call.
A worked example
from abc import ABC, abstractmethod
# The interface: anything calling itself a DataSource must read() and close()
class DataSource(ABC):
@abstractmethod
def read(self): ...
@abstractmethod
def close(self): ...
# A complete implementation — instantiates fine
class CSVSource(DataSource):
def read(self):
return [{"row": 1}, {"row": 2}]
def close(self):
print("CSVSource closed")
# An incomplete one — close() is missing
class IncompleteSource(DataSource):
def read(self):
return []
src = CSVSource()
print("CSVSource read:", src.read())
src.close()
# The contract is enforced at construction time, not deep in a later call
try:
IncompleteSource()
except TypeError as e:
print("IncompleteSource():", e)
# register(): tell the ABC an unrelated class already satisfies the interface
class LegacySource:
def read(self): return []
def close(self): pass
DataSource.register(LegacySource)
print("LegacySource is DataSource?", isinstance(LegacySource(), DataSource))
CSVSource read: [{'row': 1}, {'row': 2}]
CSVSource closed
IncompleteSource(): Can't instantiate abstract class IncompleteSource with abstract method close
LegacySource is DataSource? True
Two behaviours to take away. IncompleteSource() raised the instant you tried to build it — naming exactly which method was missing — instead of blowing up later when something called .close(). And register() told the ABC to trust LegacySource as a DataSource with no inheritance at all, so isinstance passes. That’s how Python’s own collections.abc recognises list as a Sequence without list ever subclassing it.
Structural matching with subclasshook
For genuinely duck-typed checks — “anything with a callable read” — override __subclasshook__, and isinstance passes automatically, no registration needed:
class Readable(ABC):
@classmethod
def __subclasshook__(cls, subclass):
if cls is Readable:
return hasattr(subclass, "read") and callable(subclass.read)
return NotImplemented