Skip to content

Fix RecursionError when checking an Instance with a self-referential __call__ attribute against a Callable type - #22400

Closed
Shanmukh-l33t wants to merge 1 commit into
python:masterfrom
Shanmukh-l33t:fix/22398-subtypes-visit-instance-recursion
Closed

Shanmukh-l33t wants to merge 1 commit into
python:masterfrom
Shanmukh-l33t:fix/22398-subtypes-visit-instance-recursion

Conversation

@Shanmukh-l33t

Copy link
Copy Markdown

Fixes #22398.

visit_instance() special-cases Instance <: Callable / Overloaded by following the __call__ member (mypy/subtypes.py). When that member (transitively) refers back to its own class — class C: __call__: C — the check recurses forever, so

from typing import Callable

class C:
    __call__: "C"

def h(cb: Callable[[], int]) -> None: ...

h(C())

crashes mypy with RecursionError on pure-Python builds and segfaults (exit 139) on compiled wheels, exactly as reported in the issue.

Fix

Track the TypeInfos whose __call__ is currently being followed in SubtypeContext (shared down the whole subtype query, since each recursion level constructs a fresh SubtypeVisitor but keeps the context). When the chain loops back to a class already being followed, return False: such an instance cannot be called at runtime either, so it is simply not a subtype of that Callable type. The caller then reports the normal error the issue asks for:

repro.py:12: error: Argument 1 to "h" has incompatible type "C"; expected "Callable[[], int]"
repro.py:12: note: "C.__call__" has type "C"

This complements #22362 (which guards the check_call() path in checkexpr.py); this path is subtypes.py only, as noted in the issue. The guard is scoped to the FunctionLike branch and popped in a finally, so it cannot leak into unrelated checks; sibling branches (protocol __call__, nominal checks) are untouched.

Covered variants: direct (__call__: C), Optional[C], Self, generic G[list[T]] (type args grow each round, so a pair-based guard would not fire), mutually-referencing X/Y, plus assignment-position checks, and positive controls (def __call__ classes still check clean).

Testing

  • New data-driven test testSubtypeOfCallableWithSelfReferentialCallAttribute in test-data/unit/check-classes.test: fails at parent 5dc2840 with RecursionError / INTERNAL ERROR (the exact reported crash) and passes with the fix.
  • Full suite (pytest mypy/test -q -n 2): 12574 passed, 362 skipped, 10 xfailed; the 3 failures (test_find_sources_exclude, 2× testoutput JSON-parallel) are environment-only — they spawn python -m mypy.build_worker in a venv where mypy is not pip-installed — and reproduce identically with this change stashed.
  • Targeted subsets: check-classes.test 619 passed / 1 xfailed; check-callable, check-expressions, check-protocols, check-generic-subtyping 499 passed.
  • black --check, ruff check, and mypy --config-file mypy_self_check.ini mypy/subtypes.py all clean.

Note: I built this against master 5dc2840 (the revision in the issue) before #22362 landed; the two changes touch different files, so they merge independently.

…attribute is checked against a Callable type (python#22398)

visit_instance() special-cases Instance <: Callable/Overloaded by
following the __call__ member. When that member (transitively) refers
back to its own class (e.g. "class C: __call__: C"), the check
recurses forever: h(C()) crashes mypy with RecursionError (or a segfault
in compiled builds).

Track the TypeInfos whose __call__ is currently being followed in
SubtypeContext (shared across a single subtype query) and return False
when the chain loops back: such an instance cannot be called at runtime
either, so it is simply not a subtype of the Callable type, and the
caller reports the usual arg-type error instead of crashing.

Complements python#22362, which guards the check_call() path in checkexpr.py.
@ilevkivskyi ilevkivskyi closed this Nov 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Crash on a class whose __call__ attribute is its own type, passed where a Callable is expected

2 participants