Skip to content

[red-knot] Attribute access on intersection types#16665

Merged
sharkdp merged 1 commit intomainfrom
david/attribute-access-on-intersections
Mar 12, 2025
Merged

[red-knot] Attribute access on intersection types#16665
sharkdp merged 1 commit intomainfrom
david/attribute-access-on-intersections

Conversation

@sharkdp
Copy link
Contributor

@sharkdp sharkdp commented Mar 12, 2025

Summary

Implements attribute access on intersection types, which didn't previously work. For example:

from typing import Any

class P: ...
class Q: ...

class A:
    x: P = P()

class B:
    x: Any = Q()

def _(obj: A):
    if isinstance(obj, B):
        reveal_type(obj.x)  # revealed: P & Any

Refers to this comment.

Test Plan

New Markdown tests

@sharkdp sharkdp added the ty Multi-file analysis & type inference label Mar 12, 2025
reveal_type(a_and_b.x) # revealed: int

# Same for class objects
def _(a_and_b: Intersection[type[A], type[B]]):
Copy link
Member

@AlexWaygood AlexWaygood Mar 12, 2025

Choose a reason for hiding this comment

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

I wonder if we should allow type[Intersection[A, B]] in type expressions, as a shorthand for Intersection[type[A], type[B]]. We already allow (and it's mandated by the spec) type[A | B] and type[Union[A, B]] as shorthands for type[A] | type[B].

Obviously doesn't need to be done in this PR, though

@github-actions
Copy link
Contributor

mypy_primer results

Changes were detected when running on open source projects
arrow (https://github.com/arrow-py/arrow)
- warning: lint:possibly-unbound-attribute
-    --> /tmp/mypy_primer/projects/arrow/arrow/factory.py:237:47
-     |
- 235 |             # (Arrow) -> from the object's datetime @ tzinfo
- 236 |             elif isinstance(arg, Arrow):
- 237 |                 return self.type.fromdatetime(arg.datetime, tzinfo=tz)
-     |                                               ------------ Attribute `datetime` on type `@Todo & Arrow & ~Decimal | float & Arrow` is possibly unbound
- 238 |
- 239 |             # (datetime) -> from datetime @ tzinfo
-     |
- 
- 
- error: lint:unresolved-attribute
-     |
-     |
- 
- error: lint:unresolved-attribute
-    --> /tmp/mypy_primer/projects/arrow/arrow/arrow.py:166:17
-     |
- 164 |             and hasattr(tzinfo, "localize")
- 165 |             and hasattr(tzinfo, "zone")
- 166 |             and tzinfo.zone
-     |                 ^^^^^^^^^^^ Type `@Todo & tzinfo` has no attribute `zone`
- 167 |         ):
- 168 |             tzinfo = parser.TzinfoParser.parse(tzinfo.zone)
-     |
-    --> /tmp/mypy_primer/projects/arrow/arrow/arrow.py:168:48
- 166 |             and tzinfo.zone
- 167 |         ):
- 168 |             tzinfo = parser.TzinfoParser.parse(tzinfo.zone)
-     |                                                ^^^^^^^^^^^ Type `@Todo & tzinfo` has no attribute `zone`
- 169 |         elif isinstance(tzinfo, str):
- 170 |             tzinfo = parser.TzinfoParser.parse(tzinfo)
- Found 80 diagnostics
+ Found 77 diagnostics

x: Q = Q()

def _(a_and_b: Intersection[A, B]):
reveal_type(a_and_b.x) # revealed: P & Q
Copy link
Member

Choose a reason for hiding this comment

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

Again, not something for this PR necessarily. But I think that this implies that we should infer the following case like so:

@final
class A: ...
class B: ...
class C:
    x: A
class D:
    x: B

def f(y: C):
    if isinstance(y, D):
        reveal_type(y)  # revealed: Never

Because we know that all instances of C have an x attribute, but the inferred type of the x attribute on the y variable would be A & B, which simplifies to Never, since A is @final. An inferred attribute type of Never for the x attribute is the same thing as saying that y cannot have an attribute x in this branch, but we already established that y must have an x attribute, since y is an instance of C. The only way to solve this contradiction is to say that this branch is in fact unreachable: C & D must simplify to Never due to the fact that A & B simplifies to Never.

This is perhaps quite a long-winded proof for something that is self-evident...! I think mypy and pyright might also infer unreachable code in cases like this

Copy link
Contributor

Choose a reason for hiding this comment

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

We already (with this PR) correctly infer the type of y.x as Never in your example.

I think it is also correct to go from that to inferring C & D itself as Never, but this could be quite expensive to implement (since it would require checking every attribute of any pair of types that end up in an intersection together), and it's not clear to me how necessary/valuable it will be in practice.

Copy link
Member

Choose a reason for hiding this comment

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

We already (with this PR) correctly infer the type of y.x as Never in your example.

I know; it was the further inference that I was attempting to discuss.

Anyway, I agree that we can defer this for now and see how expensive and/or useful it is later!

x: Q = Q()

def _(a_and_b: Intersection[A, B]):
reveal_type(a_and_b.x) # revealed: P & Q
Copy link
Contributor

Choose a reason for hiding this comment

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

We already (with this PR) correctly infer the type of y.x as Never in your example.

I think it is also correct to go from that to inferring C & D itself as Never, but this could be quite expensive to implement (since it would require checking every attribute of any pair of types that end up in an intersection together), and it's not clear to me how necessary/valuable it will be in practice.

@carljm
Copy link
Contributor

carljm commented Mar 12, 2025

(mypy-primer changes look correct)

@sharkdp sharkdp merged commit a6572a5 into main Mar 12, 2025
22 checks passed
@sharkdp sharkdp deleted the david/attribute-access-on-intersections branch March 12, 2025 12:20
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