Skip to content

[ty] Avoid treating augmented assignments as attribute definitions - #27633

Merged
charliermarsh merged 10 commits into
mainfrom
charlie/conditional-implicit-attribute-bindings
Aug 13, 2026
Merged

[ty] Avoid treating augmented assignments as attribute definitions#27633
charliermarsh merged 10 commits into
mainfrom
charlie/conditional-implicit-attribute-bindings

Conversation

@charliermarsh

@charliermarsh charliermarsh commented Aug 10, 2026

Copy link
Copy Markdown
Member

Summary

Previously, we treated augmented assignments as ordinary bindings when inferring implicit attributes. However, self.value += 1 first reads self.value, so it cannot establish an otherwise missing attribute:

class Counter:
    def __init__(self) -> None:
        self.value = 0

    def increment(self) -> None:
        self.value += 1


class UninitializedCounter:
    def increment(self) -> None:
        self.value += 1  # error: [unresolved-attribute]

We now distinguish assignments that establish an attribute from augmented assignments that require an existing value. Class and instance member lookup collects augmented assignments while traversing the MRO and incorporates their inferred results only after resolving an independent attribute binding.

This preserves missing-attribute diagnostics and allows augmented assignments to contribute to inferred attribute types.

@astral-sh-bot astral-sh-bot Bot added the ty Multi-file analysis & type inference label Aug 10, 2026
@astral-sh-bot

astral-sh-bot Bot commented Aug 10, 2026

Copy link
Copy Markdown

Typing conformance results

No changes detected ✅

Current numbers
The percentage of diagnostics emitted that were expected errors held steady at 96.98%. The percentage of expected errors that received a diagnostic held steady at 93.62%. The number of fully passing files held steady at 106/133.

@astral-sh-bot

astral-sh-bot Bot commented Aug 10, 2026

Copy link
Copy Markdown

Memory usage report

Summary

Project Old New Diff Outcome
prefect 449.29MB 452.62MB +0.74% (3.33MB)
sphinx 164.79MB 165.50MB +0.43% (721.93kB)
trio 92.26MB 92.43MB +0.18% (169.59kB)
flake8 39.69MB 39.72MB +0.07% (26.43kB)

Significant changes

Click to expand detailed breakdown

prefect

Name Old New Diff Outcome
parsed_module 83.57MB 84.11MB +0.64% (550.43kB)
StaticClassLiteral<'db>::implicit_attribute_inner_ 796.60kB 1.13MB +45.66% (363.72kB)
semantic_index 87.24MB 87.56MB +0.37% (330.21kB)
Type<'db>::class_member_with_policy_inner_ 8.71MB 8.98MB +3.15% (280.84kB)
ImplicitAttributeName 555.93kB 794.52kB +42.92% (238.59kB)
CallableType 8.00MB 8.15MB +1.92% (157.09kB)
infer_definition_types 34.48MB 34.63MB +0.42% (146.70kB)
infer_deferred_types 4.89MB 5.00MB +2.29% (114.77kB)
source_text 21.90MB 21.99MB +0.41% (92.38kB)
TypePair 7.73MB 7.81MB +0.98% (77.81kB)
member_lookup_with_policy_inner 8.91MB 8.98MB +0.74% (67.12kB)
Definition 17.46MB 17.52MB +0.34% (61.17kB)
StaticClassLiteral<'db>::try_mro_specialized_ 4.22MB 4.27MB +1.18% (50.96kB)
infer_scope_types_impl 18.78MB 18.83MB +0.25% (47.45kB)
Specialization 4.57MB 4.62MB +0.96% (44.88kB)
... 133 more

sphinx

Name Old New Diff Outcome
StaticClassLiteral<'db>::implicit_attribute_inner_ 624.92kB 775.04kB +24.02% (150.12kB)
ImplicitAttributeName 459.03kB 560.94kB +22.20% (101.91kB)
Type<'db>::class_member_with_policy_inner_ 3.04MB 3.12MB +2.70% (84.05kB)
infer_definition_types 10.95MB 11.02MB +0.67% (74.77kB)
infer_expression_types_impl 11.52MB 11.58MB +0.53% (62.34kB)
member_lookup_with_policy_and_receiver_inner 1.54MB 1.59MB +3.35% (52.92kB)
member_lookup_with_policy_inner 3.54MB 3.59MB +1.41% (51.29kB)
infer_expression_type_impl 240.45kB 290.83kB +20.95% (50.38kB)
TypePair 2.12MB 2.15MB +1.57% (34.12kB)
UnionType 656.30kB 674.11kB +2.71% (17.81kB)
is_redundant_with_impl 768.35kB 782.02kB +1.78% (13.66kB)
infer_scope_types_impl 6.08MB 6.08MB +0.09% (5.55kB)
AugmentedBindings 0.00B 5.23kB +5.23kB (new)
analyze_non_terminal_call 478.10kB 474.53kB -0.75% (3.57kB) ⬇️
loop_header_reachability 245.28kB 242.63kB -1.08% (2.65kB) ⬇️
... 56 more

