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 @dataclass generates.