Dataclasses
The problem dataclasses solve
Section titled “The problem dataclasses solve”In TypeScript, you often write a class just to hold data and get a constructor for free. Or you use an interface + object literal when you do not need methods. Python’s @dataclass decorator bridges both: it is a class that auto-generates __init__, __repr__, and __eq__ from field annotations, so you only write the fields.
// TypeScript — class as data holderclass Point { constructor( public x: number, public y: number ) {}
toString(): string { return `Point(${this.x}, ${this.y})`; }}
const p = new Point(3, 4);console.log(p); // Point { x: 3, y: 4 }console.log(p.toString()); // Point(3, 4)from dataclasses import dataclass
@dataclassclass Point: x: float y: float
def distance_from_origin(self) -> float: return (self.x ** 2 + self.y ** 2) ** 0.5
p = Point(3.0, 4.0)print(p) # Point(x=3.0, y=4.0)print(p.distance_from_origin()) # 5.0
# __eq__ generated automaticallyprint(Point(1, 2) == Point(1, 2)) # TrueYou get __init__, __repr__, and __eq__ for free — no boilerplate. The @dataclass decorator reads the field annotations at class definition time and generates them.
Default values and field()
Section titled “Default values and field()”Default values work like TypeScript parameter defaults. For mutable defaults (lists, dicts), you must use field(default_factory=...) — never assign a mutable literal directly, because all instances would share the same object.
// TypeScript — parameter defaultsclass Config { constructor( public host: string = "localhost", public port: number = 8080, public tags: string[] = [] // ⚠ shared ref bug possible ) {}}
// Correct: factory pattern or spread copyclass SafeConfig { host: string; port: number; tags: string[]; constructor(opts: Partial<SafeConfig> = {}) { this.host = opts.host ?? "localhost"; this.port = opts.port ?? 8080; this.tags = [...(opts.tags ?? [])]; }}from dataclasses import dataclass, field
@dataclassclass Config: host: str = "localhost" port: int = 8080 # field(default_factory=list) prevents shared-ref bug tags: list[str] = field(default_factory=list)
cfg1 = Config()cfg2 = Config(port=9000, tags=["prod"])
print(cfg1) # Config(host='localhost', port=8080, tags=[])print(cfg2) # Config(host='localhost', port=9000, tags=['prod'])
cfg1.tags.append("dev")print(cfg1.tags) # ['dev'] — does NOT affect cfg2print(cfg2.tags) # ['prod']frozen=True: immutable records
Section titled “frozen=True: immutable records”@dataclass(frozen=True) makes all fields read-only after __init__. This is the Python equivalent of TypeScript’s Readonly<T> or a const object — except Python actually enforces it at runtime.
// TypeScript — read-only interfaceinterface Color { readonly r: number; readonly g: number; readonly b: number;}
const red: Readonly<{ r: number; g: number; b: number }> = { r: 255, g: 0, b: 0};// red.r = 100; // TS compile errorfrom dataclasses import dataclass
@dataclass(frozen=True)class Color: r: int g: int b: int
red = Color(255, 0, 0)print(red) # Color(r=255, g=0, b=0)
# frozen=True makes instances hashable — usable as dict keys!palette = {red: "primary red"}print(palette[Color(255, 0, 0)]) # primary red
try: red.r = 100 # type: ignoreexcept Exception as e: print(f"Error: {e}") # cannot assign to field 'r'Try it
Section titled “Try it”from dataclasses import dataclass, field
@dataclassclass Point: x: float y: float
def distance_from_origin(self) -> float: return (self.x ** 2 + self.y ** 2) ** 0.5
@dataclassclass Config: host: str = "localhost" port: int = 8080 tags: list[str] = field(default_factory=list)
@dataclass(frozen=True)class Color: r: int g: int b: int
# Point demop = Point(3.0, 4.0)print(p)print(f"Distance: {p.distance_from_origin()}")print(f"Equal: {Point(1, 2) == Point(1, 2)}")
# Config democfg = Config(tags=["web"])print(cfg)cfg.tags.append("api")print(f"Tags: {cfg.tags}")
# Frozen demored = Color(255, 0, 0)print(red)print(f"Hashable: {hash(red)}")try: red.r = 50 # type: ignoreexcept Exception as e: print(f"Caught: {e}")Loading Python runtime (first run only)…