python / mypy

Optional static typing for Python
https://www.mypy-lang.org/
Other
18.35k stars 2.81k forks source link

Narrow `x is Sentinel` where x is `Union[type[Sentinel], ...]` and Sentinel is `@final` #15553

Open jonathanslenders opened 1 year ago

jonathanslenders commented 1 year ago

Feature

Apply type narrowing in case of x is not C where x is of type Union[type[C], SomethingElse], from that union to SomethingElse in case when C is made final.

If C is final, then C is the only possible value that satisfies the type type[C]. So, if we compare identity, that means we can narrow the type in either direction.

Pitch

Empty class definitions are often used as sentinel values in a union, together with something else. These sentinel values can be made @final.

Consider this code:

from typing import final, Union, Literal

@final
class Sentinel:
    pass

x: Union[type[Sentinel], int]

if x is Sentinel:
    pass
else:
    # Should be `int`
    # But is: "Union[Type[x.Sentinel], builtins.int]"
    reveal_type(x)

An example can be found in the h11 library: https://github.com/python-hyper/h11/blob/cdccbeff44a39426b58010eb454a75015ec6f8bc/h11/_connection.py#L426

    def next_event(self) -> Union[Event, Type[NEED_DATA], Type[PAUSED]]:

Many libraries are doing that. A workaround that currently works is to use instantiated objects instead of types for sentinel values and narrow them by using an isinstance() check on the sentinel type, but that's far less clean, because for every sentinel we now have to deal with both the class and the instance.

sobolevn commented 1 year ago

I have a prototype ready :)

jonathanslenders commented 1 year ago

Thank you @sobolevn!