Skip to content
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

Add a strictness flag that disables the bivariant behavior of TypeGuard #11230

Closed
KotlinIsland opened this issue Sep 30, 2021 · 8 comments
Closed

Comments

@KotlinIsland
Copy link
Contributor

KotlinIsland commented Sep 30, 2021

from typing import TypeGuard


def is_str_list(val: list[object]) -> TypeGuard[list[str]]:
    return all(isinstance(x, str) for x in val)

l1: list[object] = []
l2 = l1

if is_str_list(l1):
    l2.append(1)
    impostor: str = l1[0]  # SUS ALERT!!!! 😳😳😳😳😳😳😳

I can't stand bivariance, and I believe it should always be an opt-in behavior. I don't know any other place in python typing where bivariance exists (except for an Any generic parameter).

Please add a strictness flag that enforces standard python typing variance rules on TypeGuard.

@JelleZijlstra JelleZijlstra added the topic-typeguard TypeGuard / PEP 647 label Mar 19, 2022
@JelleZijlstra
Copy link
Member

Can you clarify what error you'd want to see exactly?

@KotlinIsland
Copy link
Contributor Author

from typing import TypeGuard

def is_str_list(val: list[object]) -> TypeGuard[list[str]]:  # E: list[str] is not a subtype of list[object]
    ...

@erictraut
Copy link

What value would that error message provide? Who would use it other than you? And if you saw that error message, what would you do to fix it?

You can choose not to use this type of TypeGuard in your code if you don't like it.

@DetachHead
Copy link
Contributor

i believe this flag would be useful, however the broader issue is that TypeGuard doesn't enforce that the narrowed type is a subtype in the first place.

from the PEP:

The return type of a user-defined type guard function will normally refer to a type that is strictly “narrower” than the type of the first argument (that is, it’s a more specific type that can be assigned to the more general type). However, it is not required that the return type be strictly narrower. This allows for cases like the example above where List[str] is not assignable to List[object].

wouldn't the problem be much better solved by simply making TypeGuard take variance into account? that way, you can enforce that the narrowed type is actually narrower

What value would that error message provide? Who would use it other than you? And if you saw that error message, what would you do to fix it?

but couldn't you say the same thing about other scenarios where variance is taken into account?

def foo(value: list[object]) -> None: ...

bar: list[int]

foo(bar) # error: Argument 1 to "foo" has incompatible type "List[int]"; expected "List[object]"

in this case, you get some helpful information as well:

main.py:5: note: "List" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance
main.py:5: note: Consider using "Sequence" instead, which is covariant

how is this any different?

@KotlinIsland
Copy link
Contributor Author

KotlinIsland commented Mar 20, 2022

What value would that error message provide? ... You can choose not to use this type of TypeGuard in your code if you don't like it.

I understand the desire for unsafe narrowing, the issue I have is that it's extremely inconsistent with the rest of the typing landscape. The example from the pep: list[object] to list[int] is very obviously unsafe, but once type aliases, contra-variance and refactoring (and I'm sure other things) are taken into account, it would would be quite easy to accidentally write/introduce unsafe code.

Ideally I would want a TypeGuard and an UnsafeTypeGuard. But until such option is available I much prefer the assurance that the code I write is safe.

@erictraut
Copy link

There has been discussion in the typing-sig about a StrictTypeGuard. I've even implemented it in pyright as a proof of concept, but there hasn't been enough interest to proceed with a PEP at this time.

But until such option is available I much prefer the assurance that the code I write is safe.

Then don't use the current TypeGuard feature in your code — or at least avoid using it in a manner that you don't like. I see no value in adding an optional error in mypy to tell you not to use it.

@KotlinIsland
Copy link
Contributor Author

KotlinIsland commented Apr 6, 2024

Then don't use the current TypeGuard feature in your code — or at least avoid using it in a manner that you don't like.

The issue is that there is no way to know if the usage is safe or not, withought performing manual type checking.

This has been addressed in basedmypy:

def is_str_list(val: list[object]) -> val is list[str]:  # error: A type-guard's type must be assignable to its parameter's type. (guard has type "list[str]", parameter has type "list[object]")  [typeguard-subtype]
    return all(isinstance(x, str) for x in val)

playground

So I'm going to close it.

@JelleZijlstra
Copy link
Member

FYI, TypeIs (added by PEP 742 and implemented in the next release of mypy) also requires that the output type is a subtype of the input type.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants