Skip to content

fix Support inline namedtuple definition #2811#2815

Closed
asukaminato0721 wants to merge 1 commit intofacebook:mainfrom
asukaminato0721:2811
Closed

fix Support inline namedtuple definition #2811#2815
asukaminato0721 wants to merge 1 commit intofacebook:mainfrom
asukaminato0721:2811

Conversation

@asukaminato0721
Copy link
Contributor

Summary

Fixes #2811

now synthesizes anonymous functional namedtuple classes when collections.namedtuple(...) or typing.NamedTuple(...) appears inline in expression position, instead of only when bound to a name or used as a base class.

Test Plan

add test

@meta-cla meta-cla bot added the cla signed label Mar 17, 2026
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@asukaminato0721 asukaminato0721 marked this pull request as ready for review March 17, 2026 07:37
Copilot AI review requested due to automatic review settings March 17, 2026 07:37
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR extends pyrefly’s handling of functional namedtuple definitions so that collections.namedtuple(...) / typing.NamedTuple(...) used inline in expression position (not just assigned to a name or used as a base class) synthesize an anonymous class that the solver can type correctly, addressing issue #2811.

Changes:

  • Add binding-time synthesis for inline functional namedtuple calls (collections.namedtuple / typing.NamedTuple) when the first argument is a string literal.
  • Add solver support to recognize and return the synthesized class type for these functional namedtuple call expressions.
  • Add a regression test covering the inline-constructor pattern from #2811.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
pyrefly/lib/test/named_tuple.rs Adds regression test for inline collections.namedtuple(...)(...) constructor usage.
pyrefly/lib/binding/expr.rs Synthesizes anonymous namedtuple classes during expression binding when functional namedtuple calls appear inline.
pyrefly/lib/alt/expr.rs Returns synthesized class types during call expression inference when a matching anonymous class binding exists.
pyrefly/lib/alt/class/class_field.rs Adjusts override-consistency checking to avoid treating namedtuple elements as regular override targets.

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

Comment on lines +224 to +234
fn synthesized_functional_class_type(&self, call: &ExprCall) -> Option<Type> {
let anon_key = match call.arguments.args.first()? {
Expr::StringLiteral(name) => Key::Anon(name.range()),
_ => Key::Anon(call.range),
};
let idx = self
.bindings()
.key_to_idx_hashed_opt(Hashed::new(&anon_key))?;
matches!(self.bindings().get(idx), Binding::ClassDef(..))
.then(|| self.get_hashed(Hashed::new(&anon_key)).ty().clone())
}
Comment on lines +591 to +594
self.insert_binding(
Key::Anon(call.range),
Binding::ClassDef(class_idx, Box::new([])),
);
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@meta-codesync
Copy link

meta-codesync bot commented Mar 22, 2026

@yangdanny97 has imported this pull request. If you are a Meta employee, you can view this in D97669871.

@yangdanny97 yangdanny97 self-assigned this Mar 22, 2026
@github-actions
Copy link

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

