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

Union of Never can't be used #18779

Open
pleaseletmesearch opened this issue Mar 10, 2025 · 3 comments · May be fixed by #18707
Open

Union of Never can't be used #18779

pleaseletmesearch opened this issue Mar 10, 2025 · 3 comments · May be fixed by #18707
Labels
bug mypy got something wrong

Comments

@pleaseletmesearch
Copy link

Bug Report

Functions which optionally take a value of Never type have impossible requirements placed on usages of that optional value.

To Reproduce

https://gist.github.com/mypy-play/a49d72bdacc179ffda5f69d8b66f41ff

from typing import Never


def blah(s: Never | str) -> None:
    print(s + "hi")

Expected Behavior

The code type-checks. Since it is impossible for any variable to take a value of type Never, the type signature of blah is equivalent to str -> None. The function's body is valid when s is annotated with type str, so it should be valid when annotated with Never | str.

Actual Behavior

main.py:5: error: Unsupported left operand type for + ("Never")  [operator]
main.py:5: note: Left operand is of type "Never | str"
Found 1 error in 1 file (checked 1 source file)

Your Environment

  • Python 3.12
  • MyPy 1.15.0, invocation as performed in the Playground

Background

Pyright appears to do the right thing here: it accepts this code.

I ran into this when implementing this pattern, which is me placing types around something I found untyped in the wild:

T = TypeVar("T", contravariant=True)
class _MyProtocol(Protocol[T]):
  def _foo(self, arg: T | IO[str]) -> None: ...

try:
    import foo
    class _MyImpl(_MyProtocol[foo.Foo]):
      def _foo(self, arg: foo.Foo | IO[str]) -> None: 
        # do something here
        pass

    MyType = _MyImpl

except ImportError:
    class _MyImplNone(_MyProtocol[Never]):
      def _foo(self, arg: Never | IO[str]) -> None:
        # correctly assume arg is an IO[str] here
        pass

    MyType = _MyImplNone

That way, MyType has type _MyProtocol[foo.Foo] | _MyProtocol[Never], allowing a type-level discrimination of whether the foo functionality is present or not.

@pleaseletmesearch pleaseletmesearch added the bug mypy got something wrong label Mar 10, 2025
@jorenham
Copy link

According to the typing spec:

If B is a subtype of A, B | A is equivalent to A.

And since Never <: T for all types T, Never | T is indeed equivalent to T.

@sterliakov
Copy link
Collaborator

A safe casting (no Any, no cast) allows your code:

from typing import Never


def rejected(y: Never | str) -> None:
    y.startswith("")  # E: Item "Never" of "Never | str" has no attribute "startswith"  [union-attr]


def accepted(y: Never | str) -> None:
    x: str = y
    x.startswith("")

This means mypy models assignability rules correctly, but does not attempt filtering Never out of a union.

(I can also recommend the code above as a workaround for now: it's definitely safe and won't mask any future error)

@A5rocks
Copy link
Collaborator

A5rocks commented Mar 11, 2025

Technically #18707 fixes this (I introduced a bunch of changes around Never) but maybe mypy should more aggressively simplify unions too.

@A5rocks A5rocks linked a pull request Mar 11, 2025 that will close this issue
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

Successfully merging a pull request may close this issue.

4 participants