Phase 3 · Object-Oriented PythonModule 17~32 min read

Dataclasses, Enums & Structured Data

Eliminate boilerplate with dataclasses, and model fixed choices with enums.

What you'll learn

Most classes exist just to hold data — a point, a product, a config. Writing __init__, __repr__, and __eq__ by hand for each is tedious and error-prone. Dataclasses generate all of it from a few type-annotated fields, and enums give you safe, named sets of choices.

By the end you'll be able to:

  • Replace boilerplate classes with @dataclass
  • Use defaults, field(), frozen, and order
  • Model fixed choices with Enum
  • Choose between dataclass, namedtuple, and dict

The boilerplate problem

Here's a simple data holder written the manual way — notice how much ceremony it takes:

manual.py
class Point:                             # the tedious, hand-written way
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def __repr__(self):
        return f"Point(x={self.x}, y={self.y})"
    def __eq__(self, other):
        return (self.x, self.y) == (other.x, other.y)

That's a lot of repetitive code for "a thing with an x and a y." Dataclasses eliminate every line of it.

@dataclass

Decorate a class with @dataclass and list its fields as type-annotated class variables. Python generates __init__, __repr__, and __eq__ automatically:

point.py
from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int

p = Point(3, 4)
print(p)                 # __repr__ generated for you
print(p == Point(3, 4))  # __eq__ generated too

Frozen, defaults & ordering

Dataclasses are configurable. frozen=True makes instances immutable (great for hashable, safe-to-share values); order=True generates comparison methods so they sort. Give fields defaults directly — but for a mutable default like a list, use field(default_factory=list) to avoid the classic shared-default bug:

product.py
from dataclasses import dataclass, field

@dataclass(frozen=True, order=True)      # immutable AND comparable
class Product:
    name: str
    price: float = 0.0                   # a default value
    tags: list = field(default_factory=list)   # safe mutable default

a = Product("Pen", 2.5)
print(a.price, a.tags)
# a.price = 9   # FrozenInstanceError — can't mutate a frozen dataclass
print(Product("A", 1) < Product("B", 2))   # order=True enables <

Watch out

Never write tags: list = [] as a default — that single list would be shared by every instance. Always use field(default_factory=list) for mutable defaults. This is one of Python's most infamous gotchas.

Enums

When a value must be one of a fixed set — a status, a direction, a suit — an enum is far safer than loose strings. It prevents typos (Status.ACTIVE is checked; "actve" is not), documents the valid options, and gives you autocomplete:

status.py
from enum import Enum

class Status(Enum):
    PENDING = "pending"
    ACTIVE = "active"
    CLOSED = "closed"

s = Status.ACTIVE
print(s)              # Status.ACTIVE
print(s.value)       # active
print(s.name)        # ACTIVE
print(Status("pending"))          # look up by value
print(list(Status))               # iterate all members

Tip

Reach for an enum whenever you catch yourself comparing against "magic strings" like if status == "active". IntEnum works the same but its members also behave as integers, handy for flags and codes.

Choosing a structure

Python offers several ways to bundle data. A quick guide:

UseWhen
@dataclassA record with named fields, methods, and possible mutation — your default choice
namedtupleA lightweight, immutable record; tuple-compatible and very memory-light
TypedDictYou need a real dict (e.g. JSON) but want typed keys
dictTruly dynamic keys not known ahead of time

Recap & quick check

Key takeaways

  • @dataclass auto-generates __init__, __repr__, and __eq__ from type-annotated fields.
  • frozen=True makes instances immutable; order=True makes them comparable/sortable.
  • Use field(default_factory=list) for mutable defaults — never a bare [] default.
  • Enums model a fixed set of named choices safely, replacing error-prone magic strings.
  • Prefer a dataclass for records; namedtuple for light immutable records; TypedDict/dict for dict-shaped data.

Quick check

1. What does @dataclass generate for you?

2. How do you give a dataclass field a mutable default like a list?

3. What does frozen=True do?

4. Why use an Enum instead of strings?

5. Which is best for a simple record with named fields and behavior?

That wraps up Phase 3 — Object-Oriented Python! 🎉 Next we enter Phase 4 — Intermediate Python, starting with Module 18 — Modules & Packages.