Fix #11475: Cannot override a class's callable variable with a method#11500
Fix #11475: Cannot override a class's callable variable with a method#11500rchiodo wants to merge 3 commits into
Conversation
When a base class declares a plain callable variable (e.g. `x: Callable[..., None]`) and a subclass overrides it with a method, bind the override method's `self` parameter before comparing signatures. Accessing the override on an instance binds `self`, so the signatures are compatible. This matches the behavior of mypy and ty. Fixes microsoft#11475 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
|
Looks good overall — the bound-comparison approach is reasonable and matches other type checkers. A few non-blocking suggestions: hoist the new declaration scans inside the callable-variable guard to avoid extra work on the common override path, and broaden the negative test beyond arity (return-type and param-type mismatches). |
…override - Compare the bound override and base callable as plain (static) functions so the override comparison no longer skips the first real parameter as an unbound self. This catches first-parameter type mismatches that were previously dropped. - Apply the same bind-before-compare handling to the multiple-inheritance override path, fixing a spurious reportIncompatibleMethodOverride for callable-variable-vs-method diamonds. - Extract the shared logic into _getCallableVariableOverrideComparison and hoist the declaration scans inside the function-typed guard so the common override path pays nothing extra. - Strengthen methodOverride7 with param-type and return-type negative cases plus diamond regression coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Diff from mypy_primer, showing the effect of this PR on open source code: sympy (https://github.com/sympy/sympy)
+ .../projects/sympy/sympy/solvers/tests/test_solvers.py:1216:18 - error: Operator "+" not supported for types "Basic" and "Expr" (reportOperatorIssue)
- .../projects/sympy/sympy/solvers/tests/test_solveset.py:315:25 - error: "Set" is not iterable
- "__iter__" method not defined (reportGeneralTypeIssues)
- .../projects/sympy/sympy/solvers/tests/test_solveset.py:315:25 - error: "ConditionSet" is not iterable
- "__iter__" method not defined (reportGeneralTypeIssues)
- .../projects/sympy/sympy/solvers/tests/test_solveset.py:2030:20 - error: "__getitem__" method not defined on type "Basic" (reportIndexIssue)
- .../projects/sympy/sympy/solvers/tests/test_solveset.py:2042:20 - error: "__getitem__" method not defined on type "Basic" (reportIndexIssue)
- .../projects/sympy/sympy/solvers/tests/test_solveset.py:2055:20 - error: "__getitem__" method not defined on type "Basic" (reportIndexIssue)
- .../projects/sympy/sympy/solvers/tests/test_solveset.py:2076:16 - error: Argument of type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" cannot be assigned to parameter "obj" of type "Sized" in function "len"
- Type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" is not assignable to type "Sized"
- "Set" is incompatible with protocol "Sized"
- "__len__" is not present (reportArgumentType)
- .../projects/sympy/sympy/solvers/tests/test_solveset.py:2077:20 - error: "__getitem__" method not defined on type "Basic" (reportIndexIssue)
- .../projects/sympy/sympy/solvers/tests/test_solveset.py:2270:16 - error: Argument of type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" cannot be assigned to parameter "obj" of type "Sized" in function "len"
- Type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" is not assignable to type "Sized"
- "Set" is incompatible with protocol "Sized"
- "__len__" is not present (reportArgumentType)
- .../projects/sympy/sympy/solvers/tests/test_solveset.py:2351:16 - error: Argument of type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" cannot be assigned to parameter "obj" of type "Sized" in function "len"
- Type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" is not assignable to type "Sized"
- "Set" is incompatible with protocol "Sized"
- "__len__" is not present (reportArgumentType)
- .../projects/sympy/sympy/solvers/tests/test_solveset.py:2364:16 - error: Argument of type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" cannot be assigned to parameter "obj" of type "Sized" in function "len"
- Type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" is not assignable to type "Sized"
- "Set" is incompatible with protocol "Sized"
- "__len__" is not present (reportArgumentType)
- .../projects/sympy/sympy/solvers/tests/test_solveset.py:2370:16 - error: Argument of type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" cannot be assigned to parameter "obj" of type "Sized" in function "len"
- Type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" is not assignable to type "Sized"
- "Set" is incompatible with protocol "Sized"
- "__len__" is not present (reportArgumentType)
- .../projects/sympy/sympy/solvers/tests/test_solveset.py:2538:16 - error: Argument of type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" cannot be assigned to parameter "obj" of type "Sized" in function "len"
- Type "Unknown | FiniteSet | Set | Intersection | Union | Complement | Any | ConditionSet" is not assignable to type "Sized"
- "Set" is incompatible with protocol "Sized"
- "__len__" is not present (reportArgumentType)
- .../projects/sympy/sympy/solvers/tests/test_solveset.py:2541:20 - error: Argument of type "Unknown | Basic | Any" cannot be assigned to parameter "obj" of type "Sized" in function "len"
- Type "Unknown | Basic | Any" is not assignable to type "Sized"
- "Basic" is incompatible with protocol "Sized"
- "__len__" is not present (reportArgumentType)
- .../projects/sympy/sympy/stats/drv.py:269:22 - error: Argument of type "Generator[tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Add | Zero | NaN | Piecewise | Basic | int | None, None, None]" cannot be assigned to parameter "iterable" of type "Iterable[_SupportsSumNoDefaultT@sum]" in function "sum"
+ .../projects/sympy/sympy/stats/drv.py:269:22 - error: Argument of type "Generator[tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Zero | NaN | Piecewise | Basic | int | None, None, None]" cannot be assigned to parameter "iterable" of type "Iterable[_SupportsSumNoDefaultT@sum]" in function "sum"
- "Generator[tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Add | Zero | NaN | Piecewise | Basic | int | None, None, None]" is not assignable to "Iterable[_SupportsSumNoDefaultT@sum]"
+ "Generator[tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Zero | NaN | Piecewise | Basic | int | None, None, None]" is not assignable to "Iterable[_SupportsSumNoDefaultT@sum]"
- Type parameter "_T_co@Iterable" is covariant, but "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Add | Zero | NaN | Piecewise | Basic | int | None" is not a subtype of "_SupportsSumNoDefaultT@sum"
+ Type parameter "_T_co@Iterable" is covariant, but "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Zero | NaN | Piecewise | Basic | int | None" is not a subtype of "_SupportsSumNoDefaultT@sum"
- Type "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Add | Zero | NaN | Piecewise | Basic | int | None" is not assignable to type "_SupportsSumWithNoDefaultGiven"
+ Type "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Zero | NaN | Piecewise | Basic | int | None" is not assignable to type "_SupportsSumWithNoDefaultGiven"
- Type "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Add | Zero | NaN | Piecewise | Basic | int | None" is not assignable to type "_SupportsSumWithNoDefaultGiven"
+ Type "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Zero | NaN | Piecewise | Basic | int | None" is not assignable to type "_SupportsSumWithNoDefaultGiven"
- .../projects/sympy/sympy/stats/drv_types.py:293:16 - error: Operator "*" not supported for types "Expr" and "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Add | Zero | NaN | Piecewise | Basic"
+ .../projects/sympy/sympy/stats/drv_types.py:293:16 - error: Operator "*" not supported for types "Expr" and "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Zero | NaN | Piecewise | Basic"
- .../projects/sympy/sympy/stats/rv.py:473:25 - error: Argument of type "Expr | Lambda | Zero | One | Integral | Unknown | Probability | tuple[Unknown, ...] | Sum | ZeroMatrix | Add | NaN | Piecewise | Basic | NegativeOne | Integer | ComplexInfinity | Rational | Infinity | NegativeInfinity | Float | Number | int" cannot be assigned to parameter "args" of type "Expr | complex" in function "__new__"
+ .../projects/sympy/sympy/stats/rv.py:473:25 - error: Argument of type "Expr | Lambda | Zero | One | Integral | Unknown | Probability | tuple[Unknown, ...] | Sum | ZeroMatrix | NaN | Piecewise | Basic | NegativeOne | Integer | ComplexInfinity | Rational | Infinity | NegativeInfinity | Float | Number | int" cannot be assigned to parameter "args" of type "Expr | complex" in function "__new__"
- Type "Expr | Lambda | Zero | One | Integral | Unknown | Probability | tuple[Unknown, ...] | Sum | ZeroMatrix | Add | NaN | Piecewise | Basic | NegativeOne | Integer | ComplexInfinity | Rational | Infinity | NegativeInfinity | Float | Number | int" is not assignable to type "Expr | complex"
+ Type "Expr | Lambda | Zero | One | Integral | Unknown | Probability | tuple[Unknown, ...] | Sum | ZeroMatrix | NaN | Piecewise | Basic | NegativeOne | Integer | ComplexInfinity | Rational | Infinity | NegativeInfinity | Float | Number | int" is not assignable to type "Expr | complex"
- .../projects/sympy/sympy/stats/stochastic_process_types.py:1839:25 - error: Argument of type "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Add | Zero | NaN | Piecewise | Basic | Integral | Any | int" cannot be assigned to parameter "args" of type "Expr | complex" in function "__new__"
+ .../projects/sympy/sympy/stats/stochastic_process_types.py:1839:25 - error: Argument of type "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Zero | NaN | Piecewise | Basic | Integral | Any | int" cannot be assigned to parameter "args" of type "Expr | complex" in function "__new__"
- Type "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Add | Zero | NaN | Piecewise | Basic | Integral | Any | int" is not assignable to type "Expr | complex"
+ Type "tuple[Unknown, ...] | Unknown | Sum | Expr | ZeroMatrix | Zero | NaN | Piecewise | Basic | Integral | Any | int" is not assignable to type "Expr | complex"
- Return type mismatch: base method returns type "Self@Basic", override returns type "Basic | Expectation | Expr | tuple[Unknown, ...] | Unknown | Sum | ZeroMatrix | Add | Zero | NaN | Piecewise | Integral | Any | Self@Expectation | Literal[0]"
+ Return type mismatch: base method returns type "Self@Basic", override returns type "Basic | Expectation | Expr | tuple[Unknown, ...] | Unknown | Sum | ZeroMatrix | Zero | NaN | Piecewise | Integral | Any | Self@Expectation | Literal[0]"
... (truncated 514 lines) ...
core (https://github.com/home-assistant/core)
- .../projects/core/homeassistant/components/mqtt/device_tracker.py:155:9 - error: Method "_process_update_extra_state_attributes" overrides class "MqttAttributesMixin" in an incompatible manner
- Positional parameter count mismatch; base method has 1, but override has 2 (reportIncompatibleMethodOverride)
- .../projects/core/homeassistant/components/mqtt/entity.py:1651:9 - error: Method "_message_callback" overrides class "MqttAttributesMixin" in an incompatible manner
- Positional parameter count mismatch; base method has 3, but override has 4
- Parameter 2 type mismatch: base parameter is type "set[str] | None", override parameter is type "MessageCallbackType"
- Parameter 3 type mismatch: base parameter is type "ReceiveMessage", override parameter is type "set[str] | None"
- Type "set[str] | None" is not assignable to type "MessageCallbackType"
- Type "set[str]" is not assignable to type "MessageCallbackType"
- Type "ReceiveMessage" is not assignable to type "set[str] | None"
- "ReceiveMessage" is not assignable to "set[str]"
- "ReceiveMessage" is not assignable to "None" (reportIncompatibleMethodOverride)
- .../projects/core/homeassistant/components/mqtt/entity.py:1651:9 - error: Method "_message_callback" overrides class "MqttAvailabilityMixin" in an incompatible manner
- Positional parameter count mismatch; base method has 3, but override has 4
- Parameter 2 type mismatch: base parameter is type "set[str] | None", override parameter is type "MessageCallbackType"
- Parameter 3 type mismatch: base parameter is type "ReceiveMessage", override parameter is type "set[str] | None"
- Type "set[str] | None" is not assignable to type "MessageCallbackType"
- Type "set[str]" is not assignable to type "MessageCallbackType"
- Type "ReceiveMessage" is not assignable to type "set[str] | None"
- "ReceiveMessage" is not assignable to "set[str]"
- "ReceiveMessage" is not assignable to "None" (reportIncompatibleMethodOverride)
- 31854 errors, 79 warnings, 0 informations
+ 31851 errors, 79 warnings, 0 informations
dedupe (https://github.com/dedupeio/dedupe)
- .../projects/dedupe/dedupe/variables/exact.py
- .../projects/dedupe/dedupe/variables/exact.py:12:9 - error: Method "comparator" overrides class "FieldType" in an incompatible manner
- Base method is declared as an instance method but override is not (reportIncompatibleMethodOverride)
- .../projects/dedupe/dedupe/variables/exists.py:25:9 - error: Method "comparator" overrides class "FieldType" in an incompatible manner
- Positional parameter count mismatch; base method has 2, but override has 3 (reportIncompatibleMethodOverride)
- .../projects/dedupe/dedupe/variables/latlong.py:17:9 - error: Method "comparator" overrides class "FieldType" in an incompatible manner
- Base method is declared as an instance method but override is not (reportIncompatibleMethodOverride)
- .../projects/dedupe/dedupe/variables/price.py
- .../projects/dedupe/dedupe/variables/price.py:18:9 - error: Method "comparator" overrides class "FieldType" in an incompatible manner
- Base method is declared as an instance method but override is not (reportIncompatibleMethodOverride)
- 56 errors, 1 warning, 0 informations
+ 52 errors, 1 warning, 0 informations
|
| if (boundOverrideType) { | ||
| return { baseType, overrideType: boundOverrideType }; | ||
| } | ||
|
|
There was a problem hiding this comment.
📍 packages/pyright-internal/src/analyzer/checker.ts:6644
[unverified] Overloaded-override asymmetry (raised independently by the Skeptic and Architect). When bindFunctionToClassOrObject returns an OverloadedType (override is an overloaded method over a callable variable), this fallback returns { baseType (unmarked), overrideType: boundOverrideType } and marks neither side StaticMethod. The single-function branch deliberately marks both static to defeat the i===0 self-skip in validateOverrideMethodInternal (typeEvaluator.ts:~27923); without it, each bound overload likely keeps its InstanceMethod flag and the real first parameter is skipped — a potential false negative for an incompatible overloaded override. Fix: apply the same static normalization in the overloaded branch, or explicitly bail/document overloaded overrides as unsupported. Add a sample overriding cb: Callable[[int], None] with an @overloaded method whose first param is incompatible and assert it still errors.
[verified]
| overriddenType, | ||
| overrideType, | ||
| childClassType, | ||
| childClassSelf |
There was a problem hiding this comment.
📍 packages/pyright-internal/src/analyzer/checker.ts:5862
The first call-site change (multiple-inheritance path) is not exercised by any test that fails without it. The Advocate verified both DiamondAB/DiamondBA produce 0 errors with and without this hunk, because every diamond only shares the 0-arg hello, which the pre-existing isCallableAttrOverriddenByInstanceMethod heuristic already handles. Either add a parametered diamond (one base with cb: Callable[[int], None], another with def cb(self, value: int)) that is load-bearing here, or add a comment stating this is intentional parity hardening so a future maintainer doesn't delete it as dead code.
[verified]
| // a callable attribute with a method. | ||
| // | ||
| // Note: this validates only the read direction (the bound override is compatible with | ||
| // the base callable). A method is effectively read-only, so this intentionally does not |
There was a problem hiding this comment.
📍 packages/pyright-internal/src/analyzer/checker.ts:6594
Architectural two-source-of-truth (structurally confirmed by the Architect). The evaluator already special-cases "callable attribute overridden by an instance method" at typeEvaluator.ts:~27873 (hard-coded to base 0 params / override 1 param), which handles the no-arg cases (hello, ret) and which this checker-side transform now supersedes/disables for the parametered case (the heuristic requires !isStaticMethod(base), which this PR violates by marking base static). They don't double-count, but the concept now lives in two places that can drift. Consider generalizing/removing the evaluator heuristic, or add a cross-reference comment at each site so the two mechanisms stay in sync.
[verified]
|
Looks good overall — the fix correctly binds the override method's self only when the base member is a callable variable. A few non-blocking notes below about overloaded-override handling and test/code-path coverage. |
heejaechang
left a comment
There was a problem hiding this comment.
Approved via Review Center.
Description
Pylance/Pyright incorrectly flagged an error when a subclass overrode a base class's callable variable (e.g.
x: Callable[[], None]) with a real method. Other type checkers allow this, because when you access the override method on an instance, itsselfparameter is bound, leaving a signature that matches the base callable attribute.How you figured out what to do
The base-class member here is a plain
Callableattribute, not a method, so it has no implicitself. The override, being a method, does declareself. The override check compared the unbound method signature (which still hasself) against the base callable, producing a spuriousreportIncompatibleMethodOverrideerror. The fix is to bind the override method to the class before comparing, but only when the base member is actually a callable variable.Implementation
In
checker.ts, within the override-validation path for function-typed members:DeclarationType.Variable) and the override is a method (DeclarationType.Function).selfviabindFunctionToClassOrObjectand use the bound type forvalidateOverrideMethod.Testing
Added
methodOverride7.pysample plus aMethodOverride7test intypeEvaluator3.test.ts(withreportIncompatibleMethodOverride = 'error'):class B(A)overrides callable attributeshello/cbwith compatible methods — no error.class C(A)overrideshellowith an incompatible signature (extra required parameter) — still produces exactly 1 error, confirming the bound comparison continues to catch genuine mismatches.Ran the targeted Pyright type-evaluator tests to confirm the new case passes and the negative case still errors.
Addresses
Fixes https://github.com/microsoft/pylance-release/issues/11475
Generated by fix_all_my_issues pipeline