CarliJoy / intersection_examples

Python Typing Intersection examples
MIT License
20 stars 2 forks source link

Indirect Intersections with isinstance and Any #32

Closed CarliJoy closed 5 months ago

CarliJoy commented 5 months ago

There is a long discussion ongoing in how to handle Any within intersections in #1 and #31.

A form of intersections are already implemented in some type checkers, i.e. MyPy

# test.py
import typing as t

class A:
    ad_mix: "A"

class B: ... 

class C(A,B):  ...

class D:
    ad_mix: "D"

a: A

# Intersections are done already correctly
if isinstance(a, int): # A&int
    t.reveal_type(x)  # Revealed type is "__main__.<subclass of "A" and "int">"  -> A&int
    t.reveal_type(x+10) # Revealed type is "builtins.int"

# It even find a already existing class that is the subclass of both
if isinstance(a, B): # A&B
    t.reveal_type(x) # Revealed type is "test.D"  -> "A&B"

# If things can't be combined, as there is a conflict in LSP it will return `Any`
if isinstance(a, D): # A&D
    t.reveal_type(a) # Revealed type is "Any"      # Can't be resolved as there is a conflict in classes

any: t.Any

if isinstance(any, A): # Any&A
        t.reveal_type(D)  # Revealed type is "A"

The solutions discussed in #1 and #31 could change the behaviour of if isinstance(any, T). Therefore I propose to include a sentence like.

"… Even so isinstance acts similar to an intersection, types of Any for that an instance T was determined are always treated as T and never as T&Any."

A second proposal is that we include a sentence like "An isinstance of a variable of type T1 for a type T2 that isn't a subtype of T1 should be treated as T1 & T2 by a type checker in type narrowing."

Note: Any can't be used a type for isinstance.

What do you think?

I would like to prevent surprises no matter what we decide in #1 or #31. Also having only one "kind" of intersection would make things easier IMHO.

mikeshardmind commented 5 months ago

isinstance does not produce an intersection. This isn't needed, and any language that caused isinstance to create a virtual intersection could negatively impact more complex heuristics that type checkers already implement.

See eric's comment here, and specifically this section:

There has been some confusion about whether an isinstance type guard could make use of intersections. The behavior of type narrowing by type guards is not part of the typing spec, so type checkers are free to decide which type guard patterns they implement and how to trade off strictness vs pragmatism. I wouldn't want to change that, so any attempt to dictate how type guards work (including the isinstance type guard) is something I will push back on. The isinstance type guard logic in pyright is very complex and involves many tradeoffs and heuristics, and it continues to evolve over time. There are a couple of cases where it produces an intersection — or something akin to an intersection. One case (which I've already mentioned above) is where the type of the first argument is a type variable. Pyright uses "conditional types" for this case. The other case is where the type of the first argument is a type that has no apparent overlap with the type passed as the second argument. In this case, pyright (and mypy) create an intersection of the two types by synthesizing a new class that derives from both of the types. If "real intersections" were added to the type system, this would be a good candidate for their use, but the current solution works well, so "real intersections" wouldn't add any net new value here.

CarliJoy commented 5 months ago

Thanks for finding that @mikeshardmind

I will close this than :-)