What you'll learn
- Declare the methods a family of classes must provide with
abc.ABCand@abstractmethod - Understand that an abstract class cannot be instantiated, and why that is the point
- Provide shared behavior in the base class while leaving the specifics abstract
- Preview
typing.Protocol, which declares the same contract without inheritance
Concept
Duck typing trusts that an object has the right methods. Sometimes trust is not enough: you are designing a family of classes that other people will implement, and you want a missing method to fail loudly at the moment the class is created, not the first time a customer hits the missing branch. An abstract base class does that. It names the methods every subclass must provide, and Python refuses to create an instance of any subclass that has not provided them.

The mechanics are small. Inherit from abc.ABC, and mark the required methods with @abstractmethod; their bodies can be ... or a docstring. The base class itself cannot be instantiated either, which is correct: a "shape" with no specific kind has no area. A subclass becomes concrete the moment it overrides every abstract method, and only then does Subclass() work.
An abstract base class is more than a checklist. It can carry real, shared implementations that use the abstract methods: a describe() in the base that calls self.area() works for every subclass without knowing which. That combination, abstract requirements plus concrete helpers built on them, is the template pattern, and it is the main reason to reach for an ABC over a plain informal convention.
| You want | Tool |
|---|---|
| enforce that subclasses implement methods | ABC + @abstractmethod |
| share behavior built on those methods | concrete methods in the ABC |
| accept any object with the right methods, no inheritance | typing.Protocol (checked by mypy) |
| check at runtime whether an object matches | isinstance(obj, MyABC), or hasattr |
Python's standard library uses ABCs everywhere: collections.abc.Iterable, Sized, Mapping. isinstance(x, collections.abc.Iterable) is the right way to ask "can I loop over this?" because it consults those contracts rather than a fixed list of types. When you read a library and see ABC in a class list, you are looking at its contract with you.
typing.Protocol expresses the same idea without inheritance: you declare a class with the required method signatures, and any object that has them satisfies the protocol, checked by a static type checker rather than at runtime. It is duck typing made explicit and verifiable. The typing lessons cover it properly; for now know that an ABC enforces at instantiation time, a Protocol at check time, and both write down the contract that duck typing left implicit.

Code
An abstract base class with two required methods and one shared helper. The complete subclass works; the incomplete one is refused at instantiation, naming what is missing.
# shapes_abc.py
import math
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
"""Return the area."""
@abstractmethod
def perimeter(self):
"""Return the perimeter."""
def describe(self):
return f"{type(self).__name__}: area {self.area():.2f}, perimeter {self.perimeter():.2f}"
class Circle(Shape):
def __init__(self, r):
self.r = r
def area(self):
return math.pi * self.r ** 2
def perimeter(self):
return 2 * math.pi * self.r
class HalfDone(Shape):
def area(self):
return 0
print(Circle(1).describe())
print(isinstance(Circle(1), Shape))
HalfDone()
Circle: area 3.14, perimeter 6.28
True
Traceback (most recent call last):
File "shapes_abc.py", line 36, in <module>
HalfDone()
~~~~~~~~^^
TypeError: Can't instantiate abstract class HalfDone without an implementation for abstract method 'perimeter'The base class cannot be instantiated either, and the standard library's ABCs answer capability questions for any object.
# abc_checks.py
from abc import ABC, abstractmethod
from collections.abc import Iterable, Sized, Mapping
class Plugin(ABC):
@abstractmethod
def run(self):
...
try:
Plugin()
except TypeError as err:
print("abstract:", str(err)[:40], "...")
for value in ([1, 2], "abc", 42, {"k": 1}):
print(f"{type(value).__name__:<5} iterable={isinstance(value, Iterable)!s:<5} sized={isinstance(value, Sized)!s:<5} mapping={isinstance(value, Mapping)}")
abstract: Can't instantiate abstract class Plugin ...
list iterable=True sized=True mapping=False
str iterable=True sized=True mapping=False
int iterable=False sized=False mapping=False
dict iterable=True sized=True mapping=TrueA plugin system: the ABC is the contract, concrete plugins fill it in, and a runner uses only the contract. Adding a plugin means writing one class.
# plugins.py
from abc import ABC, abstractmethod
class Exporter(ABC):
extension = "txt"
@abstractmethod
def render(self, rows):
"""Return the file contents for these rows."""
def export(self, rows, name):
filename = f"{name}.{self.extension}"
return filename, self.render(rows)
class CsvExporter(Exporter):
extension = "csv"
def render(self, rows):
return "\n".join(",".join(str(v) for v in row) for row in rows)
class MarkdownExporter(Exporter):
extension = "md"
def render(self, rows):
return "\n".join("| " + " | ".join(str(v) for v in row) + " |" for row in rows)
rows = [("name", "score"), ("ada", 91), ("linus", 67)]
for exporter in (CsvExporter(), MarkdownExporter()):
filename, body = exporter.export(rows, "report")
print(filename)
print(body)
report.csv
name,score
ada,91
linus,67
report.md
| name | score |
| ada | 91 |
| linus | 67 |A preview of Protocol: the same contract, no inheritance. Square never mentions HasArea, yet it satisfies it, and runtime_checkable lets isinstance confirm that.
# protocol_preview.py
from typing import Protocol, runtime_checkable
@runtime_checkable
class HasArea(Protocol):
def area(self) -> float: ...
class Square:
def __init__(self, side):
self.side = side
def area(self):
return self.side ** 2
def total(shapes: list[HasArea]) -> float:
return sum(s.area() for s in shapes)
print(isinstance(Square(2), HasArea), isinstance("text", HasArea))
print(total([Square(2), Square(3)]))
True False
13Try it yourself
Create storage.py with an abstract Storage class requiring save(key, value) and load(key), plus a concrete has(key) helper that uses load. Implement MemoryStorage with a dictionary and FileStorage that writes one file per key using pathlib. Write a function that takes any Storage, saves two values, and reads them back, then run it against both. Try to instantiate Storage directly and read the error.
Common mistakes
- Making every class abstract. An ABC earns its place when several implementations share a contract; a single class needs no ceremony.
- Forgetting to inherit from
ABC.@abstractmethodon a class that does not inherit fromABCenforces nothing; the class instantiates happily. - Confusing ABC and Protocol. An ABC is inherited and checked when an instance is created; a Protocol is not inherited and is checked by mypy or
runtime_checkable.
What's next
- An ABC declares required methods with
@abstractmethod; incomplete subclasses cannot be instantiated. - Put shared behavior that uses the abstract methods in the base class; that is what makes an ABC more than a checklist.
Protocolstates the same contract without inheritance, verified by a type checker; the standard library'scollections.abcanswers capability questions.
Contracts are explicit now. Next you will learn the design skill that ties the OOP module together: composing objects instead of inheriting, and keeping classes small.