-
Notifications
You must be signed in to change notification settings - Fork 249
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Type guard doesn't intersect types like instance check #1351
Comments
This isn't a bug, it's precisely how TypeGuard is specified (https://peps.python.org/pep-0647/#typeguard-type: "the first positional argument to the type guard function should be assumed by a static type checker to take on the type specified in the TypeGuard return type"). A separate mechanism will need a new PEP. |
Thanks for the clear explanation. Do you think it's a good idea for me to propose this in typing-sig? |
Should probably read https://mail.python.org/archives/list/typing-sig@python.org/thread/JTO3WKRKQFW5YFP3ZSMNTRIEA5NPJEM6/ first. I think Eric has a well-developed proposal but hasn't pushed it forward because of insufficient interest. |
Okay, I've read that thread. It's not something I'm used to thinking about, so it takes me a bit longer. The proposal in my issue is for the positive case to do narrowing. It seems like Erik's proposals are about what to do in the negative case. Also, regarding MyPy specifically, would this be easy for you to do since you're calling some kind of narrowing function for |
@NeilGirdhar, it's not clear to me what you're proposing, so I can't offer any thoughts. Can you clarify what you're proposing? Also as Jelle noted, If you want the semantics of an |
Thanks Erik for your detailed reply!
I would like asserting on the type guard to narrow the argument type to the intersection of its original type This would fix the problem whereby
Why not?
How do we do that with
I read your strict type guard idea. I'm not sure if it's related. I'm not interested in the negative case. What's 'union narrowing"? |
Because
I don't think I can answer that question because I don't know enough about |
Sorry, I should have linked it: is_dataclass is a standard library function that checks if an object is a dataclass instance, or if a type is a dataclass. That's why we can't simply use However, it's not clear to me why that's necessary. When a type checker sees
It's a library function. It think it checks for various attributes. It can't check |
That's not how I'll also note that there is currently no notion of an intersection in the Python type system. If and when such a concept is added, you could use a |
Sorry, but I don't see it. Consider the Also, you could wrap up any single-class type guard
Yes, I understand that. However, both Pyright and Mypy seem to have a rudimentary intersection that they seem to use with instance assertions. |
If you read PEP 647, you will not see the word "intersection" anywhere within the spec. When I wrote the PEP, it didn't even occur to me to include intersections for several reasons. First, there is no such concept in the Python type system today. Second, intersections don't make sense for many of the use cases I envisioned for I'm not sure if you're arguing that a reading of PEP 647 implies intersections or that you wish it had. In either case, I think the point is moot because it doesn't. The spec is final, and mypy, pyright and other type checkers implement You're welcome to propose a new variant of To make my point about intersections being inappropriate for def is_str_list(val: List[object]) -> TypeGuard[List[str]]: ... You're arguing that the narrowed type should be The |
The PEP is not as clear as it could be. The phrase "takes on the type" is used informally and never defined in the PEP. It might mean that it has exactly the type, or it might mean that it has that type in addition to its other types (like isinstance). But I agree that if you step back from the PEP you'll realize that it unfortunately can't be an intersection. I say unfortunate, because we can't "combine" user defined typeguards like this: if is_serializable(x):
if is_dog(x):
# We've forgotten it's serializable.
elif is_cat(x):
# Forgotten serializable. The simple technical reason it can't be an intersection is because the PEP wants to allow type guards that don't make sense statically, like the motivating A simpler example: def is_int(x: None) -> TypeGuard[int]: ... Here the intersection About For @NeilGirdhar, you could try to sidestep user-defined type guards. Instead of user-defined type guards, you could try user-defined downcast functions. These return their argument unchanged or You can cast a type to a consistent subtype ("strict narrowing") by returning the argument unchanged like: def as_str_list(val: list[Any]) -> list[str] | None:
return val if all(isinstance(x, str) for x in val) else None And you will get narrowing for free by using it like: ss = as_str_list(xs)
if ss:
# ss is list[str] You can also write the unsafe version by using typing.cast: def unsafe_as_str_list(val: list[object]) -> list[str] | None:
if all(isinstance(x, str) for x in val):
return cast(list[str], val) Which has the feature that the library writer has to acknowledge that the cast doesn't make sense statically. With type guards, you can accidentally write something that you think is safe but it's not, like the example from the PEP. You still don't get intersections like you do with is_instance, so you have to remember which name to use. You have to write: x: X
d = as_dataclass(x)
if d:
# Use d for the dataclass and x when you need type X. [Edit: fixed type annotation.] |
That's interesting. I wondered how you did it. Here you have to be careful not to depend on the order of the bases though. Pyright accepts this: class A:
def f(self, x: int) -> int: return x
class B:
def f(self, x: int, y: int) -> int: return y
def test(x: B) -> None:
if isinstance(x, A):
reveal_type(x)
x.f(0, 1) and reveals def test(x: A) -> None:
if isinstance(x, B):
reveal_type(x)
x.f(0, 1) and reveals These cases should be symmetrical. Mypy is doing something more subtle. It accepts both without errors. But it doesn't reveal any type, which does indicate that it knows that code is unreachable --- there can be no subclasses of both It appears more like it's treating this as something symmetrical like |
Just to clarify this: if the argument to
|
@erictraut Thanks for your detailed reply. I've learned a lot from your github comments over the years and I always appreciate them.
PEP 647 says "Type checkers should assume that type narrowing should be applied to the expression that is passed as the first positional argument to a user-defined type guard." The way I read it was that it is saying that it doesn't matter what the parameter annotation (
Exactly 😄 If we go to the top of this issue. It was submitted to get x: T
assert is_dataclass(x) Ideally, this would result in However, that seems like too much work for a partial solution. In an ideal world, I believe they would return the intersection of the argument type and the check type. What do you think?
If you need type forgetting behaviour, can you not cast to Any?
This was extremely clever! Thanks for teaching me something new. |
The new PEP 724 proposes:
(The second part is this issue.) |
Basedmypy has typeguards that intersect, read more here: |
I think this can be closed now that |
For future reference, adding a link to the section on |
The type guard doesn't seem to work like an instance check:
This is causing problems with the definition of
dataclasses.is_dataclass
python/typeshed#9723.The above code was tested with MyPy and Pyright.
The text was updated successfully, but these errors were encountered: