based enums¶
basedpython supports algebraic sum types — "based enums" — with pythonic surface syntax. variants can carry typed payloads, support pattern matching, and integrate with ty's exhaustiveness checking. methods, classmethods, properties, and generics live directly on the enum body. type-directed emit lowers the surface form to a sealed dataclass hierarchy at runtime:
enum class Shape:
case Circle(radius: float)
case Rectangle(width: float, height: float)
def area(self) -> float:
match self:
case Shape.Circle(r): 3.14 * r * r
case Shape.Rectangle(w, h): w * h
goals¶
- sum types (tagged unions)
- pythonic body:
deffor methods,[T]for generics, no foreign keywords - type-directed emit: clean surface, valid python 3.10+ runtime
- no syntax cages: variants can be payload-less or carry typed fields with defaults; methods, properties, classmethods, and staticmethods all live on the enum; recursive and generic forms supported
surface syntax¶
variants are declared with the case keyword (as in swift/scala); one case
line may declare several comma-separated variants. anything that isn't a
case line is an ordinary class-body statement
enum class Shape:
case Circle(radius: float)
case Rectangle(width: float, height: float)
case Point
case Polygon(sides: list[Point], closed: bool = True)
def area(self) -> float:
match self:
case Shape.Circle(r): 3.14 * r * r
case Shape.Rectangle(w, h): w * h
case Shape.Point: 0.0
case Shape.Polygon(sides, closed=True): shoelace(sides)
case Shape.Polygon(): 0.0
@classmethod
def unit_circle(cls) -> Shape:
return Shape.Circle(1.0)
three variant forms, mix freely within one enum (and within one case line):
- unit —
case Point— a singleton value (reached asShape.Point, matchedcase Shape.Point:), no payload - positional —
case Circle(float)— positional construct, anonymous fields - named —
case Circle(radius: float)— named fields available for pattern matching, attribute access, and keyword construction; fields may carry defaults (case Polygon(sides: int, closed: bool = True)), defaulted fields last
the compact comma form reads best for unit variants:
generic and recursive forms:
enum class Tree[T]:
case Leaf
case Node(T, Tree[T], Tree[T])
def depth(self) -> int:
match self:
case Tree.Leaf: 0
case Tree.Node(_, l, r): 1 + max(l.depth(), r.depth())
enum class Result[T, E]:
case Ok(T)
case Err(E)
bounds use the same syntax as based generics elsewhere:
enum class E[T: Hashable], enum class E[T in (int, str)]
variant access¶
variants lower to subclasses of the enum attached as class attributes, so
they are reached qualified through the enum name — Shape.Circle(2.0),
Shape.Point — everywhere: inside the enum body, in pattern contexts
(case Shape.Circle(r):), and at module scope. variant constructors are real
classes at runtime, so x is Shape.Circle works (recall is is basedpython's
isinstance; use type(x) === Shape.Circle for an
exact-class check). because variants are qualified, the same variant name may
appear in two different enums (A.Same vs B.Same) without collision
where the expected type is the enum, a variant may also be written
unqualified — s: Shape = Circle(2.0), p: Shape = Point. see
context-sensitive resolution
methods and other members¶
the enum body is a regular suite. anything that can appear in a python class body can appear in an enum body:
defmethods (dispatched on the union; usually implemented viamatch self)@classmethod,@staticmethod,@property- nested types
- class-level constants
methods live in the enum class body. variant-specific methods can be
declared by narrowing the receiver type: def f(self: Shape.Circle) -> float
pattern matching¶
based enums plug directly into python's match statement. variant patterns
use the variant constructor form:
match shape:
case Shape.Circle(r): ...
case Shape.Rectangle(w, h): ...
case Shape.Point: ...
case Shape.Polygon(sides=s, closed=True): ...
case Shape.Polygon(sides=s, closed=c): ...
ty checks exhaustiveness: a match over a based enum that fails to
cover every variant produces a diagnostic. wildcard case _: opts out of
exhaustiveness for that match. exhaustiveness is enforced in both statement
and expression positions
expression-form match¶
match can appear in expression position (e.g. the body of a one-line
method, the rhs of =):
def area(self) -> float:
match self:
case Shape.Circle(r): 3.14 * r * r
case Shape.Rectangle(w, h): w * h
case Shape.Point: 0.0
every arm's body must produce a value of the common type. ty infers the union and applies exhaustiveness as usual
variant as type¶
a single variant name is usable as a type. ty narrows the receiver:
assignability follows the obvious rule: Shape.Circle is a subtype of
Shape, but Shape is not a subtype of Shape.Circle
the variant set is closed, so a variant is final — subclassing one is an
error. that is what makes two sibling variants disjoint, so narrowing to one
variant discards the rest
derived behaviour¶
payload variants lower to frozen dataclasses, so they come with __eq__,
__hash__, __repr__, and __match_args__ derived from their fields.
equality is structural (two Circle(2.0) values compare equal), values are
hashable (usable as dict keys / in sets) when every payload field is hashable,
and repr reads as the construction form (Shape.Circle(radius=2.0))
see also: destructuring with if let and destructuring
prelude enums¶
based prelude ships Option[T] and Result[T, E] as based enums. user
code gets them without an import:
def find(xs: list[int], target: int) -> Option[int]:
for i, x in enumerate(xs):
if x == target:
return Some(i)
return None
None here is the based enum unit variant, not python's None. at runtime
the prelude maps based None to a sentinel that is ==-equivalent to
python None for ergonomic interop (open question, see below)
transpiler output¶
an enum whose variants are all unit (no payloads) lowers to an idiomatic
enum.Enum with auto() members — enum class Color: case Red, Green becomes
class Color(Enum): Red = auto(); Green = auto(). this is the form the reverse
transform recognises. because the lowering is a real Enum, the type checker
models the class with the Enum base too: members expose name / value and
the class iterates like any python enum
a top-level assignment in the body disqualifies that form, even when every
variant is unit: python's Enum would turn MAX = 10 into a member, so
E.MAX + 5 would fail at runtime while the checker types MAX as a constant.
such an enum takes the sealed hierarchy below instead, which keeps a constant a
constant — and is not iterable, because it is not an Enum
any other enum (one or more payload-carrying variants) lowers to a sealed
hierarchy: the enum class holds the shared members, and each variant becomes a
module-level subclass of the enum attached back as Shape.Circle — payload
variants as frozen dataclasses, unit variants as singleton values. subclassing
is what makes methods declared on the enum body dispatch on the variants.
Shape.Circle(2.0) constructs; the enum name itself is the type (the type
checker treats it as the union of its variants). the output is prefixed with
from __future__ import annotations so mutually-recursive references
(recursive enums → themselves) resolve lazily:
from __future__ import annotations
from dataclasses import dataclass
from typing import final
class Shape:
def area(self) -> float: ... # methods live in the enum body
@final
@dataclass(frozen=True, slots=True)
class _Shape_Circle(Shape):
radius: float
_Shape_Circle.__name__ = "Circle"
_Shape_Circle.__qualname__ = Shape.__qualname__ + ".Circle"
Shape.Circle = _Shape_Circle
class _Shape_Point(Shape):
__slots__ = ()
def __repr__(self): return "Point"
def __reduce__(self): return type(self).__qualname__
_Shape_Point.__name__ = "Point"
_Shape_Point.__qualname__ = Shape.__qualname__ + ".Point"
Shape.Point = _Shape_Point() # the variant is the singleton value, not the class
Enum.Variant is the only name a variant is reachable by, so __qualname__ is
built from the enum's own — a private enum class is renamed to _Enum and a
spelled-out path would resolve to nothing. that path is what copy and pickle
look a variant up through, and a unit variant's __reduce__ returns it so both
hand back the singleton rather than a second instance
__match_args__ comes from dataclass for payload variants. generic enums lower
to a class Shape[T]: (PEP 695) with the variant subclasses parametrised the
same way