Dataclasses & __slots__ Interview Questions & Answers
Python interview questions on @dataclass — generated methods, frozen=True, field(default_factory=...) for mutable defaults, __slots__ for memory and speed, dataclass vs namedtuple vs NamedTuple, and __post_init__.
@dataclass auto-generates the boilerplate dunder methods from the
class's annotated fields — primarily __init__, __repr__, and
__eq__. You declare fields with type hints (and optional defaults) and
skip writing the constructor by hand.
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int = 0 # field with a default
p = Point(1, 2)
p # Point(x=1, y=2) — generated __repr__
p == Point(1, 2) # True — generated __eq__
# __init__(self, x, y=0) was generated automatically
You can opt into more (order=True for comparison operators, frozen=True for
immutability). It removes the repetitive plumbing while leaving normal methods
up to you.
@dataclass(frozen=True) makes instances immutable — assigning to a field
after creation raises FrozenInstanceError. As a bonus, frozen dataclasses
get a generated __hash__, so they're usable as dict keys and set
members.
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: int
y: int
p = Point(1, 2)
p.x = 9 # FrozenInstanceError — immutable
{p: "origin"} # hashable — works as a dict key
{Point(1, 2), Point(1, 2)} # one element — equal and hashable
Frozen dataclasses are the concise, modern way to define immutable value objects. Use them whenever an object represents a value that shouldn't change after creation.
A bare mutable default (tags: list = []) would be shared across all
instances — the same default-argument trap as in regular functions — so
dataclasses forbid it and raise ValueError. Instead, pass
field(default_factory=list), a zero-arg callable that creates a fresh
object per instance.
from dataclasses import dataclass, field
@dataclass
class Article:
title: str
tags: list = field(default_factory=list) # new list each instance
# tags: list = [] # would raise ValueError at class-definition time
a, b = Article("A"), Article("B")
a.tags.append("python")
b.tags # [] — independent, no shared state
Use default_factory for any mutable default (list, dict, set) or for a
value that must be computed at construction time. Plain immutable defaults
(numbers, strings, tuples) are fine inline.
__slots__ declares a fixed set of allowed attributes, so instances store
them in a compact array instead of a per-instance __dict__. This saves
significant memory (no dict per object) and gives faster attribute access.
The trade-off: you can't add new attributes not listed in slots.
class Point:
__slots__ = ("x", "y") # no per-instance __dict__
def __init__(self, x, y):
self.x, self.y = x, y
p = Point(1, 2)
p.x # 2 — fast access
p.z = 3 # AttributeError — 'z' not in __slots__
# p.__dict__ # also AttributeError — there is no dict
from dataclasses import dataclass
@dataclass(slots=True) # dataclasses can generate slots (3.10+)
class Fast:
x: int
y: int
Reach for __slots__ when you create huge numbers of small objects and
memory matters. For ordinary classes the flexibility of __dict__ is usually
worth more than the savings.
A namedtuple/typing.NamedTuple is an immutable tuple subclass —
lightweight, hashable, iterable, and index-accessible, but values can't change.
A @dataclass is a regular class — mutable by default, supports methods,
inheritance, and default_factory, but isn't a tuple (no unpacking/indexing
unless you add it).
from typing import NamedTuple
from dataclasses import dataclass
class PointNT(NamedTuple): # immutable, tuple-like
x: int
y: int
x, y = PointNT(1, 2) # unpacks like a tuple
@dataclass
class PointDC: # mutable, full class
x: int
y: int
def shift(self, dx): # can hold real methods
self.x += dx
PointNT(1, 2)[0] # 1 — index access (tuple)
PointDC(1, 2).shift(5) # mutate in place
Choose a NamedTuple for small immutable records and tuple semantics; choose a
dataclass when you need mutability, methods, or richer field control. Use
frozen=True dataclasses for immutable value objects that still want class
features.
__post_init__ runs automatically right after the generated __init__ —
it's the hook for validation and derived fields that depend on the
constructor arguments. It pairs with field(init=False) to declare attributes
that aren't constructor parameters but are computed afterward.
from dataclasses import dataclass, field
@dataclass
class Rectangle:
width: float
height: float
area: float = field(init=False) # not a constructor arg
# filled in after __init__:
def __post_init__(self):
if self.width <= 0 or self.height <= 0:
raise ValueError("dimensions must be positive") # validation
self.area = self.width * self.height # derived field
r = Rectangle(3, 4)
r.area # 12 — computed in __post_init__
Rectangle(-1, 4) # ValueError
Use __post_init__ whenever a dataclass needs logic the auto-generated
__init__ can't express — validation, normalization, or fields derived from
others.
field() tunes per-attribute behavior: init=False (exclude from __init__),
repr=False (hide from __repr__), compare=False (exclude from __eq__/
ordering), default/default_factory, and metadata (arbitrary dict for
tools).
from dataclasses import dataclass, field
@dataclass
class User:
name: str
password: str = field(repr=False) # keep out of repr/logs
id: int = field(default=0, compare=False) # ignored in equality
tags: list = field(default_factory=list)
u = User("ada", "secret")
u # User(name='ada', tags=[]) — no password shown
Rule of thumb: use field() to fine-tune which attributes participate in init,
repr, and comparison — handy for secrets, caches, and derived values.
Use dataclasses.asdict() and astuple() — they recurse into nested
dataclasses, lists, and dicts, producing plain data structures (handy for JSON).
replace() makes a modified copy.
from dataclasses import dataclass, asdict, astuple, replace
@dataclass
class Point:
x: int
y: int
p = Point(1, 2)
asdict(p) # {'x': 1, 'y': 2}
astuple(p) # (1, 2)
replace(p, y=9) # Point(x=1, y=9) — new object, p unchanged
Rule of thumb: asdict/astuple for serialization (they deep-copy nested data),
replace for immutable-style "copy with changes."
Fields combine in MRO order, base fields first. Because Python won't allow a
non-default parameter after a default one, a subclass can't add a required field
after the base supplied defaults — it raises TypeError at class creation.
from dataclasses import dataclass
@dataclass
class Base:
a: int
b: int = 0
@dataclass
class Sub(Base):
c: int # TypeError: non-default 'c' follows default 'b'
# fix: give c a default, or make b required
Rule of thumb: once a base dataclass introduces a defaulted field, every later field (including in subclasses) must also have a default.
@dataclass(kw_only=True) (3.10+) makes fields keyword-only in __init__, which
sidesteps the default-ordering problem — keyword-only params have no positional
ordering constraint. You can also mark individual fields with field(kw_only=True).
from dataclasses import dataclass
@dataclass(kw_only=True)
class Config:
a: int = 0
b: int # required, but keyword-only — order is fine
Config(b=5) # OK — must pass by keyword
Config(5) # TypeError — positional not allowed
Rule of thumb: use kw_only=True to mix required and defaulted fields freely
(especially across inheritance) and to force explicit, self-documenting calls.
A name can't be both a slot and a class attribute — listing x in __slots__
and also assigning x = 0 in the body raises ValueError (the class attribute
would shadow the slot descriptor). With @dataclass(slots=True) this is handled, but
hand-written slots classes hit it.
class Bad:
__slots__ = ("x",)
x = 0 # ValueError: 'x' in __slots__ conflicts with class variable
class Ok:
__slots__ = ("x",)
def __init__(self, x=0):
self.x = x # set defaults in __init__, not the class body
Rule of thumb: with manual __slots__, supply defaults in __init__ rather than as
class-body assignments to the slotted names.
If any class in the hierarchy lacks __slots__, instances get a __dict__
anyway, erasing the savings. Every class in the chain (including the base) must
define __slots__ — and the subclass should only list its new attributes.
class Base: # no __slots__ -> instances get __dict__
pass
class Sub(Base):
__slots__ = ("x",) # useless: __dict__ still present via Base
Sub().__dict__ # exists! no memory win
class Base2:
__slots__ = ()
class Sub2(Base2):
__slots__ = ("x",) # now truly dict-free
Rule of thumb: for slots to pay off, give every class in the MRO __slots__ (use
__slots__ = () on otherwise-empty bases) and don't re-list inherited slots.
@dataclass(order=True) generates the comparison dunders (__lt__, __le__,
__gt__, __ge__) that compare instances field-by-field as a tuple, in
declaration order. This makes dataclasses sortable.
from dataclasses import dataclass
@dataclass(order=True)
class Version:
major: int
minor: int
sorted([Version(1, 2), Version(1, 0), Version(0, 9)])
# [Version(0, 9), Version(1, 0), Version(1, 2)]
Version(1, 0) < Version(1, 2) # True
Rule of thumb: add order=True to make value objects sortable; control the sort key
by field order, and use field(compare=False) to exclude a field.
Add a dedicated sort_index field with field(init=False, repr=False), set it
in __post_init__, and exclude the real data fields from comparison. Ordering then
uses only your computed key.
from dataclasses import dataclass, field
@dataclass(order=True)
class Item:
sort_index: float = field(init=False, repr=False)
name: str = field(compare=False)
priority: int = field(compare=False)
def __post_init__(self):
self.sort_index = self.priority
sorted([Item("a", 3), Item("b", 1)]) # ordered by priority
Rule of thumb: a sort_index field plus compare=False on data fields is the
canonical recipe for custom-keyed ordering (e.g. priority queues).
Roughly: a plain dataclass carries a per-instance __dict__ (largest);
slots=True drops the dict (much smaller, fixed attributes); a NamedTuple is
a tuple subclass (smallest, immutable, but no per-instance methods state).
from dataclasses import dataclass
from typing import NamedTuple
@dataclass
class A: x: int; y: int # has __dict__
@dataclass(slots=True)
class B: x: int; y: int # no __dict__ — leaner
class C(NamedTuple): x: int; y: int # tuple-backed, immutable, leanest
Rule of thumb: many objects + mutability → slots=True dataclass; immutable records
→ NamedTuple; convenience over footprint → plain dataclass.
More Object-Oriented Programming interview questions
More ways to practice
The self-quiz is live. Join our channel for updates, new content & tech tips.