Could you summarize your proposal in a few lines? Use PEP 593 `Annotated` the way Adrian has proposed, but with an additional parameter which maps the type guard on the given function parameter name:
def check_int_and_str(x, y) -> Annotated[bool, TypeGuard(int, "x"), TypeGuard(str, "y")]:
return isinstance(x, int) and isinstance(y, str)
Provide the following shortcuts: - `TypeGuard` second parameter can be omitted, i.e. `TypeGuard(int)`; the type guard then apply to the first parameter of the function - `TypeGuard` can be used like a type, i.e. `def is_int(x) -> TypeGuard[int]` (and the type guard apply then to the first parameter), but it has to be evaluated to `Annotated[bool, TypeGuard(int)]` at runtime (by implementing `TypeGuard.__class_getitem__`). To be interpreted by type checkers, type guard must be evaluated as a boolean condition (exactly as the current proposal): ```python def is_int(x) -> TypeGuard[int]: return isinstance(x, int) x = ... if is_int(x): # reveal_type(x) -> int while is_int(x): # reveal_type(x) -> int def foo(x): assert is_int(x) # reveal_type(x) -> int ``` Restriction: `TypeGuard` second parameter must be a string literal corresponding to the name of a function whose return type is annotated, or must be omitted. A possible implementation of `TypeGuard` would be: ```python 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 __class_getitem__(self, item): return Annotated[bool, TypeGuard(item)] ``` (It's not really different than my first mail, but I've fixed my mistake by using `__class_getitem__` instead of `__getitem__) I see the following advantages of using PEP 593: - it will not break type checkers implementation when it will be released (at the condition of using the raw `Annotated` form and not the second shortcut) - the second shortcut makes it as easy to use than the current proposal using a special form - it allows supporting type guard for multiple parameters (but this use case should be quite occasional) - it will not break tools using runtime introspection (I think the case where return type of type guard predicate is inspected at runtime will also be very uncommon) - it allows returning other thing than a boolean, for example an `Optional` value: ```python def get_bar(foo) -> Annotated[Optional[Bar], TypeGuard(Foo)]: return foo.bar if isinstance(foo, Foo) else None foo = … if bar := get_bar(foo): # foo will be inferred as Foo ``` but this use case should be quite occasional too (and it's kind of tricky because if the type guard function returns an `Optional[int]` and the return is `0`, the type guard will still *fail*) - lastly, it would be a good way to introduce PEP 593 into the standard library, as I see other use cases for it (especially `ClassVar` and `InitVar`), but that's an other subject. The drawbacks: - `TypeGuard` will not be interpreted when present in a `Callable` signature, and the following example will not be possible: ```python def infer_list(l: list, guard: Callable[[Any], TypeGuard[T]]) -> TypeGuard[list[T]]: return all(map(guard, l)) ``` but this use case should also be occasional (especially because of the lack of generic type var, that make impossible to replace `list` by a `TypeVar`). By the way, I don't know if things like this are possible in TypeScript. - your quote:
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.
because I quite agree with you; contrary to `ClassVar` and `InitVar` which are used at runtime, `TypeGuard` should only be used by type checkers. So there are pros and cons, but they are mostly about occasional use cases. Personally, I would be more in favor of PEP 593, but the current proposal if also fine to me.