Courses/Python Mastery/Module 9: Object-Oriented Python
Module 9 · Lesson 515 minBeginner

Dataclasses: OOP Without Boilerplate

Lesson goal
Write data-holding classes in three lines.

Dataclasses: OOP Without Boilerplate

Look at the Student class you've written for three lessons: __init__ assigning every field, __str__ formatting them, __eq__ comparing them — all of it mechanical. Python 3.7's @dataclass decorator writes all of that for you.

The before and after

The classic way — 15 lines of boilerplate:

code
class Student:
    def __init__(self, name, age, marks):
        self.name = name
        self.age = age
        self.marks = marks

    def __str__(self):
        return f"Student(name={self.name}, age={self.age}, marks={self.marks})"

    def __eq__(self, other):
        return (self.name == other.name and
                self.age == other.age and
                self.marks == other.marks)

The dataclass way — the same behavior in 5 lines:

code
from dataclasses import dataclass

@dataclass
class Student:
    name: str
    age: int
    marks: float

s1 = Student("Aarav", 16, 92.0)
s2 = Student("Aarav", 16, 92.0)

print(s1)          # Student(name='Aarav', age=16, marks=92.0)
print(s1 == s2)    # True

Declare the fields with their types — the decorator generates __init__, __repr__, and __eq__ automatically. That's the deal: you describe the data; Python writes the plumbing.

Reading the field syntax

code
@dataclass
class Student:
    name: str          # field: type
    age: int
    marks: float = 0.0     # fields can have defaults (must come last)

The name: str parts are type hints — labels saying what type each field should hold. Python doesn't enforce them at runtime (a str field will happily hold 5), but they document the class, power your editor's autocomplete, and catch mistakes in editors before running.

Methods mix in normally

Dataclasses don't replace methods — just the boilerplate:

code
@dataclass
class Student:
    name: str
    marks: list

    def average(self):
        return sum(self.marks) / len(self.marks)

s = Student("Aarav", [85, 90, 78])
print(s.average())       # 84.33...
print(s)                 # Student(name='Aarav', marks=[85, 90, 78])

Generated __init__ + your own methods: the best of both.

Default values and default_factory

code
from dataclasses import dataclass, field

@dataclass
class Student:
    name: str
    marks: list = field(default_factory=list)     # mutable defaults done RIGHT
    grade: str = "B"                              # simple default

s = Student("Aarav")
s.marks.append(90)     # each student gets their OWN list
print(s)               # Student(name='Aarav', marks=[90], grade='B')

Remember the mutable-default trap from functions? Dataclasses solve it properly: mutable defaults use field(default_factory=list), which creates a fresh list per object.

When to use @dataclass vs a plain class

SituationChoice
Class mostly holds data (a record)@dataclass — instant win
Data + a few computed methods@dataclass + methods
Complex setup logic in __init__Plain class
Behavior-heavy, little dataPlain class

Students, products, tasks, settings, API responses — the "records" of your programs are dataclass territory. This is also exactly how modern Python projects (and AI libraries) model their data.

Common Errors & Fixes

  • `TypeError: missing 2 required positional arguments` — fields without defaults are required at creation. Give defaults or pass values.
  • Fields with defaults before fields without — same rule as functions: marks: float = 0.0 must come after required fields.
  • Mutable default crash: `ValueError: mutable default <class 'list'>` — lists/dicts need field(default_factory=list), never = [].

  • ✅ Checkpoint

  • What three methods does @dataclass generate? *(__init__, __repr__, __eq__)*
  • What are name: str lines called? *(Type hints — documented field types)*
  • Correct default for a list field? *(field(default_factory=list))*
  • Dataclass or plain class for a complex __init__ with validation? *(Plain class)*
  • Module 9 checkpoint reached — classes, self, inheritance, dunders, dataclasses. You can now read and write the OOP that every framework speaks.

    Next module: Power Tools — comprehensions, generators, and the Python that feels like Python.