Dunders and dataclasses
Dunder methods ("double underscore", e.g. __init__) are special methods Python calls for you in particular situations. Two make your objects far nicer to work with:
__str__— the *human-readable* string, used byprint()andstr(). Friendly, for end users.__repr__— the *developer* string, used in the REPL, in debuggers, and inside containers. Convention: make it look like the code that would recreate the object, e.g.Job(ref='HC-001', ...).
If you don't define __repr__, you get the unhelpful default <__main__.Job object at 0x10c3f...>. Defining at least __repr__ is a cheap, high-value habit. (If you define only __str__, repr() still falls back to the default — so prefer __repr__.)
Writing __init__, __repr__ and __eq__ by hand for a plain data-holder is tedious. @dataclass generates all of them from type-annotated fields. You declare the fields; the decorator writes the boilerplate. Defaults go right on the field (category: str = 'general'). One catch: for a *mutable* default like a list, you must use field(default_factory=list), not = [].
Syntax
class Job:
def __init__(self, ref, title, status='open'):
self.ref, self.title, self.status = ref, title, status
def __str__(self):
return f'[{self.ref}] {self.title} ({self.status})'
def __repr__(self):
return f"Job(ref='{self.ref}', title='{self.title}', status='{self.status}')"
from dataclasses import dataclass, field
@dataclass
class Note:
text: str
category: str = 'general' # default value
tags: list = field(default_factory=list) # mutable defaultWorked examples
__str__ for humans, __repr__ for developers
class Job:
def __init__(self, ref, title, status='open'):
self.ref = ref
self.title = title
self.status = status
def __str__(self):
return f'[{self.ref}] {self.title} ({self.status})'
def __repr__(self):
return f"Job(ref='{self.ref}', title='{self.title}', status='{self.status}')"
j = Job('HC-001', 'Fix fence')
print(str(j)) # [HC-001] Fix fence (open)
print(repr(j)) # Job(ref='HC-001', title='Fix fence', status='open')
print(j) # [HC-001] Fix fence (open) (print uses __str__)@dataclass writes the boilerplate for you
from dataclasses import dataclass
@dataclass
class Note:
text: str
category: str = 'general'
def summary(self):
return f'[{self.category}] {self.text}'
n = Note('Call client', 'reminder')
print(repr(n)) # Note(text='Call client', category='reminder')
print(n.summary()) # [reminder] Call client
print(Note('Idea').category) # general (default used)Dataclasses get __eq__ for free
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
print(Point(1, 2) == Point(1, 2)) # True (compares by value)
print(Point(1, 2) == Point(3, 4)) # False
# A plain class would compare by identity → both would be False.Common mistakes & gotchas
The default repr is useless for debugging
Without __repr__, printing a list of your objects shows [<__main__.Job object at 0x...>, ...] — no idea which is which. Defining __repr__ once makes every debug session and log line readable. It's the highest-value dunder to add.
Mutable default in a dataclass
tags: list = [] is an error in a dataclass (and a shared-state bug in normal code) — every instance would share the *same* list. Use field(default_factory=list) so each instance gets its own fresh list.
@dataclass needs type annotations
A field only becomes a dataclass field if it has a type annotation. category = 'general' (no : str) is treated as a plain class attribute and is silently left out of the generated __init__. Always annotate: category: str = 'general'.
Why it matters
`__repr__` pays for itself the first time you debug a list of objects, and `@dataclass` removes a whole category of repetitive, error-prone boilerplate — you'll see it everywhere in modern Python, from config objects to API models. Together they make your own types feel like first-class citizens of the language.