Catching up...

On Sun, Nov 29, 2020 at 11:25 AM David Foster <davidfstr@gmail.com> wrote:
A few comments:


1. The current syntax appears to require that TypeGuard only apply to the
   *first* parameter of a function (ignoring self or cls, if present) but
   it might be valuable to apply to a different parameter in certain cases.
   For example:

    def conforms_to(type: Type[T], value: object) -> TypeGuard[value=T]: ...
                                            tentative spelling ^^^^^^

Considering prior art, TypeScript's version of TypeGuard
(a "type predicate") allows naming the parameter it applies to:

    function isNumber(x: any): x is number { ... }

I might suggest that a syntax like `TypeGuard[param=T]` be permitted to
specify a particular parameter whose type should be narrowed.

Note that keyword argument syntax can't be used in square brackets []
unless/until PEP 637 ("Support for indexing with keyword arguments") at [1]
is accepted. Also that PEP is currently slated for Python 3.11+ whereas this
PEP is currently slated for Python 3.10+.

I could potentially see breaking off the addition of keyword syntax to
TypeGuard to a later PEP, to avoid making this PEP depend on PEP 637
("Support for indexing with keyword arguments").

Heh. I talked to Anders about this and he said he couldn't recall a single example of a function where the parameter under consideration isn't the first parameter (in fact, he couldn't remember any examples where there even was another parameter). So I think this is pretty theoretical, and I would like not to complicate the proposal to handle this. **But...**

Sebastian Kreft seems to have executed a search that came to the same conclusion as Anders (typeguards are one-argument functions), but he is pushing for a way to define a typeguard as a method that guards `self`, e.g. `if query.empty(): ...`. I'm not sure how to address this without adopting David's idea except to propose that if `self` is the *only* argument the typeguard applies to `self`. But that's pretty ad-hoc and ugly. (Note that the TS equivalent would have to involve `this`, which is not explicit in TS, so apparently in TS there's no great need for this.)

So in the end David's suggestion of using PEP 637 keyword indexing could be adopted if in the future the need for this use case becomes overwhelming. (It's interesting that PEP 637 seems so popular in typing-sig -- it's needed for variadic generics as well. :-)
 

2. The discussions in the "Rejected Ideas" section of this PEP need some more
   elaboration IMHO:

2.1. §"Decorator Syntax":
> The use of a decorator was considered for defining type guards.

Here, it would be useful to show an example of what the proposed decorator
syntax was. Presumably you're referring to the syntax from PEP 586
§"Interactions with narrowing" and Ivan's comment:

    from typing import *

    @overload
    def is_int_like(x: Union[int, List[int]]) -> Literal[True]: ...
    @overload
    def is_int_like(x: object) -> bool: ...
    def is_int_like(x): ...

    def foo(scalar: Union[int, str]) -> None:
        if is_int_like(scalar):
            scalar += 3          # type of 'scalar' should be narrowed to 'int'
        else:
            scalar += "foo"      # type of 'scalar' should be narrowed to 'str'

I don't think it was that. The PEP 586 example doesn't actually work, and can't work without changing the specification of @overload.

The idea was probably something like this:
```
@typeguard(Union[int, List[int]])
def is_int_like(x: object) -> bool:
    >implementation>
```
This is inferior because it requires runtime evaluation of the type in the decorator, which would constrain forward references.

Definitely worth elaborating.
 
2.2. §"Enforcing Strict Narrowing":
> Strict type narrowing enforcement was considered, but this eliminates
> numerous valuable use cases for this functionality.

I don't actually know what this is talking about.

Presumably this is about whether the type T in TypeGuard[T] must be a subtype of the argument type. (The PEP mentions a reason why this is a bad idea: container types that are invariant.) Sebastian Kreft pointed out that allowing non-strict narrowing by default might cause false negatives for some obvious nonsense (e.g. `def f(a: int) -> TypeGuard[str]`). As a remedy, the type checker could either double check that the types have some overlap (e.g. their join isn't `object`); or (Sebastian's proposal) we could require passing an extra flag to allow non-strict narrowing. I'll let Eric choose here; I'd be okay with either (though probably the ergonomics of the implicit form are slightly better).

Again the PEP could use some more words on this topic.

Per Sebastian Kreft's suggestion, the PEP could also use some words comparing the proposal to TypeScript.

Sebastian Kreft had something about negative type guards (the problem that you can't be sure that the target is *not* of the given type if the assertion fails). I would like to give up on this and proceed as the PEP proposes.


Another Sebastian (Rittau) asked about "type assertions" which raise an exception instead of returning False. I wonder if that couldn't be handled by writing an explicit assert statement, e.g.
```
def f(x: Tuple[str, ...]):
    assert is_two_element_tuple(x)
    # Now x has type Tuple[T, T]
```
Maybe if the typeguard function raises a more user-friendly exception, it could still be declared as a TypeGuard, and the assert would just exist to help the type checker?


Conclusion: There are some loose ends, but I will sponsor this PEP and likely approve it after only minor updates. Eric, please use PEP number 647.

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