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