Skip to main content
  1. Readings/
  2. Books/
  3. Fluent Python: Clear, Concise, and Effective Programming – Luciano Ramalho/

Chapter 13. Interfaces, Protocols, and ABCs

··5447 words·26 mins
  • python has 4 ways to define and use interfaces:

    1. Duck typing

    2. goose typing: using ABCs

      ^ focus of this chapter

    3. static typing: traditional static typing using the typing module

    4. static 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:

  1. runtime vs static checking

  2. 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.Protocol

  • ABCs 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:

    1. 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

    2. Defensive code leveraging duck types can also include logic to handle different types without using isinstance() or hasattr() tests.

      • suppose we want to type hint that “field_names must 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
        10
        
               Example 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() and issubclass()

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 register class-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:
    1. 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.

    2. 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.

    3. 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.

    4. 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.abc module (most widely used), io package, numbers package

  • from 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) returns False, you can be certain that obj is not hashable. But if the return is True, 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 over obj using __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:

    1. 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 abstract pick function and load to return it to the original state.

    2. Before ABCs existed, abstract methods would raise NotImplementedError to signal that subclasses were responsible for their implementation.

    3. NOTE: an @abstractmethod method can have a base implementation. The subclass will still need to override it but the subclass will also be able to access it using super() and build onto / directly use the super functionality.

    4. 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, @abstractproperty but they’re deprecated now because we can decorator stack

  • when decorator stacking, @abc.abstractmethod MUST be the innermost decorator

    the order of decorators matter.

    1
    2
    3
    4
    5
    
      class 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from random import randrange

from tombola import Tombola

@Tombola.register  # <1> NOTE: being used as a decorator here, could have been a plain function invocation as well
class TomboList(list):  # <2>

    def pick(self):
        if self:  # <3>
            position = randrange(len(self))
            return self.pop(position)  # <4>
        else:
            raise LookupError('pop from empty TomboList')

    load = list.extend  # <5>

    def loaded(self):
        return bool(self)  # <6>

    def inspect(self):
        return tuple(self)

# Tombola.register(TomboList)  # <7>
  • it’s a “trust me bro” but if we lie, we still get caught by the usual runtime exceptions

  • issubclass and isinstance will 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__
  • 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 issubclass checks.

  • 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__ for Sized checks 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 NotImplemented
      
    • we 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.Protocol can be used for both static and runtime checking

    if we want to use it for runtime checking, then we need to add @runtime_checkable to the protocol definition

    how this works is that typing.Protocol is an ABC and so it supports __subclass__ hook and adding the runtime checkable decorator allows us to make the protocol support isinstance / issubclass checks. Because Protocol inherits from ABC-related machinery, @runtime_checkable allows 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.SupportsComplex

        1
        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:
                              pass
        
        • RECIPE: 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
          2
          
                  import numbers
                  isinstance(c, numbers.Complex)
          

          type checkers don’t seem to recognise the ABCs within the numbers abc

        • typing.SupportsFloat

  • “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
      4
      
          if 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.Complex ABC

      1
      2
      3
      4
      
          if 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
      4
      
          try:
                  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 isinstance and issubclass checks against static protocols

  • problem: isinstance / issubclass checks 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 x is T: it’s about determining that the type of x is consistent-with T, 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 annotations allows 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.

    this is the postponed evaluation of annotations

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
    6
    
      from 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

 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
import random
from typing import Any, Iterable, TYPE_CHECKING

from randompick import RandomPicker  # <1>

class SimplePicker:  # <2>
    def __init__(self, items: Iterable) -> None:
        self._items = list(items)
        random.shuffle(self._items)

    def pick(self) -> Any:  # <3>
        return self._items.pop()

def test_isinstance() -> None:  # <4>
    popper: RandomPicker = SimplePicker([1])  # <5>
    assert isinstance(popper, RandomPicker)  # <6>

def test_item_type() -> None:  # <7>
    items = [1, 2]
    popper = SimplePicker(items)
    item = popper.pick()
    assert item in items
    if TYPE_CHECKING:
        reveal_type(item)  # <8>
    assert isinstance(item, int)
  • observations:
    1. not necessary to import the static protocol to define a class that implements it

Best Practices for Protocol Design #

  1. Align with Interface Segregation Principle: clients should not be forecd to depend on interfaces they don’t use. This gives the two following advice:

    1. Narrow interfaces (often with a single method) are more useful.

      Ref Martin Fowler post on role interfaces

    2. 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.

  2. 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\) SupportsX e.g. SupportsRead

    • read/write attrs or getter/setter methods \(\rightarrow\) HasX eg. HasItems

  3. Create Minimalistic protocols and extend them later by creating derived protocols

Extending a Protocol #

1
2
3
4
5
6
from typing import Protocol, runtime_checkable
from randompick import RandomPicker

@runtime_checkable  # <1> need to reimport, this won't get inherited
class LoadableRandomPicker(RandomPicker, Protocol):  # <2> have to define the Protocol
    def load(self, Iterable) -> None: ...  # <3> OOP-like, only need to include the extended function, the super protocol's functions will be "inherited"
  • 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_checkable needs to be re-applied

    • in the super class fields, we still need to add Protocol along with the rest of the protocols that we are extending

    • similar 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 numbers ABCs.

  • Current Approach: use the numeric protocols within typing module

  • numbers.Number has no methods \(\implies\) numeric tower not useful for static type checking (it’s useful for runtime type checking though)

  • GOTCHA: decimal.Decimal is 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:

    1. The numbers ABCs are fine for runtime type checking, but unsuitable for static typing.

    2. 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 #

  1. contrasted dynamic protocols (that support duck typing) and static protocols (static duck typing)

    1. for both, just need to implement necessary methods, no explicit registration needed

    2. runtime effect:

      Static protocol no runtime effect.

      Dynamic protocol is runtime checkable. Aka when we @runtime_checkable a static protocol, then it becomes a dynamic protocol.

    3. 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

  2. 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.

  3. monkey patching: adhering to the protocol @ runtime

  4. defensive programming: detect structural types using try/except and failing fast instead of explicit checks using isinstance or hasattr checks

    IDIOM: This is a widely advocated Python idiom: “EAFP (Easier to Ask Forgiveness than Permission)”,

  5. 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)

  6. Static protocols

    • is kind of the sttuctural interface in the python world.

    • @runtime_checkable actually 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.
  7. Numbers ABCs and Numeric Protocols:

    • numeric static protocols (e.g . SupportsFloats) has shortcomings
  8. 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

  1. 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.

  2. `_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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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**

Further Reading #