dataclasses, enum & contextlib
dataclasses reduce boilerplate for data objects. enum gives named constants with type safety. contextlib simplifies context managers (contextmanager, suppress, ExitStack).
Recipe
from dataclasses import dataclass
from enum import StrEnum
from contextlib import contextmanager
class Status(StrEnum):
PENDING = "pending"
DONE = "done"
@dataclass(frozen=True)
class Job:
id: int
status: Status = Status.PENDINGWhen to reach for this:
- DTOs and config records -> dataclass
- Mode/state constants -> StrEnum
- Quick resource scopes -> @contextmanager
- Optional file absent -> suppress(FileNotFoundError)
- Many nested managers -> ExitStack
Working Example
from contextlib import contextmanager, suppress
from dataclasses import dataclass, field
from enum import StrEnum
class Role(StrEnum):
ADMIN = "admin"
USER = "user"
@dataclass
class Account:
email: str
role: Role = Role.USER
tags: list[str] = field(default_factory=list)
@contextmanager
def temp_role(account: Account, role: Role):
old = account.role
account.role = role
try:
yield account
finally:
account.role = old
ada = Account("ada@example.com")
with temp_role(ada, Role.ADMIN):
print("elevated", ada.role)
print("restored", ada.role)
with suppress(FileNotFoundError):
open("missing.cfg").read()What this demonstrates:
frozen=Truefor immutable value objectsfield(default_factory=list)avoids mutable default pitfall- StrEnum serializes cleanly to JSON strings
- contextmanager for reversible temporary state
Deep Dive
dataclass options
slots=True(3.10+) memory savingskw_only=True(3.10+) explicit construction__post_init__validation
enum flavors
StrEnumfor JSON APIsIntEnumfor numeric codes@uniquedecorator prevents alias duplicates
Gotchas
- Mutable dataclass as dict key - unhashable unless frozen. Fix: frozen=True or don't key by instance.
- Enum comparison to raw string - StrEnum compares equal to str in 3.11+ but be explicit in APIs. Fix: type hints Role not str.
- contextmanager without try/finally - teardown skipped on error. Fix: wrap yield in try/finally.
- ExitStack forgotten - manual close errors. Fix: always
with ExitStack(). - dataclass vs Pydantic - dataclass no validation. Fix: Pydantic at HTTP boundary.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Pydantic BaseModel | Validation/coercion | Hot inner loop DTO |
| TypedDict | JSON-shaped dicts | Need methods |
| attrs | attrs ecosystem | Stdlib-only |
FAQs
dataclass vs namedtuple?
dataclass flexible defaults and mutability control.
auto() enums?
Enum.auto() assigns values automatically for non-StrEnum.
contextlib.closing?
Wraps close()-able objects lacking context manager.
asynccontextmanager?
Async counterpart in contextlib for async with blocks.
field(repr=False)?
Hide sensitive fields from repr output.
slots memory?
Measurable on huge instance counts - profile before defaulting slots everywhere.
JSON serialize enum?
StrEnum .value is str - json.dumps handles in dict values.
match on enum?
Structural pattern matching works with enum members in 3.10+.
testing temp context?
Assert state before/after with block raises.
Django choices?
TextChoices/IntegerChoices mirror enum pattern in Django 5.2.
Related
- Context Managers as a Pattern - pattern depth
- json & Serialization - StrEnum in JSON
- collections recap - Counter with enums
- Standard Library Best Practices - ergonomic rules
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.