I would have use for StrictTypeGuard. Given how normal typeguard modifies types, I’d have more use for the strict version than the original - currently having TG in any if/else block broadens type for the rest of the function body.
One aspect I’d suggest to change is not to imply for TG[X] else clause is definitely not X - but instead to have two arguments for the type guard, new one specifying type narrowing for else. I wonder if we can still use Any to mean “same type as TG input” and make this default for else clause.
In a previous typing-sig thread (https://mail.python.org/archives/list/typing-sig@python.org/thread/EMUD2D424OI53DCWQ4H5L6SJD2IXBHUL/) there was a discussion about the existing TypeGuard (PEP 647) and the desire to support a form that allows for type narrowing in the negative case.
I previously proposed extending the existing TypeGuard to support an optional second type argument that represents the negative type. This proposal received lukewarm reception because it didn't address some desired use cases.
Based on this feedback, I proposed a new idea in the python/typing discussion forum (https://github.com/python/typing/discussions/1013#discussioncomment-1966238). This new proposal introduces a form called "StrictTypeGuard". It also describes variants called "TypeAssert" and "StrictTypeAssert".
I haven't received much feedback in that forum thread, so I figured I'd repost here.
--
Taking in all of the feedback, here's an alternative proposal that involves the introduction of another form of TypeGuard which has "strict" type narrowing semantics. It would be less flexible than the existing TypeGuard, but the added constraints would allow it to be used for these alternate use cases.
*StrictTypeGuard*
This proposal would introduce a new flavor of TypeGuard that would have strict narrowing semantics. We'd need to come up with a new name for this, perhaps StrictTypeGuard, ExactTypeGuard, or PreciseTypeGuard. Other suggestions are welcome. For now, I'll refer to it as StrictTypeGuard.
This new flavor of type guard would be similar to the more flexible version defined in PEP 647 with the following three differences:
The type checker would enforce the requirement that the type guard type (specified as a return type of the type guard function) is a subtype of the input type (the declared type of the first parameter to the type guard function). In other words, the type guard type must be strictly narrower than the input type. This precludes some of the use cases anticipated in the original PEP 647.
```python
def is_marsupial(val: Animal) -> StrictTypeGuard[Kangaroo | Koala]: # This is allowed
return isinstance(val, Kangaroo | Koala)
def has_no_nones(val: list[T | None]) -> StrictTypeGuard[list[T]]: # Error: "list[T]" cannot be assigned to "list[T | None]"
return None not in val
```
Type narrowing would be applied in the negative ("else") case. This may still lead to incorrect assumptions, but it's less likely to be incorrect with restriction 1 in place. Consider, for example,
```python
def is_black_cat(val: Animal) -> StrictTypeGuard[Cat]:
return isinstance(val, Cat) and val.color == Color.Black
def func(val: Cat | Dog):
if is_black_cat(val):
reveal_type(val) # Cat
else:
reveal_type(val) # Dog - which is potentially wrong here
```
If the return type of the type guard function includes a union, the type checker would apply additional type narrowing based on the type of the argument passed to the type guard call, eliminating union elements that are impossible given the argument type. For example:
```python
def is_cardinal_direction(val: str) -> StrictTypeGuard[Literal["N", "S", "E", "W"]]:
return val in ("N", "S", "E", "W")
def func(direction: Literal["NW", "E"]):
if is_cardinal_direction(direction):
reveal_type(direction) # Literal["E"] # The type cannot be "N", "S" or "W" here because of argument type
else:
reveal_type(direction) # Literal["NW"]
```
* TypeAssert and StrictTypeAssert*
As mentioned above, there has also been a desire to support a "type assert" function. This is similar to a "type guard" function except that it raises an exception (similar to an assert statement, except that its behavior is not dependent on 'debug' mode) if the input's type doesn't match the declared return type. This is analogous to "type predicate functions" supported in TypeScript, like the following:
```typescript
function assertAuthenticated(user: User | null): asserts user is User {
if (user === null) {
throw new Error('Unauthenticated');
}
}
```
We propose to add two new forms TypeAssert and StrictTypeAssert, which would be analogous to TypeGuard and StrictTypeGuard. While type guard functions are expected to return a bool value, "type assert" forms would return None (if the input type matches) or raise an exception (if the input type doesn't match). Type narrowing in the negative ("else") case doesn't apply for type assertions because it is assumed that an exception is raised in this event.
Here are some examples:
```python
def verify_no_nones(val: list[None | T]) -> TypeAssert[list[T]]:
if None in val:
raise ValueError()
def func(x: list[int | None]):
verify_no_nones(x)
reveal_type(x) # list[int]
and
def assert_is_one_dimensional(val: tuple[T, ...] | T) -> StrictTypeAssert[tuple[T] | T]:
if isinstance(val, tuple) and len(val) != 1:
raise ValueError()
def func(x: float, y: tuple[float, ...]):
assert_is_one_dimensional(x)
reveal_type(x) # float
assert_is_one_dimensional(y)
reveal_type(y) # tuple[float]
```
Thoughts? Suggestions?
If we move forward with the above proposal (or some subset thereof), it will probably require a new PEP, as opposed to a modification to PEP 647.
- Eric
--
Eric Traut
Contributor to Pyright & Pylance
Microsoft
_______________________________________________
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: ikamenshchikov@gmail.com
--
Best Regards,
--
Ilya Kamen