On Sun, Feb 14, 2021 at 2:21 PM Joseph Perez <joperez@hotmail.fr> wrote:
I'm quoting here the content of my two comments on the mypy issue https://github.com/python/mypy/issues/5206:

# Quote starts here
Why do we need a boolean return?

A direct solution to this issue would be to implement type-guard functions to return the checked argument, like a checked cast in fact:
def as_integer(x) -> int:
    if not isinstance(x, int):
        raise ValueError("not an int")
    return x
If-else logic can simply be achieved using try-catch-else blocks.
    checked_x = as_integer(x)
except ValueError:
    ...  # use checked_x as an integer in this block

The existing logic in type checkers does not deal with control flow due to try/except/else, because it's hard to reason about when an exception can or can't happen. Even if we could address this, to human readers try/except/else is an unwieldy construct to grasp compared to if/else, plus the latter allows for multiple elif clauses as well.

Given the existing support for constructs like `isinstance(x, C)`, `x is not None` and such I think it's reasonable to also allow user-defined expressions to influence the control flow analysis that already exists in type checkers.

Note that it's also easy to implement your as_integer() using a type guard:
def is_integer(x: object) -> TypeGuard[int]:
    return isinstance(x, int)
def as_integer(x: object) -> int:
    if is_integer(x):
        return x
        raise ValueError(...)
This checked-cast functions can also be used as expression, which is convenient for case like `expect_an_int(as_integer(x))` (but with risks of exception mix up between expect_an_int and as_integer).

Checked-cast functions could also return an `Optional` in order to avoid exception (but that's maybe not suited for some cases)
def as_integer(x) -> Optional[int]:
    return x if isinstance(x, int) else None

if (checked_x := as_integer(x)) is not None :

Moreover, if several parameters needs to be type-checked, a tuple returns can do the job:
def check_int_and_str(x, y) -> tuple[int, str]:
    if not isinstance(x, int) or not isinstance(y, str):
        raise ValueError("bad types")
    return x, y

checked_x, checked_y = check_int_and_str(x, y)

But yes, this solution imply to assign an additional variable (with an additional name), so it's heavier, but **it already works** out of the box and do the job, no PEP required.

That's not the problem the proposal is solving.
That's being said, if boolean type-guard functions have to be implemented in the language (and I would be happy to use them to replace my heavier checked-cast), why not using [PEP 593](https://www.python.org/dev/peps/pep-0593/) `Annotated`?
By adding a standard type annotation (for `Annotated`), one could write something like
from typing import Annotated, TypeGuard

def is_integer(x) -> Annotated[bool, TypeGuard(int, "x")]:  # map the type-guard to the corresponding parameter
    return isinstance(x, int)
That could allow type-guarding of several parameters:
def check_int_and_str(x, y) -> Annotated[bool, TypeGuard(int, "x"), TypeGuard(str, "y")]:
    return isinstance(x, int) and isinstance(y, str)
Using a PEP 593 type annotation instead of a whole new type has the advantage of **not impacting every tools using type annotation** (as metadata can just be ignored).
This is also exactly the purpose of PEP 593 as it states:
> a type T can be annotated with metadata x via the typehint Annotated[T, x]. **This metadata can be used for either static analysis** or at runtime.

The type-guard is indeed a metadata of the function result, but the function still returns a `bool`.

I see PEP 593 as a verbose solution to the problem "how do we use annotations for static typing and for runtime metadata simultaneously". Type guards solve a problem that's entirely in the realm of static typing, so IMO it would be an abuse of Annotated.
But yes, this solution seems to be a little bit heavier than @vbraun proposal or [PEP 647](https://www.python.org/dev/peps/pep-0647/), but nothing prevent the specification and implementation of my proposal to provide the following shortcuts:
- when no parameter name is passed to `TypeGuard`, i.e. `TypeGuard(int)`, it applies to the first parameter (or the second in case of a method)
- `TypeGuard` has a `__getitem__` method which gives the following result: `TypeGuard[T] == Annotated[bool, TypeGuard(T)]`

A simple implementation would be:
class TypeGuard:
    def __init__(self, tp, param=None):
        self.tp = tp
        if param is not None and not isinstance(param, str):
            raise TypeError("Type guard parameter mapping must be a string")
        self.param = param

    def __getitem__(self, item):
        return Annotated[bool, TypeGuard(item)]

It would then possible to write
def is_integer(x) -> TypeGuard[int]: ...
# which would give in fact `def is_integer(x) -> Annotated[bool, TypeGuard(int)]`
# which would thus be equivalent to `def is_integer(x) -> Annotated[bool, TypeGuard(int, "x")]`
As easy, but more powerful (support arbitrary parameters), and again, less complexity (no additional `SpecialForm`),  less impact on existent tools.
# Quote ends here

To sum up, checked cast can already "do the job", at the cost of additional variable declaration and exception-catching/optional-checking. Is the PEP worth the cost?

In this case, I think a new "special form" type is not the best way to implement type guard, and I would rather use PEP 593 the way I've described above.

Ideally we'd use new syntax, but (like many things around static typing for Python, starting with PEP 484 itself) we have to compromise. TypeGuard is ersatz syntax, just like TypeVar, Union, Callable and many others. It stands to reason that the constructs that are most commonly used are the first ones to get actual new syntax assigned to them, and Callable is next in line. TypeGuard still has to prove itself.

--Guido van Rossum (python.org/~guido)