Python Module 9 – Object-oriented Python and the data model
Abstract base classes and duck typing
Define interfaces with abc.ABC and abstractmethod, check capabilities with collections.abc, and type-check duck typing with typing.Protocol.
What you will learn
- Define interfaces with abc.ABC and @abstractmethod, and read the error for a missing method
- Check what a value can do with the abstract base classes of collections.abc
- Explain structural typing with typing.Protocol and what @runtime_checkable checks
- Choose between duck typing, an abstract base class and a protocol for a design
Before you start
What you’ll learn · Before you start
What you will learn
- Define interfaces with abc.ABC and @abstractmethod, and read the error for a missing method
- Check what a value can do with the abstract base classes of collections.abc
- Explain structural typing with typing.Protocol and what @runtime_checkable checks
- Choose between duck typing, an abstract base class and a protocol for a design
Before you start
On this page
A function that sends an alert calls channel.send(message), and it works with an email sender, a text-message
sender or a test stand-in: three classes with nothing in common but that one method. That is
duck typing: Judging an object by what it can do rather than by its class: code calls the methods it needs, and any object that has them works. A wrong object is found only when the call runs.: Python decides what an object can do when the code runs, not by the class
it belongs to, so a wrong object is found only when the call reaches it. When a design needs a stated interface,
Python offers two tools. abstract base class (ABC): A class, usually built on abc.ABC, that declares methods its subclasses must implement. Python refuses to create an object of a class that still lacks one of them, and the error names the missing method., built with the abc module,
declare methods that subclasses must implement, and Python refuses to create an object of a class that is missing
one. protocol (typing.Protocol): A class from typing that lists the methods an interface needs. Any class with those methods matches it without inheriting anything, and a type checker verifies the match before the program runs., from typing, describe an interface by its methods alone: any class with
those methods matches without inheriting anything, and a type checker verifies the match before the program runs.
The collections.abc module adds ready-made abstract base classes, such as Iterable and Mapping, for the kinds of
container. Below you will build the same interface three ways and compare them.
Duck typing judges an object by what it can do
broadcast() sends one message through every notifier it is given, and its second call is given a string:
class EmailNotifier:
def send(self, message):
return f"email: {message}"
class SmsNotifier:
def send(self, message):
return f"sms: {message[:20]}"
class RecordingNotifier: # a stand-in for tests: it only remembers what it was sent
def __init__(self):
self.sent = []
def send(self, message):
self.sent.append(message)
return "recorded"
def broadcast(notifiers, message):
return [notifier.send(message) for notifier in notifiers] # asks no question about the class
recorder = RecordingNotifier()
print(broadcast([EmailNotifier(), SmsNotifier(), recorder], "Your order has shipped"))
print(recorder.sent)
try:
broadcast(["not a notifier"], "hello")
except AttributeError as error:
print("AttributeError:", error) Output
['email: Your order has shipped', 'sms: Your order has shipp', 'recorded'] ['Your order has shipped'] AttributeError: 'str' object has no attribute 'send'
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 duck_typing.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
broadcast() never asks what class an object is; it calls send() and uses the answer. The three notifiers share
no base class, and the recording stand-in, which a test would use, works as well as the real ones. Python’s glossary
calls this style duck typing, after the saying about a bird that looks and quacks like a duck: an object is accepted
for what it does. Step through both calls:
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- An email notifier. broadcast() calls send() on the first object without asking what it is. EmailNotifier returns 'email: Your order has shipped'.
- A text-message notifier. SmsNotifier shares no base class with EmailNotifier, but it has a send() method, which keeps the first 20 characters of the message.
- A stand-in for tests. RecordingNotifier only remembers the message and returns 'recorded'. A test can hand it to broadcast() and then read what it was sent from recorder.sent.
- A string, found out late. The second call passes a string. broadcast() calls send() on it like any other object, and only then does Python find that a str has no send() and raise AttributeError.
The price is paid when something is wrong. A string has no send(), and the mistake surfaces only when broadcast()
reaches it, as an AttributeError, possibly after the first messages have gone out. Python code often goes one step
further and simply tries an operation, handling the error if it fails, a style that the glossary calls
EAFP: Easier to ask for forgiveness than permission: trying an operation and handling the exception if it fails, instead of checking first. The Python documentation calls it the common Python style. More in “Handling exceptions with try and except”: it is easier to ask for forgiveness than permission.
Checkpoint
Check: Duck typing
An abstract base class refuses objects that miss a method
Here the interface is a class, and one of its three implementations forgets a method:
from abc import ABC, abstractmethod
class PaymentMethod(ABC):
@abstractmethod
def pay(self, amount):
"""Charge amount, in the smallest unit of the currency, and return a reference."""
@abstractmethod
def refund(self, reference):
"""Give back the money of an earlier payment."""
def charge(self, amount): # a concrete method, shared by every payment method
if amount <= 0:
raise ValueError("the amount must be more than 0")
return self.pay(amount)
class InstantTransfer(PaymentMethod): # money moves from one bank account to another at once
def __init__(self, address):
self.address = address
def pay(self, amount):
return f"IT-{self.address}-{amount}"
def refund(self, reference):
return f"refunded {reference}"
class Card(PaymentMethod):
def __init__(self, last4):
self.last4 = last4
def pay(self, amount):
return f"CARD-{self.last4}-{amount}"
def refund(self, reference):
return f"refunded {reference}"
class GiftVoucher(PaymentMethod): # forgot refund()
def pay(self, amount):
return f"VOUCHER-{amount}"
for method in (InstantTransfer("asha@bank"), Card("4242")):
reference = method.charge(49900)
print(reference, "|", method.refund(reference))
for cls in (PaymentMethod, GiftVoucher):
try:
cls()
except TypeError as error:
print("TypeError:", error)
print(sorted(PaymentMethod.__abstractmethods__), sorted(GiftVoucher.__abstractmethods__)) Output
IT-asha@bank-49900 | refunded IT-asha@bank-49900 CARD-4242-49900 | refunded CARD-4242-49900 TypeError: Can't instantiate abstract class PaymentMethod without an implementation for abstract methods 'pay', 'refund' TypeError: Can't instantiate abstract class GiftVoucher without an implementation for abstract method 'refund' ['pay', 'refund'] ['refund']
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 payment_abc.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
PaymentMethod inherits from ABC and marks pay() and refund() with @abstractmethod, which makes each an
abstract method: A method marked with @abstractmethod in an abstract base class. A subclass must override it before Python creates objects of the subclass; __abstractmethods__ lists the ones still missing.. Each concrete class implements both and inherits charge(), an
ordinary method that checks the amount and then calls the subclass’s own pay(). PaymentMethod itself cannot be
instantiated, and neither can GiftVoucher, which forgot refund(): the error names the missing method at the moment
someone creates the object, long before a customer asks for a refund. __abstractmethods__ holds the names that a
class still lacks. Step through the three classes and see when Python checks each of them:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Two abstract methods. PaymentMethod inherits from ABC and marks pay and refund with @abstractmethod. charge() is an ordinary method, which every subclass inherits.
- An instant transfer. InstantTransfer implements both abstract methods, so nothing is missing and Python creates the object.
- charge(49900). charge() is found in PaymentMethod. It checks that the amount is more than 0 and calls self.pay(), which runs the subclass's own pay and returns IT-asha@bank-49900.
- A card. Card implements both methods as well, so it is created, and its charge() returns CARD-4242-49900.
- The interface itself. PaymentMethod still has both methods missing, so creating one fails with TypeError, and the message names pay and refund.
- A voucher without refund. GiftVoucher has pay but no refund. Python refuses it the moment someone tries to create one, and the message names refund, long before any customer asks for a refund.
Abstract base classes suit interfaces that other people implement, such as plugins, because the rule is enforced in one place, for every implementation.
Checkpoint
Check: Abstract base classes
collections.abc names the kinds of container
The program asks which of six values, and of one class of its own, count as each kind of container:
from collections.abc import Callable, Iterable, Mapping, Sequence, Sized
class Countdown: # inherits from nothing; it only has __iter__
def __init__(self, start):
self.start = start
def __iter__(self):
return iter(range(self.start, 0, -1))
checks = [Iterable, Sized, Sequence, Mapping, Callable]
values = {"[1, 2]": [1, 2], "'ab'": "ab", "{'k': 1}": {"k": 1}, "{1, 2}": {1, 2}, "Countdown(3)": Countdown(3), "len": len}
print(f"{'':13}" + "".join(f"{abc.__name__:>9}" for abc in checks))
for label, value in values.items():
print(f"{label:13}" + "".join(f"{'yes' if isinstance(value, abc) else '-':>9}" for abc in checks))
print(list(Countdown(3)), Iterable in Countdown.__mro__) Output
Iterable Sized Sequence Mapping Callable
[1, 2] yes yes yes - -
'ab' yes yes yes - -
{'k': 1} yes yes - yes -
{1, 2} yes yes - - -
Countdown(3) yes - - - -
len - - - - yes
[3, 2, 1] False
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 container_abcs.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The module defines abstract base classes for the kinds of container: Iterable, Sized, Sequence, Mapping,
Callable and many more. A list and a string are sequences, a dictionary is a mapping, and a set is neither.
Countdown inherits from nothing, yet it counts as Iterable: the simplest of these classes recognise a class by the
one method they need, here __iter__, although Iterable is not in its MRO. Sequence and Mapping recognise only
classes that inherit from them or are registered with them, because __getitem__ and __len__ alone cannot tell a
sequence from a mapping. Checking isinstance(value, Mapping) instead of isinstance(value, dict) lets a function
accept every mapping, not only dictionaries.
Checkpoint
Check: The kinds of container
A protocol is duck typing that a type checker verifies
The same idea can be written down as a class that lists the methods:
import io
from typing import Protocol, runtime_checkable
@runtime_checkable
class Sender(Protocol):
def send(self, message: str) -> str: ...
class Pager: # no base class, no registration: it just has send()
def send(self, message: str) -> str:
return f"page: {message}"
def alert(channel: Sender, message: str) -> str:
return channel.send(message)
print(alert(Pager(), "disk 91% full"))
print(isinstance(Pager(), Sender), Sender in Pager.__mro__, isinstance("text", Sender))
def first_line(source: io.Reader[str]) -> str: # io.Reader is new in Python 3.14
return source.read().splitlines()[0]
print(first_line(io.StringIO("line one\nline two")), isinstance(io.StringIO(), io.Reader)) Output
page: disk 91% full True False False line one True
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 protocol_send.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Sender is a protocol: a class whose body lists the methods an interface needs. Pager never mentions it and still
matches it, because it has a send() method of the right shape. A static type checker, such as mypy or pyright,
checks every call of alert() against the protocol before the program runs, signatures included. That is
structural typing: Matching a type by its shape, the methods and attributes it has, as typing.Protocol does, instead of by the classes it inherits from., matching by shape, where abstract base classes use
nominal typing: Matching a type by name: an object fits only if its class inherits from the named class or is registered with it, as with abstract base classes., matching by the inherited name. @runtime_checkable also allows
isinstance() checks against a protocol, which test only that the methods exist: isinstance(Pager(), Sender) is
true, while Sender is not in the MRO of Pager.
Version note
New in Python 3.14: io.Reader and io.Writer are protocols for the simplest streams, an object with read() and
one with write(). first_line() accepts any object that can read() text, such as an open file or an
io.StringIO.
The diagram below sets the three ways side by side, each panel named after the moment a missing method is found.
Three ways to state an interface
Text description of the diagram
The diagram has three panels, one above the other, each named after when a missing method is found.
- Duck typing, at the call: broadcast() calls send() on each item, and any object with a send() method works. A missing method shows up only when the call is made.
- ABC, when an object is made: Card inherits from PaymentMethod, an abstract base class. Python refuses to create an object of a class that has not implemented every abstract method.
- Protocol, by a type checker: Pager does not inherit from Sender, a Protocol. A dashed arrow labelled "matches" shows that it fits the protocol because it has the methods the protocol describes; a static type checker verifies the match before the program runs.
| Criterion | Duck typing | ABC | Protocol |
|---|---|---|---|
| Stated by | the calls alone | abstract methods | a list of methods |
| Classes | have the methods | inherit or register | have the methods |
| Caught | when the call runs | when an object is made | by a type checker |
| When to choose | Small code that calls a method or two | Interfaces that other people implement | Classes you cannot change, checked statically |
Checkpoint
Check: Protocols and structural typing
Check yourself
Practice that adapts to you
Practise this lesson
12 questions about 12 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
How abstract base classes decide, with four traps reproduced Pro: opens with a Pro, Premium or Ultimate pass
An isinstance() check against an abstract base class can come out true for three different reasons, and only one of
them involves checking that methods exist. This part takes the three apart, reproduces four traps of the abc
module, counts the work that collections.abc does for a class that inherits from it, and gives an abstract method a
body.
In this part:
- Inherit, register or hook: three reasons for isinstance to say yes
- Four traps reproduced, from silent iterables to ignored decorators
- Two methods written, five inherited: the mixin methods of Sequence, counted
- Abstract does not mean empty: a method with a body, reused through super()
4 runnable examples (Python) · about 7 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Protocols in practice: what each check covers, measured, and how to choose Pro: opens with a Pro, Premium or Ultimate pass
A protocol is checked thoroughly by a type checker before the program runs, and only shallowly by isinstance()
while it runs. This part shows exactly what the run-time check covers, measures what it costs, tests the ready-made
protocols of typing, inherits a protocol on purpose, and ends with a decision table and an exercise.
In this part:
- What a run-time protocol check lets through, reproduced
- The cost of a run-time protocol check, measured
- Ready-made protocols in typing, and what they really test
- Inheriting a protocol on purpose, to get its default methods
- Choosing between duck typing, an ABC and a protocol
- Exercise: notifiers, an ABC and anything with send()
- A notifier interface, and a broadcast that takes anything with send()
4 runnable examples (Python) · 1 exercise with sample tests · about 14 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Interview questions
-
Fresher · Warm-up
What is duck typing?
Duck typing is the Python habit of judging an object by what it can do rather than by its class. A function that calls source.read() accepts a file, a network stream or a test stand-in, as long as each has a read method, and nothing has to inherit from a common base class. The name comes from the saying about a bird that looks and quacks like a duck. It goes with the EAFP style: try the operation and handle the exception if it fails, instead of checking types first. The cost is that a wrong object is found only when the call runs. Abstract base classes and typing.Protocol add structure on top: an ABC enforces an interface when an object is created, and a protocol lets a type checker verify duck typing before the program runs.
-
Mid to senior
Your library's StorageBackend abstract base class gained a new abstract method, delete_many(), in a minor release. A dozen backends written by other teams now fail with TypeError: Can't instantiate abstract class. How should you have added the method, and how do you roll out the change now?
-
Mid to senior
You are designing the cache interface of an open-source library that other projects will implement and call. Would you publish it as an abstract base class or as a typing.Protocol? Defend your choice, and say what you would give implementers either way.
-
Mid to senior
Code checks isinstance(conn, SupportsClose), a runtime-checkable Protocol, before calling conn.close(timeout=5), and it still crashes with TypeError for some connection objects. Why did the check pass, what does it actually test, and what would you do instead?
Key takeaways
- Duck typing accepts any object that has the methods a function calls; mistakes show up when the call runs.
- Abstract base classes declare methods with
@abstractmethod, and Python refuses to create objects of classes that have not implemented them. collections.abcdescribes the kinds of container: checkisinstance(value, Mapping)rather than a concrete type.- A
typing.Protocolis matched by its methods alone; a type checker verifies it before the program runs, and@runtime_checkableadds a shallowisinstance()check. - Use abstract base classes for interfaces that others implement, and protocols for accepting classes you cannot change.
Terms in this lesson
- abstract base class (ABC)
- A class, usually built on abc.ABC, that declares methods its subclasses must implement. Python refuses to create an object of a class that still lacks one of them, and the error names the missing method.
- abstract method
- A method marked with @abstractmethod in an abstract base class. A subclass must override it before Python creates objects of the subclass; __abstractmethods__ lists the ones still missing.
- duck typing
- Judging an object by what it can do rather than by its class: code calls the methods it needs, and any object that has them works. A wrong object is found only when the call runs.
- nominal typing
- Matching a type by name: an object fits only if its class inherits from the named class or is registered with it, as with abstract base classes.
- protocol (typing.Protocol)
- A class from typing that lists the methods an interface needs. Any class with those methods matches it without inheriting anything, and a type checker verifies the match before the program runs.
- structural typing
- Matching a type by its shape, the methods and attributes it has, as typing.Protocol does, instead of by the classes it inherits from.
References
- abc: abstract base classes (Python Software Foundation)
- collections.abc: abstract base classes for containers (Python Software Foundation)
- typing.Protocol (Python Software Foundation)
- typing.runtime_checkable (Python Software Foundation)
- typing.get_protocol_members() (Python Software Foundation)
- io: io.Reader and io.Writer (Python Software Foundation)
- Glossary: duck-typing and EAFP (Python Software Foundation)
- PEP 3119: introducing abstract base classes (Python Software Foundation)
- PEP 544: protocols, structural subtyping (Python Software Foundation)
- os: os.environ (Python Software Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress