DevHireLab
Tutorials
BootcampProblemsCode SimulatorAI InterviewSoonContact
DevHireLab
Tutorials
BootcampProblemsCode SimulatorAI InterviewSoonContact
Getting Started
01
What is Python and Why Learn It in 2026
02
Installing Python on Windows, Mac, and Linux
03
Setting up VS Code / PyCharm for Python
04
Understanding the Python Interpreter and REPL
05
Writing and Running Your First Python Program
06
Python Syntax and Indentation Rules
Variables and Data Types
01
Variables and Naming Conventions in Python
02
Numbers in Python (int, float, complex)
03
Strings and String Methods in Python
04
Booleans in Python
05
Type Conversion / Casting in Python
06
Understanding the None Type in Python
Operators
01
Arithmetic Operators in Python
02
Comparison Operators in Python
03
Logical Operators in Python
04
Assignment Operators in Python
05
Bitwise Operators in Python
06
Identity (is) and Membership (in) Operators
07
Walrus Operator (:=) in Python
Input, Output & Strings
01
input() and print() Functions in Python
02
f-strings and String Formatting in Python
03
String Slicing and Indexing in Python
04
Common String Methods (split, join, strip, replace)
05
Escape Characters in Python Strings
Control Flow
01
if, elif, else Statements in Python
02
Nested Conditionals in Python
03
for Loops in Python
04
while Loops in Python
05
break, continue, and pass Statements
06
Loop with else Clause in Python
07
match-case (Structural Pattern Matching)
Data Structures
01
Python Lists: Creation, Indexing, Slicing, Methods
02
Python Tuples and Immutability
03
Python Sets and Set Operations
04
Python Dictionaries: Keys, Values, and Methods
05
List Comprehensions in Python
06
Dictionary & Set Comprehensions in Python
07
Working with Nested Lists and Dictionaries
Functions
01
Defining and Calling Functions in Python
02
Function Arguments: Positional, Keyword, Default
03
*args and **kwargs in Python
04
Return Statements in Python Functions
05
Lambda Functions in Python
06
Recursion in Python
07
Variable Scope: Local, Global, Nonlocal
08
Docstrings and Function Annotations
Functional Programming Basics
01
map(), filter(), and reduce() in Python
02
Closures in Python Explained
03
Python Decorators: Basic to Advanced
04
Generators and yield in Python
Object-Oriented Programming (OOP)
01
Classes and Objects in Python
02
The init Constructor in Python
03
Instance vs Class Variables in Python
04
Inheritance in Python (Single, Multiple, Multilevel)
05
Polymorphism in Python
06
Encapsulation in Python
07
Abstraction in Python with the abc Module
08
Magic/Dunder Methods in Python
09
Static and Class Methods in Python
10
The @property Decorator in Python
11
Python Dataclasses Explained
Error Handling
01
try, except, finally in Python
02
Handling Multiple Exceptions in Python
03
Raising Custom Exceptions in Python
04
Understanding Python's Exception Hierarchy
05
Assertions in Python
File Handling
01
Reading and Writing Text Files in Python
02
Working with the with Statement in Python
03
Working with CSV Files in Python
04
Working with JSON Files in Python
05
Working with Directories (os, pathlib) in Python
Modules and Packages
01
Importing Built-in Modules in Python
02
Creating Your Own Modules in Python
03
Understanding Packages and init.py
04
Python Standard Library Overview
05
Installing Packages with pip
Advanced Core Concepts
01
Iterators and Iterables in Python
02
Context Managers in Python (with, contextlib)
03
Multithreading Basics in Python
04
Multiprocessing Basics in Python
05
Asyncio Basics: async/await in Python
06
Memory Management and Garbage Collection in Python
07
Type Hints in Python (typing module)

Python Dataclasses Explained

python dataclasses exist to eliminate a specific, extremely common kind of boilerplate: writing a class whose entire job is holding a handful of structured fields, but still requiring you to manually write __init__, __repr__, and __eq__ by hand every single time. The @dataclass decorator generates all of that automatically, based purely on type-annotated field declarations.

Introduction: The Boilerplate Problem Dataclasses Solve

The pain point

Consider a plain class meant to hold nothing more than a few related fields:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Point(x={self.x}, y={self.y})"

    def __eq__(self, other):
        if not isinstance(other, Point):
            return NotImplemented
        return self.x == other.x and self.y == other.y

That's a genuine amount of boilerplate — __init__, __repr__, and __eq__, all hand-written — for a class that conceptually holds nothing more than two related values.

