@@ -37,7 +37,9 @@ reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown
3737# See https://github.com/astral-sh/ruff/issues/15960 for a related discussion.
3838reveal_type(c_instance.inferred_from_param) # revealed: Unknown | int | None
3939
40- reveal_type(c_instance.declared_only) # revealed: bytes
40+ # TODO : Should be `bytes` with no error, like mypy and pyright?
41+ # error: [unresolved-attribute]
42+ reveal_type(c_instance.declared_only) # revealed: Unknown
4143
4244reveal_type(c_instance.declared_and_bound) # revealed: bool
4345
@@ -64,12 +66,10 @@ C.inferred_from_value = "overwritten on class"
6466# This assignment is fine:
6567c_instance.declared_and_bound = False
6668
67- # TODO : After this assignment to the attribute within this scope, we may eventually want to narrow
68- # the `bool` type (see above) for this instance variable to `Literal[False]` here. This is unsound
69- # in general (we don't know what else happened to `c_instance` between the assignment and the use
70- # here), but mypy and pyright support this. In conclusion, this could be `bool` but should probably
71- # be `Literal[False]`.
72- reveal_type(c_instance.declared_and_bound) # revealed: bool
69+ # Strictly speaking, inferring this as `Literal[False]` rather than `bool` is unsound in general
70+ # (we don't know what else happened to `c_instance` between the assignment and the use here),
71+ # but mypy and pyright support this.
72+ reveal_type(c_instance.declared_and_bound) # revealed: Literal[False]
7373```
7474
7575#### Variable declared in class body and possibly bound in ` __init__ `
@@ -149,14 +149,16 @@ class C:
149149c_instance = C(True )
150150
151151reveal_type(c_instance.only_declared_in_body) # revealed: str | None
152- reveal_type(c_instance.only_declared_in_init) # revealed: str | None
152+ # TODO : should be `str | None` without error
153+ # error: [unresolved-attribute]
154+ reveal_type(c_instance.only_declared_in_init) # revealed: Unknown
153155reveal_type(c_instance.declared_in_body_and_init) # revealed: str | None
154156
155157reveal_type(c_instance.declared_in_body_defined_in_init) # revealed: str | None
156158
157159# TODO : This should be `str | None`. Fixing this requires an overhaul of the `Symbol` API,
158160# which is planned in https://github.com/astral-sh/ruff/issues/14297
159- reveal_type(c_instance.bound_in_body_declared_in_init) # revealed: Unknown | str | None
161+ reveal_type(c_instance.bound_in_body_declared_in_init) # revealed: Unknown | Literal["a"]
160162
161163reveal_type(c_instance.bound_in_body_and_init) # revealed: Unknown | None | Literal["a"]
162164```
@@ -187,7 +189,9 @@ reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown
187189
188190reveal_type(c_instance.inferred_from_param) # revealed: Unknown | int | None
189191
190- reveal_type(c_instance.declared_only) # revealed: bytes
192+ # TODO : should be `bytes` with no error, like mypy and pyright?
193+ # error: [unresolved-attribute]
194+ reveal_type(c_instance.declared_only) # revealed: Unknown
191195
192196reveal_type(c_instance.declared_and_bound) # revealed: bool
193197
@@ -260,8 +264,8 @@ class C:
260264 self .w += None
261265
262266# TODO : Mypy and pyright do not support this, but it would be great if we could
263- # infer `Unknown | str` or at least `Unknown | Weird | str` here .
264- reveal_type(C().w) # revealed: Unknown | Weird
267+ # infer `Unknown | str` here (`Weird` is not a possible type for the `w` attribute) .
268+ reveal_type(C().w) # revealed: Unknown
265269```
266270
267271#### Attributes defined in tuple unpackings
@@ -410,14 +414,41 @@ class C:
410414 [... for self .a in IntIterable()]
411415 [... for (self .b, self .c) in TupleIterable()]
412416 [... for self .d in IntIterable() for self .e in IntIterable()]
417+ [[... for self .f in IntIterable()] for _ in IntIterable()]
418+ [[... for self .g in IntIterable()] for self in [D()]]
419+
420+ class D :
421+ g: int
413422
414423c_instance = C()
415424
416- reveal_type(c_instance.a) # revealed: Unknown | int
417- reveal_type(c_instance.b) # revealed: Unknown | int
418- reveal_type(c_instance.c) # revealed: Unknown | str
419- reveal_type(c_instance.d) # revealed: Unknown | int
420- reveal_type(c_instance.e) # revealed: Unknown | int
425+ # TODO : no error, reveal Unknown | int
426+ # error: [unresolved-attribute]
427+ reveal_type(c_instance.a) # revealed: Unknown
428+
429+ # TODO : no error, reveal Unknown | int
430+ # error: [unresolved-attribute]
431+ reveal_type(c_instance.b) # revealed: Unknown
432+
433+ # TODO : no error, reveal Unknown | str
434+ # error: [unresolved-attribute]
435+ reveal_type(c_instance.c) # revealed: Unknown
436+
437+ # TODO : no error, reveal Unknown | int
438+ # error: [unresolved-attribute]
439+ reveal_type(c_instance.d) # revealed: Unknown
440+
441+ # TODO : no error, reveal Unknown | int
442+ # error: [unresolved-attribute]
443+ reveal_type(c_instance.e) # revealed: Unknown
444+
445+ # TODO : no error, reveal Unknown | int
446+ # error: [unresolved-attribute]
447+ reveal_type(c_instance.f) # revealed: Unknown
448+
449+ # This one is correctly not resolved as an attribute:
450+ # error: [unresolved-attribute]
451+ reveal_type(c_instance.g) # revealed: Unknown
421452```
422453
423454#### Conditionally declared / bound attributes
@@ -721,10 +752,7 @@ reveal_type(C.pure_class_variable) # revealed: Unknown
721752# error: [invalid-attribute-access] "Cannot assign to instance attribute `pure_class_variable` from the class object `<class 'C'>`"
722753C.pure_class_variable = " overwritten on class"
723754
724- # TODO : should be `Unknown | Literal["value set in class method"]` or
725- # Literal["overwritten on class"]`, once/if we support local narrowing.
726- # error: [unresolved-attribute]
727- reveal_type(C.pure_class_variable) # revealed: Unknown
755+ reveal_type(C.pure_class_variable) # revealed: Literal["overwritten on class"]
728756
729757c_instance = C()
730758reveal_type(c_instance.pure_class_variable) # revealed: Unknown | Literal["value set in class method"]
@@ -762,19 +790,12 @@ reveal_type(c_instance.variable_with_class_default2) # revealed: Unknown | Lite
762790c_instance.variable_with_class_default1 = " value set on instance"
763791
764792reveal_type(C.variable_with_class_default1) # revealed: str
765-
766- # TODO : Could be Literal["value set on instance"], or still `str` if we choose not to
767- # narrow the type.
768- reveal_type(c_instance.variable_with_class_default1) # revealed: str
793+ reveal_type(c_instance.variable_with_class_default1) # revealed: Literal["value set on instance"]
769794
770795C.variable_with_class_default1 = " overwritten on class"
771796
772- # TODO : Could be `Literal["overwritten on class"]`, or still `str` if we choose not to
773- # narrow the type.
774- reveal_type(C.variable_with_class_default1) # revealed: str
775-
776- # TODO : should still be `Literal["value set on instance"]`, or `str`.
777- reveal_type(c_instance.variable_with_class_default1) # revealed: str
797+ reveal_type(C.variable_with_class_default1) # revealed: Literal["overwritten on class"]
798+ reveal_type(c_instance.variable_with_class_default1) # revealed: Literal["value set on instance"]
778799```
779800
780801#### Descriptor attributes as class variables
0 commit comments