Skip to content

[ty] treat properties as full structural types#23925

Merged
carljm merged 1 commit intomainfrom
cjm/structural-property
Mar 13, 2026
Merged

[ty] treat properties as full structural types#23925
carljm merged 1 commit intomainfrom
cjm/structural-property

Conversation

@carljm
Copy link
Contributor

@carljm carljm commented Mar 13, 2026

Summary

Fixes astral-sh/ty#3032

The bug there was that type relation judgements between two PropertyInstance ended up falling back on both sides to using KnownClass::Property, meaning that we would always consider any two PropertyInstance (even with entirely different getters/setters) as equivalent.

Fixing this bug required more clearly deciding on the semantics of a PropertyInstance type. Either it is a singleton type representing exactly one property object, or else it is a structural type with getter and setter members and full structural subtype relations accordingly.

The existing code sort of leaned towards the singleton understanding, but that interpretation isn't sustainable consistently with the current Salsa-interned implementation of PropertyInstanceType. If we create two different properties in our Python code, but with the same getter and setter type, those will become the same Salsa-interned PropertyInstanceType, and thus necessarily treated as equivalent. In other words, we can't truly treat a type as a singleton type unless it is either known to be interned at runtime (e.g. None), or its Salsa-interned attributes include a Definition or similar differentiator (e.g. class and function literals).

So I instead chose to go the other direction, and implement full structural type relations for PropertyInstanceType, which isn't that complex since they only have two possible attributes.

Test Plan

Added an mdtest which fails on main representing the user-observable behavior where a union of property instances collapsed.

Added comprehensive mdtests for type relations between property instances.

@astral-sh-bot astral-sh-bot bot added the ty Multi-file analysis & type inference label Mar 13, 2026
@astral-sh-bot
Copy link

astral-sh-bot bot commented Mar 13, 2026

Typing conformance results

No changes detected ✅

Current numbers
The percentage of diagnostics emitted that were expected errors held steady at 85.29%. The percentage of expected errors that received a diagnostic held steady at 78.13%. The number of fully passing files held steady at 64/132.

Copy link
Member

@charliermarsh charliermarsh left a comment

Choose a reason for hiding this comment

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

Nice summary, this makes sense to me.

@astral-sh-bot
Copy link

astral-sh-bot bot commented Mar 13, 2026

Memory usage report

Summary

Project Old New Diff Outcome
sphinx 265.10MB 265.15MB +0.02% (54.21kB)
flake8 47.89MB 47.89MB -
prefect 704.98MB 704.98MB -
trio 117.74MB 117.74MB -

Significant changes

Click to expand detailed breakdown

sphinx

Name Old New Diff Outcome
is_redundant_with_impl::interned_arguments 2.06MB 2.07MB +0.60% (12.63kB)
is_redundant_with_impl 1.80MB 1.81MB +0.61% (11.24kB)
Type<'db>::apply_specialization_ 1.66MB 1.67MB +0.54% (9.14kB)
infer_definition_types 24.01MB 24.01MB +0.02% (4.91kB)
infer_scope_types_impl 15.57MB 15.58MB +0.03% (4.75kB)
FunctionType 3.12MB 3.12MB +0.08% (2.52kB)
TupleType<'db>::to_class_type_ 158.75kB 160.88kB +1.34% (2.12kB)
FunctionType<'db>::last_definition_signature_ 213.65kB 215.56kB +0.90% (1.91kB)
Type<'db>::member_lookup_with_policy_ 6.09MB 6.09MB +0.02% (1.55kB)
FunctionType<'db>::signature_ 2.27MB 2.28MB +0.06% (1.37kB)
IntersectionType 892.98kB 894.12kB +0.13% (1.15kB)
PropertyInstanceType 28.27kB 28.88kB +2.13% (616.00B)
UnionType 1.23MB 1.23MB +0.03% (336.00B)

@astral-sh-bot
Copy link

astral-sh-bot bot commented Mar 13, 2026

mypy_primer results