What @dataclass does

@dataclass, added in Python 3.7, generates exactly this boilerplate automatically, based on type-annotated field declarations directly in the class body:

from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int
Before and after, side by side
# Before — hand-written, ~10 lines for two fields
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def __repr__(self):
        return f"Point(x={self.x}, y={self.y})"
    def __eq__(self, other):
        return isinstance(other, Point) and self.x == other.x and self.y == other.y

# After — @dataclass, same behavior, 3 lines
@dataclass
class Point:
    x: int
    y: int
p1 = Point(1, 2)
p2 = Point(1, 2)
print(p1)          # Point(x=1, y=2) — readable __repr__, generated automatically
print(p1 == p2)     # True — value-based equality, generated automatically

Both versions behave identically from the outside — @dataclass just eliminates the manual work of writing the equivalent dunder methods yourself.

Basic Fields, Defaults, and the Generated Methods

Declaring fields

Fields are declared with type annotations directly in the class body — no __init__ needs to be written manually at all:

from dataclasses import dataclass

@dataclass
class Book:
    title: str
    author: str
    pages: int

book = Book("Dune", "Frank Herbert", 412)
print(book.title, book.pages)   # Dune 412
Adding default values

Defaults work exactly the same way as default function arguments, covered in the earlier function arguments article:

@dataclass
class Book:
    title: str
    author: str
    year: int = 1965

book = Book("Dune", "Frank Herbert")
print(book.year)   # 1965 — falls back to the default
What gets generated automatically

Three things, purely from the field declarations:

  • __init__ — accepting each field as a parameter, in the order declared, with defaults respected.

  • A readable __repr__ — showing the class name and every field's current value, exactly like Point(x=1, y=2) above.

  • Value-based __eq__ — comparing instances field by field, rather than Python's default identity-based comparison (which, as covered in the earlier magic methods article, only checks whether two variables point to the exact same object):

p1 = Point(1, 2)
p2 = Point(1, 2)
print(p1 == p2)   # True — value comparison, not identity
An important caveat: annotations aren't enforced

Type annotations are required syntactically to define a dataclass field at all — x: int needs that annotation to be recognized as a field — but, exactly as covered in the earlier docstrings and function annotations article, they're not enforced at runtime by Python itself:

@dataclass
class Point:
    x: int
    y: int

p = Point("hello", "world")   # runs fine — no error, despite the type hints
print(p)   # Point(x='hello', y='world')

If you genuinely need runtime type validation, a static type checker like Mypy (run separately, before your program executes) remains the correct tool — @dataclass itself provides zero runtime type checking, purely structural boilerplate generation based on the annotations you provide.

Mutable Defaults with field(default_factory=...)

The problem, and how dataclasses actually prevent it

You've seen the mutable default argument trap several times throughout this series — writing items: list = [] as a dataclass field would create exactly that same shared-object bug. But dataclasses actually go a step further than a plain function: they actively detect this and raise an error at class-definition time, rather than letting the bug slip through silently:

@dataclass
class ShoppingCart:
    items: list = []
# ValueError: mutable default <class 'list'> for field items is not allowed:
# use default_factory

This is a genuinely helpful safety net — the exact bug that would otherwise silently corrupt shared state across every instance (as shown in the earlier classes article) is caught immediately, loudly, before your program even finishes defining the class.

The fix: default_factory
from dataclasses import dataclass, field

@dataclass
class ShoppingCart:
    items: list = field(default_factory=list)

cart1 = ShoppingCart()
cart2 = ShoppingCart()

cart1.items.append("apple")
print(cart1.items)   # ['apple']
print(cart2.items)   # [] — correctly independent

field(default_factory=list) tells the dataclass to call list() fresh, once per instance, at creation time — giving each object its own genuinely independent list, rather than sharing one across every instance. default_factory accepts any zero-argument callable — list, dict, set, or even a custom function you've defined yourself.

Other useful field() options

repr=False — excludes a field from the generated __repr__, useful for hiding sensitive data like passwords from being accidentally printed or logged:

@dataclass
class User:
    username: str
    password: str = field(repr=False)

user = User("alex99", "secret123")
print(user)   # User(username='alex99') — password is excluded from the output

compare=False — excludes a field from the generated __eq__, useful when a field shouldn't factor into whether two instances are considered "equal":

@dataclass
class Event:
    name: str
    timestamp: str = field(compare=False)

