Python/Type Hints/Protocols & Structural Typing

Protocols & Structural Typing

A Protocol defines an interface by the methods and attributes a type must have, so any class that structurally matches is accepted by a type checker — duck typing, verified statically, without inheritance.

Intermediate30mInterview Production

Learning objectives

  • Distinguish nominal subtyping (inheritance) from structural subtyping (shape)
  • Define a `typing.Protocol` and use it as a static interface
  • Use `@runtime_checkable` for isinstance checks and know its limits
  • Choose between Protocols and ABCs, and apply Protocols for dependency inversion

Prerequisites

Type hints & annotationsClasses & inheritanceDuck typing

Version notes

  • · `typing.Protocol` was added in Python 3.8 (PEP 544); on 3.7 and earlier it is available from `typing_extensions`.
  • · `@typing.runtime_checkable` (also 3.8+) enables `isinstance`/`issubclass` against a Protocol, but only checks that the named methods exist, not their signatures.
  • · From Python 3.12 you can write generic Protocols with the type-parameter syntax, e.g. `class Getter[T](Protocol): ...`.

Simple explanation

What it is. A Protocol is a class (subclassing `typing.Protocol`) that declares an interface — a set of methods and attributes with their signatures — without any implementation. A type checker treats any object that *has* those members as a valid instance of the Protocol, whether or not it inherits from it.