rotki (https://github.com/rotki/rotki)
- ERROR rotkehlchen/tests/utils/mock.py:74:16-50: Expected a callable, got `NamedTuple` [not-callable]
- ERROR rotkehlchen/tests/utils/mock.py:74:38-49: Expected 1 positional argument, got 2 in function `tuple.__new__` [bad-argument-count]
+ ERROR rotkehlchen/tests/utils/mock.py:74:27-36: Expected valid functional named tuple definition [bad-class-definition]
+ ERROR rotkehlchen/tests/utils/mock.py:74:51-58: Unexpected keyword argument `version` in function `Version.__new__` [unexpected-keyword]

dd-trace-py (https://github.com/DataDog/dd-trace-py)
- ERROR ddtrace/vendor/psutil/_pslinux.py:274:61-64: Argument `float` is not assignable to parameter `iterable` with type `Iterable[Any]` in function `tuple.__new__` [bad-argument-type]
- ERROR ddtrace/vendor/psutil/_pslinux.py:274:66-69: Expected 1 positional argument, got 3 in function `tuple.__new__` [bad-argument-count]

scipy (https://github.com/scipy/scipy)
- ERROR scipy/stats/_mstats_basic.py:311:48-55: Class member `ModeResult.count` overrides parent class `NamedTupleFallback` in an inconsistent manner [bad-override]
- ERROR scipy/stats/_stats_py.py:471:48-55: Class member `ModeResult.count` overrides parent class `NamedTupleFallback` in an inconsistent manner [bad-override]
- ERROR scipy/stats/_stats_py.py:2234:31-38: Class member `HistogramResult.count` overrides parent class `NamedTupleFallback` in an inconsistent manner [bad-override]

cloud-init (https://github.com/canonical/cloud-init)
- ERROR tests/unittests/analyze/test_show.py:13:64-71: Expected 1 positional argument, got 2 in function `tuple.__new__` [bad-argument-count]
- ERROR tests/unittests/config/test_cc_install_hotplug.py:29:9-24: Expected 1 positional argument, got 6 in function `tuple.__new__` [bad-argument-count]

vision (https://github.com/pytorch/vision)
- ERROR torchvision/datasets/celeba.py:13:36-43: Class member `CSV.index` overrides parent class `NamedTupleFallback` in an inconsistent manner [bad-override]

mypy (https://github.com/python/mypy)
- ERROR mypy/typeshed/stdlib/platform.pyi:55:13-22: Class member `uname_result.processor` overrides parent class `_uname_result_base` in an inconsistent manner [bad-override]

core (https://github.com/home-assistant/core)
- ERROR homeassistant/components/modbus/validators.py:72:9-16: Class member `PARM_IS_LEGAL.count` overrides parent class `NamedTupleFallback` in an inconsistent manner [bad-override]

paasta (https://github.com/yelp/paasta)
- ERROR paasta_tools/mesos_tools.py:67:48-55: Class member `SlaveTaskCount.count` overrides parent class `NamedTupleFallback` in an inconsistent manner [bad-override]

spack (https://github.com/spack/spack)
- ERROR lib/spack/spack/util/timer.py:21:72-79: Class member `TimeTracker.count` overrides parent class `NamedTupleFallback` in an inconsistent manner [bad-override]

spark (https://github.com/apache/spark)
- ERROR python/pyspark/tests/test_context.py:291:72-76: Argument `None` is not assignable to parameter `iterable` with type `Iterable[Any]` in function `tuple.__new__` [bad-argument-type]

scipy-stubs (https://github.com/scipy/scipy-stubs)
+ ERROR scipy-stubs/stats/_mstats_basic.pyi:123:1-2: Unused `# pyrefly: ignore` comment for code(s): bad-override [unused-ignore]
+ ERROR scipy-stubs/stats/_stats_py.pyi:219:1-2: Unused `# pyrefly: ignore` comment for code(s): bad-override [unused-ignore]
+ ERROR scipy-stubs/stats/_stats_py.pyi:223:1-2: Unused `# pyrefly: ignore` comment for code(s): bad-override [unused-ignore]

@github-actions
Copy link

Primer Diff Classification

❌ 1 regression(s) | ✅ 9 improvement(s) | ➖ 1 neutral | 11 project(s) total | +5, -15 errors

1 regression(s) across scipy-stubs. error kinds: unused-ignore. 9 improvement(s) across dd-trace-py, scipy, cloud-init, vision, mypy, core, paasta, spack, spark.

Project Verdict Changes Error Kinds Root Cause
rotki ➖ Neutral +2, -2 bad-argument-count, bad-class-definition bind_inline_functional_named_tuple()
dd-trace-py ✅ Improvement -2 bad-argument-count, bad-argument-type synthesized_functional_class_type()
scipy ✅ Improvement -3 bad-override pyrefly/lib/alt/class/class_field.rs
cloud-init ✅ Improvement -2 bad-argument-count bind_inline_functional_named_tuple()
vision ✅ Improvement -1 bad-override pyrefly/lib/alt/class/class_field.rs
mypy ✅ Improvement -1 bad-override pyrefly/lib/alt/class/class_field.rs
core ✅ Improvement -1 bad-override pyrefly/lib/alt/class/class_field.rs
paasta ✅ Improvement -1 bad-override pyrefly/lib/alt/class/class_field.rs
spack ✅ Improvement -1 bad-override pyrefly/lib/alt/class/class_field.rs
spark ✅ Improvement -1 bad-argument-type bind_inline_functional_named_tuple()
scipy-stubs ❌ Regression +3 unused-ignore pyrefly/lib/alt/class/class_field.rs
Detailed analysis

❌ Regression (1)

scipy-stubs (+3)

The PR's is_named_tuple_element skip in class_field.rs is too broad — it skips ALL override checking for NamedTuple elements, including legitimate bad-override cases like count: SomeType shadowing tuple.count(value) -> int. The # type: ignore[assignment] and # pyright: ignore[reportIncompatibleMethodOverride] comments on the same lines prove these ARE real type issues that other checkers also flag. By silencing the bad-override check, pyrefly (1) lost the ability to catch a real type violation, and (2) now produces false unused-ignore errors on comments that were correctly placed. This is a regression on both counts.
Attribution: The PR changed pyrefly/lib/alt/class/class_field.rs to add a check if is_named_tuple_element { continue; } at line 3069. This skips the bad-override check entirely for NamedTuple elements. Previously, pyrefly would report bad-override for NamedTuple fields like count that shadow tuple.count(), and the stubs had # pyrefly: ignore [bad-override] comments to suppress those errors. Now that pyrefly no longer reports bad-override for NamedTuple elements at all, the suppression comments become unused, triggering unused-ignore errors. The bad-override for count on a NamedTuple IS a real type issue (both mypy and pyright agree, as evidenced by the # type: ignore[assignment] and # pyright: ignore[reportIncompatibleMethodOverride] comments on the same lines). So the PR silenced a real check, and now the suppression comments are stale. This is a regression: pyrefly lost the ability to detect a genuine bad-override on NamedTuple fields that shadow tuple methods, AND it now produces nuisance unused-ignore errors on comments that were correctly suppressing real issues.

✅ Improvement (9)

dd-trace-py (-2)

Both removed errors were false positives caused by pyrefly's inability to handle inline namedtuple(...) calls. The expression namedtuple('scputimes', 'user system idle')(0.0, 0.0, 0.0) is valid Python that creates a namedtuple class and immediately instantiates it with 3 arguments matching its 3 fields. Pyrefly previously fell through to tuple.__new__ semantics, producing incorrect errors. The PR fix correctly synthesizes the namedtuple class in expression position, resolving the false positives.
Attribution: The PR adds synthesized_functional_class_type() in pyrefly/lib/alt/expr.rs and bind_inline_functional_named_tuple() in pyrefly/lib/binding/expr.rs. These changes detect when collections.namedtuple(...) or typing.NamedTuple(...) appears inline in expression position (not just when assigned to a name or used as a base class) and synthesize an anonymous namedtuple class definition. This allows pyrefly to correctly resolve the type of the call result as a namedtuple instance rather than falling back to tuple.__new__, which eliminates the false positive bad-argument-type and bad-argument-count errors.

scipy (-3)

All three removed errors were false positives where pyrefly incorrectly flagged namedtuple field definitions (e.g., count in ModeResult and HistogramResult) as bad overrides of the parent class NamedTupleFallback. Named tuple fields are properties that naturally shadow tuple methods like tuple.count() — this is inherent to how namedtuples work and is universally accepted by Python type checkers. The fix in class_field.rs correctly skips override checking for named tuple elements.
Attribution: The change in pyrefly/lib/alt/class/class_field.rs adds is_named_tuple_element detection and a continue statement that skips the override check when the field being checked is a named tuple element. This directly removes the false positive bad-override errors for namedtuple fields like count that shadow tuple.count(). The broader PR also adds inline functional namedtuple support (pyrefly/lib/alt/expr.rs and pyrefly/lib/binding/expr.rs), but the override fix is the specific change that removes these three errors.

cloud-init (-2)

Both errors were false positives caused by pyrefly not recognizing inline functional namedtuple definitions (where namedtuple(...) is called and immediately instantiated without being assigned to a variable first). Pyrefly was falling back to tuple.__new__ which expects 1 positional argument, incorrectly flagging the valid instantiation calls. The PR correctly fixes this by synthesizing anonymous namedtuple class definitions for inline usage.
Attribution: The PR adds bind_inline_functional_named_tuple() in pyrefly/lib/binding/expr.rs which detects namedtuple(...) calls in expression position (not just when assigned to a name) and synthesizes the appropriate class definition. The synthesized_functional_class_type() method in pyrefly/lib/alt/expr.rs then resolves these inline namedtuple calls to the correct type, bypassing the generic tuple.__new__ resolution that was causing the false bad-argument-count errors.

vision (-1)

This is a clear improvement. The removed error was a false positive — CSV = namedtuple('CSV', ['header', 'index', 'data']) is valid Python code where the index field intentionally shadows tuple.index. Pyrefly was incorrectly treating namedtuple field definitions as method overrides of the parent class. The PR fix correctly skips override checking for namedtuple elements, eliminating this false positive.
Attribution: The fix is in pyrefly/lib/alt/class/class_field.rs in the override checking logic. Lines added at ~3042-3044 compute is_named_tuple_element by checking if the field name is in the namedtuple's elements list. Then at ~3069-3071, when is_named_tuple_element is true, the code continues past the parent class override check, preventing the false bad-override error. The broader PR also adds inline functional namedtuple support (pyrefly/lib/alt/expr.rs and pyrefly/lib/binding/expr.rs), which enables pyrefly to properly synthesize the CSV namedtuple class from the namedtuple() call expression.

mypy (-1)

This is an improvement. The removed error was a false positive — pyrefly was incorrectly flagging a type-compatible override of a NamedTuple element with a property. The _uname_result_base NamedTuple defines processor: str, and uname_result overrides it with @property def processor(self) -> str, which is fully type-compatible. This pattern is intentional in typeshed (see comments on lines 42-44) and is not flagged by mypy or pyright. The PR fix correctly identifies named tuple elements and skips the override check for them.
Attribution: The change in pyrefly/lib/alt/class/class_field.rs at lines 3066-3068 adds if is_named_tuple_element { continue; } which skips the bad-override check when the field being overridden is a named tuple element. This directly removes the false positive bad-override error for uname_result.processor overriding _uname_result_base.processor.

core (-1)

This is a clear improvement. The PARM_IS_LEGAL namedtuple at line 69 defines a field called count. In pyrefly's type model, namedtuples have a parent class called NamedTupleFallback (as shown in the error message), which includes methods like count() inherited from tuple. Pyrefly was incorrectly flagging the count field as a bad-override because the namedtuple field count (which becomes a property-like accessor returning the field value) has a different type signature than the count() method inherited from the parent. However, namedtuple fields commonly shadow tuple methods like count and index - this is a known consequence of Python's namedtuple implementation and is valid Python code. The PR correctly adds a check to skip override validation for named tuple elements, removing this false positive.
Attribution: The fix is in pyrefly/lib/alt/class/class_field.rs. The new is_named_tuple_element variable (line ~3042) checks if the field being analyzed is a named tuple element, and the continue statement at line ~3069 skips the parent class override check for such elements. This directly removes the false positive bad-override error for PARM_IS_LEGAL.count. Additionally, the changes in pyrefly/lib/binding/expr.rs and pyrefly/lib/alt/expr.rs add support for inline functional namedtuple definitions, which enables pyrefly to properly recognize PARM_IS_LEGAL as a namedtuple class and apply the named tuple element skip logic.

paasta (-1)

The removed error was a false positive where pyrefly incorrectly flagged count as a bad override of NamedTupleFallback. The field name count does technically shadow the inherited tuple.count() method (since namedtuples inherit from tuple), and NamedTupleFallback is pyrefly's internal representation of this base class. However, this shadowing is intentional and expected behavior in Python — collections.namedtuple deliberately allows field names like count that replace inherited tuple methods with field accessors. The PR correctly adds logic to skip override checking for NamedTuple elements, which is the right behavior since this kind of shadowing is by design in Python's namedtuple implementation and should not be treated as an inconsistent override.
Attribution: The change to AnswersSolver in pyrefly/lib/alt/class/class_field.rs added the is_named_tuple_element check (lines 3042-3044) and the continue statement (lines 3069-3071) that skips override checking for NamedTuple elements. This directly removes the false positive bad-override error for SlaveTaskCount.count. The broader PR also adds inline functional namedtuple support in pyrefly/lib/alt/expr.rs and pyrefly/lib/binding/expr.rs, but the specific fix for this error is the override-skip logic in class_field.rs.

spack (-1)

This is a clear improvement. The bad-override error on TimeTracker.count was a false positive — count is a legitimate namedtuple field that naturally shadows tuple.count. This is standard Python behavior that all type checkers should accept. The PR correctly identifies named tuple elements and skips the override check for them.
Attribution: The fix is in pyrefly/lib/alt/class/class_field.rs. The new code at lines 3042-3044 computes is_named_tuple_element by checking if the field name is in the namedtuple's elements list. Then at lines 3069-3071, if the field is a named tuple element, it continues past the override check, skipping the bad-override validation. This directly removes the false positive for TimeTracker.count overriding tuple.count.

spark (-1)

This is a clear improvement. The removed error was a false positive caused by pyrefly's inability to handle inline namedtuple definitions. The code namedtuple('MockGatewayParameters', 'auth_token')(None) is valid Python — it creates a namedtuple class with one field and instantiates it with None. The PR correctly synthesizes the namedtuple class in expression position, so pyrefly no longer misinterprets this as a tuple.__new__ call.
Attribution: The changes in pyrefly/lib/binding/expr.rs (specifically bind_inline_functional_named_tuple() and the new Expr::Call match arm) and pyrefly/lib/alt/expr.rs (specifically synthesized_functional_class_type() and its use in the Expr::Call branch) together enable pyrefly to recognize inline namedtuple(...) calls. This means namedtuple('MockGatewayParameters', 'auth_token')(None) is now correctly understood as creating a namedtuple instance rather than being treated as a raw tuple.__new__ call, removing the false positive bad-argument-type error.

➖ Neutral (1)

rotki (+2, -2)

The PR traded 2 false positives for 2 different false positives on the same line. The old errors (not-callable, bad-argument-count) were clearly wrong since NamedTuple used functionally is callable and returns a class. The new errors (bad-class-definition, unexpected-keyword) are also wrong — NamedTuple('Version', ['version']) is a valid functional form of NamedTuple. While the more standard typed forms are NamedTuple('Name', [('field', type)]) or NamedTuple('Name', field=type), passing a list of plain strings (field names without types) is accepted at runtime by Python. The bad-class-definition error incorrectly rejects this valid form, and the unexpected-keyword error is a downstream consequence: because pyrefly fails to recognize the NamedTuple definition, it doesn't know the resulting class has a version field, so it rejects version=1 as an unexpected keyword. The net effect is neutral: the error count is unchanged (2 removed, 2 added), and the line still has false positives. The new errors are arguably worse because they claim the definition itself is invalid rather than just misunderstanding callability, and the unexpected-keyword error is entirely a cascading failure from the first. This represents a regression — the PR partially improved NamedTuple handling but doesn't fully support this valid functional syntax variant.
Attribution: The changes in pyrefly/lib/binding/expr.rs (bind_inline_functional_named_tuple()) and pyrefly/lib/alt/expr.rs (synthesized_functional_class_type()) added support for inline functional NamedTuple definitions. This correctly removed the old false positives. However, synthesize_typing_named_tuple_def in the binding phase doesn't properly handle the ['version'] (list-of-strings) form for typing.NamedTuple, causing the new bad-class-definition error, which cascades into the unexpected-keyword error.

Suggested fixes

Summary: The is_named_tuple_element skip in class_field.rs is too broad — it skips ALL override checking for NamedTuple elements, but some overrides (like count shadowing tuple.count()) are real type issues that should still be reported.

1. In the override checking loop in pyrefly/lib/alt/class/class_field.rs (around line 3069), the current code does if is_named_tuple_element { continue; } which skips ALL parent override checks for named tuple elements. Instead, it should only skip the check when the parent class is the NamedTuple fallback/tuple base class AND the parent member is NOT a method that the field genuinely conflicts with. Specifically, the skip should only apply when the parent field is also a named tuple element (i.e., the override is from a NamedTuple parent redefining the same field), not when the parent field is an inherited method like tuple.count() or tuple.index(). Change the guard from if is_named_tuple_element { continue; } to something like if is_named_tuple_element && parent_is_named_tuple_element_too { continue; } — where parent_is_named_tuple_element_too checks whether the parent class's metadata also lists field_name as a named tuple element. This way, NamedTuple-to-NamedTuple field inheritance is allowed (no false positive), but a NamedTuple field like count that shadows tuple.count() method still gets flagged as bad-override.

Files: pyrefly/lib/alt/class/class_field.rs
Confidence: high
Affected projects: scipy-stubs
Fixes: unused-ignore
The scipy-stubs regression has 3 new pyrefly-only unused-ignore errors. These occur because # pyrefly: ignore [bad-override] comments were placed on lines where NamedTuple fields like count shadow tuple.count(). The PR's blanket skip of override checking for all named tuple elements means bad-override is no longer reported for these real conflicts, making the ignore comments unused. The fix is to narrow the skip: only skip when the parent also defines the field as a named tuple element (NamedTuple inheritance), not when the parent defines it as a regular method (like tuple.count). This preserves the 9 improvements (which are all cases where a NamedTuple field shadows a tuple method on the NamedTupleFallback base — wait, actually those improvements ARE the tuple.count shadowing case). Let me reconsider: the 9 improvements are ALL about fields like count and index shadowing tuple.count() and tuple.index(). The scipy-stubs regression is about the SAME pattern but where the project had # pyrefly: ignore [bad-override] comments. So the real issue is that pyrefly should handle the unused-ignore case gracefully. Actually, looking more carefully: the cross-check shows 0/3 mypy and 0/3 pyright for the scipy-stubs regression, meaning the 3 new unused-ignore errors are pyrefly-only. The root cause is that the bad-override errors were suppressed by ignore comments, and now that the errors are gone, the ignore comments are unused. The proper fix might be to NOT report unused-ignore when the ignore comment also suppresses errors for other tools (i.e., when the same line has # type: ignore or # pyright: ignore). But that's a different approach. The most targeted fix for the named tuple case specifically: instead of blanket-skipping all override checks, only skip when the parent member being overridden is specifically from tuple (object/builtins) — this still eliminates the false positives (all 9 improvements) but keeps reporting bad-override when a NamedTuple field overrides something from a non-tuple parent. However, since ALL the improvements and the regression involve the same pattern (field shadowing tuple methods), a different approach is needed. The simplest fix: keep the is_named_tuple_element skip but DON'T skip the override check — instead, just don't REPORT the error. That way the ignore comments remain 'used'. Actually the cleanest fix: keep the current behavior (skip override check for named tuple elements) but in the unused-ignore detection, treat # pyrefly: ignore [bad-override] as 'used' if the line defines a named tuple element. Alternatively, the most practical fix: narrow the skip to only apply when the parent class is specifically the NamedTupleFallback (or tuple), since that's where the false positives come from. If scipy-stubs has a NamedTuple inheriting from another NamedTuple where count overrides a real parent method, the skip wouldn't apply. But actually, in scipy-stubs the parent IS also tuple/NamedTupleFallback. So the regression is purely about unused-ignore comments. The fix should be: keep the override skip, but when skipping, still mark any matching ignore comments as 'used'. This eliminates 3 unused-ignore errors in scipy-stubs while preserving all 9 improvements.


Was this helpful? React with 👍 or 👎

Classification by primer-classifier (11 LLM)

Copy link
Contributor

@grievejia grievejia left a comment

Choose a reason for hiding this comment

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

Review automatically exported from Phabricator review in Meta.

@meta-codesync
Copy link

meta-codesync bot commented Mar 26, 2026

@yangdanny97 merged this pull request in 9832cb7.

@asukaminato0721 asukaminato0721 deleted the 2811 branch March 26, 2026 03:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support inline namedtuple definition

4 participants