Skip to content

Fix type variable reported unbound for ParamSpec alias return type - #22423

Open
xh123637 wants to merge 1 commit into
python:masterfrom
xh123637:fix/22399-paramspec-alias-unbound-typevar
Open

xh123637 wants to merge 1 commit into
python:masterfrom
xh123637:fix/22399-paramspec-alias-unbound-typevar

Conversation

@xh123637

@xh123637 xh123637 commented Nov 9, 2026

Copy link
Copy Markdown

Fixes #22399

When a function return type is a generic alias over Callable whose
ParamSpec slot is filled with a concrete parameter list, e.g.
Alias[[int], T] where Alias = Callable[P, T], mypy reported
Type variable "T" is unbound (or Name "T" is not defined for the
PEP 695 type form), even though the equivalent literal
Callable[[int], T] is accepted. pyright, pyrefly and ty all accept
the alias form.

Two things combined to produce the error:

  • infer_type_variables scans the return type with
    include_callables = False, so FindTypeVarVisitor returns early
    on the alias application (its first argument looks like a callable
    argument list) and never collects T.
  • The TypeAlias branch of visit_unbound_type_nonoptional then
    analyzes the alias arguments in the enclosing scope before the
    alias is expanded, so the unbound T errors out before expansion.

The fix mirrors how a literal Callable is handled: when the alias
target is a CallableType and its arguments contain a type variable
that is not bound in the enclosing scope, expand the alias and
analyze the resulting callable, so the callable itself binds the
free type variable — exactly as Callable[[int], T] does. That is
why Alias[[int], T] now reveals as
def () -> def [T] (int) -> T, with T bound to the inner callable
and not to the outer function.

A second pass (ResolveUnboundTypeVars) replaces any remaining
UnboundType occurrences inside the analyzed callable with the
callable's own type variables of the same name. Analyzing a callable
does not recurse into already-analyzed instances or alias
applications, so without it a type variable nested inside a generic
instance in the alias target (e.g. Chain[P, T] in the motivating
example from the issue) stayed unresolved and broke generic
inference. If the alias cannot be expanded to a callable, the code
falls back to the previous analysis unchanged.

Disclosure: this PR was developed with the assistance of an LLM
(Muse), with the fix verified against the test suites listed below.

Tests run (all with the repo's pytest data-driven suites):

  • New tests: testParamSpecAliasUnboundTypeVar
    (check-parameter-specification.test) and
    testPEP695ParamSpecAliasUnboundTypeVar
    (check-python312.test). Both fail on unmodified code and pass
    with this change.
  • check-parameter-specification.test, check-python312.test,
    check-generic-alias.test, check-type-aliases.test,
    check-typevar-unbound.test: 373 passed.
  • check-generics.test, check-callable.test,
    check-functions.test: 518 passed, 2 skipped.
  • Full mypy/test/testcheck.py: 8318 passed, 27 skipped,
    7 xfailed.
  • mypy/test/testsemanal.py -k "typealiases or paramspec":
    28 passed.
  • python -m mypy --config-file mypy_self_check.ini mypy/typeanal.py: no issues.

@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

Diff from mypy_primer, showing the effect of this PR on open source code:

pywin32 (https://github.com/mhammond/pywin32)
  ./pythonwin/pywin/test/_dbgscript.py: error: INTERNAL ERROR -- Please try using mypy master on GitHub:
- version: 2.5.0+dev.1e2ea5350dd8fa4d44a1d381b1ae0097d880ed2d
+ version: 2.5.0+dev.7d5b2b3f0d73599f10bbac8393ee465d1cd91044

@xh123637

Copy link
Copy Markdown
Author

Primer note on the pywin32 INTERNAL ERROR in pythonwin/pywin/test/_dbgscript.py: this is pre-existing on the base commit (1e2ea53), not caused by this PR. I reproduced it locally with both the base build and this PR's build — both crash identically in the native parser (mypy/nativeparse.py::read_func_def, ValueError: invalid bool value) before any type checking runs, so it is unrelated to the typeanal.py change here. The primer diff likewise shows the error line unchanged between base and PR (only the version string differs). No change made.

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.

Type variable reported unbound when only a ParamSpec alias's return type uses it and its ParamSpec slot is not a ParamSpec

1 participant