Thanks Eric for the answers, in general I think it would be valuable to add some of these reasons to the PEP itself.
One point that hasn't been raised is what happens in multithreaded code, as nothing would guarantee that the guard is still met.
Another point not mentioned is whether typeguards will work for async functions.
> What's the rationale behind ["the first explicit argument maps to the second parameter"]?
Without this, it would be impossible to implement a user-defined type guard function as a method. If you want to apply a type guard to self, you can pass `self` as the first argument, which maps to the first explicit parameter.
That would be really not ergonomic to define and use. Imagine looking at something like result.has_data(result) or def has_data(self, self) (see example below for more context).
It's hard for me to imagine type guards really using multiple arguments, especially self or class. I get your example about allowing empty when checking for a list of strings, but that could be written as two different guards.
What would be the semantics of evaluating multi-argument type guards which are instance or classmethods. What would happen if you modify self or the class, which may affect the result of the typeguard itself. (The same applies for any multi-argument typeguard.).
For what is worth, I couldn't find any typeguards with multiple arguments in any of the JS dependencies we are using at work (node, google apis, lodash, etc).
> What's the rationale for user-defined type guards to only apply in the positive case?
The problem is that `T - guarded_type` is not well defined in the general case. This sort of type algebra is defined only in very specific cases (such as with unions). In general, the negative case must assume no narrowing. For comparison, this is how user-defined type guards work in TypeScript, by necessity.
Can you think of an example of a user-defined type guard (one that is not handled by built-in type guards) that could handle the negative case? None of the examples I provided in the draft PEP would work.
All the PEP examples would work in the negative case, however expressing the resulting type is not possible with current types.
One case would be something like the following snippet based on some real TS code written by a third party.
class QueryResult(Generic[T]):
_data: Optional[bytes]
def data(self) -> Optional[T]:
return expensive_parse_operation(self._data)
def empty(self) -> TypeGuard[MissingQueryResult]:
return self._data is None
where
class MissingQueryResult(Protocol):
def data(self) -> None: ...
def empty(self) -> Literal[True]: ...
Note that without an option to provide the types for the negative case we have to implement another guard
def has_data(self) -> TypeGuard[ExistingQueryResult]:
return self._data is not None
which is not that terrible from the implementor's POV, however for the user of the API, it's a problem as now they do need to remember which variant to use in which case.
Incidentally, I've posted more detailed documentation for the [type narrowing behaviors and all of the built-in type guards that pyright supports](https://github.com/microsoft/pyright/blob/master/docs/type-concepts.md#type-narrowing).
_______________________________________________
Typing-sig mailing list -- typing-sig@python.org
To unsubscribe send an email to typing-sig-leave@python.org
https://mail.python.org/mailman3/lists/typing-sig.python.org/
Member address: skreft@gmail.com
return True
def unsound_guard_union(arg: TypeA | TypeB) -> TypeGuard[Typea]: # Note the typo here
return True
For both of the cases above TS does emit an error:
A type predicate's type must be assignable to its parameter's type. Type 'string' is not assignable to type 'boolean'.
A type predicate's type must be assignable to its parameter's type. Type 'string' is not assignable to type 'number | boolean'.
It even checks compatibility of interfaces (protocols).
--
Sebastian Kreft