Chapter 4 · Mid-level Developer

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 by print() and str(). 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 default

Worked 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.