Skip to content

Python 3.14: inspect.getfullargspec raises TypeError: unsupported callable for functions with TYPE_CHECKING only annotations (PEP 649) #322

Description

@kozlek

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions