Skip to content

[ty] Handle enum flag in if/else and match#23808

Open
silamon wants to merge 2 commits intoastral-sh:mainfrom
silamon:flags
Open

[ty] Handle enum flag in if/else and match#23808
silamon wants to merge 2 commits intoastral-sh:mainfrom
silamon:flags

Conversation

@silamon
Copy link
Contributor

@silamon silamon commented Mar 8, 2026

Summary

Add support for handling enum flags better in if/else and match statements.

Test Plan

Mdtests

@astral-sh-bot
Copy link

astral-sh-bot bot commented Mar 8, 2026

Typing conformance results improved 🎉

The percentage of diagnostics emitted that were expected errors increased from 87.10% to 87.20%. The percentage of expected errors that received a diagnostic increased from 77.81% to 77.91%. The number of fully passing files improved from 63/131 to 64/131.

Summary

How are test cases classified?

Each test case represents one expected error annotation or a group of annotations sharing a tag. Counts are per test case, not per diagnostic — multiple diagnostics on the same line count as one. Required annotations (E) are true positives when ty flags the expected location and false negatives when it does not. Optional annotations (E?) are true positives when flagged but true negatives (not false negatives) when not. Tagged annotations (E[tag]) require ty to flag exactly one of the tagged lines; tagged multi-annotations (E[tag+]) allow any number up to the tag count. Flagging unexpected locations counts as a false positive.

Metric Old New Diff Outcome
True Positives 810 811 +1 ⏫ (✅)
False Positives 120 119 -1 ⏬ (✅)
False Negatives 231 230 -1 ⏬ (✅)
Total Diagnostics 1005 1005 +0
Precision 87.10% 87.20% +0.11% ⏫ (✅)
Recall 77.81% 77.91% +0.10% ⏫ (✅)
Passing Files 63/131 64/131 +1 ⏫ (✅)

Test file breakdown

1 file altered
File True Positives False Positives False Negatives Status
enums_expansion.py 1 (+1) ✅ 0 (-1) ✅ 0 (-1) ✅ ✅ Newly Passing 🎉
Total (all files) 811 (+1) ✅ 119 (-1) ✅ 230 (-1) ✅ 64/131

True positives added (1)

1 diagnostic
Test case Diff

enums_expansion.py:53

+error[type-assertion-failure] Type `CustomFlags` does not match asserted type `Literal[CustomFlags.FLAG3]`

False positives removed (1)

1 diagnostic
Test case Diff

enums_expansion.py:52

-error[type-assertion-failure] Type `Literal[CustomFlags.FLAG3]` does not match asserted type `CustomFlags`

@astral-sh-bot
Copy link

astral-sh-bot bot commented Mar 8, 2026

mypy_primer results

Changes were detected when running on open source projects
scikit-build-core (https://github.com/scikit-build/scikit-build-core)
- src/scikit_build_core/build/wheel.py:99:20: error[no-matching-overload] No overload of bound method `__init__` matches arguments
- Found 62 diagnostics
+ Found 61 diagnostics

@astral-sh-bot
Copy link

astral-sh-bot bot commented Mar 8, 2026

Memory usage report

Summary

Project Old New Diff Outcome
prefect 693.71MB 693.76MB +0.01% (48.78kB)
sphinx 265.43MB 265.44MB +0.00% (13.36kB)
trio 117.86MB 117.86MB +0.00% (5.38kB)
flake8 47.90MB 47.91MB +0.00% (1.50kB)

Significant changes

Click to expand detailed breakdown

prefect

Name Old New Diff Outcome
enum_metadata 2.70MB 2.74MB +1.78% (49.03kB)
infer_definition_types 87.36MB 87.36MB -0.00% (512.00B)
known_class_to_class_literal 7.27kB 7.41kB +1.88% (140.00B)
StaticClassLiteral<'db>::is_typed_dict_ 606.72kB 606.61kB -0.02% (108.00B)
place_by_id 4.47MB 4.47MB +0.00% (88.00B)
StaticClassLiteral<'db>::decorators_ 441.05kB 441.12kB +0.02% (80.00B)
place_by_id::interned_arguments 3.28MB 3.28MB +0.00% (72.00B)
place_table 723.28kB 723.22kB -0.01% (60.00B)
use_def_map 457.09kB 457.03kB -0.01% (60.00B)
KnownClassArgument 2.73kB 2.79kB +2.00% (56.00B)
infer_expression_types_impl 59.59MB 59.59MB +0.00% (24.00B)
infer_scope_types_impl 51.93MB 51.93MB +0.00% (12.00B)
IntersectionType<'db>::from_two_elements_ 332.70kB 332.71kB +0.00% (12.00B)

sphinx

Name Old New Diff Outcome
enum_metadata 740.60kB 754.27kB +1.84% (13.66kB)
infer_definition_types 24.06MB 24.06MB -0.00% (524.00B)
known_class_to_class_literal 6.59kB 6.72kB +2.08% (140.00B)
StaticClassLiteral<'db>::is_typed_dict_ 184.05kB 183.95kB -0.06% (108.00B)
place_by_id 1.37MB 1.37MB +0.01% (88.00B)
StaticClassLiteral<'db>::decorators_ 121.43kB 121.50kB +0.06% (80.00B)
place_by_id::interned_arguments 1.03MB 1.03MB +0.01% (72.00B)
place_table 226.46kB 226.41kB -0.03% (60.00B)
use_def_map 139.86kB 139.80kB -0.04% (60.00B)
KnownClassArgument 2.46kB 2.52kB +2.22% (56.00B)

trio

Name Old New Diff Outcome
enum_metadata 241.61kB 246.60kB +2.06% (4.98kB)
known_class_to_class_literal 6.08kB 6.22kB +2.25% (140.00B)
place_by_id 550.75kB 550.84kB +0.02% (88.00B)
StaticClassLiteral<'db>::decorators_ 44.79kB 44.86kB +0.17% (80.00B)
place_by_id::interned_arguments 409.71kB 409.78kB +0.02% (72.00B)
StaticClassLiteral<'db>::is_typed_dict_ 67.30kB 67.24kB -0.09% (60.00B)
KnownClassArgument 2.30kB 2.35kB +2.38% (56.00B)
infer_definition_types 7.59MB 7.59MB +0.00% (24.00B)

flake8

Name Old New Diff Outcome
enum_metadata 67.60kB 69.41kB +2.68% (1.81kB)
infer_definition_types 1.87MB 1.87MB -0.03% (524.00B)
known_class_to_class_literal 4.85kB 4.99kB +2.82% (140.00B)
StaticClassLiteral<'db>::is_typed_dict_ 28.64kB 28.54kB -0.37% (108.00B)
place_by_id 143.13kB 143.21kB +0.06% (88.00B)
StaticClassLiteral<'db>::decorators_ 13.35kB 13.43kB +0.59% (80.00B)
place_by_id::interned_arguments 106.45kB 106.52kB +0.07% (72.00B)
place_table 25.20kB 25.14kB -0.23% (60.00B)
use_def_map 19.75kB 19.69kB -0.30% (60.00B)
KnownClassArgument 1.80kB 1.86kB +3.03% (56.00B)

Copy link
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

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

thank you!

@codspeed-hq
Copy link

codspeed-hq bot commented Mar 8, 2026

Merging this PR will not alter performance

✅ 26 untouched benchmarks
⏩ 30 skipped benchmarks1


Comparing silamon:flags (0eca5b0) with main (4e7c8db)

Open in CodSpeed

Footnotes

  1. 30 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@AlexWaygood AlexWaygood added the ty Multi-file analysis & type inference label Mar 8, 2026
STOPPED = 4

reveal_type(Status.READY) # revealed: Literal[Status.READY]
reveal_type(Status.READY | Status.RUNNING) # revealed: Literal[Status.READY]
Copy link
Member

Choose a reason for hiding this comment

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

hmm, this seems incorrect. At runtime, this produces a value that is not equal to either Status.READY or to Status.RUNNING:

>>> from enum import Flag
... from typing import Literal
... 
... class Permissions(Flag):
...     READ = 1
...     WRITE = 2
...     EXECUTE = 4
...     
>>> Permissions.READ | Permissions.WRITE
<Permissions.READ|WRITE: 3>
>>> Permissions.READ | Permissions.WRITE == Permissions.READ
False
>>> Permissions.READ | Permissions.WRITE == Permissions.WRITE
False

It honestly seems quite strange to me to consider Flag enum members to inhabit Literal types at all -- but every other type checker does, so I suppose we have to follow suit on that one.

Pyright, zuban and pyrefly all have the same "bug" here -- which is really just us following typeshed's annotations correctly, so perhaps typeshed just needs to change here. So maybe we should just add a comment noting that this isn't accurate with respect to the runtime?

EXECUTE = 4

reveal_type(Permissions.READ) # revealed: Literal[Permissions.READ]
reveal_type(Permissions.READ | Permissions.WRITE) # revealed: Literal[Permissions.READ, Permissions.WRITE]
Copy link
Member

Choose a reason for hiding this comment

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

same here as https://github.com/astral-sh/ruff/pull/23808/changes#r2902187320 -- this seems wrong; it's not what happens at runtime. We should probably just add a TODO comment for now.

def test(f: Flags) -> None:
match f:
case Flags.A | Flags.B:
# Pattern matching on flags does not narrow to specific literals
Copy link
Member

Choose a reason for hiding this comment

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

why not?

assert_type(f, Flags)
```

### Difference from regular enums
Copy link
Member

Choose a reason for hiding this comment

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

isn't this section testing the same thing as the ### Flag narrowing with if/else section above?

@carljm carljm removed their request for review March 9, 2026 21:27
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.

3 participants