Skip to content

[ty] Validate annotated assignments to declared attributes#25191

Open
snoopuppy582 wants to merge 1 commit into
astral-sh:mainfrom
snoopuppy582:fix-ty-annotated-attribute-assignment
Open

[ty] Validate annotated assignments to declared attributes#25191
snoopuppy582 wants to merge 1 commit into
astral-sh:mainfrom
snoopuppy582:fix-ty-annotated-attribute-assignment

Conversation

@snoopuppy582
Copy link
Copy Markdown

Summary

  • validate annotated attribute assignments against the existing attribute type after the annotation check succeeds
  • add mdtest coverage for assigning a value compatible with the inline annotation but incompatible with the declared attribute type

Fixes astral-sh/ty#509.

Test Plan

  • cargo fmt --check
  • cargo test -p ty_python_semantic --test mdtest -- diagnostics/attribute_assignment.md
  • cargo test -p ty_python_semantic --test mdtest -- assignment/annotations.md
  • cargo test -p ty_python_semantic --lib

@astral-sh-bot astral-sh-bot Bot requested a review from charliermarsh May 15, 2026 22:50
@astral-sh-bot astral-sh-bot Bot added the ty Multi-file analysis & type inference label May 15, 2026
@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot Bot commented May 16, 2026

Typing conformance results

The percentage of diagnostics emitted that were expected errors held steady at 89.36%. The percentage of expected errors that received a diagnostic held steady at 85.49%. The number of fully passing files held steady at 88/134.

Summary

How are test cases classified?

Each test case represents one expected error annotation or a group of annotations sharing a tag. Counts are per test case, not per diagnostic — multiple diagnostics on the same line count as one. Required annotations (E) are true positives when ty flags the expected location and false negatives when it does not. Optional annotations (E?) are true positives when flagged but true negatives (not false negatives) when not. Tagged annotations (E[tag]) require ty to flag exactly one of the tagged lines; tagged multi-annotations (E[tag+]) allow any number up to the tag count. Flagging unexpected locations counts as a false positive.

Metric Old New Diff Outcome
True Positives 907 907 +0
False Positives 108 108 +0
False Negatives 154 154 +0
Total Diagnostics 1065 1067 +2
Precision 89.36% 89.36% +0.00%
Recall 85.49% 85.49% +0.00%
Passing Files 88/134 88/134 +0

True positives changed (2)

2 diagnostics
Test case Diff

qualifiers_final_annotation.py:62

-error[invalid-assignment] Cannot assign to final attribute `id3` on type `Self@method1`: `Final` attributes can only be assigned in the class body or `__init__`
+error[invalid-assignment] Cannot assign to final attribute `id3` on type `Self@method1`: `Final` attributes can only be assigned in the class body or `__init__`
+error[invalid-assignment] Cannot assign to final attribute `id3` on type `Self@method1`: `Final` attributes can only be assigned in the class body or `__init__`

qualifiers_final_annotation.py:63

-error[invalid-assignment] Cannot assign to final attribute `id4` on type `Self@method1`: `Final` attributes can only be assigned in the class body or `__init__`
+error[invalid-assignment] Cannot assign to final attribute `id4` on type `Self@method1`: `Final` attributes can only be assigned in the class body or `__init__`
+error[invalid-assignment] Cannot assign to final attribute `id4` on type `Self@method1`: `Final` attributes can only be assigned in the class body or `__init__`

@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot Bot commented May 16, 2026

Memory usage report

Summary

Project Old New Diff Outcome
sphinx 256.20MB 256.45MB +0.10% (257.45kB)
prefect 685.54MB 685.79MB +0.04% (247.79kB)
trio 115.59MB 115.62MB +0.02% (24.76kB)
flake8 47.43MB 47.44MB +0.03% (12.40kB)

Significant changes

Click to expand detailed breakdown

sphinx