e1 = Event("Launch", "10:00 AM")
e2 = Event("Launch", "10:05 AM")
print(e1 == e2)   # True — timestamp is excluded from the comparison
4. Validation and Derived Fields with post_init
What post_init does

__post_init__ is a method that runs automatically immediately after the generated __init__ finishes — the natural place for validation logic, or for computing a field derived from other fields already set.

Validation example
from dataclasses import dataclass

@dataclass
class Rectangle:
    width: float
    height: float

    def __post_init__(self):
        if self.width <= 0 or self.height <= 0:
            raise ValueError("Width and height must be positive")

rect = Rectangle(4, 5)   # fine

bad_rect = Rectangle(-1, 5)
# ValueError: Width and height must be positive
Computing a derived field
from dataclasses import dataclass, field

@dataclass
class Rectangle:
    width: float
    height: float
    area: float = field(init=False)

    def __post_init__(self):
        self.area = self.width * self.height

rect = Rectangle(4, 5)
print(rect.area)   # 20 — computed automatically, not passed in

field(init=False) excludes area from the generated __init__'s parameter list entirely — you can't (and shouldn't) pass it in directly when constructing a Rectangle, since __post_init__ computes it correctly from width and height right after construction.

A brief mention of InitVar

Sometimes you need a value passed into __post_init__ for use in some calculation, but you don't want it stored as an actual instance attribute afterward. InitVar handles exactly this case:

from dataclasses import dataclass, InitVar

@dataclass
class Rectangle:
    width: float
    height: float
    scale: InitVar[float] = 1.0

    def __post_init__(self, scale):
        self.width *= scale
        self.height *= scale

rect = Rectangle(4, 5, scale=2.0)
print(rect.width, rect.height)   # 8.0 10.0
print(hasattr(rect, "scale"))     # False — scale was never stored as an attribute

scale gets passed into __init__ and forwarded to __post_init__, where it's used to adjust width and height — but it's never actually kept around as a stored attribute afterward, exactly as intended for a value that's only relevant during construction itself.

Immutability, Memory, and When to Use Dataclasses

frozen=True for read-only instances
from dataclasses import dataclass

@dataclass(frozen=True)
class Point:
    x: int
    y: int

p = Point(1, 2)
p.x = 100
# dataclasses.FrozenInstanceError: cannot assign to field 'x'

frozen=True makes instances read-only after creation — any attempt to reassign a field raises FrozenInstanceError immediately, mirroring the immutability guarantees covered in the earlier tuples article, but applied to a full dataclass instead.

A caveat worth flagging: frozen=True only prevents reassigning a field directly — it doesn't make the contents of a mutable field immutable, the exact same structure-vs-contents nuance covered in the earlier tuples article:

@dataclass(frozen=True)
class Container:
    items: list

c = Container([1, 2, 3])
c.items.append(4)   # this still works — the list itself is mutable
print(c.items)   # [1, 2, 3, 4]

c.items = []
# FrozenInstanceError — but reassigning the field itself is blocked
slots=True for reduced memory usage

Since Python 3.10, @dataclass(slots=True) automatically generates __slots__ for the class, which can meaningfully reduce memory usage — particularly valuable when creating large numbers of instances:

@dataclass(slots=True)
class Point:
    x: int
    y: int

This achieves the same memory optimization you'd get from manually writing __slots__ = ("x", "y") yourself, without needing to write and keep that list in sync with your field declarations by hand. (Note the functools.cached_property incompatibility with __slots__, mentioned in the earlier @property article, applies here too — a slots=True dataclass can't use cached_property on its fields.)

Comparison to alternatives

namedtuple (covered in the earlier tuples article) is a good fit for simpler, genuinely tuple-like immutable records, where you want positional unpacking and minimal overhead, without needing validation logic, mutability, or the fuller dataclass feature set.

A plain class remains the better choice when a class's behavior — its methods and how it operates on its own data — matters more than the raw data it holds. Dataclasses are specifically optimized for the "structured bag of data" case; a class with substantial custom behavior beyond field storage doesn't gain much from @dataclass, and might read more clearly as a conventional class instead.

Dataclasses hit a genuine sweet spot: structured data with type hints, automatic __init__/__repr__/__eq__ generation, validation via __post_init__, and optional immutability via frozen=True — all built directly into Python's standard library, with zero external dependencies required (unlike the popular third-party attrs library, which offers a broadly similar feature set but requires installing a separate package).

PREVIOUSNEXT LESSON