trio

Name Old New Diff Outcome
parsed_module 22.18MB 22.23MB +0.25% (56.90kB)
infer_expression_types_impl 5.21MB 5.17MB -0.88% (46.72kB) ⬇️
semantic_index 20.22MB 20.26MB +0.21% (44.28kB)
StaticClassLiteral<'db>::implicit_attribute_inner_ 103.91kB 143.16kB +37.76% (39.24kB)
ImplicitAttributeName 64.33kB 93.18kB +44.84% (28.85kB)
infer_definition_types 4.43MB 4.41MB -0.56% (25.61kB) ⬇️
Type<'db>::class_member_with_policy_inner_ 1.17MB 1.19MB +1.91% (22.88kB)
analyze_non_terminal_call 414.98kB 401.31kB -3.29% (13.66kB) ⬇️
Definition 4.29MB 4.30MB +0.21% (9.22kB)
source_text 4.42MB 4.43MB +0.18% (8.26kB)
FunctionType<'db>::signature_ 1.19MB 1.20MB +0.50% (6.09kB)
all_narrowing_constraints_for_expression 639.20kB 633.72kB -0.86% (5.48kB) ⬇️
infer_deferred_types 1.28MB 1.28MB +0.42% (5.47kB)
FunctionType 1.23MB 1.23MB +0.42% (5.23kB)
member_lookup_with_policy_inner 1.22MB 1.22MB +0.33% (4.08kB)
... 57 more

flake8

Name Old New Diff Outcome
StaticClassLiteral<'db>::implicit_attribute_inner_ 44.66kB 55.89kB +25.16% (11.23kB)
ImplicitAttributeName 28.68kB 35.98kB +25.47% (7.30kB)
Type<'db>::class_member_with_policy_inner_ 266.44kB 269.91kB +1.30% (3.47kB)
member_lookup_with_policy_inner 285.13kB 285.94kB +0.28% (824.00B)
implicit_attribute_names 14.51kB 15.24kB +5.03% (747.00B)
all_narrowing_constraints_for_expression 107.05kB 107.73kB +0.64% (704.00B)
member_lookup_with_policy_and_receiver_inner 54.49kB 54.89kB +0.73% (408.00B)
TypePair 230.81kB 231.19kB +0.16% (384.00B)
UnionType 75.59kB 75.95kB +0.48% (368.00B)
infer_definition_types 1.06MB 1.06MB +0.03% (304.00B)
AugmentedBindings 0.00B 248.00B +248.00B (new)
is_redundant_with_impl 82.29kB 82.45kB +0.19% (160.00B)
infer_scope_types_impl 537.09kB 537.23kB +0.03% (144.00B)
infer_expression_types_impl 762.71kB 762.83kB +0.01% (116.00B)
lookup_dunder_new_inner 10.62kB 10.71kB +0.81% (88.00B)
... 4 more

@astral-sh-bot

astral-sh-bot Bot commented Aug 10, 2026

Copy link
Copy Markdown

ecosystem-analyzer results

Lint rule Added Removed Changed
unresolved-attribute 34 0 1
unsupported-operator 0 14 2
invalid-argument-type 0 1 2
unsound-return-statement 0 2 0
not-iterable 0 0 1
Total 34 17 6

Flaky changes detected. This PR summary excludes flaky changes; see the HTML report for details.

