StrictTypeGuard and TypeAssert
In a previous typing-sig thread (https://mail.python.org/archives/list/typing-sig@python.org/thread/EMUD2D424...) 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
In a previous typing-sig thread (https://mail.python.org/archives/list/typing-sig@python.org/thread/EMUD2D424...) 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.
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
I agree that supporting a negative case would be useful. I generally like the initial specification for StrictTypeGuard: the type guard function). In other words, the type guard type must be strictly narrower than the input type. -0. This restriction feels a bit arbitrary to me. Does it potentially allow for better typechecker error messages or a simplified typechecker checking implementation?
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.
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
+1. Great. Avoids the need to introduce something like `Not[T]`, as in `TypeGuard[Not[T]]`. the argument passed to the type guard call, eliminating union elements that are impossible given the argument type. +0. Not sure how useful this is, considering that the specific example with Literals isn't similar to any code I've written in the last few years. But the behavior certainly makes sense.
We'd need to come up with a new name for this, perhaps StrictTypeGuard, ExactTypeGuard, or PreciseTypeGuard. Other suggestions are welcome.
Perhaps `TypeGuard[T, strict=True]` would be a possible spelling? I seem to vaguely recall that the ability to add kwargs to brackets like [] was added to legal syntax. I am lukewarm RE the introduction of either TypeAssert or StaticTypeAssert. You can rewrite imperative "assert" functions of the type in the examples as functional "parse" functions with existing type annotations: Instead of:
def assert_is_one_dimensional(val: tuple[T, ...] | T) -> StrictTypeAssert[tuple[T] | T]: if isinstance(val, tuple) and len(val) != 1: raise ValueError()
You could write:
def parse_one_dimensional(val: tuple[T, ...] | T) -> Optional[tuple[T] | T]: if isinstance(val, tuple) and len(val) != 1: return None else: return val
-- David Foster | Seattle, WA, USA Contributor to TypedDict support for mypy
El mar, 1 feb 2022 a las 22:05, David Foster (<davidfstr@gmail.com>) escribió:
In a previous typing-sig thread ( https://mail.python.org/archives/list/typing-sig@python.org/thread/EMUD2D424...)
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 agree that supporting a negative case would be useful. I generally like the initial specification for StrictTypeGuard:
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.
-0. This restriction feels a bit arbitrary to me. Does it potentially allow for better typechecker error messages or a simplified typechecker checking implementation?
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.
+1. Great. Avoids the need to introduce something like `Not[T]`, as in `TypeGuard[Not[T]]`.
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.
+0. Not sure how useful this is, considering that the specific example with Literals isn't similar to any code I've written in the last few years. But the behavior certainly makes sense.
We'd need to come up with a new name for this, perhaps StrictTypeGuard, ExactTypeGuard, or PreciseTypeGuard. Other suggestions are welcome.
Perhaps `TypeGuard[T, strict=True]` would be a possible spelling? I seem to vaguely recall that the ability to add kwargs to brackets like [] was added to legal syntax.
This was proposed in PEP 637 but rejected.
I am lukewarm RE the introduction of either TypeAssert or StaticTypeAssert. You can rewrite imperative "assert" functions of the type in the examples as functional "parse" functions with existing type annotations:
Instead of:
def assert_is_one_dimensional(val: tuple[T, ...] | T) -> StrictTypeAssert[tuple[T] | T]: if isinstance(val, tuple) and len(val) != 1: raise ValueError()
You could write:
def parse_one_dimensional(val: tuple[T, ...] | T) -> Optional[tuple[T] | T]: if isinstance(val, tuple) and len(val) != 1: return None else: return val
-- David Foster | Seattle, WA, USA Contributor to TypedDict support for mypy _______________________________________________ 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: jelle.zijlstra@gmail.com
PEP 637 -- Support for indexing with keyword arguments Probably you mean some other PEP.. On Wed 2. Feb 2022 at 07:20, Jelle Zijlstra <jelle.zijlstra@gmail.com> wrote:
El mar, 1 feb 2022 a las 22:05, David Foster (<davidfstr@gmail.com>) escribió:
In a previous typing-sig thread ( https://mail.python.org/archives/list/typing-sig@python.org/thread/EMUD2D424...)
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 agree that supporting a negative case would be useful. I generally like the initial specification for StrictTypeGuard:
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.
-0. This restriction feels a bit arbitrary to me. Does it potentially allow for better typechecker error messages or a simplified typechecker checking implementation?
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.
+1. Great. Avoids the need to introduce something like `Not[T]`, as in `TypeGuard[Not[T]]`.
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.
+0. Not sure how useful this is, considering that the specific example with Literals isn't similar to any code I've written in the last few years. But the behavior certainly makes sense.
We'd need to come up with a new name for this, perhaps StrictTypeGuard, ExactTypeGuard, or PreciseTypeGuard. Other suggestions are welcome.
Perhaps `TypeGuard[T, strict=True]` would be a possible spelling? I seem to vaguely recall that the ability to add kwargs to brackets like [] was added to legal syntax.
This was proposed in PEP 637 but rejected.
I am lukewarm RE the introduction of either TypeAssert or StaticTypeAssert. You can rewrite imperative "assert" functions of the type in the examples as functional "parse" functions with existing type annotations:
Instead of:
def assert_is_one_dimensional(val: tuple[T, ...] | T) -> StrictTypeAssert[tuple[T] | T]: if isinstance(val, tuple) and len(val) != 1: raise ValueError()
You could write:
def parse_one_dimensional(val: tuple[T, ...] | T) -> Optional[tuple[T] | T]: if isinstance(val, tuple) and len(val) != 1: return None else: return val
-- David Foster | Seattle, WA, USA Contributor to TypedDict support for mypy _______________________________________________ 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: jelle.zijlstra@gmail.com
_______________________________________________ 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
El sáb, 5 feb 2022 a las 8:27, Ilya Kamenshchikov (<ikamenshchikov@gmail.com>) escribió:
PEP 637 -- Support for indexing with keyword arguments
Probably you mean some other PEP..
No, I mean PEP 637. I was responding specifically to David's comment "I seem to vaguely recall that the ability to add kwargs to brackets like [] was added to legal syntax."
On Wed 2. Feb 2022 at 07:20, Jelle Zijlstra <jelle.zijlstra@gmail.com> wrote:
El mar, 1 feb 2022 a las 22:05, David Foster (<davidfstr@gmail.com>) escribió:
In a previous typing-sig thread ( https://mail.python.org/archives/list/typing-sig@python.org/thread/EMUD2D424...)
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 agree that supporting a negative case would be useful. I generally like the initial specification for StrictTypeGuard:
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.
-0. This restriction feels a bit arbitrary to me. Does it potentially allow for better typechecker error messages or a simplified typechecker checking implementation?
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.
+1. Great. Avoids the need to introduce something like `Not[T]`, as in `TypeGuard[Not[T]]`.
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.
+0. Not sure how useful this is, considering that the specific example with Literals isn't similar to any code I've written in the last few years. But the behavior certainly makes sense.
We'd need to come up with a new name for this, perhaps StrictTypeGuard, ExactTypeGuard, or PreciseTypeGuard. Other suggestions are welcome.
Perhaps `TypeGuard[T, strict=True]` would be a possible spelling? I seem to vaguely recall that the ability to add kwargs to brackets like [] was added to legal syntax.
This was proposed in PEP 637 but rejected.
I am lukewarm RE the introduction of either TypeAssert or StaticTypeAssert. You can rewrite imperative "assert" functions of the type in the examples as functional "parse" functions with existing type annotations:
Instead of:
def assert_is_one_dimensional(val: tuple[T, ...] | T) -> StrictTypeAssert[tuple[T] | T]: if isinstance(val, tuple) and len(val) != 1: raise ValueError()
You could write:
def parse_one_dimensional(val: tuple[T, ...] | T) -> Optional[tuple[T] | T]: if isinstance(val, tuple) and len(val) != 1: return None else: return val
-- David Foster | Seattle, WA, USA Contributor to TypedDict support for mypy _______________________________________________ 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: jelle.zijlstra@gmail.com
_______________________________________________ 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
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. On Wed 2. Feb 2022 at 05:45, Eric Traut <eric@traut.com> wrote:
In a previous typing-sig thread ( https://mail.python.org/archives/list/typing-sig@python.org/thread/EMUD2D424...) 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
participants (4)
-
David Foster
-
Eric Traut
-
Ilya Kamenshchikov
-
Jelle Zijlstra