Skip to content

Dataclasses & __slots__ Interview Questions & Answers

15 questions Updated 2026-06-18 Share:

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

Read the in-depth guidePython Dataclasses and __slots__ Explained — @dataclass, frozen, and field()(opens in new tab)
15 of 15

@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 ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel