fix special-case NotImplementedType in binop signatures to match runtime semantics #1129#2677
fix special-case NotImplementedType in binop signatures to match runtime semantics #1129#2677asukaminato0721 wants to merge 1 commit intofacebook:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes issue #1129, where binary operations involving dunder methods that return NotImplementedType would incorrectly leak the NotImplementedType into the inferred result type instead of following Python's runtime semantics (trying the reflected dunder when the forward one signals NotImplemented).
Changes:
- The
try_binop_callslogic inoperators.rsis updated to stripNotImplementedTypefrom successful dunder return types, accumulate non-NotImplementedTyperesults, and continue searching for reflected dunders when needed. NotImplementedTypeis added as a new stdlib entry inStdlib, gated to Python ≥ 3.10 (consistent with howEllipsisTypeis handled).- A regression test is added covering both the pure-
NotImplementedTypefallback and the mixedint | NotImplementedTypescenario.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
pyrefly/lib/alt/operators.rs |
Core fix: strips NotImplementedType from successful dunder results, accumulates partial results, and continues to reflected dunders when needed |
crates/pyrefly_types/src/stdlib.rs |
Adds NotImplementedType as an Option<StdlibResult<ClassType>> stdlib entry, guarded by version >= 3.10 |
pyrefly/lib/test/operators.rs |
Regression test covering the bug from issue #1129 for both pure and mixed NotImplementedType return types |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| errors.extend(callee_errors); | ||
| return ret; | ||
| if ret_without_not_implemented != ret { | ||
| successful_ret = self.union(successful_ret, ret_without_not_implemented); | ||
| continue; |
There was a problem hiding this comment.
When a dunder returns int | NotImplementedType, the code extends errors with callee_errors at line 204 and then continues to look for a reflected dunder. If callee_errors is non-empty (e.g., because the method is not callable as a call target), those errors are emitted unconditionally — even if the reflected dunder later succeeds cleanly. This results in spurious error reporting.
The callee_errors extension should be deferred: accumulate them alongside successful_ret and only emit them at the return site (line 209 or line 215), similar to how first_call defers error emission until it is determined whether the call is ultimately the "best" result.
| }), | ||
| None => ret.clone(), | ||
| }; | ||
| if ret_without_not_implemented.is_never() { |
There was a problem hiding this comment.
When all dunder methods exist and have no call errors, but all return only NotImplementedType (so ret_without_not_implemented.is_never() is true for every iteration), the code falls through to the "Cannot find __add__ or __radd__" error message. This message is misleading: the methods do exist, they just always return NotImplemented. A more accurate message could indicate that all matching operator methods always return NotImplemented.
Note: this scenario only arises in uncommon code where every dunder is annotated to always return NotImplementedType.
| if ret_without_not_implemented.is_never() { | |
| if ret_without_not_implemented.is_never() { | |
| // All branches of this dunder call resolved to NotImplementedType. | |
| // Record this call as the first attempted call (if none recorded yet) | |
| // so that later error handling can distinguish "methods exist but | |
| // always return NotImplemented" from "no dunder methods found". | |
| if first_call.is_none() { | |
| first_call = Some((callee_errors, call_errors, ret)); | |
| } |
|
Diff from mypy_primer, showing the effect of this PR on open source code: werkzeug (https://github.com/pallets/werkzeug)
+ ERROR tests/test_datastructures.py:713:13-33: `|` is not supported between `EnvironHeaders` and `dict[str, str]` [unsupported-operation]
+ ERROR tests/test_datastructures.py:719:13-34: `|=` is not supported between `EnvironHeaders` and `dict[str, str]` [unsupported-operation]
|
Primer Diff Classification❌ 1 regression(s) | 1 project(s) total 1 regression(s) across werkzeug. error kinds:
Detailed analysis❌ Regression (1)werkzeug (+2)
Suggested FixSummary: The new NotImplementedType handling incorrectly flags legitimate NotImplemented returns as type errors when they should result in runtime TypeError. 1. In
Was this helpful? React with 👍 or 👎 Classification by primer-classifier (1 LLM) |
Summary
Fixes #1129
fixed the binop resolution path so NotImplementedType no longer leaks as a possible operator result.
successful dunder calls now strip only the exact NotImplementedType branch, keep searching reflected dunders when needed, and union any concrete results that can actually occur at runtime.
Test Plan
a regression test covering both pure fallback and mixed `int | NotImplementedType behavior.