Fix: Python Protocol Not Working — Type Checker Rejects Compatible Class, runtime_checkable Fails, or Protocol Not Recognized
Quick Answer
How to fix Python Protocol class issues — structural subtyping vs nominal typing, runtime_checkable, Protocol inheritance, TypeVar constraints, and common mypy/pyright errors with Protocol.
The Problem
A class that implements all the required methods is rejected by the type checker:
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
class Circle:
def draw(self) -> None:
print("Drawing circle")
def render(shape: Drawable) -> None:
shape.draw()
render(Circle()) # mypy error: Argument 1 has incompatible type "Circle"Or isinstance() raises a TypeError at runtime:
from typing import Protocol
class Serializable(Protocol):
def to_json(self) -> str: ...
obj = MyObject()
isinstance(obj, Serializable) # TypeError: Protocols with non-method members don't support issubclassOr a Protocol with a class variable isn’t matched:
class HasName(Protocol):
name: str # Class variable requirement
class Person:
def __init__(self, name: str):
self.name = name # Instance attribute
obj: HasName = Person("Alice") # Works in some type checkers, rejected in othersWhy This Happens
Python’s Protocol implements structural subtyping (duck typing with type checking), but has specific rules:
- Structural != nominal — unlike abstract base classes, you don’t need to
inheritfrom a Protocol. A class automatically satisfies a Protocol if it has the required attributes and methods with compatible signatures. But signatures must match exactly — return types, parameter types, and names all matter. isinstance()requires@runtime_checkable— by default, Protocols are only for static type checking. To use them withisinstance(), you must decorate the Protocol with@runtime_checkable. Even then, runtime checks only verify the presence of attributes/methods, not their signatures.- Class variables vs instance attributes — a Protocol that declares
name: strrequires that the class or instance has anameattribute of typestr. Instance attributes defined in__init__do satisfy this, but type checkers differ in how strictly they enforce this. - Method signatures must be compatible — if the Protocol requires
def process(self, value: int) -> str, a class methoddef process(self, value: float) -> strmay or may not satisfy it depending on variance rules.
Fix 1: Define Protocols Correctly
Protocol methods use ... or pass as the body — they define the interface, not the implementation:
from typing import Protocol
# WRONG — Protocol with actual implementation (should use ABC instead)
class Drawable(Protocol):
def draw(self) -> None:
print("default draw") # This becomes an implementation, not just a signature
# CORRECT — Protocol with ellipsis bodies
class Drawable(Protocol):
def draw(self) -> None: ...
def resize(self, factor: float) -> None: ...
# CORRECT — Protocol with properties
class HasArea(Protocol):
@property
def area(self) -> float: ...
@property
def perimeter(self) -> float: ...
# CORRECT — Protocol with class methods
class Factory(Protocol):
@classmethod
def create(cls, data: dict) -> 'Factory': ...Implementing a Protocol — no inheritance required:
class Circle:
def __init__(self, radius: float):
self.radius = radius
def draw(self) -> None:
print(f"Drawing circle with radius {self.radius}")
def resize(self, factor: float) -> None:
self.radius *= factor
# Circle implicitly satisfies Drawable — no need to inherit from it
def render(shape: Drawable) -> None:
shape.draw()
render(Circle(5.0)) # OK — Circle has .draw() and .resize()Fix 2: Enable runtime_checkable for isinstance Checks
If you need isinstance() checks at runtime, decorate the Protocol:
from typing import Protocol, runtime_checkable
# WRONG — no decorator, isinstance() raises TypeError
class Serializable(Protocol):
def to_json(self) -> str: ...
isinstance({}, Serializable) # TypeError!
# CORRECT — add @runtime_checkable
@runtime_checkable
class Serializable(Protocol):
def to_json(self) -> str: ...
class JsonDocument:
def to_json(self) -> str:
return '{"type": "document"}'
isinstance(JsonDocument(), Serializable) # True
isinstance({}, Serializable) # False — dict has no to_json methodWarning:
@runtime_checkableonly checks for the presence of attributes and methods — not their type signatures.isinstance(obj, Serializable)returnsTrueifobjhas ato_jsonattribute, even if it’s not callable or returns the wrong type.
Practical runtime_checkable pattern:
@runtime_checkable
class Closeable(Protocol):
def close(self) -> None: ...
def safe_close(resource: object) -> None:
if isinstance(resource, Closeable):
resource.close()
# No error if resource doesn't support closeFix 3: Fix Signature Mismatch Errors
Protocol method signatures must be compatible with the implementing class:
from typing import Protocol
class Processor(Protocol):
def process(self, data: str) -> int: ...
# WRONG — parameter name mismatch (mypy is lenient about this, pyright is strict)
class MyProcessor:
def process(self, input: str) -> int: # 'input' vs 'data'
return len(input)
# WRONG — return type incompatible
class BadProcessor:
def process(self, data: str) -> float: # float is not int
return len(data) * 1.5
# WRONG — extra required parameter
class AlsoWrong:
def process(self, data: str, encoding: str) -> int: # extra required param
return len(data)
# CORRECT — extra optional parameter is OK
class GoodProcessor:
def process(self, data: str, encoding: str = 'utf-8') -> int:
return len(data.encode(encoding))Use Protocol with TypeVar for generic protocols:
from typing import Protocol, TypeVar
T = TypeVar('T')
class Comparable(Protocol[T]):
def __lt__(self, other: T) -> bool: ...
def __le__(self, other: T) -> bool: ...
class Score:
def __init__(self, value: int):
self.value = value
def __lt__(self, other: 'Score') -> bool:
return self.value < other.value
def __le__(self, other: 'Score') -> bool:
return self.value <= other.value
def find_minimum(items: list[Comparable]) -> Comparable:
return min(items)Fix 4: Protocol Inheritance and Composition
Combine Protocols to build composite interfaces:
from typing import Protocol
class Readable(Protocol):
def read(self, n: int = -1) -> bytes: ...
class Writable(Protocol):
def write(self, data: bytes) -> int: ...
class Seekable(Protocol):
def seek(self, pos: int) -> int: ...
def tell(self) -> int: ...
# Compose protocols
class ReadWritable(Readable, Writable, Protocol): ...
class BinaryFile(Readable, Writable, Seekable, Protocol): ...
# A class satisfying BinaryFile must implement all methods from all three protocols
import io
def process_file(f: BinaryFile) -> None:
data = f.read(1024)
f.seek(0)
f.write(data)
# io.BytesIO satisfies BinaryFile
process_file(io.BytesIO(b"test data")) # OKExtend Protocol with abstract behavior:
from typing import Protocol
from abc import abstractmethod
class Repository(Protocol):
@abstractmethod
def find_by_id(self, id: int): ...
@abstractmethod
def save(self, entity): ...
def find_all(self):
# Protocol CAN have default implementations
# Subclasses can override or use this default
raise NotImplementedErrorFix 5: Protocols with Class Variables and Properties
Declaring class-level variables and properties in a Protocol:
from typing import Protocol, ClassVar
class Configuration(Protocol):
# Instance attribute — implementing class must have this as instance or class attr
debug: bool
# Class variable — must be on the class, not the instance
VERSION: ClassVar[str]
# Property
@property
def name(self) -> str: ...
class AppConfig:
VERSION = "1.0.0" # Class variable ✓
def __init__(self, debug: bool = False):
self.debug = debug # Instance attribute ✓
@property
def name(self) -> str: # Property ✓
return "MyApp"
# AppConfig satisfies Configuration
config: Configuration = AppConfig() # OKDataclass as Protocol implementation:
from dataclasses import dataclass
from typing import Protocol
class Point2D(Protocol):
x: float
y: float
@dataclass
class CartesianPoint:
x: float
y: float
@dataclass
class PolarPoint:
r: float
theta: float
@property
def x(self) -> float: # Property satisfies 'x: float' in Protocol
import math
return self.r * math.cos(self.theta)
@property
def y(self) -> float:
import math
return self.r * math.sin(self.theta)
def distance_from_origin(p: Point2D) -> float:
return (p.x ** 2 + p.y ** 2) ** 0.5
distance_from_origin(CartesianPoint(3.0, 4.0)) # 5.0
distance_from_origin(PolarPoint(5.0, 0.927)) # ~5.0Fix 6: Explicit Protocol Implementation with Typing
When you want to be explicit that a class implements a Protocol (useful for documentation and early error detection):
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
# Option 1: Inherit from Protocol (makes it explicit but class is now a Protocol itself)
class Circle(Drawable, Protocol): # This makes Circle a Protocol, not an implementation!
... # DON'T do this for concrete classes
# Option 2: Use runtime_checkable + assert in tests
@runtime_checkable
class Drawable(Protocol):
def draw(self) -> None: ...
class Circle:
def draw(self) -> None:
print("Drawing circle")
# In tests — verify implementation at development time
assert isinstance(Circle(), Drawable), "Circle must implement Drawable"
# Option 3: TYPE_CHECKING guard for explicit annotation
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import assert_type
c = Circle()
assert_type(c, Drawable) # Type checker error if Circle doesn't satisfy Drawable
# Option 4: Python 3.12+ — @override for method implementations
from typing import override
class Circle:
@override # Explicit, but only works with class inheritance — not Protocols
def draw(self) -> None:
print("Drawing circle")Protocol with __init__ — use a factory Protocol instead:
# Protocols can't constrain __init__ signatures meaningfully
# Use a factory Protocol instead:
class WidgetFactory(Protocol):
def __call__(self, width: int, height: int) -> 'Widget': ...
class ButtonFactory:
def __call__(self, width: int, height: int) -> 'Widget':
return Button(width, height)
def create_widget(factory: WidgetFactory, w: int, h: int) -> 'Widget':
return factory(w, h)Still Not Working?
mypy and pyright disagree on Protocol compatibility — mypy and pyright implement Protocol checking with slightly different strictness. If one passes and the other fails, check: (1) property vs attribute compatibility, (2) contravariance/covariance in method parameters, and (3) whether you have strict = true in your config. pyright is generally stricter.
Protocol check fails for __dunder__ methods — some special methods require exact signatures. For example, __len__ must return int, not float. Check the exact signature required by the Protocol and match it precisely.
Callable Protocol for function-like objects — if you need to type a callable with a specific signature, use Callable from typing or a Protocol with __call__:
from typing import Callable, Protocol
# Using Callable — simpler for plain functions
Handler = Callable[[str, int], bool]
# Using Protocol — allows for additional attributes on the callable
class HandlerProtocol(Protocol):
retry_count: int # Callable with extra attributes
def __call__(self, event: str, code: int) -> bool: ...TypeVar bound to Protocol for generic functions:
from typing import TypeVar, Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
T = TypeVar('T', bound=Drawable)
def draw_twice(shape: T) -> T:
shape.draw()
shape.draw()
return shape # Returns the same type as input, not just Drawable
circle: Circle = draw_twice(Circle()) # Returns Circle, not DrawableFor related Python type hint issues, see Fix: Python mypy Type Error and Fix: Python Decorator Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Python contextmanager Not Working — GeneratorExit, Missing yield, or Cleanup Not Running
How to fix Python context manager issues — @contextmanager generator, __enter__ and __exit__, exception handling inside with blocks, async context managers, and common pitfalls.
Fix: Python pathlib Not Working — Path Object Errors, Joins, and Common Pitfalls
How to fix Python pathlib issues — TypeError with string concatenation, path joining, glob patterns, reading files, cross-platform paths, and migrating from os.path.
Fix: Python asyncio.gather Not Handling Errors — Exceptions Swallowed or All Tasks Cancelled
How to fix asyncio.gather error handling — return_exceptions parameter, partial failures, task cancellation propagation, TaskGroup alternatives, and exception isolation patterns.
Fix: Python Decorator Not Working — Function Signature Lost or Decorator Not Applied
How to fix Python decorator issues — functools.wraps, decorator factories with arguments, class decorators, stacking order, async function decorators, and common pitfalls.