Skip to content

fix Recognize explicitly constructed properties as such #3141#3144

Open
asukaminato0721 wants to merge 3 commits intofacebook:mainfrom
asukaminato0721:3141
Open

fix Recognize explicitly constructed properties as such #3141#3144
asukaminato0721 wants to merge 3 commits intofacebook:mainfrom
asukaminato0721:3141

Conversation

@asukaminato0721
Copy link
Copy Markdown
Contributor

Summary

Fixes #3141

The change normalizes property(...) assignments in class bodies into Pyrefly’s internal property representation instead of leaving them as generic descriptors.

That makes override checking treat p = property(lambda self: None) the same way as @property.

Test Plan

add test

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@asukaminato0721 asukaminato0721 marked this pull request as ready for review April 16, 2026 09:32
Copilot AI review requested due to automatic review settings April 16, 2026 09:32
Copy link
Copy Markdown

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 fixes override checking so that explicitly constructed properties (p = property(...)) are normalized into Pyrefly’s internal property representation, making them behave like @property for descriptor/property compatibility checks (fixes #3141).

Changes:

  • Add special-casing in class field postprocessing to recognize property(...) assignments and attach PropertyMetadata so they are treated as properties.
  • Introduce helpers to extract property() constructor args and wrap callable types into functions with synthetic metadata.
  • Add a regression test covering @property overridden by property(lambda ...).

Reviewed changes

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

File Description
pyrefly/lib/alt/class/class_field.rs Detects property(...) in class bodies and converts it to the internal property representation used by override checking.
pyrefly/lib/test/class_overrides.rs Adds a regression test for the @property vs property(...) override case from #3141.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pyrefly/lib/alt/class/class_field.rs Outdated
Comment thread pyrefly/lib/alt/class/class_field.rs Outdated
Comment thread pyrefly/lib/alt/class/class_field.rs Outdated
@github-actions github-actions bot added size/m and removed size/m labels Apr 16, 2026
@github-actions github-actions bot added size/m and removed size/m labels Apr 16, 2026
@github-actions
Copy link
Copy Markdown

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

dulwich (https://github.com/dulwich/dulwich)
+ ERROR dulwich/bitmap.py:852:36-49: Cannot index into `BaseObjectStore` [bad-index]
+ ERROR dulwich/dumb.py:541:30-43: Argument `bytes` is not assignable to parameter `element` with type `ObjectID` in function `set.add` [bad-argument-type]
+ ERROR dulwich/gc.py:176:32-45: Argument `bytes` is not assignable to parameter `x` with type `ObjectID` in function `collections.deque.append` [bad-argument-type]
+ ERROR dulwich/gc.py:177:31-44: Argument `bytes` is not assignable to parameter `element` with type `ObjectID` in function `set.add` [bad-argument-type]
+ ERROR dulwich/object_filters.py:523:31-50: Argument `tuple[bytes, Literal[''], Literal[0]]` is not assignable to parameter `object` with type `tuple[ObjectID, str, int]` in function `list.append` [bad-argument-type]
+ ERROR dulwich/object_store.py:252:25-28: Cannot index into `ObjectContainer` [bad-index]
+ ERROR dulwich/object_store.py:307:29-32: Cannot index into `ObjectContainer` [bad-index]
+ ERROR dulwich/object_store.py:2826:63-71: Argument `list[bytes]` is not assignable to parameter `lst` with type `Iterable[ObjectID]` in function `_split_commits_and_tags` [bad-argument-type]
+ ERROR dulwich/object_store.py:2997:31-81: Argument `list[tuple[bytes, None, int, bool]]` is not assignable to parameter `entries` with type `Iterable[tuple[ObjectID, bytes | None, int | None, bool]]` in function `MissingObjectFinder.add_todo` [bad-argument-type]
+ ERROR dulwich/object_store.py:3636:9-23: `bytes` is not assignable to `ObjectID | RawObjectID` [bad-assignment]
+ ERROR dulwich/objectspec.py:512:41-48: Cannot index into `PackCapableObjectStore` [bad-index]
+ ERROR dulwich/porcelain/__init__.py:4712:44-47: Argument `bytes` is not assignable to parameter `sha` with type `ObjectID | RawObjectID` in function `dulwich.repo.BaseRepo.get_object` [bad-argument-type]
+ ERROR dulwich/porcelain/__init__.py:4900:40-69: Cannot set item in `dict[bytes, tuple[int, list[bytes], str]]` [unsupported-operation]
+ ERROR dulwich/porcelain/__init__.py:6232:21-25: Argument `bytes | None` is not assignable to parameter `data` with type `bytes` in function `dulwich.objects.Blob._set_data` [bad-argument-type]
+ ERROR dulwich/porcelain/__init__.py:6537:39-52: Argument `bytes` is not assignable to parameter `sha` with type `ObjectID | RawObjectID` in function `dulwich.repo.BaseRepo.get_object` [bad-argument-type]
+ ERROR dulwich/refs.py:1644:20-34: Object of class `bytes` has no attribute `get_object` [missing-attribute]
+ ERROR dulwich/repo.py:2415:44-47: Argument `bytes` is not assignable to parameter `sha` with type `ObjectID | RawObjectID` in function `BaseRepo.get_object` [bad-argument-type]
+ ERROR dulwich/walk.py:192:24-37: Argument `bytes` is not assignable to parameter `object_id` with type `ObjectID` in function `_CommitTimeQueue._push` [bad-argument-type]
+ ERROR dulwich/worktree.py:560:25-36: Argument `Sequence[ObjectID] | list[ObjectID]` is not assignable to parameter `value` with type `list[ObjectID]` in function `dulwich.objects.Commit._set_parents` [bad-argument-type]
+ ERROR dulwich/worktree.py:566:29-40: Argument `Sequence[ObjectID] | list[ObjectID]` is not assignable to parameter `value` with type `list[ObjectID]` in function `dulwich.objects.Commit._set_parents` [bad-argument-type]
+ ERROR dulwich/worktree.py:657:29-40: Argument `Sequence[ObjectID] | list[ObjectID]` is not assignable to parameter `value` with type `list[ObjectID]` in function `dulwich.objects.Commit._set_parents` [bad-argument-type]
+ ERROR dulwich/worktree.py:716:46-49: Argument `bytes` is not assignable to parameter `sha` with type `ObjectID | RawObjectID` in function `dulwich.repo.BaseRepo.get_object` [bad-argument-type]

jax (https://github.com/google/jax)
+ ERROR jax/_src/stages.py:424:3-12: Class member `Traced.args_info` overrides parent class `Stage` in an inconsistent manner [bad-override]

comtypes (https://github.com/enthought/comtypes)
+ ERROR comtypes/_post_coinit/unknwn.py:305:5-10: Class member `_compointer_base.value` overrides parent class `c_void_p` in an inconsistent manner [bad-override]

strawberry (https://github.com/strawberry-graphql/strawberry)
+ ERROR strawberry/experimental/pydantic/conversion.py:93:24-59: No matching overload found for function `getattr` called with arguments: (Unknown, str | None, None) [no-matching-overload]
+ ERROR strawberry/field_extensions/input_mutation.py:39:23-44: Cannot set item in `dict[str, Any]` [unsupported-operation]
+ ERROR strawberry/printer/printer.py:160:59-60: Argument `StrawberryField` is not assignable to parameter `obj` with type `HasGraphQLName` in function `strawberry.schema.name_converter.NameConverter.get_graphql_name` [bad-argument-type]
+ ERROR strawberry/schema/name_converter.py:104:38-43: Argument `StrawberryField` is not assignable to parameter `obj` with type `HasGraphQLName` in function `NameConverter.get_graphql_name` [bad-argument-type]
+ ERROR strawberry/schema/schema.py:482:64-69: Argument `StrawberryField` is not assignable to parameter `obj` with type `HasGraphQLName` in function `strawberry.schema.name_converter.NameConverter.get_graphql_name` [bad-argument-type]
+ ERROR strawberry/schema/schema_converter.py:381:64-69: Argument `StrawberryField` is not assignable to parameter `obj` with type `HasGraphQLName` in function `strawberry.schema.name_converter.NameConverter.get_graphql_name` [bad-argument-type]
+ ERROR strawberry/types/field.py:234:46-62: Argument `str | None` is not assignable to parameter with type `str` [bad-argument-type]
+ ERROR strawberry/types/info.py:135:16-39: Returned type `str | None` is not assignable to declared return type `str` [bad-return]
+ ERROR strawberry/types/type_resolver.py:145:51-68: Argument `str | None` is not assignable to parameter `field_name` with type `str` in function `strawberry.exceptions.private_strawberry_field.PrivateStrawberryFieldError.__init__` [bad-argument-type]
+ ERROR strawberry/types/type_resolver.py:154:21-38: Argument `str | None` is not assignable to parameter `field_name` with type `str` in function `strawberry.exceptions.FieldWithResolverAndDefaultValueError.__init__` [bad-argument-type]
+ ERROR strawberry/types/type_resolver.py:167:21-38: Argument `str | None` is not assignable to parameter `field_name` with type `str` in function `strawberry.exceptions.FieldWithResolverAndDefaultFactoryError.__init__` [bad-argument-type]

zope.interface (https://github.com/zopefoundation/zope.interface)
- ERROR src/zope/interface/declarations.py:217:9-18: Class member `_ImmutableDeclaration.__bases__` overrides parent class `Declaration` in an inconsistent manner [bad-override]
+ ERROR src/zope/interface/declarations.py:217:9-18: Class member `_ImmutableDeclaration.__bases__` overrides parent class `Declaration` in an inconsistent manner [bad-override-param-name]

pandas (https://github.com/pandas-dev/pandas)
+ ERROR pandas/core/generic.py:9741:43-58: Argument `ndarray[tuple[Any, ...], dtype[Unknown]]` is not assignable to parameter `values` with type `Sequence[Hashable]` in function `pandas.core.indexes.base.Index._set_names` [bad-argument-type]
- ERROR pandas/core/indexes/base.py:1853:16-25: Argument `Hashable | Sequence[Hashable] | list[Hashable | None] | Any | None` is not assignable to parameter `obj` with type `Sized` in function `len` [bad-argument-type]
- ERROR pandas/core/indexes/base.py:1855:75-84: Argument `Hashable | Sequence[Hashable] | list[Hashable | None] | Any | None` is not assignable to parameter `obj` with type `Sized` in function `len` [bad-argument-type]
+ ERROR pandas/core/indexes/base.py:1853:16-25: Argument `FrozenList | Hashable | Sequence[Hashable] | list[Hashable | None] | None` is not assignable to parameter `obj` with type `Sized` in function `len` [bad-argument-type]
+ ERROR pandas/core/indexes/base.py:1855:75-84: Argument `FrozenList | Hashable | Sequence[Hashable] | list[Hashable | None] | None` is not assignable to parameter `obj` with type `Sized` in function `len` [bad-argument-type]
- ERROR pandas/core/indexes/base.py:1861:16-25: Returned type `Hashable | Sequence[Hashable] | list[Hashable | None] | Any | None` is not assignable to declared return type `list[Hashable]` [bad-return]
+ ERROR pandas/core/indexes/base.py:1861:16-25: Returned type `FrozenList | Hashable | Sequence[Hashable] | list[Hashable | None] | None` is not assignable to declared return type `list[Hashable]` [bad-return]
+ ERROR pandas/core/indexes/multi.py:1750:5-10: Class member `MultiIndex.names` overrides parent class `Index` in an inconsistent manner [bad-override-param-name]
- ERROR pandas/core/indexes/multi.py:1812:16-21: Returned type `int | integer | Unknown` is not assignable to declared return type `int` [bad-return]
+ ERROR pandas/core/indexes/multi.py:1812:16-21: Returned type `int | integer | Any` is not assignable to declared return type `int` [bad-return]
- ERROR pandas/io/pytables.py:3411:49-54: Argument `ExtensionArray | Index | Series | ndarray | Any` is not assignable to parameter `value` with type `ExtensionArray | ndarray` in function `GenericFixed.write_array_empty` [bad-argument-type]
+ ERROR pandas/io/pytables.py:3411:49-54: Argument `ExtensionArray | Index | Series | ndarray` is not assignable to parameter `value` with type `ExtensionArray | ndarray` in function `GenericFixed.write_array_empty` [bad-argument-type]
- ERROR pandas/io/pytables.py:3452:45-50: Argument `ExtensionArray | Index | Series | ndarray | Any` is not assignable to parameter `value` with type `ExtensionArray | ndarray` in function `GenericFixed.write_array_empty` [bad-argument-type]
+ ERROR pandas/io/pytables.py:3452:45-50: Argument `ExtensionArray | Index | Series | ndarray` is not assignable to parameter `value` with type `ExtensionArray | ndarray` in function `GenericFixed.write_array_empty` [bad-argument-type]
+ ERROR pandas/tests/series/methods/test_matmul.py:32:46-56: Argument `ExtensionArray | ndarray` is not assignable to parameter `b` with type `_Buffer | _NestedSequence[bytes | complex | str] | _NestedSequence[_SupportsArray[dtype]] | _SupportsArray[dtype] | bytes | complex | str` in function `numpy._core.multiarray.dot` [bad-argument-type]
+ ERROR pandas/tests/series/methods/test_matmul.py:67:46-56: Argument `ExtensionArray | ndarray` is not assignable to parameter `b` with type `_Buffer | _NestedSequence[bytes | complex | str] | _NestedSequence[_SupportsArray[dtype]] | _SupportsArray[dtype] | bytes | complex | str` in function `numpy._core.multiarray.dot` [bad-argument-type]
+ ERROR pandas/tests/series/methods/test_matmul.py:73:46-56: Argument `ExtensionArray | ndarray` is not assignable to parameter `b` with type `_Buffer | _NestedSequence[bytes | complex | str] | _NestedSequence[_SupportsArray[dtype]] | _SupportsArray[dtype] | bytes | complex | str` in function `numpy._core.multiarray.dot` [bad-argument-type]

optuna (https://github.com/optuna/optuna)
+ ERROR optuna/importance/_ped_anova/evaluator.py:248:47-55: Argument `list[float] | None` is not assignable to parameter `obj` with type `Sized` in function `len` [bad-argument-type]
+ ERROR optuna/samplers/nsgaii/_elite_population_selection_strategy.py:84:24-44: Argument `list[float] | None` is not assignable to parameter `obj` with type `Sized` in function `len` [bad-argument-type]
+ ERROR optuna/samplers/nsgaii/_elite_population_selection_strategy.py:89:12-35: `None` is not subscriptable [unsupported-operation]
+ ERROR optuna/samplers/nsgaii/_elite_population_selection_strategy.py:89:39-63: `None` is not subscriptable [unsupported-operation]
+ ERROR optuna/samplers/nsgaii/_elite_population_selection_strategy.py:92:33-48: `None` is not subscriptable [unsupported-operation]
+ ERROR optuna/study/_multi_objective.py:31:16-24: Argument `list[float] | None` is not assignable to parameter `obj` with type `Sized` in function `len` [bad-argument-type]
+ ERROR optuna/study/_multi_objective.py:37:50-58: Argument `list[float] | None` is not assignable to parameter `iter1` with type `Iterable[float]` in function `zip.__new__` [bad-argument-type]
+ ERROR optuna/visualization/_param_importances.py:109:34-56: `None` is not subscriptable [unsupported-operation]
+ ERROR optuna/visualization/_pareto_front.py:336:12-24: Returned type `list[float] | None` is not assignable to declared return type `Sequence[float]` [bad-return]
+ ERROR optuna/visualization/_rank.py:273:68-80: Argument `list[float] | None` is not assignable to parameter `iterable` with type `Iterable[float]` in function `enumerate.__new__` [bad-argument-type]
+ ERROR tests/storages_tests/rdb_tests/test_storage.py:297:30-45: `None` is not subscriptable [unsupported-operation]
+ ERROR tests/storages_tests/rdb_tests/test_storage.py:298:29-44: `None` is not subscriptable [unsupported-operation]
+ ERROR tests/study_tests/test_study.py:1158:36-55: Argument `list[float] | None` is not assignable to parameter `values` with type `list[float]` in function `optuna.study.study.Study._log_completed_trial` [bad-argument-type]
+ ERROR tests/study_tests/test_study.py:1163:36-55: Argument `list[float] | None` is not assignable to parameter `values` with type `list[float]` in function `optuna.study.study.Study._log_completed_trial` [bad-argument-type]
+ ERROR tests/study_tests/test_study.py:1168:36-55: Argument `list[float] | None` is not assignable to parameter `values` with type `list[float]` in function `optuna.study.study.Study._log_completed_trial` [bad-argument-type]
+ ERROR tests/study_tests/test_study.py:1254:19-27: Argument `list[float] | None` is not assignable to parameter `iterable` with type `Iterable[float]` in function `tuple.__new__` [bad-argument-type]
+ ERROR tests/visualization_tests/test_contour.py:523:26-45: `None` is not subscriptable [unsupported-operation]
+ ERROR tests/visualization_tests/test_edf.py:172:33-52: `None` is not subscriptable [unsupported-operation]
+ ERROR tests/visualization_tests/test_parallel_coordinate.py:780:33-52: `None` is not subscriptable [unsupported-operation]
+ ERROR tests/visualization_tests/test_param_importances.py:273:26-46: `None` is not subscriptable [unsupported-operation]
+ ERROR tests/visualization_tests/test_param_importances.py:295:26-46: `None` is not subscriptable [unsupported-operation]
+ ERROR tests/visualization_tests/test_pareto_front.py:333:21-42: Argument `(t: FrozenTrial) -> float | Unknown` is not assignable to parameter `targets` with type `((FrozenTrial) -> Sequence[float]) | None` in function `optuna.visualization._pareto_front._get_pareto_front_info` [bad-argument-type]
+ ERROR tests/visualization_tests/test_pareto_front.py:333:31-42: `None` is not subscriptable [unsupported-operation]
+ ERROR tests/visualization_tests/test_pareto_front.py:351:32-43: `None` is not subscriptable [unsupported-operation]
+ ERROR tests/visualization_tests/test_pareto_front.py:351:45-56: `None` is not subscriptable [unsupported-operation]
+ ERROR tests/visualization_tests/test_pareto_front.py:351:58-69: `None` is not subscriptable [unsupported-operation]
+ ERROR tests/visualization_tests/test_rank.py:615:26-45: `None` is not subscriptable [unsupported-operation]
+ ERROR tests/visualization_tests/test_slice.py:403:26-45: `None` is not subscriptable [unsupported-operation]
+ ERROR tests/visualization_tests/test_utils.py:131:16-44: `None` is not subscriptable [unsupported-operation]

@github-actions
Copy link
Copy Markdown

Primer Diff Classification

❌ 5 regression(s) | ✅ 1 improvement(s) | ➖ 1 neutral | 7 project(s) total | +76, -7 errors

5 regression(s) across dulwich, jax, strawberry, pandas, optuna. error kinds: bad-argument-type: bytes vs ObjectID, bad-index: Cannot index into BaseObjectStore, bad-assignment and unsupported-operation. caused by get_property_class_field_type(). 1 improvement(s) across comtypes.

Project Verdict Changes Error Kinds Root Cause
dulwich ❌ Regression +22 bad-argument-type: bytes vs ObjectID get_property_class_field_type()
jax ❌ Regression +1 bad-override get_property_class_field_type()
comtypes ✅ Improvement +1 bad-override get_property_class_field_type()
strawberry ❌ Regression +11 bad-argument-type, bad-return get_property_class_field_type()
zope.interface ➖ Neutral +1, -1 bad-override, bad-override-param-name get_property_class_field_type()
pandas ❌ Regression +11, -6 bad-argument-type (ndarray vs Sequence[Hashable]) get_property_class_field_type()
optuna ❌ Regression +29 unsupported-operation: None is not subscriptable on FrozenTrial.values get_property_class_field_type()
Detailed analysis

❌ Regression (5)

dulwich (+22)

bad-argument-type: bytes vs ObjectID: 15 errors where bytes is not assignable to ObjectID. These arise because the new property normalization in get_property_class_field_type() likely changes how Tag.object property's return type is inferred, losing the NewType wrapper. Since ObjectID = NewType('ObjectID', bytes), the property getter's return type is being resolved as plain bytes instead of ObjectID.
bad-index: Cannot index into BaseObjectStore: 4 cascade errors from the same root cause. BaseObjectStore.__getitem__ expects ObjectID but receives bytes due to the property type inference regression.
bad-assignment and unsupported-operation: 2 cascade errors from the same bytes-vs-ObjectID mismatch propagating through assignments and dict operations.
missing-attribute: bytes.get_object: 1 error where a value expected to be an object with get_object method is inferred as bytes. Pyright also flags this (1/1), suggesting this specific case may have a pre-existing issue, but it's still likely exacerbated by the property inference change.

Overall: The PR introduces a new property normalization path that appears to degrade type inference for properties defined via property() constructor in dulwich. All 22 errors stem from the same root cause: property return types are being resolved as bytes instead of ObjectID (a NewType over bytes). Since 21/22 errors are pyrefly-only and dulwich is a well-tested project, these are false positives introduced by the change.

Per-category reasoning:

  • bad-argument-type: bytes vs ObjectID: 15 errors where bytes is not assignable to ObjectID. These arise because the new property normalization in get_property_class_field_type() likely changes how Tag.object property's return type is inferred, losing the NewType wrapper. Since ObjectID = NewType('ObjectID', bytes), the property getter's return type is being resolved as plain bytes instead of ObjectID.
  • bad-index: Cannot index into BaseObjectStore: 4 cascade errors from the same root cause. BaseObjectStore.__getitem__ expects ObjectID but receives bytes due to the property type inference regression.
  • bad-assignment and unsupported-operation: 2 cascade errors from the same bytes-vs-ObjectID mismatch propagating through assignments and dict operations.
  • missing-attribute: bytes.get_object: 1 error where a value expected to be an object with get_object method is inferred as bytes. Pyright also flags this (1/1), suggesting this specific case may have a pre-existing issue, but it's still likely exacerbated by the property inference change.

Attribution: The get_property_class_field_type() method added in pyrefly/lib/alt/class/class_field.rs normalizes property(...) constructor calls into pyrefly's internal property representation. This new code path likely changes how dulwich's Tag.object property (and similar properties defined via property() constructor) resolves its return type, causing it to infer bytes instead of ObjectID. The property_constructor_callable() method converts Callable types to Function types with synthetic metadata, which may lose NewType information in the process.

jax (+1)

The parent class Stage declares args_info: Any (line 333) as a plain class-level annotation. The child Traced defines args_info = property(_traced_args_info) (line 424). Since the parent type is Any, overriding it with a property of any return type should be compatible — Any is compatible with all types per the typing spec. Neither mypy nor pyright flag this. The PR's new property recognition logic correctly identifies property(...) calls as properties (fixing #3141), but this has the side effect of triggering a spurious bad-override when the parent attribute is typed as Any. This is a false positive — the override is valid because Any is the parent type. Note that Lowered (line 534/538) and Compiled (line 653/656) also override args_info from Stage without issue, because they re-declare it with the type annotation args_info: Any (matching the parent's type), rather than using property() calls. The Lowered class lists args_info in __slots__ and annotates it as args_info: Any at line 538, and Compiled similarly lists it in __slots__ and annotates it as args_info: Any at line 656. These matching type annotations don't trigger the override check, whereas the property() descriptor in Traced does.
Attribution: The PR change in pyrefly/lib/alt/class/class_field.rs adds get_property_class_field_type() which now recognizes property(...) constructor calls and converts them into pyrefly's internal property representation. Before this change, args_info = property(_traced_args_info) was treated as a generic descriptor assignment. Now it's treated as a proper property. This new property representation triggers the bad-override check against the parent Stage.args_info: Any. The parent has a plain Any-typed attribute, and the child now has a property — pyrefly sees this as an inconsistent override.

strawberry (+11)

All 11 errors are pyrefly-only and stem from the PR's property constructor normalization changing the inferred type of StrawberryField.python_name from str to str | None. This cascades into false positives across the codebase. The code is correct and works at runtime — StrawberryField satisfies HasGraphQLName, dict[str, Any] supports item assignment, and getattr accepts these arguments.
Attribution: The new get_property_class_field_type() method in pyrefly/lib/alt/class/class_field.rs normalizes property(...) constructor calls into internal property representations. This likely changes how StrawberryField.python_name (which is defined as a property) is resolved — the getter's return type str | None is now surfaced instead of the previously inferred str. This cascades into all 11 errors: python_name being str | None causes protocol mismatches (HasGraphQLName), getattr overload failures, dict key type errors, and return type mismatches.

pandas (+11, -6)

bad-argument-type (ndarray vs Sequence[Hashable]): 8 new pyrefly-only errors where ndarray is not accepted as assignable to Sequence[Hashable]. While ndarray does not formally register as a collections.abc.Sequence subclass, it does support __getitem__, __len__, and __iter__, and both mypy and pyright accept ndarray where Sequence is expected through special-cased handling or structural subtyping. These are false positives — pyrefly lacks the equivalent handling that mypy/pyright provide for ndarray-to-Sequence compatibility.
bad-return (wider union from property getter): 2 new pyrefly-only errors where the property getter's return type union doesn't match the declared return type. These are false positives from changed type inference due to the new property recognition.
bad-override-param-name on MultiIndex.names: 1 new pyrefly-only error claiming MultiIndex.names inconsistently overrides Index.names. Neither mypy nor pyright flag this — likely a false positive from the new property recognition.
removed bad-argument-type and bad-return errors: 6 removed errors were false positives involving types with Any in unions. Removing them is an improvement.

Overall: The PR fixes a real issue (recognizing property(...) as properties) but introduces 11 new false positive errors in pandas, all pyrefly-only. The 6 removed errors were also false positives. Net: 6 false positives removed, 11 new false positives added. The new errors involve ndarray not being accepted where Sequence[Hashable] is expected, and union types from property getters not matching declared return types. Since none of the 11 new errors are flagged by mypy or pyright, and the code is correct, this is a net regression.

Attribution: The change to get_property_class_field_type() in pyrefly/lib/alt/class/class_field.rs now recognizes property(...) constructor calls as properties. This changes how Index.names and MultiIndex.names are typed, which cascades into the bad-argument-type, bad-return, and bad-override-param-name errors. The more precise property type resolution removed Any from some unions (fixing 6 old false positives) but introduced 11 new false positives where the narrower types don't match expected parameter/return types.

optuna (+29)

unsupported-operation: None is not subscriptable on FrozenTrial.values: FrozenTrial.values is accessed with indexing (e.g., trial.values[i]) throughout optuna after filtering for complete trials. Pyrefly now infers the type as nullable, causing 18 false positive subscript errors. 0/18 co-reported by mypy/pyright.
bad-argument-type: nullable values passed to len/Sized: Passing trial.values or derived lists to len() triggers errors because pyrefly now sees the type as potentially None. These are downstream effects of the same property type inference change. 0/10 co-reported.
bad-return: nullable return type: Returning values derived from the nullable property type causes return type mismatch. Same root cause. 0/1 co-reported.

Overall: The PR's property normalization change appears to have altered how FrozenTrial.values is typed, introducing 29 false positive errors across optuna. All errors stem from pyrefly now treating values as potentially None in contexts where it cannot be. Neither mypy nor pyright flag any of these, and the code is well-tested production code in a major library. The errors cascade from a single root cause (incorrect/overly-strict property return type inference).

Per-category reasoning:

  • unsupported-operation: None is not subscriptable on FrozenTrial.values: FrozenTrial.values is accessed with indexing (e.g., trial.values[i]) throughout optuna after filtering for complete trials. Pyrefly now infers the type as nullable, causing 18 false positive subscript errors. 0/18 co-reported by mypy/pyright.
  • bad-argument-type: nullable values passed to len/Sized: Passing trial.values or derived lists to len() triggers errors because pyrefly now sees the type as potentially None. These are downstream effects of the same property type inference change. 0/10 co-reported.
  • bad-return: nullable return type: Returning values derived from the nullable property type causes return type mismatch. Same root cause. 0/1 co-reported.

Attribution: The change to get_property_class_field_type() in pyrefly/lib/alt/class/class_field.rs normalizes property(...) constructor calls into pyrefly's internal property representation. This likely changed how FrozenTrial.values (which may be defined via property(...)) is resolved, causing pyrefly to infer a nullable return type (tuple[float, ...] | None) where previously it inferred a non-nullable type. The .or_else(|| self.get_property_class_field_type(...)) insertion at line 2145 is the trigger — it now intercepts property constructor patterns before falling through to the default value inference path.

✅ Improvement (1)

comtypes (+1)

This is a genuine type inconsistency. c_void_p.value is typed as int | None in typeshed (it returns the raw pointer value as an integer). _compointer_base deliberately overrides this with a property that returns Self (the instance itself) — line 302-305 shows def __get_value(self) -> "hints.Self": return self wrapped in value = property(__get_value, ...). The return type Self is not compatible with int | None, making this a real Liskov substitution violation at the type level. The code comment on line 301 explicitly says 'redefine the .value property' — this is an intentional but type-incompatible override. Pyright also flags this. The PR's change to recognize property(...) constructor calls as proper property definitions is working correctly — it now sees the type mismatch that was previously invisible because the property wasn't being recognized as such.
Attribution: The change in pyrefly/lib/alt/class/class_field.rs added get_property_class_field_type() which now recognizes value = property(__get_value, doc="""Return self.""") as a property definition (previously it was treated as a generic descriptor). This means pyrefly now properly understands the type of _compointer_base.value as a property returning hints.Self, and can compare it against the parent c_void_p.value (typed as int | None in typeshed). The type mismatch between Self (the compointer instance) and int | None triggers the bad-override error.

➖ Neutral (1)

zope.interface (+1, -1)

This is a neutral change — the same override issue at the same location (line 217, _ImmutableDeclaration.__bases__) is being reported, just with a more specific error code (bad-override-param-name instead of bad-override). The PR's property normalization logic now recognizes @property definitions properly, which causes pyrefly to perform more specific override checking (comparing parameter names) rather than generic override checking. The underlying issue being flagged is the same: _ImmutableDeclaration.__bases__ (a property) overrides Declaration.__bases__ (inherited from Specification) in an inconsistent manner. The error message text is identical ('overrides parent class Declaration in an inconsistent manner'), only the error code changed. This is a message/classification refinement with no behavioral impact.
Attribution: The PR change in pyrefly/lib/alt/class/class_field.rs adds get_property_class_field_type() which normalizes property(...) constructor calls into pyrefly's internal property representation. Previously, _ImmutableDeclaration.__bases__ defined via @property decorator was being compared against the parent class's __bases__ (which comes from Specification, likely a C extension class) as a generic descriptor override. Now that pyrefly recognizes the property properly, it can do more specific override checking — comparing the property getter/setter signatures rather than just the generic type. This caused the error to shift from the generic bad-override to the more specific bad-override-param-name, which checks parameter name consistency between the child's property and the parent's attribute.

Suggested fixes

Summary: The new get_property_class_field_type() function correctly recognizes property() constructor calls but surfaces nullable/wider return types from property getters that were previously hidden, causing 74 pyrefly-only false positives across 5 projects.

1. In get_property_class_field_type() in pyrefly/lib/alt/class/class_field.rs, the function constructs property metadata but when there is a setter, it returns the setter type (which represents the property's write type). When there is no setter, it returns the getter type. However, the issue is that the property getter's return type is being exposed directly as the field type, whereas previously the field was treated as a plain assignment and the type was inferred differently (often from type annotations or as a descriptor). The core problem is that when a class has a type annotation for the field AND a property() assignment, the annotation should take precedence. Add a guard at the top of get_property_class_field_type() that returns None if the field already has a type annotation in the class body. This would let the annotation-based path handle the type (preserving NewType wrappers like ObjectID, and respecting declared non-nullable types), while still handling the case where property() is used without an annotation (the #3141 fix).

Files: pyrefly/lib/alt/class/class_field.rs
Confidence: medium
Affected projects: dulwich, strawberry, optuna, pandas
Fixes: bad-argument-type, bad-index, bad-assignment, unsupported-operation, missing-attribute, bad-return
In dulwich, Tag.object likely has a type annotation declaring ObjectID return type, but the property() constructor path now overrides that with the inferred bytes type from the getter function. In optuna, FrozenTrial.values likely has an annotation declaring a non-nullable type, but the property getter returns Optional. In strawberry, StrawberryField.python_name's getter returns str | None but the annotation says str. By checking if there's already a type annotation and deferring to it, we preserve the declared types while still fixing #3141 for unannotated property() calls. This would eliminate ~63 errors across dulwich (22), strawberry (11), optuna (29), and partially pandas.

2. In get_property_class_field_type() in pyrefly/lib/alt/class/class_field.rs, when the getter type is obtained via property_constructor_callable() from a Callable type (not a Function), the synthetic_property_function_metadata() creates metadata with class and name but loses the original function's type information including NewType wrappers. Specifically, when the property getter is a bound method or lambda whose return type is a NewType (like ObjectID = NewType('ObjectID', bytes)), the Callable -> Function conversion in property_constructor_callable() preserves the Callable's signature but the signature may have already been simplified to the base type. Ensure that when converting Callable to Function, the original return type annotation is preserved exactly, including NewType wrappers.

Files: pyrefly/lib/alt/class/class_field.rs
Confidence: medium
Affected projects: dulwich
Fixes: bad-argument-type, bad-index, bad-assignment, unsupported-operation, missing-attribute
In dulwich, ObjectID is NewType('ObjectID', bytes). The property getter likely has return type ObjectID in its annotation, but when the Callable representation is created, it may normalize ObjectID to bytes. The property_constructor_callable() then wraps this in a Function, but the damage is already done. This specifically explains the 22 dulwich errors where bytes appears instead of ObjectID.

3. In get_property_class_field_type() in pyrefly/lib/alt/class/class_field.rs, when the function returns the setter type (the branch if let Some(mut setter) = setter), this causes the property's read type to be determined by the setter's parameter type rather than the getter's return type. For override checking, the getter's return type should be the primary type exposed. However, the bigger issue for jax is that when a parent class has args_info: Any, overriding with a property should be compatible since Any is compatible with everything. Add a check in the override validation logic (not in this function) that treats overriding an Any-typed parent attribute with a property as compatible, OR ensure get_property_class_field_type() does not trigger override checks when the parent type is Any.

Files: pyrefly/lib/alt/class/class_field.rs
Confidence: medium
Affected projects: jax
Fixes: bad-override
The jax error is a bad-override where Traced.args_info = property(_traced_args_info) overrides Stage.args_info: Any. Since Any is the parent type, any override should be compatible. This is 1 pyrefly-only error in jax. The fix should be in override checking rather than property recognition, but the property recognition is what triggers it.

4. In get_property_class_field_type() in pyrefly/lib/alt/class/class_field.rs, the function should check whether the property getter/setter return types are consistent with any existing type stub annotations for the same field. For pandas specifically, the MultiIndex.names property override of Index.names triggers bad-override-param-name because the property signatures differ. The property recognition is correct but the override checking is too strict for property-to-property overrides where parameter names differ (e.g., 'self' parameter name differences or getter function name differences). This is a known issue with property override checking that should be handled by comparing only the return types of property getters, not their full signatures including parameter names.

Files: pyrefly/lib/alt/class/class_field.rs
Confidence: medium
Affected projects: pandas
Fixes: bad-override-param-name
The pandas bad-override-param-name error (1 error) occurs because MultiIndex.names property overrides Index.names property but with different internal parameter names. Property overrides should compare return types and setter parameter types, not getter parameter names. The 8 bad-argument-type errors in pandas (ndarray vs Sequence[Hashable]) are a separate pre-existing issue with ndarray compatibility that was unmasked by the property type change. The 2 bad-return errors are from the property getter returning a wider union than expected.


Was this helpful? React with 👍 or 👎

Classification by primer-classifier (7 LLM)

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.

Recognize explicitly constructed properties as such

2 participants