Skip to content

False negative: typing.Literal[<Union expression>] #17260

Open

Description

@bzoracler

Bug Report, To Reproduce, & Actual Behaviour

See mypy playground

from typing import Literal

a: Literal[1 | None]
reveal_type(a)
note: Revealed type is "Union[Literal[1], None]"
Success: no issues found in 1 source file

For the runtime counterpart, this will result in a TypeError: unsupported operand type(s) for |: 'int' and 'NoneType' if the module doesn't have from __future__ import annotations or invalid results for runtime annotation inspectors (e.g. in the case of typing.Literal[1 | 2]).

Expected Behavior

Maybe:

error: Invalid type: Literal[...] cannot contain arbitrary expressions  [valid-type]
note: Revealed type is "Any"

Your Environment

  • Mypy version used: 1.10
  • Mypy command-line flags: None
  • Mypy configuration options from mypy.ini (and other config files): None
  • Python version used: 3.10 & 3.12

Activity

  1. 97littleleaf11 commented on May 24, 2026

    @97littleleaf11
    Collaborator

    My understanding is that Literal[...] inside a union is valid, such as Union[Literal[1], None]. Also, Literal[1, None] is valid and equivalent to Literal[1] | None.

    However, a union expression inside Literal[...] should not be valid, e.g. Literal[1 | None] or Literal[Union[Literal[1], None]](please correct me if I am wrong, cc @hauntsaninja ).

    The main open question is aliases:

    Alias1 = Literal[1, None]
    x: Literal[Alias1]        # valid? since it's a nested Literal
    
    Alias2 = Literal[1] | None
    y: Literal[Alias2]        # valid, or should this also be rejected?
    

    Alias1 current is handled by the mypy via the existing UnionType flattening logic in analyze_literal_param:

        def analyze_literal_param(self, idx: int, arg: Type, ctx: Context) -> list[Type] | None:
            ...
            elif isinstance(arg, UnionType):
                out = []
                for union_arg in arg.items:
                    union_result = self.analyze_literal_param(idx, union_arg, ctx)
                    if union_result is None:
                        return None
                    out.extend(union_result)
                return out
  2. hauntsaninja commented on May 25, 2026

    @hauntsaninja
    Collaborator

    Yeah, it makes sense to disallow this, at least because runtime typing will not work well.

    Note you might want to also add a test in the conformance test suite for this https://github.com/python/typing

  3. bzoracler commented on May 25, 2026

    @bzoracler
    ContributorAuthor

    The main open question is aliases:

    Alias1 = Literal[1, None]
    x: Literal[Alias1]        # valid? since it's a nested Literal
    
    Alias2 = Literal[1] | None
    y: Literal[Alias2]        # valid, or should this also be rejected?
    

    @97littleleaf11 The typing specs say those are valid:

    Literal may also be parameterized by other literal types, or type aliases to other literal types.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions