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

generic fails to narrow, something to do with using NoReturn on contravariant generic #11508

Open
DetachHead opened this issue Nov 10, 2021 · 6 comments
Labels
bug mypy got something wrong

Comments

@DetachHead
Copy link
Contributor

DetachHead commented Nov 10, 2021

from typing import Generic, Mapping, NoReturn, Sequence, TypeVar

T = TypeVar("T", contravariant=True)


class Foo(Generic[T]):
    ...


foo = Foo[int]()
bar = Foo[str]()

a1: Sequence[Foo[NoReturn]] = [foo]  # no error
a2: Sequence[Foo[NoReturn]] = [bar]  # no error

# error (this one seemed to be fixed around commit 32448b88370f8327c390f4e21666065c87ca95e2, then broken again)
a3: Sequence[Foo[NoReturn]] = [foo, bar]

a4: Sequence[Foo[NoReturn]] = [foo] or [bar]  # error

a5: Mapping[str, Sequence[Foo[NoReturn]]] = {  # error
    "foo": [foo],
    "bar": [],  # error goes away if you change this to [foo]
}

a6: Sequence[Sequence[Foo[NoReturn]]] = [[foo], []]  # error

https://mypy-play.net/?mypy=latest&python=3.10&gist=175deca029e02ea95af74f7c4a02856c

@DetachHead DetachHead added the bug mypy got something wrong label Nov 10, 2021
@DetachHead DetachHead changed the title dict value type fails to narrow when it can't be inferred for all values in the dict, when bounded to a type with a contravariant generic bounded to NoReturn dict value type fails to narrow when it can't be inferred for all its values, when bounded to a type with a contravariant generic bounded to NoReturn Nov 10, 2021
@KotlinIsland
Copy link
Contributor

KotlinIsland commented Nov 10, 2021

from typing import Generic, NoReturn, TypeVar

T_cont = TypeVar("T_cont", contravariant=True)


class Foo(Generic[T_cont]):
    ...

foo = Foo[object]()

a1: list[Foo[bool]] = [foo]
a2: list[Foo[NoReturn]] = [foo]  # error: Incompatible types in assignment (expression has type "List[Foo[int]]", variable has type "List[Foo[NoReturn]]")  [assignment]
# main.py:12: note: "List" is invariant

🤔

@erictraut
Copy link

You pointed out that "the error goes away if you change this to [foo]". That looks like a bug. It should be an error in this case too. Keep in mind that no type is assignable to NoReturn, and NoReturn is not assignable to any type.

Also, PEP 484 specifically says:

The NoReturn type is only valid as a return annotation of functions, and considered an error if it appears in other positions.

So mypy should generate an error for the type annotation Sequence[Foo[NoReturn]]].

@DetachHead DetachHead changed the title dict value type fails to narrow when it can't be inferred for all its values, when bounded to a type with a contravariant generic bounded to NoReturn generic fails to narrow, something to do with using NoReturn on contravariant generic Nov 10, 2021
@KotlinIsland
Copy link
Contributor

So mypy should remove a lot of functionality it has implemented around NoReturn to act like an uninhabited type?

@erictraut
Copy link

I'm not sure what you mean by "remove a lot of functionality it has implemented around NoReturn". I'm just pointing out that the semantics of NoReturn are well documented in PEP 484. Has mypy implemented functionality that is in conflict with the specification?

@DetachHead
Copy link
Contributor Author

mypy lets you use it like a Never type

from typing import NoReturn

foo: NoReturn

bar: int = foo

(relevant discussion from a previous issue i raised: #11291 (comment))

@erictraut
Copy link

Yes, you're right that NoReturn does act like a Never type. This is useful for explicit validation of exhaustive completion checks, like the following:

def assert_never(x: NoReturn) -> NoReturn:
    assert False, "Unhandled type: {}".format(type(x).__name__)

def handle_my_union(x: int | str) -> None:
    if isinstance(x, int):
        pass
    elif isinstance(x, str):
        pass
    else:
        assert_never(x)

You can find more details about this use case if you search the issue tracker for assert_never.

I guess this is a use case that PEP 484 didn't anticipate and technically doesn't allow (since NoReturn is being used in this context as a parameter type annotation, not a return type annotation). To my knowledge, this exception to PEP 484 is support by all Python type checkers.

This works because of the property I mentioned above. The only type that can be assigned to NoReturn is NoReturn (or Never, which is another name for the same type).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug mypy got something wrong
Projects
None yet
Development

No branches or pull requests

3 participants