Name Old New Diff Outcome
Type<'db>::class_member_with_policy_ 7.63MB 7.73MB +1.35% (105.33kB)
infer_definition_types 21.99MB 22.03MB +0.17% (39.12kB)
Type<'db>::class_member_with_policy_::interned_arguments 4.04MB 4.07MB +0.86% (35.45kB)
StaticClassLiteral<'db>::implicit_attribute_inner_ 2.36MB 2.38MB +0.89% (21.52kB)
infer_scope_types_impl 12.97MB 12.99MB +0.15% (19.80kB)
StaticClassLiteral<'db>::implicit_attribute_inner_::interned_arguments 1.93MB 1.95MB +0.87% (17.25kB)
StringLiteralType 2.03MB 2.05MB +0.49% (10.31kB)
Type<'db>::member_lookup_with_policy_::interned_arguments 2.67MB 2.67MB +0.10% (2.84kB)
Type<'db>::member_lookup_with_policy_ 6.85MB 6.85MB +0.04% (2.52kB)
BoundTypeVarInstance 624.80kB 625.64kB +0.14% (864.00B)
CallableType 1.17MB 1.18MB +0.06% (768.00B)
Type<'db>::try_call_dunder_get_ 4.88MB 4.88MB +0.01% (568.00B)
infer_statement_types_impl 17.92kB 18.25kB +1.83% (336.00B)
infer_expression_types_impl 18.54MB 18.54MB +0.00% (220.00B)
place_by_id 1.37MB 1.37MB +0.01% (176.00B)
... 7 more

prefect

Name Old New Diff Outcome
Type<'db>::class_member_with_policy_ 17.69MB 17.83MB +0.76% (136.82kB)
Type<'db>::class_member_with_policy_::interned_arguments 10.31MB 10.35MB +0.43% (45.09kB)
infer_scope_types_impl 47.83MB 47.86MB +0.05% (26.59kB)
StringLiteralType 5.71MB 5.72MB +0.16% (9.50kB)
StaticClassLiteral<'db>::implicit_attribute_inner_ 6.86MB 6.87MB +0.11% (7.39kB)
StaticClassLiteral<'db>::implicit_attribute_inner_::interned_arguments 5.61MB 5.61MB +0.11% (6.09kB)
Type<'db>::member_lookup_with_policy_::interned_arguments 6.23MB 6.23MB +0.08% (5.28kB)
infer_definition_types 81.48MB 81.48MB +0.01% (4.80kB)
Type<'db>::member_lookup_with_policy_ 16.44MB 16.45MB +0.03% (4.45kB)
BoundTypeVarInstance 1.48MB 1.48MB +0.09% (1.41kB)
is_redundant_with_impl::interned_arguments 2.18MB 2.18MB +0.01% (176.00B)
UnionType 910.97kB 911.08kB +0.01% (112.00B)
is_redundant_with_impl 1.94MB 1.94MB +0.00% (96.00B)
TypeVarInstance 434.62kB 434.72kB +0.02% (96.00B)
infer_expression_types_impl 52.53MB 52.53MB -0.00% (60.00B)
... 1 more

trio

Name Old New Diff Outcome
Type<'db>::class_member_with_policy_ 2.04MB 2.06MB +0.61% (12.78kB)
Type<'db>::class_member_with_policy_::interned_arguments 1.13MB 1.13MB +0.38% (4.37kB)
infer_scope_types_impl 4.02MB 4.03MB +0.07% (2.79kB)
StringLiteralType 525.48kB 526.99kB +0.29% (1.52kB)
StaticClassLiteral<'db>::implicit_attribute_inner_ 741.81kB 742.60kB +0.11% (808.00B)
StaticClassLiteral<'db>::implicit_attribute_inner_::interned_arguments 599.25kB 599.91kB +0.11% (672.00B)
infer_definition_types 7.24MB 7.24MB +0.01% (648.00B)
Type<'db>::apply_specialization_::interned_arguments 630.86kB 631.17kB +0.05% (320.00B)
Type<'db>::member_lookup_with_policy_::interned_arguments 922.19kB 922.49kB +0.03% (312.00B)
Type<'db>::member_lookup_with_policy_ 1.95MB 1.95MB +0.01% (264.00B)
Type<'db>::apply_specialization_ 620.58kB 620.80kB +0.04% (224.00B)
BoundTypeVarInstance 162.70kB 162.84kB +0.09% (144.00B)

flake8

Name Old New Diff Outcome
Type<'db>::class_member_with_policy_ 575.23kB 582.35kB +1.24% (7.12kB)
Type<'db>::class_member_with_policy_::interned_arguments 312.30kB 314.74kB +0.78% (2.44kB)
infer_scope_types_impl 823.30kB 824.71kB +0.17% (1.42kB)
StringLiteralType 194.15kB 194.65kB +0.26% (512.00B)
StaticClassLiteral<'db>::implicit_attribute_inner_ 311.39kB 311.61kB +0.07% (224.00B)
Type<'db>::member_lookup_with_policy_::interned_arguments 230.75kB 230.95kB +0.09% (208.00B)
StaticClassLiteral<'db>::implicit_attribute_inner_::interned_arguments 256.97kB 257.16kB +0.07% (192.00B)
Type<'db>::member_lookup_with_policy_ 554.42kB 554.59kB +0.03% (176.00B)
BoundTypeVarInstance 44.93kB 45.07kB +0.31% (144.00B)