Why it exists. Python code relies on duck typing ('if it has .read(), it's file-like'), but plain type hints are nominal: `x: Reader` normally requires x to be a Reader subclass. Protocols let the type system express duck typing, so you get static checking without forcing every provider to import and inherit your interface.

The problem it solves. It decouples code from concrete classes. A function can declare it needs 'something with a read() -> str method' and accept files, sockets, StringIO, or a test double — all checked at build time. This is dependency inversion done with types instead of a base class.

How it works. Define `class Readable(Protocol): def read(self) -> str: ...` and annotate parameters with it. Any class implementing `read` matches structurally — no `class MyReader(Readable)` needed. mypy or pyright verify conformance during analysis. Add `@runtime_checkable` if you also need `isinstance(obj, Readable)` at runtime, remembering it only checks method names exist.

Analogy: A job description versus a diploma. Nominal typing hires by diploma — 'you must be a graduate of *this* school (base class)'. Structural typing (Protocols) hires by ability — 'you can perform these tasks (methods), so you qualify', regardless of where you learned them.

Common misconceptions

  • "A class must inherit from the Protocol to satisfy it." — No; matching the members is enough. Inheriting is optional and only useful to make the checker flag you if you drift out of conformance.
  • "@runtime_checkable makes isinstance fully validate the Protocol." — It only checks that the named methods *exist*; it ignores signatures, argument types, and return types. A method with the wrong signature still passes the isinstance check.
  • "Protocols replace ABCs." — They overlap but differ: ABCs use nominal inheritance and can provide shared implementation; Protocols are structural and implementation-free. Use ABCs when you own the hierarchy and share code; Protocols when you want to accept types you do not control.
  • "Protocols behave like Java interfaces at runtime." — They are primarily a static-checking construct; without @runtime_checkable a Protocol has no isinstance behaviour at all, and even with it the check is shallow.

Common alternatives

  • Abstract base classes (abc.ABC) when you control the implementers and want shared code or nominal enforcement
  • Plain duck typing with no annotations — works at runtime, but gives no static guarantees
  • Concrete base classes or a dependency-injection framework for wiring implementations together

Code examples

BeginnerDefine and match a Protocol
python
Loading editor…

Note: A type checker accepts FileLike because its shape matches — no inheritance from Readable required.

Behind the scenes

  • `Protocol` is built on a metaclass (`_ProtocolMeta`); classes that subclass Protocol are marked `_is_protocol = True` and cannot be instantiated directly.
  • Structural conformance is enforced by the *type checker* (mypy/pyright) at analysis time — at runtime a Protocol has no effect unless you add `@runtime_checkable`.
  • `@runtime_checkable` installs a `__subclasshook__` that checks only for the presence of the required methods (roughly `hasattr`); it deliberately ignores signatures and, historically, non-method data members.
  • You can subclass a Protocol explicitly (`class C(Readable)`) to opt into nominal checking too — the checker then errors if C stops matching — but it stays structural for everyone else.
  • Protocols can be generic (`class Container(Protocol[T])`), can declare attributes and properties, and may include default method bodies that act as a mixin only when a class explicitly inherits the Protocol.

Interview insights

Key definitions

  • · Structural subtyping: 'if it has the right shape (methods/attributes), it is a subtype' — compatibility judged by structure, not declared inheritance.
  • · Protocol: a typing construct (PEP 544) that defines an interface checked structurally by static type checkers.
What is the difference between nominal and structural subtyping, and where do Protocols fit?

Concise answer: Nominal subtyping decides compatibility by declared inheritance (B is an A only if it subclasses A). Structural subtyping decides by shape (B is an A if it has A's members). Protocols bring structural typing to Python's static checkers — duck typing that mypy can verify.

Detailed answer: Regular class-based hints are nominal: `x: Base` requires x to be a Base subclass, which forces providers to import and inherit your interface and creates coupling. A Protocol declares required members; any class with matching members satisfies it, no inheritance required. That mirrors how Python actually works (duck typing) while giving static guarantees. ABCs are nominal (you inherit or register); Protocols are structural.

Common mistake: Assuming a class must inherit from the Protocol to be accepted — structural matching needs no inheritance.

Follow-ups:

  • · When would you still reach for an ABC?
  • · How does @runtime_checkable change the picture?
What does @runtime_checkable let you do, and what are its limits?

Concise answer: It enables isinstance()/issubclass() against a Protocol at runtime. The limit: it only checks that the named methods exist — it ignores signatures, argument/return types, and (for isinstance) non-method data attributes.

Detailed answer: By default Protocols are static-only, and isinstance against one raises TypeError. Decorating with @runtime_checkable adds a __subclasshook__ so isinstance works by checking for each required method. Because the check is shallow, a class whose method has the wrong signature still passes. Use it for coarse runtime guards, not to validate full conformance — rely on mypy/pyright for that.

Common mistake: Trusting isinstance(x, SomeProtocol) to guarantee correct signatures — it only confirms the method names are present.

Follow-ups:

  • · Why can't it check signatures?
  • · Does it check data attributes?
When would you choose a Protocol over an abstract base class (ABC)?

Concise answer: Use a Protocol when you want to accept types you do not own or cannot modify and need no shared implementation — structural matching means providers never import your interface. Use an ABC when you control the hierarchy and want nominal enforcement or shared code.

Detailed answer: Protocols shine for dependency inversion: your code says 'I need something with these methods', and callers pass anything that fits — stdlib types, third-party classes, test doubles — with the checker verifying. ABCs are better when you own the implementers, want to share method bodies (mixins), or want explicit registration and nominal errors. They are not exclusive: a class can inherit an ABC and still satisfy other Protocols structurally.

Common mistake: Forcing every provider to inherit an ABC when a Protocol would decouple them.

Follow-ups:

  • · Can a class satisfy a Protocol and an ABC at once?
  • · How do generic Protocols work?

Knowledge check

Multiple choiceQuestion 1 of 3

For a class to satisfy a typing.Protocol (as checked by mypy), it must:

What's the output?Question 2 of 3

What does this print?

python
from typing import Protocol, runtime_checkable

@runtime_checkable
class Sized(Protocol):
    def __len__(self) -> int: ...

print(isinstance([1, 2, 3], Sized))
print(isinstance(123, Sized))
ScenarioQuestion 3 of 3

You want a function to accept any object with a `.read() -> str` method, including third-party classes you cannot modify. Best tool?

Score: 0/3