Skip to content

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

Description

@Garrett-R

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

Activity

  1. vnmabus commented on Nov 24, 2023

    @vnmabus

    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!
  2. A5rocks commented on Jan 30, 2025

    @A5rocks
    Collaborator

    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
  3. added and removed
    bugmypy got something wrong
    on Jan 30, 2025
  4. added a commit that references this issue on Jan 27, 2026
    e64a84b
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions