Skip to content

Fix #11475: Cannot override a class's callable variable with a method#11500

Open
rchiodo wants to merge 3 commits into
microsoft:mainfrom
rchiodo:fix11475
Open

Fix #11475: Cannot override a class's callable variable with a method#11500
rchiodo wants to merge 3 commits into
microsoft:mainfrom
rchiodo:fix11475

Conversation

@rchiodo

@rchiodo rchiodo commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

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, its self parameter 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 Callable attribute, not a method, so it has no implicit self. The override, being a method, does declare self. The override check compared the unbound method signature (which still has self) against the base callable, producing a spurious reportIncompatibleMethodOverride error. 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:

  • Detect when the base member is a callable variable (all declarations are DeclarationType.Variable) and the override is a method (DeclarationType.Function).
  • In that case, bind the override method's self via bindFunctionToClassOrObject and use the bound type for validateOverrideMethod.
  • All other cases are unchanged, so normal method-vs-method override checking still applies.

Testing

Added methodOverride7.py sample plus a MethodOverride7 test in typeEvaluator3.test.ts (with reportIncompatibleMethodOverride = 'error'):

  • class B(A) overrides callable attributes hello / cb with compatible methods — no error.
  • class C(A) overrides hello with 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

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>
@github-actions

This comment has been minimized.

Comment thread packages/pyright-internal/src/analyzer/checker.ts
Comment thread packages/pyright-internal/src/analyzer/checker.ts
Comment thread packages/pyright-internal/src/analyzer/checker.ts Outdated
Comment thread packages/pyright-internal/src/tests/samples/methodOverride7.py
Comment thread packages/pyright-internal/src/tests/typeEvaluator3.test.ts
Comment thread packages/pyright-internal/src/analyzer/checker.ts Outdated
Comment thread packages/pyright-internal/src/tests/samples/methodOverride7.py
Comment thread packages/pyright-internal/src/analyzer/checker.ts Outdated
@rchiodo

rchiodo commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator Author

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).

rchiodo and others added 2 commits June 19, 2026 10:47
…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>
@github-actions

Copy link
Copy Markdown
Contributor

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 };
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📍 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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📍 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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📍 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]

@heejaechang

Copy link
Copy Markdown
Collaborator

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 heejaechang left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved via Review Center.

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.

2 participants