Changes were detected when running on open source projects
pydantic (https://github.com/pydantic/pydantic)
- pydantic/_internal/_core_metadata.py:87:54: error[invalid-assignment] Invalid assignment to key "pydantic_js_extra" with declared type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | ((dict[str, Divergent], type[Any], /) -> None)` on TypedDict `CoreMetadata`: value of type `dict[object, object]`
+ pydantic/_internal/_core_metadata.py:87:54: error[invalid-assignment] Invalid assignment to key "pydantic_js_extra" with declared type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | ((dict[str, int | float | str | ... omitted 3 union elements], type[Any], /) -> None)` on TypedDict `CoreMetadata`: value of type `dict[object, object]`
- pydantic/fields.py:949:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
+ pydantic/fields.py:949:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
- pydantic/fields.py:989:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
+ pydantic/fields.py:989:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
- pydantic/fields.py:1032:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
+ pydantic/fields.py:1032:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
- pydantic/fields.py:1072:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
+ pydantic/fields.py:1072:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
- pydantic/fields.py:1115:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
+ pydantic/fields.py:1115:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
- pydantic/fields.py:1154:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
+ pydantic/fields.py:1154:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
- pydantic/fields.py:1194:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`
+ pydantic/fields.py:1194:5: error[invalid-parameter-default] Default value of type `PydanticUndefinedType` is not assignable to annotated parameter type `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`
- pydantic/fields.py:1573:13: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, Divergent] | ((dict[str, Divergent], /) -> None) | None`, found `dict[str, Divergent] | dict[Never, Never] | (((dict[str, Divergent], /) -> None) & ~Top[dict[Unknown, Unknown]]) | None`
+ pydantic/fields.py:1573:13: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, int | float | str | ... omitted 3 union elements] | ((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) | None`, found `dict[str, int | float | str | ... omitted 3 union elements] | dict[Never, Never] | (((dict[str, int | float | str | ... omitted 3 union elements], /) -> None) & ~Top[dict[Unknown, Unknown]]) | None`

mitmproxy (https://github.com/mitmproxy/mitmproxy)
- mitmproxy/proxy/layers/quic/_raw_layers.py:51:28: error[unresolved-attribute] Object of type `property` has no attribute `Layer`
+ mitmproxy/proxy/layers/quic/_raw_layers.py:51:28: error[unresolved-attribute] Object of type `property | property` has no attribute `Layer`

strawberry (https://github.com/strawberry-graphql/strawberry)
+ strawberry/types/field.py:88:17: error[invalid-type-form] Variable of type `property` is not allowed in a type expression
- Found 342 diagnostics
+ Found 343 diagnostics

scikit-build-core (https://github.com/scikit-build/scikit-build-core)
- src/scikit_build_core/build/wheel.py:99:20: error[no-matching-overload] No overload of bound method `__init__` matches arguments
- Found 58 diagnostics
+ Found 57 diagnostics

django-stubs (https://github.com/typeddjango/django-stubs)
+ django-stubs/contrib/gis/geos/point.pyi:26:25: error[invalid-type-form] Invalid subscript of object of type `property` in type expression
+ django-stubs/contrib/gis/geos/point.pyi:30:24: error[invalid-type-form] Invalid subscript of object of type `property` in type expression
- Found 566 diagnostics
+ Found 568 diagnostics

@astral-sh-bot
Copy link

astral-sh-bot bot commented Mar 13, 2026

ecosystem-analyzer results

Lint rule Added Removed Changed
unresolved-attribute 0 0 1
Total 0 0 1

Full report with detailed diff (timing results)

@carljm
Copy link
Contributor Author

carljm commented Mar 13, 2026

Hmm, ecosystem report shows that since we always display properties as just property, fixing this bug means we can now show the confusing type property | property. Trying to decide if we should show more details for all properties always, or do a disambiguation thing only when they appear within the same type.

EDIT: It kinda seems like we should always show more information, since we are certainly (in various ways) treating it as a more specific type than the general property type.

EDIT 2: This is showing up rarely enough (exactly once in the ecosystem) that I'm inclined to leave it to a separate lower-priority follow-up, if or when someone complains about it.

@carljm carljm marked this pull request as ready for review March 13, 2026 02:38
@carljm carljm merged commit 76f2488 into main Mar 13, 2026
52 checks passed
@carljm carljm deleted the cjm/structural-property branch March 13, 2026 02:50
carljm added a commit that referenced this pull request Mar 13, 2026
* main: (94 commits)
  Fix shell injection via `shell=True` in subprocess calls (#23894)
  [ty] Refactor `relation.rs` to store state on a struct rather than passing around 7 arguments every time we recurse (#23837)
  Don't return code actions for non-Python documents (#23905)
  [ty] Make the default database truly statically infallible (#23929)
  [ty] Add `Download` button to ty playground which creates a zip export (#23478)
  [ty] Respect `kw_only` overwrites in dataclasses (#23930)
  [ty] Clarify in diagnostics that `from __future__ import annotations` only stringifies type annotations (#23928)
  [ty]  Add a `Copy Markdown` button to playground (#23002)
  [ty] Fix folding range classification of lines starting with `#` (#23831)
  [ty] Fix folding ranges for notebooks (#23830)
  [ty] fix too-many-cycle panics when inferring literal type loop variables (#23875)
  Add `RegularCallableTypeOf` and `into_regular_callable` in `ty_extensions` (#23909)
  [ty] treat properties as full structural types (#23925)
  [ty] Avoid duplicated work during multi-inference (#23923)
  [ty]: make `possibly-missing-attribute` ignored by default
  [ty]: split out `possibly-missing-submodule` from `possibly-missing-attribute`
  Update astral-sh/setup-uv action to v7.5.0 (#23922)
  [ty] Show truthiness in ConstraintSet display and simplify falsy error message (#23913)
  Bump 0.15.6 (#23919)
  [ty] Narrow type context during collection literal inference (#23844)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ecosystem-analyzer ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Conditionally defined @propertys don't create union of types

3 participants