Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[ty] Narrow equality across IntEnum classes#26079

Merged
charliermarsh merged 5 commits into
mainfrom
charlie/cross-intenum-equality
Jun 17, 2026
Merged

[ty] Narrow equality across IntEnum classes#26079
charliermarsh merged 5 commits into
mainfrom
charlie/cross-intenum-equality

Conversation

@charliermarsh

@charliermarsh charliermarsh commented Jun 17, 2026

Copy link
Copy Markdown
Member

Summary

IntEnum members from different classes compare using their integer values, but we previously treated those members as unequal because they belonged to different enum classes:

from enum import IntEnum

class First(IntEnum):
    ONE = 1

class Second(IntEnum):
    ONE = 1

def handle(value: First | Second) -> None:
    if value == First.ONE:
        reveal_type(value)  # Literal[First.ONE, Second.ONE]

Using the precise member values now modeled for standard-library enums, this narrows comparisons across integer- and string-valued enum classes, including ==, !=, and match patterns. Negative comparisons against builtin literals also exclude enum members known to compare equal to that literal; for example, value != 1 removes every finite enum member whose value is 1.

@charliermarsh charliermarsh added the ty Multi-file analysis & type inference label Jun 17, 2026
@astral-sh-bot

astral-sh-bot Bot commented Jun 17, 2026

Copy link
Copy Markdown

Typing conformance results

No changes detected ✅

Current numbers
The percentage of diagnostics emitted that were expected errors held steady at 94.37%. The percentage of expected errors that received a diagnostic held steady at 89.00%. The number of fully passing files held steady at 94/134.

@astral-sh-bot

astral-sh-bot Bot commented Jun 17, 2026

Copy link
Copy Markdown

Memory usage report

Memory usage unchanged ✅

@astral-sh-bot

astral-sh-bot Bot commented Jun 17, 2026

Copy link
Copy Markdown

ecosystem-analyzer results

No diagnostic changes detected ✅

Flaky changes detected. This PR summary excludes flaky changes; see the HTML report for details.

Full report with detailed diff (timing results)

@charliermarsh charliermarsh force-pushed the charlie/cross-intenum-equality branch from 271078e to 1f3a791 Compare June 17, 2026 01:30
@charliermarsh charliermarsh marked this pull request as ready for review June 17, 2026 01:33
@charliermarsh charliermarsh requested a review from a team as a code owner June 17, 2026 01:33
@astral-sh-bot astral-sh-bot Bot requested a review from carljm June 17, 2026 01:33
@charliermarsh charliermarsh marked this pull request as draft June 17, 2026 01:37
@charliermarsh charliermarsh force-pushed the charlie/cross-intenum-equality branch from 1f3a791 to ed905b3 Compare June 17, 2026 01:57
@charliermarsh charliermarsh changed the base branch from main to charlie/refactor-enum-value-construction June 17, 2026 01:57
@charliermarsh charliermarsh force-pushed the charlie/refactor-enum-value-construction branch from 5c45ea2 to 066d24c Compare June 17, 2026 02:09
@charliermarsh charliermarsh force-pushed the charlie/cross-intenum-equality branch from a4005c2 to 9ac4bc2 Compare June 17, 2026 02:19
@charliermarsh charliermarsh marked this pull request as ready for review June 17, 2026 02:20
@charliermarsh

Copy link
Copy Markdown
Member Author

\cc @AlexWaygood RE: #25788 (comment)

Base automatically changed from charlie/refactor-enum-value-construction to main June 17, 2026 10:53
@charliermarsh charliermarsh force-pushed the charlie/cross-intenum-equality branch from 9ac4bc2 to ff03b22 Compare June 17, 2026 10:53
@AlexWaygood

Copy link
Copy Markdown
Member

Nice! Looks like there are still some issues with inequality narrowing on this branch:

from enum import Enum, IntEnum, StrEnum

class A(int, Enum):
    X = 1
    Y = 2

class B(int, Enum):
    X = 1
    Y = 2

class C(IntEnum):
    X = 1
    Y = 2

class D(StrEnum):
    X = "X"
    Y = "Y"

class E(str, Enum):
    X = "X"
    Y = "Y"