@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot Bot commented May 16, 2026

ecosystem-analyzer results

Lint rule Added Removed Changed
invalid-assignment 13 0 0
unused-type-ignore-comment 0 5 0
Total 13 5 0

Large timing changes:

Project Old Time New Time Change
pandas-stubs 9.77s 15.62s +60%
Raw diff (18 changes)
PyGithub (https://github.com/PyGithub/PyGithub)
- github/Requester.py:1441:52 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive

core (https://github.com/home-assistant/core)
+ homeassistant/components/switchbot_cloud/light.py:80:9 error[invalid-assignment] Object of type `tuple[int, ...] | None` is not assignable to attribute `_attr_rgb_color` of type `tuple[int, int, int] | None`

discord.py (https://github.com/Rapptz/discord.py)
+ discord/components.py:458:9 error[invalid-assignment] Cannot assign to read-only property `type` on object of type `Self@__init__`: Attempted assignment to `Self@__init__.type` here

isort (https://github.com/pycqa/isort)
+ isort/settings.py:288:9 error[invalid-assignment] Property `_known_patterns` defined in `Self@__init__` is read-only
+ isort/settings.py:289:9 error[invalid-assignment] Property `_section_comments` defined in `Self@__init__` is read-only
+ isort/settings.py:290:9 error[invalid-assignment] Property `_section_comments_end` defined in `Self@__init__` is read-only
+ isort/settings.py:291:9 error[invalid-assignment] Property `_skips` defined in `Self@__init__` is read-only
+ isort/settings.py:292:9 error[invalid-assignment] Property `_skip_globs` defined in `Self@__init__` is read-only
+ isort/settings.py:293:9 error[invalid-assignment] Property `_sorting_function` defined in `Self@__init__` is read-only

pandas (https://github.com/pandas-dev/pandas)
+ pandas/io/common.py:1104:9 error[invalid-assignment] Object of type `ZipFile` is not assignable to attribute `buffer` of type `BytesIO`

pip (https://github.com/pypa/pip)
+ src/pip/_vendor/urllib3/exceptions.py:287:9 error[invalid-assignment] Object of type `int` is not assignable to attribute `partial` of type `bytes`

prefect (https://github.com/PrefectHQ/prefect)
+ src/prefect/server/utilities/database.py:302:13 error[invalid-assignment] Object of type `type[TypeEngine[Any]] | TypeEngine[Any]` is not assignable to attribute `impl` of type `<class 'JSON'>`

pyppeteer (https://github.com/pyppeteer/pyppeteer)
- pyppeteer/connection.py:99:53 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- pyppeteer/connection.py:100:40 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- pyppeteer/connection.py:228:53 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive
- pyppeteer/connection.py:229:40 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive

tornado (https://github.com/tornadoweb/tornado)
+ tornado/web.py:1957:17 error[invalid-assignment] Object of type `dict[str, UIModule]` is not assignable to attribute `_active_modules` on type `Self@_ui_module & ~<Protocol with members '_active_modules'>`

urllib3 (https://github.com/urllib3/urllib3)
+ src/urllib3/exceptions.py:287:9 error[invalid-assignment] Object of type `int` is not assignable to attribute `partial` of type `bytes`

Full report with detailed diff (timing results)

@snoopuppy582 snoopuppy582 force-pushed the fix-ty-annotated-attribute-assignment branch from a43f1f9 to 2206589 Compare May 16, 2026 22:20
@snoopuppy582
Copy link
Copy Markdown
Author

Pushed a follow-up in 2206589989 after the CI/conformance reports showed duplicate Final attribute diagnostics.

The attribute-assignment validation added here now skips the normal attribute assignment path when the inline annotation itself is Final, leaving the existing Final-specific diagnostic responsible for that case.

Verified locally on Windows:

  • cargo fmt --check
  • cargo test -p ty_python_semantic --test mdtest -- type_qualifiers/final
  • cargo test -p ty_python_semantic --test mdtest -- assignment/annotations
  • cargo test -p ty_python_semantic --lib

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Annotated assignments of non-name targets are not checked

2 participants