Two ways to describe “what an object must be able to do”: ABCs check it by inheritance (nominal typing), Protocols by shape (structural typing — static duck typing).
abc.ABC and @abstractmethod
An abstract base class can't be instantiated while it has abstract methods. Subclasses must implement every one of them — the error appears when you create an object, not when you call the missing method.
from abc import ABC, abstractmethod
class Exporter(ABC):
"""Template method pattern: export() fixes the algorithm, subclasses fill in the steps."""
def export(self, rows):
return [self.header()] + [self.format_row(r) for r in rows]
@abstractmethod
def header(self) -> str: ...
@abstractmethod
def format_row(self, row) -> str: ...
class CsvExporter(Exporter):
def header(self):
return "name,score"
def format_row(self, row):
return f"{row[0]},{row[1]}"
class Broken(Exporter):
def header(self):
return "x"
print(CsvExporter().export([("asha", 91), ("ravi", 84)])) # ['name,score', 'asha,91', 'ravi,84']
try:
Broken()
except TypeError as e:
print(e) # Can't instantiate abstract class Broken without an implementation for abstract method 'format_row'
print(sorted(Exporter.__abstractmethods__)) # ['format_row', 'header']Abstract properties & class methods
from abc import ABC, abstractmethod
class Shape(ABC):
@property
@abstractmethod # @abstractmethod must be the innermost decorator
def sides(self) -> int: ...
@classmethod
@abstractmethod
def unit(cls) -> "Shape": ...
def describe(self): # ABCs can hold ordinary, shared methods too
return f"{type(self).__name__} with {self.sides} sides"
class Triangle(Shape):
sides = 3 # a plain class attribute satisfies the abstract property
@classmethod
def unit(cls):
return cls()
print(Triangle.unit().describe()) # Triangle with 3 sidescollections.abc: get methods for free
The ABCs in collections.abc are mixins: implement the few abstract methods and inherit the rest. Implement __getitem__ + __len__ and Sequence gives you in, iteration, reversed, index and count.
from collections.abc import Sequence
class Countdown(Sequence):
def __init__(self, n):
self.n = n
def __len__(self):
return self.n
def __getitem__(self, i):
if isinstance(i, slice):
return [self[j] for j in range(*i.indices(len(self)))]
if i < 0:
i += self.n
if not 0 <= i < self.n:
raise IndexError(i)
return self.n - i
c = Countdown(5)
print(list(c), c[-1], c[1:3]) # [5, 4, 3, 2, 1] 1 [4, 3]
print(3 in c, c.index(2), c.count(9)) # True 3 0
print(list(reversed(c))) # [1, 2, 3, 4, 5]| ABC | You implement | You get for free |
|---|---|---|
Iterable | __iter__ | — |
Iterator | __next__ | __iter__ |
Sequence | __getitem__, __len__ | __contains__, __iter__, __reversed__, index, count |
MutableSequence | + __setitem__, __delitem__, insert | append, extend, pop, remove, +=… |
Mapping | __getitem__, __iter__, __len__ | get, keys, items, values, ==, in |
MutableMapping | + __setitem__, __delitem__ | pop, setdefault, update, clear… |
Set | __contains__, __iter__, __len__ | <=, |, &, -, ^, isdisjoint |
isinstance with ABCs, register & __subclasshook__
from collections.abc import Iterable, Mapping, Sized, Hashable, Callable
print(isinstance([1], Iterable), isinstance({}, Mapping)) # True True
print(isinstance("abc", Hashable), isinstance([], Hashable)) # True False
class Bag: # doesn't inherit from Sized...
def __len__(self):
return 0
print(isinstance(Bag(), Sized), isinstance(len, Callable)) # True TrueSome ABCs (Sized, Iterable, Hashable, Callable…) check structurally through __subclasshook__: any class with the right method counts. You can also declare a virtual subclass with register.
from abc import ABC, abstractmethod
import io
class Drawable(ABC):
pass
class LegacyWidget: # third-party class you can't modify
def draw(self):
return "drawn"
Drawable.register(LegacyWidget) # a *virtual* subclass
print(isinstance(LegacyWidget(), Drawable), Drawable in LegacyWidget.__mro__) # True False
class Closeable(ABC):
@abstractmethod
def close(self): ...
@classmethod
def __subclasshook__(cls, C): # structural check: "has a close() method"
if cls is Closeable:
return any("close" in B.__dict__ for B in C.__mro__) or NotImplemented
return NotImplemented
print(isinstance(io.StringIO(), Closeable), isinstance(42, Closeable)) # True Falsetyping.Protocol — structural typing
A Protocol lists the methods a type needs. Any class with those methods matches — no inheritance, no registration. Type checkers (mypy, pyright) verify it statically; @runtime_checkable also enables isinstance.
from typing import Protocol, runtime_checkable
@runtime_checkable
class SupportsArea(Protocol):
def area(self) -> float: ...
class Square: # no mention of SupportsArea anywhere
def __init__(self, s):
self.s = s
def area(self):
return self.s ** 2
class Rect:
def __init__(self, w, h):
self.w, self.h = w, h
def area(self):
return self.w * self.h
def total_area(shapes: list[SupportsArea]) -> float:
return sum(s.area() for s in shapes)
print(total_area([Square(2), Rect(2, 3)])) # 10
print(isinstance(Square(1), SupportsArea), isinstance("x", SupportsArea)) # True FalseABC or Protocol?
ABC (abc.ABC) | Protocol (typing.Protocol) | |
|---|---|---|
| Typing style | Nominal — must inherit or register | Structural — just have the methods |
| Enforced | At instantiation (TypeError) | By the static type checker |
| Shared code | Yes — concrete methods, template methods | Possible, but only for explicit subclasses |
| Works with third-party classes | Only via register | Automatically |
| Best for | Frameworks and class hierarchies you own | Library APIs, callbacks, “anything with .read()” |
Interview angle
Why can't you instantiate a class with abstract methods?
Nominal vs structural typing?
What does collections.abc give you beyond isinstance checks?