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

Type-narrowing Union with Literal using comparison to Literal variable #16465

Open
Garrett-R opened this issue Nov 11, 2023 · 2 comments · May be fixed by #18574
Open

Type-narrowing Union with Literal using comparison to Literal variable #16465

Garrett-R opened this issue Nov 11, 2023 · 2 comments · May be fixed by #18574
Labels
feature topic-type-narrowing Conditional type narrowing / binder

Comments

@Garrett-R
Copy link

Bug Report

Strangely, type-narrowing for unions including a Literal works for int but not datetime

To Reproduce

Mypy Playground. Copied here for convenience:

from datetime import datetime
from typing import Final, Literal

Unspecified = Literal['unspecified']
UNSPECIFIED: Final[Unspecified] = 'unspecified'

def do_thing(x: datetime | Unspecified) -> None:
    if x != UNSPECIFIED:
        print(x.utcnow())  # Unexpected error!

def do_thing_2(x: int | Unspecified) -> None:
    if x != UNSPECIFIED:
        print(x.real)  # No prob

Expected Behavior

No errors.

Actual Behavior

Has errors:

main.py:9: error: Item "str" of "datetime | Literal['unspecified']" has no attribute "utcnow"  [union-attr]

Your Environment

  • Mypy version used: 1.7.0
  • Mypy command-line flags: <none>
  • Mypy configuration options from mypy.ini (and other config files): <none>
  • Python version used: 3.11
@Garrett-R Garrett-R added the bug mypy got something wrong label Nov 11, 2023
@vnmabus
Copy link

vnmabus commented Nov 24, 2023

I would say that this is indeed a bug. It won't be a bug in the positive case (as a class that redefines __eq__ MAY be equal to a string), but it should definitely narrow in the negative case. Here you can see that the problem manifest itself depending on if you redefined __eq__ or not:

from datetime import datetime
from typing import Final, Literal

Unspecified = Literal['unspecified']
UNSPECIFIED: Final[Unspecified] = 'unspecified'

class KK():
    
    def kk(self):
        pass

class KK2():
    
    def kk(self):
        pass
    
    def __eq__(self, other):
        return False

def do_thing(x: datetime | Unspecified) -> None:
    if x != UNSPECIFIED:
        print(x.utcnow())  # Unexpected error!

def do_thing_2(x: int | Unspecified) -> None:
    if x != UNSPECIFIED:
        print(x.real)  # No prob
        
def do_thing_3(x: KK | Unspecified) -> None:
    if x != UNSPECIFIED:
        print(x.kk)  # No prob
        
def do_thing_4(x: KK2 | Unspecified) -> None:
    if x != UNSPECIFIED:
        print(x.kk)  # Unexpected error!

@A5rocks
Copy link
Collaborator

A5rocks commented Jan 30, 2025

Copying over my smaller reproducer from #18569:

from typing import Literal

class A:
    def __eq__(self, other: object) -> bool:  # necessary
        return isinstance(other, A)

def f(v: A | Literal["text"]) -> A | None:
    if v == "text":
        reveal_type(v)  # N: Revealed type is "Union[__main__.A, Literal['text']]"
        return None
    else:
        reveal_type(v)  # N: Revealed type is "Union[__main__.A, Literal['text']]"
        return v

@A5rocks A5rocks added feature and removed bug mypy got something wrong labels Jan 30, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature topic-type-narrowing Conditional type narrowing / binder
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants