Replies: 11 comments 52 replies
-
Based on feedback, it sounds like the above proposal does not sufficiently meet the needs or expectations of users who are dissatisfied with the current Taking in all of the feedback, here's an alternative proposal that involves the introduction of another form of StrictTypeGuard This new flavor of type guard would be similar to the more flexible version defined in PEP 647 with the following three differences:
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
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
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 function assertAuthenticated(user: User | null): asserts user is User {
if (user === null) {
throw new Error('Unauthenticated');
}
} We propose to add two new forms Here are some examples: 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. |
Beta Was this translation helpful? Give feedback.
-
It's not clear how I can use import typing as t
import typing_extensions as te
from collections import abc
@overload
def is_collection(item: str) -> te.Literal[False]:
...
@overload
def is_collection(item: bytes) -> te.Literal[False]:
...
@overload
def is_collection(item: t.Collection) -> te.Literal[True]:
...
def is_collection(item: t.Any) -> bool:
"""Return True if the item is a collection class but not a string."""
return not isinstance(item, (str, bytes)) and isinstance(item, abc.Collection)
def is_present(collection: t.Set, item: t.Union[str, t.Collection]) -> bool:
if is_collection(item):
# item is still a union here and mypy will flag `str` as unusable for `&`
return bool(collection & item)
return item in collection |
Beta Was this translation helpful? Give feedback.
-
I would like to add my support for this For instance, I have these tests which currently document the present behavior, with commented out code indicating what the true/desired behavior should be. A feature that can enable this would be a substantial improvement: |
Beta Was this translation helpful? Give feedback.
-
All this stuff seems like syntactic sugar, for example see @parched's example here. For a large codebase something like Also the assert stuff seems like a step on the path of making a statically-typed version of Python. With code like: # validate_* functions use TypeAssert
def my_func(x: str, y: int) -> None:
validate_str(x)
validate_int(y) it makes sense to want sugar in the function signature to validate all inputs at once, and there we are. I like Python and static typing so I'm fine with that, but in that case the question is whether |
Beta Was this translation helpful? Give feedback.
-
Without A = TypeVar("A")
B = TypeVar("B")
def is_A(x: A | B) -> TypeGuard[A]:
raise NotImplementedError
def after_is_A(x: A | B) -> TypeGuard[B]:
return True
def test(x: A | B):
if is_A(x):
reveal_type(x)
return
assert after_is_A(x)
reveal_type(x)
return Such that everything is enclosed in |
Beta Was this translation helpful? Give feedback.
-
I'd like to revive this thread. There continues to be a need for a In this discussion thread, I proposed a
If we want to move forward with this proposal, I think there are two approaches we could consider:
The first option would definitely require a new PEP; the second option probably would too (although perhaps not, since it would affect only type checking behavior, not runtime behavior). I have a slight preference for option 2, but I'd love to hear other thoughts. Another (somewhat orthogonal) question is whether we should add |
Beta Was this translation helpful? Give feedback.
-
Hi, I'd like to write the PEP for Eric's second option above: #1013 (comment) We think just modifying TypeGuard in specific cases would provide most of the value in this discussion. When the type checker can determine the return type is a strict subtype of the input type, then the negative case can be assumed as well. Example: A = TypeVar("A")
B = TypeVar("B")
def is_A(x: A | B) -> TypeGuard[A]:
return isinstance(x, A)
def foo(x: A | B):
if is_A(x):
reveal_type(x) <-- A
else
reveal_type(x) <-- B here This is only true because the TypeGuard function is narrowing the input type. This example however would not know anything about the negative case. def is_A_nonstrict(x: Any) -> TypeGuard[A]:
return isinstance(x, A)
def foo(x: A | B):
if is_A_nonstrict(x):
reveal_type(x) <-- A
else:
reveal_type(x) <-- A | B |
Beta Was this translation helpful? Give feedback.
-
@JelleZijlstra would you be willing to sponsor a PEP based on Eric's idea above? I've created a new PEP here: |
Beta Was this translation helpful? Give feedback.
-
Could we use existing @overload
def is_cat(val: Cat) -> Literal[True]: ...
@overload
def is_cat(val: Dog) -> Literal[False]: ...
def is_cat(val: Cat | Dog) -> bool:
return isinstance(val, Cat)
def func(val: Cat | Dog):
if is_cat(val):
reveal_type(val) # Cat
else:
reveal_type(val) # Dog |
Beta Was this translation helpful? Give feedback.
-
I think something deserving at least a mention in considered ideas (and consideration) is TypeGuard[A,B] where B, Advantages:
Disadvantages:
|
Beta Was this translation helpful? Give feedback.
-
I'd like to contribute an example of when supporting narrowing in the negative case would be useful: import asyncio
from collections.abc import Coroutine
from typing import Any, Callable, ParamSpec, TypeVar, overload
async def coroutine_func() -> str:
return 'hello world!'
def func() -> str:
return 'hello world!'
P = ParamSpec('P')
R = TypeVar('R')
@overload
def async_or_non_async_wrapper(func: Callable[P, Coroutine[Any, Any, R]]) -> Callable[P, Coroutine[Any, Any, R]]:
...
@overload
def async_or_non_async_wrapper(func: Callable[P, R]) -> Callable[P, R]:
...
def async_or_non_async_wrapper(func: Callable[P, R] | Callable[P, Coroutine[Any, Any, R]]) -> Callable[P, R] | Callable[P, Coroutine[Any, Any, R]]:
if asyncio.iscoroutinefunction(func):
reveal_type(func) # Type of "func" is "(**P@overload_func) -> Coroutine[Any, Any, Any]"
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
return await func(*args, **kwargs)
return wrapper
reveal_type(func) # Type of "func" is "((**P@overload_func) -> R@overload_func) | ((**P@overload_func) -> Coroutine[Any, Any, R@overload_func])"
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
return func(*args, **kwargs) # Error: Type "R@async_or_non_async_wrapper | Coroutine[Any, Any, R@async_or_non_async_wrapper]" cannot be assigned to type "R@async_or_non_async_wrapper"
return func |
Beta Was this translation helpful? Give feedback.
-
[I posted this in the typing-sig last week but haven't yet received much input. Reposting here for visibility.]
Since the introduction of PEP 647 (User-defined Type Guards), we've received a steady stream of input from users saying that they don't like the limitation that type narrowing is applied only in the positive case and is not applied in the negative case.
In general, type narrowing is not safe to perform if a user-defined type guard returns False, so I've stood by the original decision not to provide type narrowing in the negative case, but there are cases where such narrowing is safe and desirable.
In [a recent thread](#996 (comment)
In this discussion) where this was discussed in some detail, @ikamensh proposed a solution. The proposal is to extend the existing
TypeGuard
to support an optional second type argument. If present, the second argument indicates the narrowed type in the negative type narrowing situation.Here's a simple (admittedly contrived) example:
I've implemented this proposal in pyright so folks can experiment with it and see if they like it. If there's general consensus that it's the right approach, I can file an amendment for the existing PEP 647 to include this functionality. It was trivial to add to pyright, so I'm optimistic that it would likewise be easy to add to the other type checkers.
Another common request for PEP 647 is the desire for an "assert" form of TypeGuard — a way to indicate that a function performs runtime validation of a type, raising an exception if the type is incorrect. It occurred to me that this two-argument form of
TypeGuard
could also be used to handle this use case. The second argument would be specified as aNoReturn
. I've provisionally implemented this in pyright as well.Here's what this would look like:
I'm interested in input on these proposals.
-Eric
--
Eric Traut
Contributor to Pyright & Pylance
Microsoft
Beta Was this translation helpful? Give feedback.
All reactions