Look at yesterday's Item:
class Item:
def __init__(self, name, value):
self.name = name
self.value = valueFour lines just to say "Item has name and value." The standard library has a shorter version:
from dataclasses import dataclass
@dataclass
class Item:
name: str
value: int
a = Item("a", 5)
print(a.name, a.value)No __init__ at all? Where did it go?
@dataclass writes it for you. The decorator (we cover decorators in week 2) reads the type-hinted fields and synthesises __init__(self, name, value) — same body as you'd have written. It also writes a __repr__ (so print(a) shows Item(name='a', value=5) instead of <Item object at 0x...>) and an __eq__ (so two items with the same fields compare equal).
What about methods I want to add?
Same as before — def is_high(self): inside the class body. @dataclass only generates the boilerplate; you still write the behaviour.
When would I write a hand-written class instead of @dataclass?
When the constructor needs significant logic — validating arguments, computing derived fields, raising on bad input. @dataclass gives you the no-logic version. For the 80% of cases that is the no-logic version, @dataclass is the right default.
@dataclass — boilerplate-free classesfrom dataclasses import dataclass
@dataclass
class Item:
name: str
value: intThat's it. The decorator reads the type-annotated fields and generates:
__init__(self, name: str, value: int) — assigns the args to attributes__repr__ — Item(name='a', value=5) instead of <Item object at 0x...>__eq__ — Item("a", 5) == Item("a", 5) is True# Hand-written
class Item:
def __init__(self, name, value):
self.name = name
self.value = value
def __repr__(self):
return f"Item(name={self.name!r}, value={self.value!r})"
def __eq__(self, other):
if not isinstance(other, Item):
return NotImplemented
return self.name == other.name and self.value == other.value
# @dataclass
@dataclass
class Item:
name: str
value: intSame behaviour. Three lines vs. eleven.
@dataclass
class Item:
name: str
value: int = 0 # defaultItem("a") works — value defaults to 0. Same rules as function default arguments: defaults must come after non-defaults.
@dataclass
class Item:
name: str
value: int
def is_high(self) -> bool:
return self.value > 10
a = Item("a", 5)
print(a.is_high()) # False__repr__ for free is a real wina = Item("a", 5)
print(a) # Item(name='a', value=5)
print([a, Item("b", 12)]) # [Item(name='a', value=5), Item(name='b', value=12)]Debugging is much nicer when print actually shows the data instead of <...>.
@dataclass__init__ by hand@dataclass(frozen=True)For 80% of the "named bag of fields" cases, @dataclass is the right default.
Look at yesterday's Item:
class Item:
def __init__(self, name, value):
self.name = name
self.value = valueFour lines just to say "Item has name and value." The standard library has a shorter version:
from dataclasses import dataclass
@dataclass
class Item:
name: str
value: int
a = Item("a", 5)
print(a.name, a.value)No __init__ at all? Where did it go?
@dataclass writes it for you. The decorator (we cover decorators in week 2) reads the type-hinted fields and synthesises __init__(self, name, value) — same body as you'd have written. It also writes a __repr__ (so print(a) shows Item(name='a', value=5) instead of <Item object at 0x...>) and an __eq__ (so two items with the same fields compare equal).
What about methods I want to add?
Same as before — def is_high(self): inside the class body. @dataclass only generates the boilerplate; you still write the behaviour.
When would I write a hand-written class instead of @dataclass?
When the constructor needs significant logic — validating arguments, computing derived fields, raising on bad input. @dataclass gives you the no-logic version. For the 80% of cases that is the no-logic version, @dataclass is the right default.
@dataclass — boilerplate-free classesfrom dataclasses import dataclass
@dataclass
class Item:
name: str
value: intThat's it. The decorator reads the type-annotated fields and generates:
__init__(self, name: str, value: int) — assigns the args to attributes__repr__ — Item(name='a', value=5) instead of <Item object at 0x...>__eq__ — Item("a", 5) == Item("a", 5) is True# Hand-written
class Item:
def __init__(self, name, value):
self.name = name
self.value = value
def __repr__(self):
return f"Item(name={self.name!r}, value={self.value!r})"
def __eq__(self, other):
if not isinstance(other, Item):
return NotImplemented
return self.name == other.name and self.value == other.value
# @dataclass
@dataclass
class Item:
name: str
value: intSame behaviour. Three lines vs. eleven.
@dataclass
class Item:
name: str
value: int = 0 # defaultItem("a") works — value defaults to 0. Same rules as function default arguments: defaults must come after non-defaults.
@dataclass
class Item:
name: str
value: int
def is_high(self) -> bool:
return self.value > 10
a = Item("a", 5)
print(a.is_high()) # False__repr__ for free is a real wina = Item("a", 5)
print(a) # Item(name='a', value=5)
print([a, Item("b", 12)]) # [Item(name='a', value=5), Item(name='b', value=12)]Debugging is much nicer when print actually shows the data instead of <...>.
@dataclass__init__ by hand@dataclass(frozen=True)For 80% of the "named bag of fields" cases, @dataclass is the right default.
Create a free account to get started. Paid plans unlock all tracks.