Dataclasses and Enums
|
This section documents the current Python release line as published at the official Python documentation, which is the reference these pages are written and verified against. No specific patch version is pinned. This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
@dataclass generates the boilerplate methods a plain attribute-holding class needs, and enum.Enum
gives a fixed, named set of values — both are covered end to end by the standard library’s
dataclasses and
enum modules, which are the only sources behind this page:
@dataclass is recent enough that there is no book cross-reference for this page; the dataclasses and enum
module docs above are the only sources.
@dataclass Basics
Decorating a class with
@dataclass inspects its class-level
variable annotations and generates _init_, _repr_, and _eq_ from them — no more copying each
attribute name into three different methods by hand:
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
p1 = Point(3, 4)
p2 = Point(3, 4)
print(p1) # Point(x=3, y=4) -- generated __repr__
print(p1 == p2) # True -- generated __eq__ compares field by field
print(p1 is p2) # False -- still two distinct objects
Field Defaults and default_factory
An annotation can carry a plain default just like a function parameter, but a mutable default (a list or
dict) must go through
field(default_factory=…) instead — using a bare mutable default is rejected at class-creation time, precisely to avoid the shared-mutable-default
trap:
from dataclasses import dataclass, field
@dataclass
class Cart:
owner: str
items: list = field(default_factory=list) # a *new* list per instance
discount_pct: float = 0.0 # plain default: fine for immutable values
# @dataclass
# class Broken:
# items: list = [] # ValueError: mutable default <class 'list'> for field items is not allowed
cart_a = Cart("Alice")
cart_b = Cart("Bob")
cart_a.items.append("book")
print(cart_a.items, cart_b.items) # ['book'] [] -- lists are independent
frozen=True for Immutability
Passing frozen=True to the decorator makes every field read-only after _init_: assigning to an
attribute raises FrozenInstanceError, and a generated _hash_ is added so frozen instances can be used as
dict keys or set members. This parameter is documented alongside the rest of
`@dataclass’s options:
from dataclasses import dataclass, FrozenInstanceError
@dataclass(frozen=True)
class Coordinate:
lat: float
lon: float
home = Coordinate(40.4168, -3.7038)
print(home) # Coordinate(lat=40.4168, lon=-3.7038)
try:
home.lat = 0.0
except FrozenInstanceError as exc:
print(f"cannot mutate: {exc}") # cannot mutate: cannot assign to field 'lat'
waypoints = {home} # OK: frozen dataclasses are hashable
@dataclass vs. a Hand-Written Class vs. dict/tuple
A hand-written class gives the exact same behavior as a @dataclass, just typed out manually — @dataclass
saves that typing without changing what the class can do, and you can still add your own methods, validation
in _post_init_, or override any generated method:
# Equivalent by hand -- @dataclass generates exactly this for the Point example above
class PointByHand:
def __init__(self, x: int, y: int) -> None:
self.x = x
self.y = y
def __repr__(self) -> str:
return f"PointByHand(x={self.x!r}, y={self.y!r})"
def __eq__(self, other: object) -> bool:
if not isinstance(other, PointByHand):
return NotImplemented
return (self.x, self.y) == (other.x, other.y)
A plain dict or tuple needs no class at all, but trades away named, checked attributes for positional or
string-keyed access — fine for a short-lived, throwaway grouping; a liability once the shape matters or the
data is passed between functions:
# dict: flexible, but no fixed shape -- a typo in a key is only caught at runtime
p_dict = {"x": 3, "y": 4}
print(p_dict["x"]) # 3
# print(p_dict["z"]) # KeyError at runtime, not a static check
# tuple: fixed shape, but fields are unnamed -- readers must remember the order
p_tuple = (3, 4)
print(p_tuple[0]) # 3, but what is "0"? readers must know it means x
# @dataclass: fixed shape *and* named, type-hinted fields, with __init__/__repr__/__eq__ for free
p_data = Point(3, 4)
print(p_data.x) # 3 -- self-documenting
Reach for @dataclass whenever a group of related values has a fixed shape and outlives one function call;
keep dict/tuple for quick, local, shape-free groupings; write a full hand-written class only when you need
custom invariants or behavior beyond what @dataclass’s hooks (_post_init_`, custom methods) comfortably
express — Classes and Objects covers writing that class by hand.
enum.Enum
An Enum defines a fixed set of named constants — members — instead of scattering plain strings or integers through the code where any typo silently passes:
from enum import Enum
class Direction(Enum):
NORTH = "N"
SOUTH = "S"
EAST = "E"
WEST = "W"
heading = Direction.NORTH
print(heading) # Direction.NORTH
print(heading.name) # NORTH
print(heading.value) # N
print(heading is Direction.NORTH) # True -- members are singletons
Auto Values with auto()
When the actual value doesn’t matter — only that each member is distinct — auto() assigns increasing integers (starting at 1)
without spelling them out:
from enum import Enum, auto
class Status(Enum):
PENDING = auto()
ACTIVE = auto()
DONE = auto()
for member in Status:
print(member.name, member.value)
# PENDING 1
# ACTIVE 2
# DONE 3
IntEnum for Integer Interop
IntEnum members are integers — they compare
equal to and can be used anywhere a plain int is expected (a regular Enum member never equals a plain
value):
from enum import IntEnum, Enum
class HttpStatus(IntEnum):
OK = 200
NOT_FOUND = 404
print(HttpStatus.OK == 200) # True -- IntEnum compares equal to int
print(HttpStatus.OK + 0) # 200 -- usable as an int
class PlainStatus(Enum):
OK = 200
print(PlainStatus.OK == 200) # False -- a plain Enum member never equals a raw value
Flag and IntFlag for Bitwise Combinations
Flag (and its integer-compatible sibling
IntFlag) members can be combined with | and
tested with &, which suits sets of independent on/off options:
from enum import Flag, auto
class Permission(Flag):
READ = auto()
WRITE = auto()
EXECUTE = auto()
editor = Permission.READ | Permission.WRITE
print(editor) # Permission.READ|WRITE
print(Permission.WRITE in editor) # True
print(Permission.EXECUTE in editor) # False
print(editor & Permission.READ) # Permission.READ
Iterating Members: .name and .value
Every Enum subclass is iterable in definition order, yielding each member so its .name and .value can be
read without hardcoding the member list elsewhere:
class Direction(Enum):
NORTH = "N"
SOUTH = "S"
for member in Direction:
print(f"{member.name} -> {member.value}")
# NORTH -> N
# SOUTH -> S
names = [member.name for member in Direction]
print(names) # ['NORTH', 'SOUTH']
See Also
-
Type Hints — annotating dataclass fields and enum members with the type-hint syntax.
-
Classes and Objects — writing the equivalent class by hand, and the
_init_/_repr_/_eq_methods@dataclassgenerates.