Skip to content

Fix false positive for super() call with constrained type variable - #21931

Open
Ananthr16 wants to merge 1 commit into
python:masterfrom
Ananthr16:fix/constrained-typevar-super-false-positive
Open

Fix false positive for super() call with constrained type variable#21931
Ananthr16 wants to merge 1 commit into
python:masterfrom
Ananthr16:fix/constrained-typevar-super-false-positive

Conversation

@Ananthr16

Copy link
Copy Markdown

Fixes #17757.
Fixes #14774.

Problem

super().<method>(...) inside a method of a class with a value-restricted (constrained) type variable raises a false-positive arg-type error, even when the argument's type matches the constrained type variable exactly:

from typing import Generic, TypeVar

T = TypeVar("T", float, int)

class C(Generic[T]):
    def __init__(self, i: T) -> None:
        self.i: T = i

class B(C[T]):
    def __init__(self, i: T) -> None:
        super().__init__(i)  # false positive
error: Argument 1 to "__init__" of "C" has incompatible type "float"; expected "T"
error: Argument 1 to "__init__" of "C" has incompatible type "int"; expected "T"

Same root cause with a distinct subclass type variable bound through the base class (#17757's repro):

class C2(C[N]):  # N = TypeVar("N", int, float)
    def __init__(self, c: N):
        super().__init__(c)  # same false positive

Root cause

checker.py's expand_typevars type-checks a method with a value-restricted type variable once per constraint value, producing a copy of the function via expand_func with that value substituted everywhere it appears in the function's own AST (parameter types, local variable types, etc.).

The self/cls argument is usually unannotated, so its Var.type is None -- there's nothing there for expand_func's substitution to rewrite. When checkexpr.py's _super_arg_types resolves a zero-argument super(), it falls back to fill_typevars(e.info), which recomputes the self type fresh from the class's TypeInfo -- entirely unaware of which constraint value is currently being checked. analyze_member_access then substitutes the base class's generic parameter using this unsubstituted self type, so the expected argument type for the inherited method stays the literal, unsubstituted type variable (T/N), while the actual argument (correctly substituted by expand_func) is a concrete member type (float/int). Hence the mismatch, on every one of the type variable's constraint values.

(There's also a separate, subtler version of the same gap: even an explicitly annotated self argument's substituted type is inaccessible from _super_arg_types, because check_func_def pushes the original defn onto the checker scope for body-checking -- not the substituted copy expand_typevars produced -- so self.chk.scope.current_function() can never see the substitution either way.)

Fix

  • expand_typevars now also returns the type variable substitution mapping alongside each (FuncItem, CallableType) pair it produces.
  • check_func_def uses that mapping to compute the correctly self/cls type for the copy currently being checked (TypeChecker.self_type_for_expansion), and stores it on a new self.expanding_self_type attribute for the duration of checking that copy's body.
  • _super_arg_types consults self.chk.expanding_self_type first, before falling back to the existing (and, for this scenario, stale) checks.

This is scoped narrowly to the value-restricted-type-variable expansion path: expanding_self_type is None whenever there's no substitution mapping (i.e. for the overwhelming majority of methods, which don't have constrained type variables), so _super_arg_types falls through to its existing behavior unchanged in that case.

Testing

Added three regression cases to check-generics.test:

I also re-verified the existing testConstrainedGenericSuper test, which intentionally passes mismatched concrete str/bytes literals to a method expecting a single constrained type variable for both parameters (a genuine error, unrelated to this bug). That test's expected output needed updating: previously mypy reported both arguments as expected "AnyStr" (the literal, unsubstituted type variable name) for both of the two expansion passes; with this fix the error message correctly reports the concrete type expected in each pass (expected "bytes" in one pass, expected "str" in the other), which is strictly more informative and still correctly flags the call as an error.

  • python -m mypy --config-file mypy_self_check.ini -p mypy: clean.
  • python runtests.py check-generics.test check-typevar-values.test check-generic-subtyping.test: all pass (199/54/54 respectively, plus skips).
  • python runtests.py pytest-fast: 12849 passed, 351 skipped, 8 xfailed -- no regressions.
  • python runtests.py pytest-slow: 37 failed / 18 passed, but identical failure set on unmodified main (daemon/socket tests and a mypyc C-unit-test build step, both environment-dependent in this sandbox, unrelated to this change) -- confirmed via a side-by-side run.
  • black --check / ruff check on the two changed .py files: clean.

Out of scope

There's a third, related-looking issue, #13566, with a similarly-worded false positive from an explicit Base.__init__(self) call (rather than super()) on a class with a plain, unconstrained type variable. I checked it against this fix and it's unaffected -- different mechanism, no value-restricted type variable involved. Left it alone rather than trying to fold it into this PR.

expand_typevars checks a method with a value-restricted type variable
once per constraint value, substituting the value into a copy of the
function body. super() calls inside that body were still resolved
against the original, unsubstituted self type, since the checker
scope always holds the original function, not the substituted copy.
This made the expected argument type for an inherited member show the
literal type variable instead of the value being checked for this
pass, producing a spurious argument-type mismatch on every call.

expand_typevars now also returns the substitution mapping for each
copy, and check_func_def uses it to compute the correctly substituted
self/cls type while checking that copy's body. super() consults this
type instead of recomputing an unsubstituted one from the class.

Fixes python#17757.
Fixes python#14774.
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

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

anyio (https://github.com/agronholm/anyio)
+ src/anyio/_core/_tempfile.py:343: error: Unused "type: ignore" comment  [unused-ignore]
+ src/anyio/_core/_tempfile.py:357: error: Unused "type: ignore" comment  [unused-ignore]
+ src/anyio/_core/_tempfile.py:364: error: Unused "type: ignore" comment  [unused-ignore]
+ src/anyio/_core/_tempfile.py:424: error: Unused "type: ignore" comment  [unused-ignore]
+ src/anyio/_core/_tempfile.py:424: error: Argument 1 to "write" of "AsyncFile" has incompatible type "Buffer | str"; expected "str"  [arg-type]
+ src/anyio/_core/_tempfile.py:424: note: Error code "arg-type" not covered by "type: ignore[misc]" comment
+ src/anyio/_core/_tempfile.py:424: error: Argument 1 to "write" of "AsyncFile" has incompatible type "Buffer | str"; expected "Buffer"  [arg-type]
+ src/anyio/_core/_tempfile.py:452: error: Unused "type: ignore" comment  [unused-ignore]
+ src/anyio/_core/_tempfile.py:452: error: Argument 1 to "writelines" of "AsyncFile" has incompatible type "Iterable[str] | Iterable[Buffer]"; expected "Iterable[str]"  [arg-type]
+ src/anyio/_core/_tempfile.py:452: note: Error code "arg-type" not covered by "type: ignore[misc]" comment
+ src/anyio/_core/_tempfile.py:452: error: Argument 1 to "writelines" of "AsyncFile" has incompatible type "Iterable[str] | Iterable[Buffer]"; expected "Iterable[Buffer]"  [arg-type]

xarray (https://github.com/pydata/xarray)
+ xarray/core/resample.py:61: error: Unused "type: ignore" comment  [unused-ignore]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant