Skip to content

Conversation

sharkdp
Copy link
Contributor

@sharkdp sharkdp commented Jul 22, 2025

Summary

This PR implements the following section from the typing spec on enums:

Enum classes can also be defined using a subclass of enum.Enum or any class that uses enum.EnumType (or a subclass thereof) as a metaclass. Note that enum.EnumType was named enum.EnumMeta prior to Python 3.11.

part of astral-sh/ty#183

Test Plan

New Markdown tests

@sharkdp sharkdp added the ty Multi-file analysis & type inference label Jul 22, 2025
Copy link
Contributor

github-actions bot commented Jul 22, 2025

mypy_primer results

No ecosystem changes detected ✅

Memory usage changes were detected when running on open source projects
sphinx (https://github.com/sphinx-doc/sphinx)
-     memo metadata = ~36MB
+     memo metadata = ~38MB

@sharkdp sharkdp marked this pull request as ready for review July 22, 2025 10:00
@AlexWaygood
Copy link
Member

This PR implements the following section from the typing spec on enums:

Enum classes can also be defined using a subclass of enum.Enum or any class that uses enum.EnumType (or a subclass thereof) as a metaclass. Note that enum.EnumType was named enum.EnumMeta prior to Python 3.11.

I'm surprised the spec includes this language. It's somewhat imprecise.

It's true that any class that uses EnumType as its metaclass will have some enum-like properties:

>>> from enum import EnumMeta
>>> class CustomEnumType(EnumMeta): ...
...
>>> class CustomEnumBase(metaclass=CustomEnumType): ...
... 
>>> class Color(CustomEnumBase):
...     RED = 1
...     GREEN = 2
...     BLUE = 3
...     
>>> list(Color)
[<Color.RED: 1>, <Color.GREEN: 2>, <Color.BLUE: 3>]
>>> Color.__members__
mappingproxy({'RED': <Color.RED: 1>, 'GREEN': <Color.GREEN: 2>, 'BLUE': <Color.BLUE: 3>})
>>> Color.RED
<Color.RED: 1>
>>> Color(1)
<Color.RED: 1>
>>> Color["RED"]
<Color.RED: 1>

but they will also be unlike enum classes in many ways because of the fact that Enum is not present in their respective MROs:

>>> Color.RED.name
Traceback (most recent call last):
  File "<python-input-14>", line 1, in <module>
    Color.RED.name
AttributeError: 'Color' object has no attribute 'name'
>>> Color.RED.value
Traceback (most recent call last):
  File "<python-input-15>", line 1, in <module>
    Color.RED.value
AttributeError: 'Color' object has no attribute 'value'

@sharkdp
Copy link
Contributor Author

sharkdp commented Jul 22, 2025

I'm surprised the spec includes this language. It's somewhat imprecise.

Ok, but do you think we are modeling things wrong here? We do not pretend that .name or .value exist on members of these classes (accessing them does lead to a unresolved-attribute diagnostic; I now added a test to show that).

@AlexWaygood
Copy link
Member

AlexWaygood commented Jul 22, 2025

Ok, but do you think we are modeling things wrong here? We do not pretend that .name or .value exist on members of these classes (accessing them would lead to a unresolved-attribute diagnostic).

I'm not sure... I can't immediately find any other behaviour differences between these "enum-like" classes and "proper Enum subclasses", but there's a lot of complexity in the definition of enum.Enum at runtime, so I'd honestly be quite surprised if there weren't other subtle behaviour differences between these enum-like classes and Enum subclasses. What they are exactly, I'm not sure -- the enum.py source code is very complex -- but all that code in the Enum class definition has to exist for a reason...

@erictraut
Copy link

I agree with Alex that a class that uses the EnumType metaclass but does not derive from Enum is likely to be an odd duck — having some properties of an Enum but differing from a real Enum in subtle ways. More like a platypus.

Pyright's logic looks for a metaclass that derives from EnumType, but I don't think I've ever seen a custom enum-like class that uses the EnumType metaclass but doesn't derive from Enum, so it's probably fine to just look for Enum in the MRO.

I wrote the enum chapter in the typing spec, so the quote above comes from me. We could update the spec to eliminate the mention of EnumMeta and EnumType.

I did some spelunking to see who requested that pyright support EnumType, and it turns out that it was ... me. Here's the original issue. IIRC, I created this issue after running across some other forum post or issue tracker question, but I unfortunately can't find the source now.

@sharkdp
Copy link
Contributor Author

sharkdp commented Jul 22, 2025

Pyright's logic looks for a metaclass that derives from EnumType, but I don't think I've ever seen a custom enum-like class that uses the EnumType metaclass but doesn't derive from Enum, so it's probably fine to just look for Enum in the MRO.

FWIW, I found this class in the wild, which does not appear to have enum.Enum in its MRO. And here's a usage site of said graphene.Enum class.

Edit: Oh, that class doesn't actually have a metaclass that derives from typing.EnumMeta. EnumMeta references a custom class in that example.

@AlexWaygood
Copy link
Member

AlexWaygood commented Jul 22, 2025

yeah, that project looks like it's doing a lot of metaprogramming, and I think it's reasonable to declare it out of scope for ty (look at these customized class __repr__s in the MRO here!).

This session was run after cloning https://github.com/phasehq/console locally, cd-ing into the backend directory, creating a virtual environment, then running uv pip install -r requirements.txt and uv pip install -r dev-requirements.txt:

>>> from ee.billing.graphene.types import PlanTypeEnum
>>> PlanTypeEnum.__mro__
(<PlanTypeEnum meta=<EnumOptions name='PlanTypeEnum'>>, <Enum meta=None>, <class 'graphene.types.unmountedtype.UnmountedType'>, <class 'graphene.utils.orderedtype.OrderedType'>, <BaseType meta=None>, <SubclassWithMeta meta=None>, <class 'object'>)
>>> type(PlanTypeEnum)
<class 'graphene.types.enum.EnumMeta'>
>>> _.__mro__
(<class 'graphene.types.enum.EnumMeta'>, <class 'graphene.utils.subclass_with_meta.SubclassWithMeta_Meta'>, <class 'type'>, <class 'object'>)

My inclination would be to adjust the wording of the spec here, as @erictraut suggests, so that we do not have to provide support for enum-like classes that do not inherit from Enum. I think it's going to significantly complicate our ability to reason about what Python enums do if at every point where we add special behaviour for enum classes, we have to check whether the same behaviour applies for classes that have EnumType as their metaclass but do not inherit from Enum. As far as I know, this is not something the runtime enum module has ever deliberately supported (or documented support for).

@AlexWaygood
Copy link
Member

AlexWaygood commented Jul 22, 2025

(If we receive an actual request from a user asking for us to support this pattern, then that's a different question, of course. But until we get to that point, I'm inclined to stick to uses of the enum module that are documented and supported by the standard library.)

@sharkdp
Copy link
Contributor Author

sharkdp commented Jul 22, 2025

As a final note here before I close this:

  1. It's currently part of the spec; but I understand that we're open to changing the spec
  2. All other type checkers that I tested support this pattern (pyright, as you mentioned, but also mypy and pyrefly)
  3. This seems to model well what happens at runtime (accessing members on these classes creates singleton objects)
  4. We haven't seen any actual cases of where this leads to problems

@carljm
Copy link
Contributor

carljm commented Jul 22, 2025

This PR already exists and IMO does no harm. I think we should merge it and offer best-effort support that roughly matches what other type checkers offer and what the spec says, rather than letting the perfect be the enemy of the good and insisting that if we can't precisely model every detail of these "platypus" types correctly, we shouldn't support them at all.

Copy link
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

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

This looks good to me; the implementation is extremely simple and easy enough to modify later. When in doubt, I think we should prefer compatibility with the behavior of other type checkers.

@sharkdp
Copy link
Contributor Author

sharkdp commented Jul 23, 2025

Merging this for now. If someone has additional comments, let me know and I can adapt/revert.

@sharkdp sharkdp merged commit 385d6fa into main Jul 23, 2025
37 checks passed
@sharkdp sharkdp deleted the david/enumtype branch July 23, 2025 06:46
UnboundVariable pushed a commit to UnboundVariable/ruff that referenced this pull request Jul 23, 2025
* main: (28 commits)
  [ty] highlight the argument in `static_assert` error messages (astral-sh#19426)
  [ty] Infer single-valuedness for enums based on `int`/`str` (astral-sh#19510)
  [ty] Restructure submodule query around `File` dependency
  [ty] Make `Module` a Salsa ingredient
  [ty] Reachability analysis for `isinstance(…)` branches (astral-sh#19503)
  [ty] Normalize single-member enums to their instance type (astral-sh#19502)
  [ty] Invert `ty_ide` and `ty_project` dependency (astral-sh#19501)
  [ty] Implement mock language server for testing (astral-sh#19391)
  [ty] Detect enums if metaclass is a subtype of EnumType/EnumMeta (astral-sh#19481)
  [ty] perform type narrowing for places marked `global` too (astral-sh#19381)
  [ty] Use `ThinVec` for sub segments in `PlaceExpr` (astral-sh#19470)
  [ty] Splat variadic arguments into parameter list (astral-sh#18996)
  [`flake8-pyi`] Skip fix if all `Union` members are `None` (`PYI016`)  (astral-sh#19416)
  Skip notebook with errors in ecosystem check (astral-sh#19491)
  [ty] Consistent use of American english (in rules) (astral-sh#19488)
  [ty] Support iterating over enums (astral-sh#19486)
  Fix panic for illegal `Literal[…]` annotations with inner subscript expressions (astral-sh#19489)
  Move fix suggestion to subdiagnostic (astral-sh#19464)
  [ty] Implement non-stdlib stub mapping for classes and functions (astral-sh#19471)
  [ty] Disallow illegal uses of `ClassVar` (astral-sh#19483)
  ...

# Conflicts:
#	crates/ty_ide/src/goto.rs
dcreager added a commit that referenced this pull request Jul 23, 2025
* main:
  [ty] Fix narrowing and reachability of class patterns with arguments (#19512)
  [ty] Implemented partial support for "find references" language server feature. (#19475)
  [`flake8-use-pathlib`] Add autofix for `PTH101`, `PTH104`, `PTH105`, `PTH121` (#19404)
  [`perflint`] Parenthesize generator expressions (`PERF401`) (#19325)
  [`pep8-naming`] Fix `N802` false positives for `CGIHTTPRequestHandler` and `SimpleHTTPRequestHandler` (#19432)
  [`pylint`] Handle empty comments after line continuation (`PLR2044`) (#19405)
  Move concise diagnostic rendering to `ruff_db` (#19398)
  [ty] highlight the argument in `static_assert` error messages (#19426)
  [ty] Infer single-valuedness for enums based on `int`/`str` (#19510)
  [ty] Restructure submodule query around `File` dependency
  [ty] Make `Module` a Salsa ingredient
  [ty] Reachability analysis for `isinstance(…)` branches (#19503)
  [ty] Normalize single-member enums to their instance type (#19502)
  [ty] Invert `ty_ide` and `ty_project` dependency (#19501)
  [ty] Implement mock language server for testing (#19391)
  [ty] Detect enums if metaclass is a subtype of EnumType/EnumMeta (#19481)
  [ty] perform type narrowing for places marked `global` too (#19381)
AlexWaygood pushed a commit that referenced this pull request Jul 25, 2025
)

## Summary

This PR implements the following section from the [typing spec on
enums](https://typing.python.org/en/latest/spec/enums.html#enum-definition):

> Enum classes can also be defined using a subclass of `enum.Enum` **or
any class that uses `enum.EnumType` (or a subclass thereof) as a
metaclass**. Note that `enum.EnumType` was named `enum.EnumMeta` prior
to Python 3.11.

part of astral-sh/ty#183

## Test Plan

New Markdown tests
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.

4 participants