Summary
On Python 3.14, decorating a function (or registering a side effect) whose signature
contains a parameter annotated with a name imported only under if TYPE_CHECKING:
fails with TypeError: unsupported callable.
Python 3.14 ships PEP 649 (deferred evaluation of
annotations): annotations are no longer evaluated at definition time. respx introspects
decorated callables with inspect.getfullargspec(...) to detect parameters such as
respx_mock and route — and getfullargspec forces evaluation of the annotations.
When a parameter is annotated with a TYPE_CHECKING-only name, that evaluation raises
NameError, which getfullargspec re-raises as TypeError: unsupported callable.
The if TYPE_CHECKING: import pattern is extremely common (e.g. typing a db fixture
as sqlalchemy.ext.asyncio.AsyncSession without importing SQLAlchemy at runtime), so
this breaks a lot of otherwise-valid test code — typically at pytest collection time,
since @respx.mock introspects the function when the decorator is applied.
Reproduction
from typing import TYPE_CHECKING
import respx
if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession
@respx.mock
def test_thing(respx_mock, db: AsyncSession) -> None:
...
Importing this module under Python 3.14 raises immediately (at the @respx.mock line).
The same happens for a side-effect callable that declares route, e.g.
def my_side_effect(request, route): ..., since it's introspected the same way.
Traceback
Traceback (most recent call last):
File ".../annotationlib.py", line 1180, in _get_dunder_annotations
ann = getattr(obj, "__annotations__", None)
File "repro.py", line 9, in __annotate__
def test_thing(respx_mock, db: AsyncSession) -> None:
^^^^^^^^^^^^
NameError: name 'AsyncSession' is not defined
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "repro.py", line 8, in <module>
@respx.mock
^^^^^^^^^^
File ".../respx/router.py", line 397, in __call__
argspec = inspect.getfullargspec(func)
File ".../inspect.py", line 1277, in getfullargspec
raise TypeError('unsupported callable') from ex
TypeError: unsupported callable
Cause
inspect.getfullargspec() builds a full signature with annotations, which under PEP 649
evaluates them eagerly. respx only needs the parameter names (and positional defaults),
not the annotation values.
Possible fix
Read the parameter names without evaluating annotations — e.g. via inspect.signature(),
passing annotation_format=annotationlib.Format.FORWARDREF on Python 3.14+ so unresolved
names degrade to ForwardRef placeholders instead of raising (annotationlib and the
annotation_format argument are 3.14+, so the call needs to be version-guarded).
Environment
- respx 0.23.1
- Python 3.14
- httpx (current)
Summary
On Python 3.14, decorating a function (or registering a side effect) whose signature
contains a parameter annotated with a name imported only under
if TYPE_CHECKING:fails with
TypeError: unsupported callable.Python 3.14 ships PEP 649 (deferred evaluation of
annotations): annotations are no longer evaluated at definition time. respx introspects
decorated callables with
inspect.getfullargspec(...)to detect parameters such asrespx_mockandroute— andgetfullargspecforces evaluation of the annotations.When a parameter is annotated with a
TYPE_CHECKING-only name, that evaluation raisesNameError, whichgetfullargspecre-raises asTypeError: unsupported callable.The
if TYPE_CHECKING:import pattern is extremely common (e.g. typing adbfixtureas
sqlalchemy.ext.asyncio.AsyncSessionwithout importing SQLAlchemy at runtime), sothis breaks a lot of otherwise-valid test code — typically at pytest collection time,
since
@respx.mockintrospects the function when the decorator is applied.Reproduction
Importing this module under Python 3.14 raises immediately (at the
@respx.mockline).The same happens for a side-effect callable that declares
route, e.g.def my_side_effect(request, route): ..., since it's introspected the same way.Traceback
Cause
inspect.getfullargspec()builds a full signature with annotations, which under PEP 649evaluates them eagerly. respx only needs the parameter names (and positional defaults),
not the annotation values.
Possible fix
Read the parameter names without evaluating annotations — e.g. via
inspect.signature(),passing
annotation_format=annotationlib.Format.FORWARDREFon Python 3.14+ so unresolvednames degrade to
ForwardRefplaceholders instead of raising (annotationliband theannotation_formatargument are 3.14+, so the call needs to be version-guarded).Environment