- rtshkmr's digital garden/
- Readings/
- Books/
- Fluent Python: Clear, Concise, and Effective Programming – Luciano Ramalho/
- Chapter 13. Interfaces, Protocols, and ABCs/
Chapter 13. Interfaces, Protocols, and ABCs
Table of Contents
python has 4 ways to define and use interfaces:
Duck typing
goose typing: using ABCs
^ focus of this chapter
static typing: traditional static typing using the
typingmodulestatic duck typing
popularised by GoLang, supported by
typing.Protocol
this chapter is about the typing that revolves around interfaces.
The Typing Map #

The two dimensions introduced here:
runtime vs static checking
structural (based on method’s provided by the object) vs nominal (based on the name of its class/superclass)
What’s New in This Chapter #
Two Kinds of Protocols #
In both cases, we don’t need to do any sort of explicit registration for the protocol (or to use inheritance).
Dynamic Protocol
Implicit, defined by convention as per documentation.
A good example is the protocols within the interpreter, seen in the “Data Model” of the language ref. e.g. Sequence, Iterable
Can’t be verified by type checkers
Static Protocol
An explicit definition as a subclass of
typing.ProtocolABCs ca n be used to define an explicit interface (similar in outcome to static protocols).
Programming Ducks #
Python Digs Sequences #
- this is pretty cool: Python manages
to make iteration and the in operator work by invoking __getitem__ when __iter__
and __contains__ are unavailable.
The interpreter uses special methods (__getitem__, __iter__, etc.) dynamically to support iteration and membership tests even without explicit ABCs. This is a classic Python dynamic protocol idiom.
Monkey Patching: Implementing a Protocol at Runtime #
Monkey patching is dynamically changing a module, class, or function at runtime, to add features or fix bugs.
in this example, we want a custom class to automatically work with
random.shuffle()so that we can shuffle that sequence.We inspect
random.shuffle()and figure out what it’s underlying functionality is, which is to rely on the__setitem__function.So we can monkey patch the
__setitem__and we can achieve our desired outcome. This means that we change the module @ runtime.Monkey patching is powerful, but the code that does the actual patching is very tightly coupled with the program to be patched, often handling private and undocumented attributes.
Python does not let you monkey patch the built-in types. I actually consider this an advantage, because you can be certain that a str object will always have those same methods. This limitation reduces the chance that external libraries apply conflicting patches.
Defensive Programming and “Fail Fast” #
TO_HABIT: the examples here show how to do a check by checking whether it can behave like a duck instead of checking whether it’s a duck. This is a superior way of doing meaningful type checks in my opinion but there’s some possible pitfalls into doing so.
we want to be able to detect dynamic protocols without explicit checks
Failing fast means raising runtime errors as soon as possible, for example, rejecting invalid arguments right a the beginning of a function body.
Duck type checking means we should check behaviour instead of doing explicit typechecks.
Some patterns:
IDIOM: use a builtin function instead of doing type-checking \(\implies\) check for method presense
in the example, to check if the input arg is a list, instead of doing a type check at runtime, it’s suggested to use the
list()constructor because that constructor will handle any iterable that fits in memory. Naturally, this copies the data.If we can’t accept copying, then we can do runtime check using
isinstance(x, abc.MutableSequence)warning: what if infinite generator?
eliminate that by calling
len()on the arg, tuples, arrs and such will still pass this check
Defensive code leveraging duck types can also include logic to handle different types without using
isinstance()orhasattr()tests.suppose we want to type hint that “
field_namesmust be a string of identifiers separated by spaces or commas”,then our check could do something like this:
1 2 3 4 5 6 7 8 9 10Example 13-5. Duck typing to handle a string or an iterable of strings try: # this is an attempt, assumes that it's a string field_names = field_names.replace(',', ' ').split() except AttributeError: pass # if not string, then can't continue testing, just pass it # converting to a tuple ensures that it's iterable and we test our own copy of it (to prevent accidentally changing the input) field_names = tuple(field_names) if not all(s.isidentifier() for s in field_names): raise ValueError('field_names must all be valid identifiers')This is an expressive form of using duck typing to our advantage for type checking.
Goose Typing #
- ABCs help to define interfaces for explicit type checking at runtime (and also work for static type checking).
complement duck typing
introduce virtual subclasses:
- classes that don’t inherit from a class but are still recognized by
isinstance()andissubclass()
- classes that don’t inherit from a class but are still recognized by
Waterfowl and ABCs #
the strong analogy of duck typing to actual phenetics (i.e. phenotype-based) classification is great, mimics how we do duck typing (based on shape and behaviour)
how important is the explicit type checking depends on the usage-context of an object
parallel objects can produce similar traits and this is the case where we may have false positives on the classifications
that’s why we need a more “explicit” way of typechecking and that’s where “goose typing” comes into the picture.
python’s ABCs provide the
registerclass-method which lets us “declare” that a certain class becomes a “virtual” subclass of an ABC (meets name, signature and semantic contract requirements)we can declare this even if the class need not have been developed with any awareness of the ABC (and wouldn’t have inherited from it) \(\implies\) this is structural subtyping with ABCs where the structure is sufficient
registration can be implicit (without us needing to register custom classes), just have to implement the special methods.
- key advice:
When implementing a class that represents a concept from the standard library’s ABCs (e.g., Sequence, Mapping, Number, etc.):
Explicitly inherit from or register with the appropriate ABC if your class fits the contract of that ABC.
This helps make your class reliably compatible with tools, libraries, or Python code that expects these standard interfaces.
If a library or framework you use defines classes but omits to formally subclass/register with the standard ABCs:
Perform the ABC registration yourself at program startup (e.g., by manually registering the class with
collections.abc.Sequence).This will ensure
isinstance(obj, collections.abc.Sequence)checks work as intended, improving reliability and interoperability.When checking if an object matches a conceptual interface (such as being a “sequence”):
Use isinstance(the_arg, collections.abc.Sequence) rather than checking for method presence (duck typing) or relying on type names.
This is more future-proof and integrates with Python’s built-in and third-party tools.
Avoid defining your own custom ABCs or metaclasses in production code:
These advanced features are often overused and can lead to unnecessarily complex, harder-to-maintain code.
The author likens custom ABCs/metaclasses to a “shiny new hammer”: appealing when you first learn them, but prone to misuse (“all problems look like a nail”).
Emphasizes sticking to straightforward, simple code for better maintainability and happiness for you and future developers.
Tension between Duck Typing and Goose Typing #
They are a continuum, not an either/or. Use duck typing for everyday, flexible code, and goose typing (ABCs and explicit interface contracts) where precision, reliability, and maintainability matter (public APIs, reusable libraries, systems programming).
See richer elaboration here:
You've highlighted a key *tension* in Python between **duck typing** ("if it quacks like a duck, treat it as a duck") and **ABC-based explicit/virtual interface type checking** ("goose typing," using `isinstance` with domain-driven interfaces made explicit via ABCs). Let’s clarify how these approaches relate and when to use each.
### Duck Typing (Classic Pythonic Style)
**Principle**:
*Check for behavior, not type.*
If you expect an object to behave like a sequence, just try to use it as such (e.g., iterate over it, index into it). If it fails at runtime, raise an error immediately (“fail fast”).
**Code example:**
```python
def head(seq):
return seq[0] # works for any object supporting indexing
```
- If `seq` supports indexing (`__getitem__`), this just works.
- This is *pure duck typing*—no explicit type checks or interface enforcement.
### Goose Typing: ABCs and Virtual Subclassing
**Principle**:
Use Python’s Abstract Base Classes (`collections.abc.Sequence`, `MutableMapping`, etc.) to define *explicit interfaces* for families of types (not just classes).
- You can *register* a class as a "virtual subclass" of an ABC, even if it doesn't inherit from it.
- Now, `isinstance(obj, collections.abc.Sequence)` will treat your class as a Sequence.
**Benefits:**
- Enables more robust and explicit type checks.
- Useful when interoperating with other libraries, for documentation, for static-checking tools, or when you want to *fail fast* with a clear, predictable check.
- Python’s standard library and third-party tools often use `isinstance(x, ABC)` checks internally, so registering your custom class improves **interoperability**.
### How do these approaches relate?
- **Duck typing**: Write code that operates on any object with the required behaviors (methods/attributes), with no formal interface contract.
- Pro: maximal flexibility; principle of least surprise for Pythonistas.
- Con: can result in runtime errors that are sometimes hard to debug or anticipate.
- **Goose typing**: Explicitly document and/or enforce the “duck” shape via ABCs—*preferably standard library ABCs*—and use `isinstance` checks for run-time or static confirmation.
- Con: Slightly more rigid, but much clearer and more future-proof if you want predictable, explicit interfaces.
**In modern large-scale or library codebases, combining duck typing for flexibility with ABCs for explicit interface definition (“goose typing”) gives you both power and predictability.**
### Practical Guidance from Your Notes
- Use **duck typing** by default; just try the operation and handle exceptions—this is flexible, concise, and Pythonic.
- For situations where:
- You need to *fail fast*,
- You want to document and defend API boundaries,
- You’re interacting with other libraries/frameworks using ABCs,
**it’s recommended to explicitly register your classes with appropriate ABCs and use `isinstance` checks**.
- This isn't an either/or—it's a continuum. Use *duck typing* for everyday, flexible code, and *goose typing* (ABCs and explicit interface contracts) where precision, reliability, and maintainability matter (public APIs, reusable libraries, systems programming).
### Metaphor Recap
- **Duck typing**: "Does it walk and quack like a duck? Use it as a duck."
- **Goose typing**: "Let's agree on what waterfowl are. If you’re a duck, get your name on the official registry, so everyone knows for sure and tools can verify it."
### References
- [Python's isinstance docs][1]
- [Structural subtyping via ABCs and the register method][11]
**Summary:**
- *"Duck typing"* means acting on behaviors and handling failures.
- *"Goose typing"* means using ABCs for explicit, reliable interface checks (`isinstance` and virtual subclassing).
- Use each to the degree that your problem, API guarantees, and interoperability demand. Both are core to Pythonic code.
Let me know if you want idiomatic examples for when to use each or more detailed pros/cons for specific scenarios!
[1] https://realpython.com/what-does-isinstance-do-in-python/
[2] https://stackoverflow.com/questions/1549801/what-are-the-differences-between-type-and-isinstance
[3] https://ioflood.com/blog/python-isinstance-function-guide-with-examples/
[4] https://switowski.com/blog/type-vs-isinstance/
[5] https://www.reddit.com/r/learnpython/comments/hg6fyd/do_not_compare_types_use_isinstance_instead/
[6] https://github.com/python/typing/issues/1363
[7] https://stackoverflow.com/questions/3111611/is-this-use-of-isinstance-pythonic-good
[8] https://realpython.com/python-type-checking/
[9] https://www.curiousefficiency.org/posts/2004/12/type-checking-in-python/
[10] https://typing.python.org/en/latest/guides/type_narrowing.html
[11] https://peps.python.org/pep-0544/
Subclassing an ABC #
This is about using goose-typing in practice.
- we can use the subclassing as a benchmark for this section on subclassing:
needed to adhere strictly to the interface.
therefore there’s a need to implement concrete versions of all the abstract methods defined in the abc.
concrete methods are implemented in terms of the public interface of the class, so it’s possible for us to subclass without any knowledge of the internal structure of the instances.
ABCs in the Standard Library #
some places we can find useful ABCs:
collections.abcmodule (most widely used),iopackage,numberspackagefrom
collections.abc
NOTE: photo is outdated, from python 3.6 Sequence, Mapping and Set are subclassed from Collection, which is a child of Iterable, Container, Sized
Remember that each of the immutable collections have a mutable subclass.
if
insinstance(obj, Hashable)returnsFalse, you can be certain that obj is not hashable. But if the return isTrue, it may be a false positive.also for
isinstance(obj, Iterable), we might have false negatives. This is because Python may stil be able to iterate overobjusing__getitem__TO_HABIT: duck typing is the most accurate way to determine if an instance is hashable/iterable: if we just call
hash(obj)/iter(obj)
Defining and Using an ABC #
this is only for learning purposes, we should avoid implementing our own ABCs and metaclasses.
A good usecase for ABCs, descriptors, metaclasses are for building frameworks.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32# tag::TOMBOLA_ABC[] import abc class Tombola(abc.ABC): # <1> subclass abc.ABC to define an ABC @abc.abstractmethod def load(self, iterable): # <2> use this decorator, keep the body empty, can include in docstring """Add items from an iterable.""" @abc.abstractmethod def pick(self): # <3> """Remove item at random, returning it. This method should raise `LookupError` when the instance is empty. """ def loaded(self): # <4> ABC may include concrete methods. """Return `True` if there's at least 1 item, `False` otherwise.""" return bool(self.inspect()) # <5> def inspect(self): """Return a sorted tuple with the items currently inside.""" items = [] while True: # <6> try: items.append(self.pick()) except LookupError: break self.load(items) # <7> return tuple(items) # end::TOMBOLA_ABC[]some observations:
since this is abstract, we can’t know what the concrete subclasses will actually use for the implementation \(\implies\) we end up trying to use the other abstract functions more so than assuming things.
it’s OK to provide concrete methods in ABCs, as long as they only depend on other methods in the interface.
For example, for
inspect, we use the abstractpickfunction andloadto return it to the original state.Before ABCs existed, abstract methods would raise
NotImplementedErrorto signal that subclasses were responsible for their implementation.NOTE: an
@abstractmethodmethod can have a base implementation. The subclass will still need to override it but the subclass will also be able to access it usingsuper()and build onto / directly use the super functionality.LANG_LIMITATION: there’s no formal method for “adding” expected error types (exceptions) in Python Interfaces. No exception contracts.
Docs are the only practical way to make expected exceptions explicit in Python interfaces today. Writing thoughtful docstrings and custom exception classes is the accepted best practice. ABCs enforce method presence not exception contracts.
more elaboration here:
When it comes to specifying or "adding" expected error types (exceptions) in Python interfaces like abstract base classes (ABCs) or general functions, **the language itself provides no formal mechanism** to declare which exceptions a method or function should raise, unlike some statically typed languages that have checked exceptions. ### How do we communicate expected error types in Python then? 1. **Documentation is the de facto standard for specifying expected exceptions** - Docstrings are the primary place to declare what errors a method can raise. This is how Python developers indicate usage interface contracts including possible exceptions. - For example: ```python def divide(x, y): """ Divide x by y. Raises: ZeroDivisionError: If y is zero. TypeError: If inputs are not numbers. """ return x / y ``` 2. **ABCs and raising `NotImplementedError` for abstract methods** - When defining abstract methods in ABCs, it is common to raise `NotImplementedError` to indicate subclasses *must* implement that method. - This is the only *exception-related interface* that ABCs imply formally in code. 3. **Static typing tools (e.g., MyPy) do not check for exceptions raised** - Current Python type checkers mostly ignore exception flow or explicitly declared exceptions. There is no built-in or standard way to express exception contracts in type hints. 4. **Custom exception classes for domain-specific errors** - For clarity and maintainability, if your interface or library can raise expected errors, you should define and document custom exception classes. - You communicate the valid exceptions by naming them in documentation and/or user guides. ### Why does Python not have explicit exception declarations on interfaces? - Python follows an **EAFP (Easier to Ask Forgiveness than Permission)** ethos. - Explicit exception declarations would add verbosity and complexity. - Pythonic style encourages **handling exceptions where you can recover** and letting others propagate upward naturally. - **Fail-fast philosophy** encourages letting the system raise unexpected exceptions during development and handling them appropriately in higher layers. ### Summary table: | Approach | Mechanism | Remarks | |------------------------|------------------------------|------------------------------------------------| | Expected exceptions | Documented in docstrings | Widely accepted convention | | ABC interface contract | Raise `NotImplementedError` | Defines required implementations, not errors raised in general | | Static typing | No standard exception syntax | No checked exceptions like in Java, C# | | Custom exceptions | Define exception classes | Clarifies error types, improves maintainability| | Runtime enforcement | Try/except handlers | Handle errors where recovery/alternative is feasible | ### Additional notes: - If you want to **make expected exceptions more discoverable**, consider tools that generate API docs (Sphinx, pdoc) that especially call out `:raises:` sections in your docstrings. - In complex frameworks, **middleware or wrapper layers** may catch and re-raise or convert exceptions for clearer error handling without explicit declaration in the interface. - Some third-party libraries or custom frameworks might support more formal error policies (contracts), but this is not core Python. **In essence:** **Docs are the only practical way to make expected exceptions explicit in Python interfaces today.** Writing thoughtful docstrings and custom exception classes is the accepted best practice. ABCs enforce method presence **not** exception contracts. If you want, I can help you draft a template for documenting expected exceptions clearly in your Python APIs. [1] https://docs.python.org/3/library/exceptions.html [2] https://realpython.com/python-built-in-exceptions/ [3] https://stackoverflow.com/questions/57658862/making-an-abstract-base-class-that-inherits-from-exception [4] https://docs.python.org/3/library/abc.html [5] https://mypy.readthedocs.io/en/stable/error_code_list.html [6] https://labex.io/tutorials/python-how-to-handle-abstract-method-exceptions-437221 [7] https://blog.sentry.io/practical-tips-on-handling-errors-and-exceptions-in-python/ [8] https://accuweb.cloud/resource/articles/explain-python-valueerror-exception-handling-with-examples
ABC Syntax Details #
we used to have the other abstract decorators:
@abstractclassmethod,@abstractstaticmethod,@abstractpropertybut they’re deprecated now because we can decorator stackwhen decorator stacking,
@abc.abstractmethodMUST be the innermost decoratorthe order of decorators matter.
1 2 3 4 5class MyABC(abc.ABC): @classmethod @abc.abstractmethod def an_abstract_classmethod(cls, ...): pass
Subclassing an ABC #
delegation of functions (e.g. init delegates to another ABC’s functions) seems to be a good idea to keep the code consistent
whether to override the concrete implementations from the ABC is our choice to make
A Virtual Subclass of an ABC #
Here’s an example of a subclass:
| |
it’s a “trust me bro” but if we lie, we still get caught by the usual runtime exceptions
issubclassandisinstancewill work but there’s no real inheritance of any methods or attributes from the ABC- this happens because inheritance is guided by the
__mro__class attribute ( for method resolution order ) and in this case, only “real” superclasses exist in the__mro__
- this happens because inheritance is guided by the
syntax:
usually a plain function invocation, can be done in a decorator style as well
Tombola.register(TomboList)function invocation style (called after the class definition)@Tombola.register(decorator style)
Usage of register in Practice #
Structural Typing with ABCs #
typically we use nominal typing for ABCs. it happens when we have an explicit inheritance, which registers a class with its parent and this links the name of the parent to the sub class and that’s how at runtime, we can do
issubclasschecks.Dynamic and Static Duck Typing are two approaches to static typing
we can do consistent-with structural subtyping as well if the class implements the methods defined in the type
this works because parent subclass (abc.Sized) implements a special class method named
__subclasshook__. The__subclasshook__forSizedchecks whether the class argument has an attribute named__len__this is the implementaion within ABCMeta
1 2 3 4 5 6@classmethod def __subclasshook__(cls, C): if cls is Sized: if any("__len__" in B.__dict__ for B in C.__mro__): return True return NotImplementedwe shouldn’t add the hook for our custom functions. It’s not dependable to rely on this implicit behaviour.
Static Protocols #
The Typed double Function #
- duck typing allows us to write code that is future-compatible!
Runtime Checkable Static Protocols aka Dynamic Protocol #
typing.Protocolcan be used for both static and runtime checkingif we want to use it for runtime checking, then we need to add
@runtime_checkableto the protocol definitionhow this works is that
typing.Protocolis an ABC and so it supports__subclass__hook and adding the runtime checkable decorator allows us to make the protocol supportisinstance/issubclasschecks. Because Protocol inherits from ABC-related machinery,@runtime_checkableallows the__subclasshook__to behave accordingly for runtime isinstance and issubclass checks.NOTE: it’s still checking for consistent-with to check if it’s the same type.
caveat: performance/side-effect trade-offs
Careful if side effects or expensive operations if methods checked by
__subclasshook__have such costs
ready to use runtime checkables:
- check numeric convertibility:
typing.SupportsComplex1 2 3 4 5 6 7 8@runtime_checkable class SupportsComplex(Protocol): """An ABC with one abstract method __complex__.""" __slots__ = () @abstractmethod def __complex__(self) -> complex: passRECIPE: TO_HABIT: if you want to test whether an object c is a complex or SupportsComplex, you can provide a tuple of types as the second arg to
isinstance:isinstance(c, (complex, SupportsComplex))I had no idea this was a thing.
alternatively, we can use the Complex ABC within the numbers module.
1 2import numbers isinstance(c, numbers.Complex)type checkers don’t seem to recognise the ABCs within the
numbersabctyping.SupportsFloat
- check numeric convertibility:
“Duck Typing is Your Friend”
Often, ducktyping is the better approach for runtime type checking. WE just try the operations you need to do on the object.
So in the complex number situation, we have a few approaches we could take:
approach: runtime checkable static protocols
1 2 3 4if isinstance(o, (complex, SupportsComplex)): # do something that requires `o` to be convertible to complex else: raise TypeError('o must be convertible to complex')approach: goose typing using
numbers.ComplexABC1 2 3 4if isinstance(o, numbers.Complex): # do something with `o`, an instance of `Complex` else: raise TypeError('o must be an instance of Complex')approach:⭐️ duck typing and the EAFP (Easier to ask for forgiveness principle).
1 2 3 4try: c = complex(o) except TypeError as exc: raise TypeError('o must be convertible to complex') from exc
Limitations of Runtime Protocol Checks #
@ runtime, type hints are ignored, so are
isinstanceandissubclasschecks against static protocolsproblem:
isinstance/issubclasschecks only look at the presence or absence of methods, without checking their signatures, much less their type annotations. That would have been too costly.this is because that type checking is not just a matter of checking whether the type of
xisT: it’s about determining that the type ofxis consistent-withT, which may be expensive.since they only do this, we can end up getting false positives on these type checks.
Supporting a Static Protocol #
the point below is now deprecated. We can just run it as is.
using
from __future__ import annotationsallows typehints to be stored as strings, without being evaluated at import time, when functions are evaluated.so if we were to define the return type as the same class that we’re building, then we would have to use this import else it’s a use-before-definition error.
Designing a Static Protocol #
trick: single-method protocols make static duck typing more useful and flexible
After a while, if you realise a more complete protocol is required, then you can combine two or more protocols to define a new one
example Here’s the protocol definition, it has a single function
1 2 3 4 5 6from typing import Protocol, runtime_checkable, Any @runtime_checkable class RandomPicker(Protocol): # NOTE the elipsis operator usage def pick(self) -> Any: ...
and here are some tests written for it
| |
- observations:
- not necessary to import the static protocol to define a class that implements it
Best Practices for Protocol Design #
Align with Interface Segregation Principle: clients should not be forecd to depend on interfaces they don’t use. This gives the two following advice:
Narrow interfaces (often with a single method) are more useful.
Client Code Protocols: Good to define the protocol near the “client code” (where it’s being used) instead of a library.
Useful for extensibility and mock-testing.
Naming:
just name based on nouns that make sense and is minimalistic, nothing too fancy here.
clear concept \(\rightarrow\) plain names (Iterator, Container)
gives callback methods \(\rightarrow\)
SupportsXe.g. SupportsReadread/write attrs or getter/setter methods \(\rightarrow\)
HasXeg. HasItems
Create Minimalistic protocols and extend them later by creating derived protocols
Extending a Protocol #
| |
instead of adding methods to the original protocol, it’s better to derive a new protocol from it.
keeps protocols minimal and aligns with Interface Segregation Principle – is really narrow interfaces here.
GOTCHA: not entirely the same as inheritance
the decorator
@runtime_checkableneeds to be re-appliedin the super class fields, we still need to add
Protocolalong with the rest of the protocols that we are extendingsimilar to inheritance: the functions being extended will be inherited by the derived class. We only need to indicate the new functions in the derived class.
The numbers ABCs and Numeric Protocols #
Objective: we want to be able to support static type checking, and we want to be able to do this for external libraries that register their types as virtual subclasses of
numbersABCs.Current Approach: use the numeric protocols within
typingmodulenumbers.Numberhas no methods \(\implies\) numeric tower not useful for static type checking (it’s useful for runtime type checking though)
GOTCHA:
decimal.Decimalis not registered as a virtual subclass of numbers.Real. The reason is that, if you need the precision of Decimal in your program, then you want to be protected from accidental mixing of decimals with floating-point numbers that are less precise.because real (floats) are less precise and we don’t wanna interchange with them and have information losses.
Takeaways:
The numbers ABCs are fine for runtime type checking, but unsuitable for static typing.
The numeric static protocols SupportsComplex, SupportsFloat, etc. work well for static typing, but are unreliable for runtime type checking when complex numbers are involved.
Chapter Summary #

contrasted dynamic protocols (that support duck typing) and static protocols (static duck typing)
for both, just need to implement necessary methods, no explicit registration needed
runtime effect:
Static protocol no runtime effect.
Dynamic protocol is runtime checkable. Aka when we
@runtime_checkablea static protocol, then it becomes a dynamic protocol.NOTE: this is a different contrast from Dynamic Duck Typing vs Static Duck typing
Dynamic Duck typing is the fail fast approach, where we “try and see it”
Static Duck Typing is the contract based use of Protocols
This is a subtle but often confusing distinction. Dynamic duck typing is Python’s inherent runtime behavior, while static duck typing reflects the formal contract via protocols at type-checking time
Python interpreter’s support for sequence and iterable dynamic protocols.
The interpreter uses special methods (
__getitem__,__iter__, etc.) dynamically to support iteration and membership tests even without explicit ABCs. This is a classic Python dynamic protocol idiom.monkey patching: adhering to the protocol @ runtime
defensive programming: detect structural types using
try/exceptand failing fast instead of explicit checks usingisinstanceorhasattrchecksIDIOM: This is a widely advocated Python idiom: “EAFP (Easier to Ask Forgiveness than Permission)”,
Goose typing:
creating and using ABCs
traditional subclassing and registration
__subclasshook__special method as a way for ABCs to support structural typing based on methods that fulfill interface define in the ABCs (without a direct registration)
Static protocols
is kind of the sttuctural interface in the python world.
@runtime_checkableactually leverages__subclasshoook__to support structural typing at runtime,though the best use of these protocols is with static type checkers.
type hints make structural typing more reliable.
design of static protocol:
- keep the narrow interface
- keep the definition near to usage
- extend it when you need to add functionality; in line with interface segregation principle.
Numbers ABCs and Numeric Protocols:
- numeric static protocols (e.g . SupportsFloats) has shortcomings
main message of this chapter is that we have four complementary ways of programming with interfaces in modern Python, each with different advantages and drawbacks.
You are likely to find suitable use cases for each typing scheme in any modern Python codebase of significant size.
Rejecting any one of these approaches will make your work as a Python programmer harder than it needs to be.
Possible Misconceptions #
Adjacent Gotchas and Difficult Concepts You Might Misconstrue or Overlook
Runtime Checking Limits of Dynamic Protocols: Runtime `isinstance` checks with `@runtime_checkable` protocols are limited to checking presence of attributes/methods (using `hasattr` internally) and do not verify method signatures, argument types, or behavior correctness. This can give false positives if method signatures do not match—only static type checkers guarantee that.
`_subclasshook_` Complexity and Pitfalls: While powerful, implementing or overriding `_subclasshook_` can be tricky because it must handle all subclass checks gracefully and correctly, respecting caching and fallback behaviors to avoid subtle bugs. Excessive or ill-considered use may confuse the MRO and class hierarchy assumptions.
Difference Between ABC Registration and Protocol Conformance: Registering a class as a virtual subclass of an ABC influences `isinstance` checks but does not affect static type checking, whereas protocols influence static (and optionally runtime) interface conformance. Bridging these self-consistently in a codebase can sometimes be confusing.
Protocols and Inheritance vs Nominal Typing: Protocols enable structural typing, eschewing nominal inheritance for interface compatibility, but this can lead to subtle type checking behaviors where classes unintentionally conform just by method names, masking incorrect assumptions. This requires developers to design protocols and type hints thoughtfully.
Static Type Checking Requires Adoption of Tooling: The benefits of static protocols are realized only when using type checkers; pure runtime execution won’t enforce protocols unless combined with runtime checkable features. Adoption means introducing additional tooling and some learning curve for teams.
Monkey Patching Risks: While useful at runtime for dynamic protocol adherence, monkey patching comes with maintainability and debugging risks, especially when changing behaviors of widely used or critical classes. It can also mask design flaws if overused.
Difference Between Static and Runtime Failure Modes: Static protocols help catch interface noncompliance early, but dynamic duck typing detects mismatches only at runtime, often deeper within program flow, affecting error locality and debuggability.
Supporting References
- Real Python: Python Protocols: Leveraging Structural Subtyping (2024)
- The Turing Taco Tales: Static Duck Typing With Python’s Protocols (2024)
- Xebia: Protocols In Python: Why You Need Them (2022)
- PEP 544 – Protocols: Structural Subtyping (2017) (Historical and spec source)
- Python official docs on typing and Abstract Base Classes
Mental Model Summary for You as a Tech Leader
Your notes effectively capture the layered nature of interface programming in Python:
- At the lowest layer, Python runtime embraces dynamic duck typing: just try it and fail fast.
- To improve runtime type recognition and interoperability, Python uses ABCs with virtual subclassing (`register`) and `_subclasshook_` (“goose typing”), enabling `isinstance` semantics on structural grounds.
- To further support static analysis tooling, Python offers static protocols that check structure without inheritance, giving formal contracts for type checkers.
- Finally, runtime-checkable protocols bridge these worlds, allowing runtime `isinstance` checks on protocols designed primarily for static typing.
Together, these patterns compose a robust, hybrid approach adaptable to many scales and requirements—**rejecting any will unnecessarily limit your Python design flexibility and safety guarantees**