Frontend Labs
⋯

15. Classes, properties, and dataclasses

15.1 A class from first principles

class Account:
    """A bank account. Balances are integer cents (never floats for money)."""

    bank = "Frontend Bank"                 # class attribute: shared by all instances

    def __init__(self, owner: str, cents: int = 0) -> None:
        self.owner = owner                 # instance attributes: per object
        self._cents = cents                # leading underscore: internal

    def deposit(self, cents: int) -> None:
        if cents <= 0:
            raise ValueError("deposit must be positive")
        self._cents += cents

    @property
    def balance(self) -> int:              # read like an attribute, computed by a method
        return self._cents

    def __repr__(self) -> str:             # what the REPL and debuggers show
        return f"Account({self.owner!r}, cents={self._cents})"

acct = Account("ada")
acct.deposit(1500)
print(acct, acct.balance, acct.bank)
acct.balance = 10
# AttributeError: property 'balance' of 'Account' object has no setter
  • self is the instance, passed explicitly. acct.deposit(1500) is Account.deposit(acct, 1500).
  • Instance attributes live in a per-object dict (acct.__dict__) unless you use __slots__ (Ch 26).
  • @property lets you start with a plain attribute and later add validation without changing callers. Do not write Java-style get_balance() methods.

@classmethod receives the class (use it for alternative constructors); @staticmethod receives nothing (a plain function namespaced in the class):

from datetime import date

class Release:
    def __init__(self, version: str, day: date) -> None:
        self.version, self.day = version, day

    @classmethod
    def parse(cls, text: str) -> "Release":        # Release.parse("3.14.0 2025-10-07")
        v, d = text.split()
        return cls(v, date.fromisoformat(d))

    @staticmethod
    def is_valid(version: str) -> bool:
        return all(p.isdigit() for p in version.split("."))

r = Release.parse("3.14.0 2025-10-07")
print(r.version, r.day, Release.is_valid("3.14.x"))

15.2 Dataclasses: classes for data, without the boilerplate

Most classes you write hold data. @dataclass generates __init__, __repr__, and __eq__ from annotated fields:

from dataclasses import dataclass, field, asdict, replace

@dataclass(frozen=True, slots=True)
class TrainConfig:
    model: str = "tiny-lm"
    lr: float = 3e-4
    batch_size: int = 32
    layers: tuple[int, ...] = (256, 256)
    tags: frozenset[str] = field(default_factory=frozenset)

base = TrainConfig()
fast = replace(base, lr=1e-3, tags=frozenset({"sweep"}))   # a modified copy
print(fast)
print(asdict(fast)["lr"], base == TrainConfig())
base.lr = 0.1
# dataclasses.FrozenInstanceError: cannot assign to field 'lr'
  • frozen=True makes instances immutable (and hashable): the fix for the war story in Ch 2.7.
  • slots=True stores fields in fixed slots instead of a dict: less memory, faster attribute access, and typos like cfg.lerning_rate = 1 raise instead of silently creating a new attribute.
  • Mutable defaults must use field(default_factory=list); dataclass refuses tags: list = [], which protects you from Ch 2.6.
  • __post_init__ runs after the generated __init__, for validation.

When to use what:

You need Use
a quick immutable record, tuple-compatible typing.NamedTuple
a record with defaults, methods, maybe frozen @dataclass
validation and parsing of untrusted input (JSON, env, API bodies) pydantic BaseModel (Ch 44, 61)
a JSON-shaped dict with known keys, for type checking only typing.TypedDict

15.3 Enums

from enum import Enum, StrEnum, auto

class Level(StrEnum):
    DEBUG = auto()      # "debug"
    INFO = auto()
    ERROR = auto()

class State(Enum):
    PENDING = 1
    RUNNING = 2
    DONE = 3

print(Level("error") is Level.ERROR, Level.INFO == "info", State.RUNNING.name)

Use enums instead of magic strings for states and options. A typo in Level.EROR is an AttributeError at the line, and type checkers catch it before you run.


Generated from real-life-python-guide.md · 8 Oct 2026 16:36