Raw diff (57 changes)
aiortc (https://github.com/aiortc/aiortc)
- src/aiortc/rtcsctptransport.py:547:15 error[unsupported-operator] Operator `<` is not supported between objects of type `int | Unknown | None` and `int`
- src/aiortc/rtcsctptransport.py:548:21 error[invalid-argument-type] Method `__getitem__` of type `Overload[(i: SupportsIndex, /) -> DataChunk, (s: slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> list[DataChunk]]` cannot be called with key of type `None` on object of type `list[DataChunk]`
- src/aiortc/rtcsctptransport.py:555:25 error[unsupported-operator] Operator `+=` is not supported between objects of type `None` and `Literal[1]`
- src/aiortc/rtcsctptransport.py:566:21 error[unsupported-operator] Operator `+=` is not supported between objects of type `None` and `Literal[1]`
- src/aiortc/rtcsctptransport.py:571:71 error[unsupported-operator] Operator `+` is not supported between objects of type `int | Unknown | None` and `Literal[1]`
- src/aiortc/rtcsctptransport.py:574:67 error[unsupported-operator] Operator `+` is not supported between objects of type `int | Unknown | None` and `Literal[1]`
- src/aiortc/rtcsctptransport.py:581:17 error[unsupported-operator] Operator `+=` is not supported between objects of type `None` and `Literal[1]`

apprise (https://github.com/caronc/apprise)
- apprise/conversion.py:1250:23 error[unresolved-attribute] Attribute `rstrip` is not defined on `None` in union `None | Unknown | str`
+ apprise/conversion.py:1250:23 error[unresolved-attribute] Attribute `rstrip` is not defined on `None` in union `None | str | Unknown`

core (https://github.com/home-assistant/core)
- homeassistant/components/hunterdouglas_powerview/cover.py:113:13 error[unsupported-operator] Operator `|=` is not supported between objects of type `None` and `Literal[CoverEntityFeature.STOP]`
- homeassistant/components/hunterdouglas_powerview/cover.py:373:9 error[unsupported-operator] Operator `|=` is not supported between objects of type `None` and `Literal[CoverEntityFeature.OPEN_TILT]`
- homeassistant/components/lcn/cover.py:199:13 error[unsupported-operator] Operator `|=` is not supported between objects of type `None` and `Literal[CoverEntityFeature.SET_POSITION]`
- homeassistant/components/overkiz/climate/atlantic_electrical_towel_dryer.py:69:13 error[unsupported-operator] Operator `+=` is not supported between objects of type `None` and `list[str]`
- homeassistant/components/overkiz/climate/atlantic_electrical_towel_dryer.py:72:20 error[unsupported-operator] Operator `not in` is not supported between objects of type `str` and `list[str] | None`
- homeassistant/components/overkiz/climate/atlantic_electrical_towel_dryer.py:148:12 error[unsupported-operator] Operator `in` is not supported between objects of type `Literal["drying"]` and `list[str] | None`
- homeassistant/components/overkiz/climate/atlantic_electrical_towel_dryer.py:190:14 error[unsupported-operator] Operator `in` is not supported between objects of type `Literal["drying"]` and `list[str] | None`
- homeassistant/components/velux/cover.py:248:9 error[unsupported-operator] Operator `|=` is not supported between objects of type `None` and `Literal[CoverEntityFeature.OPEN_TILT]`

dulwich (https://github.com/dulwich/dulwich)
- dulwich/object_store.py:4082:20 warning[unsound-return-statement] Unsound return statement: `tuple[(EWAHBitmap & ~AlwaysFalsy) | (Unknown & ~AlwaysFalsy), Pack & ~AlwaysFalsy]` is not a subtype of `tuple[EWAHBitmap, Pack] | None`
- dulwich/objects.py:1040:16 warning[unsound-return-statement] Unsound return statement: `list[bytes | Unknown]` is not a subtype of `list[bytes]`

manticore (https://github.com/trailofbits/manticore)
+ manticore/platforms/evm.py:2982:9 error[unresolved-attribute] Object of type `Self@add_refund` has no attribute `_refund`
+ manticore/platforms/evm.py:2985:9 error[unresolved-attribute] Object of type `Self@sub_refund` has no attribute `_refund`

pip (https://github.com/pypa/pip)
+ src/pip/_vendor/pygments/filters/__init__.py:833:12 error[unresolved-attribute] Object of type `Self@__init__` has no attribute `tabs`
+ src/pip/_vendor/pygments/filters/__init__.py:834:13 error[unresolved-attribute] Object of type `Self@__init__` has no attribute `tabs`
+ src/pip/_vendor/pygments/filters/__init__.py:835:12 error[unresolved-attribute] Object of type `Self@__init__` has no attribute `newlines`
+ src/pip/_vendor/pygments/filters/__init__.py:836:13 error[unresolved-attribute] Object of type `Self@__init__` has no attribute `newlines`
+ src/pip/_vendor/pygments/filters/__init__.py:842:20 error[unresolved-attribute] Object of type `Self@filter` has no attribute `tabs`
+ src/pip/_vendor/pygments/filters/__init__.py:843:24 error[unresolved-attribute] Object of type `Self@filter` has no attribute `newlines`
+ src/pip/_vendor/pygments/filters/__init__.py:859:51 error[unresolved-attribute] Object of type `Self@filter` has no attribute `tabs`
+ src/pip/_vendor/pygments/filters/__init__.py:859:62 error[unresolved-attribute] Object of type `Self@filter` has no attribute `newlines`

pywin32 (https://github.com/mhammond/pywin32)
+ pythonwin/pywin/scintilla/formatter.py:670:24 error[unresolved-attribute] Object of type `Self@RegisterStyle` has no attribute `nextstylenum`
+ pythonwin/pywin/scintilla/formatter.py:671:13 error[unresolved-attribute] Object of type `Self@RegisterStyle` has no attribute `nextstylenum`

scipy (https://github.com/scipy/scipy)
+ scipy/special/_orthogonal.py:187:9 error[unresolved-attribute] Object of type `Self@_scale` has no attribute `_coeffs`

spack (https://github.com/spack/spack)
+ lib/spack/spack/vendor/pyrsistent/_pvector.py:35:9 error[unresolved-attribute] Unresolved attribute `_count` on type `Self@__new__`
+ lib/spack/spack/vendor/pyrsistent/_pvector.py:41:29 error[unresolved-attribute] Object of type `Self@__new__` has no attribute `_count`
+ lib/spack/spack/vendor/pyrsistent/_pvector.py:45:16 error[unresolved-attribute] Object of type `Self@__len__` has no attribute `_count`
+ lib/spack/spack/vendor/pyrsistent/_pvector.py:59:22 error[unresolved-attribute] Object of type `Self@__getitem__` has no attribute `_count`
+ lib/spack/spack/vendor/pyrsistent/_pvector.py:81:64 error[unresolved-attribute] Object of type `Self@__eq__` has no attribute `_count`
+ lib/spack/spack/vendor/pyrsistent/_pvector.py:274:18 error[unresolved-attribute] Object of type `Self@set` has no attribute `_count`
+ lib/spack/spack/vendor/pyrsistent/_pvector.py:276:21 error[unresolved-attribute] Object of type `Self@set` has no attribute `_count`
+ lib/spack/spack/vendor/pyrsistent/_pvector.py:280:38 error[unresolved-attribute] Object of type `Self@set` has no attribute `_count`
+ lib/spack/spack/vendor/pyrsistent/_pvector.py:282:34 error[unresolved-attribute] Object of type `Self@set` has no attribute `_count`
+ lib/spack/spack/vendor/pyrsistent/_pvector.py:284:17 error[unresolved-attribute] Object of type `Self@set` has no attribute `_count`
+ lib/spack/spack/vendor/pyrsistent/_pvector.py:317:13 error[unresolved-attribute] Object of type `Self@_create_new_root` has no attribute `_count`
+ lib/spack/spack/vendor/pyrsistent/_pvector.py:329:34 error[unresolved-attribute] Object of type `Self@append` has no attribute `_count`
+ lib/spack/spack/vendor/pyrsistent/_pvector.py:333:30 error[unresolved-attribute] Object of type `Self@append` has no attribute `_count`
+ lib/spack/spack/vendor/pyrsistent/_pvector.py:350:9 error[unresolved-attribute] Object of type `Self@_mutating_fill_tail` has no attribute `_count`
+ lib/spack/spack/vendor/pyrsistent/_pvector.py:361:29 error[unresolved-attribute] Object of type `Self@_mutating_extend` has no attribute `_count`
+ lib/spack/spack/vendor/pyrsistent/_pvector.py:389:23 error[unresolved-attribute] Object of type `Self@_push_tail` has no attribute `_count`

spark (https://github.com/apache/spark)
- python/pyspark/pandas/frame.py:12066:35 error[unsupported-operator] Operator `|` is not supported between objects of type `Column | (Unknown & ~None)` and `None | Column | Unknown`
+ python/pyspark/pandas/frame.py:12066:35 error[unsupported-operator] Operator `|` is not supported between objects of type `Column | (Unknown & ~None)` and `None | Column`
- python/pyspark/pandas/frame.py:12081:61 error[invalid-argument-type] Argument to bound method `InternalFrame.with_filter` is incorrect: Expected `Column | Series[Unknown]`, found `None | Column | Unknown`
+ python/pyspark/pandas/frame.py:12081:61 error[invalid-argument-type] Argument to bound method `InternalFrame.with_filter` is incorrect: Expected `Column | Series[Unknown]`, found `None | Column`
- python/pyspark/pandas/frame.py:12094:61 error[invalid-argument-type] Argument to bound method `InternalFrame.with_filter` is incorrect: Expected `Column | Series[Unknown]`, found `None | Column | Unknown`
+ python/pyspark/pandas/frame.py:12094:61 error[invalid-argument-type] Argument to bound method `InternalFrame.with_filter` is incorrect: Expected `Column | Series[Unknown]`, found `None | Column`

sympy (https://github.com/sympy/sympy)
- sympy/core/add.py:1238:28 error[unsupported-operator] Operator `not in` is not supported between objects of type `Unknown` and `None | set[Unknown] | (Unknown & ~AlwaysFalsy)`
+ sympy/core/add.py:1238:28 error[unsupported-operator] Operator `not in` is not supported between objects of type `Unknown` and `None | set[Unknown]`
- sympy/core/add.py:1244:26 error[not-iterable] Object of type `None | set[Unknown] | (Unknown & ~AlwaysFalsy)` may not be iterable
+ sympy/core/add.py:1244:26 error[not-iterable] Object of type `None | set[Unknown]` may not be iterable
+ sympy/geometry/polygon.py:1695:16 error[unresolved-attribute] Object of type `Self@rotation` has no attribute `_rot`
+ sympy/geometry/polygon.py:1943:9 error[unresolved-attribute] Object of type `Self@spin` has no attribute `_rot`
+ sympy/geometry/polygon.py:1965:9 error[unresolved-attribute] Object of type `Self@rotate` has no attribute `_rot`
+ sympy/geometry/polygon.py:2048:15 error[unresolved-attribute] Object of type `Self@vertices` has no attribute `_rot`
+ sympy/geometry/polygon.py:1533:53 error[unresolved-attribute] Object of type `Self@args` has no attribute `_rot`

Full report with detailed diff (timing results)

@codspeed-hq

codspeed-hq Bot commented Aug 10, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 115 untouched benchmarks
⏩ 84 skipped benchmarks1


Comparing charlie/conditional-implicit-attribute-bindings (17a8e82) with main (a9130f3)

Open in CodSpeed

Footnotes

  1. 84 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@charliermarsh
charliermarsh force-pushed the charlie/conditional-implicit-attribute-bindings branch from a867375 to 892f4c7 Compare August 10, 2026 21:05
@charliermarsh

Copy link
Copy Markdown
Member Author

@charliermarsh
charliermarsh marked this pull request as ready for review August 11, 2026 00:58
@charliermarsh
charliermarsh requested a review from a team as a code owner August 11, 2026 00:58
@charliermarsh

Copy link
Copy Markdown
Member Author

Per the report, the false positives in the ecosystem report are generally consistent with other type checkers.

@carljm

carljm commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

(I find the title of this PR confusing -- it sounds like the opposite of what the PR does, which is to stop inferring (the existence of) instance attributes from augmented assignments.)

@charliermarsh charliermarsh changed the title [ty] Infer instance attributes from augmented assignments [ty] Avoid inferring instance attributes from augmented assignments alone Aug 11, 2026
@charliermarsh charliermarsh changed the title [ty] Avoid inferring instance attributes from augmented assignments alone [ty] Require existing attributes for augmented assignments Aug 11, 2026
@charliermarsh charliermarsh changed the title [ty] Require existing attributes for augmented assignments [ty] Avoid treating augmented assignments as attribute definitions Aug 11, 2026
@charliermarsh

Copy link
Copy Markdown
Member Author

Thanks, sorry, the PR went through a few iterations. I tightened it up.

Comment on lines +4091 to +4100
// Expression inference can temporarily substitute a cycle placeholder for every
// subexpression. `None` has a stable type regardless of that cycle, so preserve it when
// deriving comparison constraints for recursively inferred attributes.
let expression_type = |expr: &ast::Expr, env: &ProgramEnvironment<'db>| {
if expr.is_none_literal_expr() {
Type::none(db, env)
} else {
inference.expression_type(expr)
}
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm still looking into this (instance attributes are hard) but one thing that caught my eye which might be worth looking into:

This seems like a narrow fix specifically targeting None literal as using anything other than None results in the same issue that this block is trying to prevent:

from typing import Literal

class Counter:
    def __init__(self, value: str | Literal[False]) -> None:
        self.value = value

    def update(self) -> None:
        if self.value is False:
            return

        self.value += "x" # A
        self.value += "x" # B

# revealed: str | Literal[False] | Unknown
reveal_type(Counter("").value)

It's interesting that this issue doesn't occur when there's single augmented assignment, it required having two augmented assignment as in the above example. I asked Codex to figure out why this is and it gave me the following (adding it as verbatim):

With only assignment A, the dependency chain is:

Infer attribute
  → Infer assignment A
    → Infer guard: self.value is False
      → Look up self.value
        → Infer assignment A again
          → Assignment A is already running: recover with Divergent
      → Guard finishes normally and still knows False is False
    → Narrowing succeeds

The cycle occurs in assignment A’s query. The guard query itself never becomes cyclic, so its literal operand retains the correct type.

With assignments A and B:

Infer attribute
  → Infer assignment A
    → Infer guard: self.value is False
      → Look up self.value
        → Infer assignment A again
          → Recover A with Divergent
        → Infer assignment B
          → Infer the same guard again
            → Guard is already running: recover the entire guard with Divergent
          → False is incorrectly treated as Divergent
          → Narrowing fails
          → Assignment B contributes Unknown

The crucial difference is that after recovering A, attribute lookup continues to B because it must consider every assignment. B then requests the same guard expression, creating a cycle in the expression-inference query, whose recovery replaces every subexpression—including False—with Divergent.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes this is a narrow fix that I added in a8e0903, because it seemed better than excluding it. I also looked at doing a broader change around it and ultimately decided it wasn't worthwhile: #27642

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Oh thanks for the context!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think it would be useful to include this context in the comment (i.e. that this is a narrow fix for a more general issue, but for now we've intentionally decided to only apply the narrow version.)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Sounds good, added.

@dhruvmanila dhruvmanila left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I wouldn't consider myself to be an expert in instance attributes but after spending some time trying to understand attribute lookup machinery between instances, classes and the MRO-based lookup, I don't see any issue with the change here. It looks good from my end. I do feel like it might be useful for @sharkdp (after he's back) to give it a quick look as well if possible.

Related to the new diagnostics in the ecosystem result, I see that those have been deliberately left out given that other type checkers also don't support it. Can you open an issue to keep track of this missing cases?

As an aside, I had to spend a lot of time trying to understand the how does the MRO-based lookup fit into the wider attribute lookup process even after David had spent a ty sharing session explaining it and I still find it confusing 🙈

reveal_type(UsesGeneratedDescriptor().generated_descriptor) # revealed: Literal["descriptor"]
```

An augmented assignment to a data descriptor on a metaclass calls the descriptor's `__set__` method.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

😵‍💫

@charliermarsh
charliermarsh merged commit 27e91ed into main Aug 13, 2026
101 of 105 checks passed
@charliermarsh
charliermarsh deleted the charlie/conditional-implicit-attribute-bindings branch August 13, 2026 13:44
@charliermarsh

Copy link
Copy Markdown
Member Author

It looks like 21 of the false-positives are covered by astral-sh/ty#1268. Eight of the diagnostics (from pip) use literals with setattr which we closed as not-planned: astral-sh/ty#2016.

This case doesn't seem to have an issue but I'm also not sure we actually want to support it:

class Example:
    def __new__(cls):
        instance = super().__new__(cls)
        instance.value = 0
        return instance

    def current(self) -> int:
        return self.value

George-Ogden pushed a commit to George-Ogden/ruff that referenced this pull request Aug 16, 2026
…stral-sh#27633)

## Summary

Previously, we treated augmented assignments as ordinary bindings when
inferring implicit attributes. However, `self.value += 1` first reads
`self.value`, so it cannot establish an otherwise missing attribute:

```python
class Counter:
    def __init__(self) -> None:
        self.value = 0

    def increment(self) -> None:
        self.value += 1


class UninitializedCounter:
    def increment(self) -> None:
        self.value += 1  # error: [unresolved-attribute]
```

We now distinguish assignments that establish an attribute from
augmented assignments that require an existing value. Class and instance
member lookup collects augmented assignments while traversing the MRO
and incorporates their inferred results only after resolving an
independent attribute binding.

This preserves missing-attribute diagnostics and allows augmented
assignments to contribute to inferred attribute types.
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.

3 participants