Skip to content

Narrowing on is / == does not remove enum Literal member from a union (regression in 0.0.39) #3606

@kang8

Description

@kang8

Summary

Narrowing a union that contains an enum-member Literal via is / is not / == / != no longer removes that member in the corresponding branch. The matching Literal survives in the union, producing false positives. This is a regression: ty 0.0.37 narrows correctly; 0.0.39 and 0.0.40 (latest) do not. isinstance() narrowing is unaffected.

Reproduction

from enum import Enum
from typing import Literal


class CM(Enum):
    EMPTY = "empty"
    QUIT = "quit"          # a 2nd member is required to trigger the bug


def f(x: list[int] | Literal[CM.EMPTY]) -> None:
    if x is CM.EMPTY:
        return
    reveal_type(x)         # expected: list[int]
                           # actual (0.0.39 / 0.0.40): list[int] | Literal[CM.EMPTY]


def g(x: list[int] | Literal[CM.EMPTY]) -> None:
    if isinstance(x, CM):  # isinstance narrows correctly
        return
    reveal_type(x)         # list[int]  -- correct on all versions

Using the value in a return/assignment surfaces the false positive directly:

def h(x: list[int] | Literal[CM.EMPTY]) -> list[int]:
    if x is CM.EMPTY:
        return []
    return x   # error[invalid-return-type]: found `list[int] | Literal[CM.EMPTY]`

Expected

In the else branch, x narrows to list[int] (the Literal[CM.EMPTY] member is removed), matching ty 0.0.37, pyright, and mypy.

Actual

x keeps the Literal[CM.EMPTY] member, so reveal_type shows list[int] | Literal[CM.EMPTY] and downstream usages are flagged.

Trigger condition

The enum must have >= 2 members, and the union must mention only one member's Literal. With a single-member enum the complement ~Literal[CM.EMPTY] is empty, so the bug does not appear.

Versions

ty version result
0.0.37 narrows to list[int] (correct)
0.0.39 keeps Literal[CM.EMPTY] (incorrect)
0.0.40 (latest) keeps Literal[CM.EMPTY] (incorrect)

is, is not, ==, != are all affected; isinstance works.

Possibly related

#3552 ("Incorrect is narrowing of code involving newtypes") — opposite symptom (over-eager intersection in the positive branch) but may share the same is-narrowing machinery.

Suspected cause

Possibly the "first-class support for enum complements" change (#24961, released in 0.0.38): the ~Literal[...] representation may not eliminate the matching Literal union member on the is / == branches. This is a hypothesis — the regression window is confirmed as 0.0.37 -> 0.0.39, but 0.0.38 was not individually bisected.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions