Abstract Classes and ABC
An abstract base class (ABC) is a class that exists to be a contract: it declares the methods a family must implement, and it refuses to be instantiated until they are. The abc module provides ABC and abstractmethod, and the mechanism is a metaclass that checks, at instantiation time, that no abstract method is left unimplemented. ABCs are the "nominal" side of the polymorphism chapter - the declared interface, checked at construction, testable with isinstance - and they are the right tool at public and plugin boundaries where an implicit duck-typing contract is too loose.
Declaring and Enforcing
from abc import ABC, abstractmethod
class Renderer(ABC):
"""Anything that can render a scene."""
def __init__(self, name: str):
self.name = name
@abstractmethod
def render(self, scene: str) -> str:
"""Produce the rendered output for a scene."""
@abstractmethod
def clear(self) -> None:
"""Reset the output surface."""
# a concrete method on the ABC: shared implementation, inherited
def describe(self) -> str:
return f"renderer: {self.name}"
class TerminalRenderer(Renderer):
def render(self, scene: str) -> str:
return f"[term] {scene}"
def clear(self) -> None:
pass
class Broken(Renderer):
def render(self, scene: str) -> str:
return scene
# clear is missing
t = TerminalRenderer("tty")
print(t.render("hello")) # [term] hello
print(t.describe()) # renderer: tty - concrete method inherited
try:
Broken("incomplete")
except TypeError as e:
print(e) # Can't instantiate abstract class Broken
# with abstract method clear
Mark methods with @abstractmethod (they may have a body, but subclasses must override them), inherit from ABC, and the metaclass does the rest: TerminalRenderer instantiates because it implements both abstract methods; Broken raises TypeError at construction naming the missing one. Concrete methods on the ABC (like describe) are inherited normally - the ABC is a real class with real code, not just a list of names. The __init__ on the ABC runs for every concrete subclass, which is where shared setup belongs. The enforcement is at instantiation, not definition - a Broken class can be defined fine; only the call Broken(...) fails, which is the right moment to discover an incomplete plugin.
The Plugin Pattern: isinstance as the Gate
from abc import ABC, abstractmethod
class Exporter(ABC):
@abstractmethod
def export(self, data: list[str]) -> str: ...
class CsvExporter(Exporter):
def export(self, data: list[str]) -> str:
return "\n".join(data)
class JsonExporter(Exporter):
def export(self, data: list[str]) -> str:
import json
return json.dumps(data)
class ExportService:
def __init__(self) -> None:
self._exporters: dict[str, Exporter] = {}
def register(self, name: str, exp: Exporter) -> None:
# the parameter type is the contract; a checker enforces it
self._exporters[name] = exp
def run(self, name: str, data: list[str]) -> str:
return self._exporters[name].export(data)
svc = ExportService()
svc.register("csv", CsvExporter())
svc.register("json", JsonExporter())
print(svc.run("csv", ["a", "b"]))
print(isinstance(CsvExporter(), Exporter)) # True - the gate works
This is the canonical ABC use: a registry keyed on the abstract type, where register accepts only things that are Exporters (the checker enforces the annotation, and isinstance is available at runtime if you want to check explicitly). New exporters plug in by implementing two methods - no change to ExportService - which is the open/closed principle made mechanical. The isinstance gate is what makes the registry honest: anything not a real Exporter is rejected at registration, not discovered as a missing export in production. For a smaller, internal boundary, duck typing is lighter; for a boundary that third parties cross, the ABC is the contract they can read and the checker can verify.
Abstract Properties and the rest of the Toolkit
from abc import ABC, abstractmethod
class Shape(ABC):
@property
@abstractmethod
def area(self) -> float:
...
@property
def perimeter(self) -> float:
# a concrete property: available to all subclasses
raise NotImplementedError("override for your shape")
import math
class Circle(Shape):
def __init__(self, r: float):
self.r = r
@property
def area(self) -> float:
return math.pi * self.r ** 2
@property
def perimeter(self) -> float:
return 2 * math.pi * self.r
c = Circle(2)
print(c.area, c.perimeter) # 12.566... 12.566...
Abstract properties combine the two decorators (@property outer, @abstractmethod inner) and require subclasses to provide a property of the same name - the contract now covers attribute-style interfaces, not just methods. The ABC toolkit extends further: abstractproperty (the explicit name), classmethod and staticmethod variants of abstractmethod, and ABC.register for virtual subclassing - marking an existing, unrelated class as satisfying the ABC without making it inherit (Exporter.register(SomeLegacyClass)), which is how a standard-library ABC like collections.abc.Hashable recognizes third-party types. Virtual registration is the bridge between the nominal (ABC) and structural (duck/Protocol) worlds when you cannot edit the class.
When to Use an ABC (and When Not)
- Use an ABC at a public or plugin boundary, when the interface is a product, and when a missing method is an expensive failure. The construction-time check and the
isinstancegate are the value. - Skip it internally when the "interface" is a handful of methods your own code calls and a plain duck-typed convention plus a type hint is enough. An ABC adds a base class and a metaclass to the MRO for a contract that no external party reads.
- Prefer a Protocol when the implementing classes cannot inherit from your base (third-party, stdlib, or you want zero coupling) - the structural contract from the polymorphism chapter is the same idea without the nominal hierarchy.
- Keep ABCs shallow. A deep tower of abstract classes is a design smell; one ABC per real interface, composed by the concrete classes, is the usual shape.
Summary
Build a contract with ABC and @abstractmethod: the metaclass blocks instantiation of any subclass that leaves an abstract method unimplemented (the TypeError names the gap), concrete methods on the ABC are inherited as shared code, and __init__ on the ABC runs for every concrete child. The ABC is the gate for registries and plugin systems - register(name, exp: Exporter) accepts only real implementors, and isinstance checks them at runtime. Extend the contract with abstract properties and ABC.register for virtual subclassing of types you do not control. Use it at boundaries where the contract is read by others and a missing method is costly; inside a module, a duck-typed convention with a type hint is usually the lighter, equally honest choice.