def eq(a: A | B | C | D | E):
    if a == 1:
        reveal_type(a)  # ✅ Literal[A.X, B.X, C.X]
    else:
        reveal_type(a)  # ❌ A | B | C | D
    
    if a == C.X:
        reveal_type(a)  # ✅ Literal[A.X, B.X, C.X]
    else:
        reveal_type(a)  # Literal[A.Y, B.Y, C.Y] | D | E
    
    if a == "X":
        reveal_type(a)  # ✅ Literal[D.X, E.X]
    else:
        reveal_type(a)  # ❌ A | B | C | D

    if a == D.X:
        reveal_type(a)  # ✅ Literal[D.X, E.X]
    else:
        reveal_type(a)  # ✅ A | B | C | Literal[D.Y, E.Y]

def ne(a: A | B | C | D | E):
    if a != 1:
        reveal_type(a)  # ❌ A | B | C | D 
    else:
        reveal_type(a)  # ✅ Literal[A.X, B.X, C.X]
    
    if a != C.X:
        reveal_type(a)  # ✅ Literal[A.Y, B.Y, C.Y] | D | E
    else:
        reveal_type(a)  # ✅ Literal[A.X, B.X, C.X]
    
    if a != "X":
        reveal_type(a)  # ❌ A | B | C | D
    else:
        reveal_type(a)  # ✅ Literal[D.X, E.X] 

    if a != D.X:
        reveal_type(a)  # ✅ A | B | C | Literal[D.Y, E.Y]
    else:
        reveal_type(a)  # ✅ Literal[D.X, E.X]

Comment on lines +108 to +110
/// Unlike `.value` inference, this trusts standard-library constructor methods whose
/// comparison behavior is modeled. User-defined or opaque constructors and custom metaclasses
/// may change that behavior arbitrarily.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm... I'm quite surprised that we don't already reveal Literal[1] for this, to be honest:

from enum import IntEnum

class Foo(IntEnum):
    X = 1

reveal_type(Foo.X.value)

should we improve our .value inference elsewhere rather than reimplementing a better version of it here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is that? IntEnum defines its own __new__. We can't trust custom __new__, in general.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but IntEnum is (as you say in the PR description, and this PR's mdtest prose) a commonly used stdlib class that seems worth adding special casing for. It does have a custom __new__, but for this particular custom __new__ we know that all it does internally is call int() on the object passed in. And we know that int(1) creates an object of type Literal[1].

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this fixing that exact behavior? That's why we track user-defined vs. standard library __new__, and feed that through to comparison_value_type.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, you're suggesting that this should just be the type of .value rather than a separate function? That seems reasonable. I think I was mostly confused by "I'm quite surprised that we don't already reveal Literal[1] for this, to be honest" followed by an exact explanation of my own explanation from the summary.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"I'm quite surprised that we don't already reveal Literal[1] for this, to be honest" suggested to me that you meant "on main", FWIW.

@AlexWaygood AlexWaygood Jun 17, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did mean "on main"! I meant I'm surprised that we don't already special-case IntEnum on main! And that rather than applying narrow special-casing for IntEnum here, we should apply it more broadly -- that it should just be the type of .value rather than a separate function

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could probably break it into a separate PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whatever's easiest

@charliermarsh charliermarsh force-pushed the charlie/cross-intenum-equality branch from ff03b22 to 7acdad9 Compare June 17, 2026 14:29
@charliermarsh charliermarsh changed the base branch from main to charlie/precise-intenum-values June 17, 2026 14:30

@AlexWaygood AlexWaygood left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice, thank you!

@charliermarsh charliermarsh force-pushed the charlie/precise-intenum-values branch from 9d0d437 to 17b768a Compare June 17, 2026 14:47
@charliermarsh charliermarsh force-pushed the charlie/cross-intenum-equality branch from 7acdad9 to e624129 Compare June 17, 2026 14:48
Base automatically changed from charlie/precise-intenum-values to main June 17, 2026 15:30
@charliermarsh charliermarsh force-pushed the charlie/cross-intenum-equality branch from e624129 to 1932e15 Compare June 17, 2026 15:33
@charliermarsh charliermarsh merged commit 43307b0 into main Jun 17, 2026
60 checks passed
@charliermarsh charliermarsh deleted the charlie/cross-intenum-equality branch June 17, 2026 15:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants