PEP 724: Stricter Type Guards

I never said that there “wasn’t a case for it”. Just like you, I’m considering the case of keeping the existing semantics, but I personally think they should be removed. (More on why below.)

Maybe we should ask the PEP writer to add a migration guide? Ultimately, any flavor of type guard can be rewritten without type guards. They are typing sugar. The current type guard can always be rewritten:

def is_u(val: T) -> bool:  ... # defined as before, but just return a Boolean.

and then used as follows:

def f(val: T):
    if is_u(val) is not None:
        u = cast(val, U)
        # Use u instead of val from here on to get the `U` type you wanted.
    else:
        # val is unchanged, as desired.

Let’s examine the future under your proposal of keeping TypeGuard versus the future that’s suggested by the PEP and pretend that we’re a new user who has to choose a type guard for a function that she’s writing, and ask which future is a better one to live in.

First, some background on the various type guards:

We have the current TypeGuard:

def is_u(val: T) -> TypeGuard[U]: ...

def f(val: T):
    if is_u(val):
        # Type of ``val`` is narrowed to ``U``.
    else:
        # Type of ``val`` remains as ``T``.

Now, the PEP 724 “strict” TypeGuard:

def is_u(val: T) -> TypeGuard[U]: ...

def f(val: T):
    if is_u(val):
        # Type of ``val`` is narrowed to ``T & U``.
    else:
        # Type of ``val`` is narrowed to ``T & Not[U]``.

Note that this is extremely logical since it can work exactly like an instance check for U.

And the proposed LaxTypeGuard:

def is_u(val: T) -> LaxTypeGuard[U]: ...

def f(val: T):
    if is_u(val):
        # Type of ``val`` is narrowed to ``T & U``.  Note the difference!
    else:
        # Type of ``val`` remains as ``T``.

Now let’s compare the benefits for a new user in each future.

In the “stable future” that you’re proposing I guess you want to keep the current type guard, and add the strict type guard? In that case, the stable future has the following problems:

  • There is doubt about which type guard is required, which requires a deep understanding of the documentation.
  • Using the current type guard requires learning a new reasoning pattern that is unlike instance checks.
  • The current type guard has a lot of surprising behavior based on all of the bug reports against it. In particular, it does not narrow T (replacing it with U in the positive case). This will probably necessitate adding LaxTypeGuard, and maybe deprecating it anyway.

The “progressive” future that I’m proposing would have the strict type guard only. Thus,

  • There is no doubt about which type guard is required, which guides new users to the obvious choice.
  • Using the type guard can work exactly like an instance check, which makes it easy to understand.
  • If there’s a need, a LaxTypeGuard can be added. It has the benefit of mirroring the strict type guard–without the surprising behavior. It’s a bit trickier to desugar than the current type guard, so the case for adding it is stronger too.

These are two futures that I was comparing, and this is the basis for my motivation. I understand the desire to mitigate upgrade pains, but I think they’re outweighed by the benefits of creating the better future.

2 Likes