Overview
Curated: · Written: · Reviewed:
Python OOP and dataclasses in Python 3.14
Python's object model is dynamic but not vague. Attribute access follows descriptors and a method-resolution order; equality and hashing have strict container consequences; dataclasses generate specific methods under specific flags. Interview answers should connect those mechanisms to substitutability, construction invariants, memory layout, and interface evolution.
self and bound methods
self is the conventional name for the instance passed to an instance method; Python performs that binding through descriptor lookup.
Accessing obj.method produces a bound method carrying obj, while Class.method(obj, ...) calls the underlying function explicitly.
Interview trap. Calling self a keyword or believing methods capture an instance at class definition time gives the wrong model.
Engineering practice. Keep self explicit, use instance methods for behavior depending on instance state, and test through the public behavior rather than binding internals.
cls and class methods
A classmethod receives the class used for lookup as cls and supports polymorphic alternate constructors.
The classmethod descriptor binds a subclass when invoked through that subclass, so cls(...) preserves the dynamic type.
Interview trap. Hard-coding the base class in an alternate constructor defeats subclassing even though the method is marked classmethod.
Engineering practice. Use classmethods when construction or behavior depends on the dynamic class; use staticmethod only when no instance or class binding is needed.
instance versus class attributes
Attribute lookup normally checks the instance and then its class hierarchy, while assignment to self.name creates or replaces an instance attribute.
A mutable class attribute is shared until an instance shadows that name, making class-level containers a frequent accidental global.
Interview trap. Declaring items = [] in a class body does not give each instance an independent list.
Engineering practice. Initialize per-instance mutable state in init or a dataclass default_factory and reserve class attributes for deliberate shared configuration.
method resolution order
Python uses a deterministic C3 method resolution order to search classes consistently in multiple inheritance.
Class.mro exposes the linearization, which preserves local precedence and monotonicity rather than doing a naive depth-first search.
Interview trap. Reasoning that Python simply checks the left parent and all its ancestors before the right parent fails in diamonds.
Engineering practice. Inspect the MRO, keep mixins focused, and avoid inheritance graphs whose cooperative call chain cannot be explained.
cooperative super
super delegates lookup to the next class in the current MRO, not necessarily to a named parent.
Zero-argument super uses the defining class and first argument; every participant must use compatible signatures and call super for a cooperative chain.
Interview trap. Describing super as parent.method hides why the same source works differently in multiple-inheritance MROs.
Engineering practice. Use keyword-friendly compatible methods, call super exactly once where cooperation is required, and test realistic mixin orders.
initialization across inheritance
init initializes an already-created instance; base initialization is not automatic when a subclass overrides it.
A cooperative subclass calls super().init, allowing each MRO participant to consume its arguments and establish invariants.
Interview trap. Directly naming one base initializer can skip another base in a diamond or initialize shared ancestry twice.
Engineering practice. Design cooperative signatures deliberately or prefer composition when constructors cannot share a stable protocol.
composition versus inheritance
Inheritance models substitutability and shared protocol, while composition delegates to owned collaborators and reduces coupling to hierarchy internals.
Subclass code participates in MRO, constructors, and protected assumptions; composed objects can be replaced behind a narrower interface.
Interview trap. Using inheritance only to reuse implementation can expose methods and invariants the subtype cannot honor.
Engineering practice. Ask whether every consumer of the base can safely accept the subtype; otherwise inject or wrap the reusable behavior.
special method lookup
Many implicit operations look up special methods on the type rather than through ordinary instance attribute lookup.
Assigning instance.len generally does not change len(instance); the interpreter consults type(instance).len through special lookup.
Interview trap. Monkey-patching one instance's dunder and expecting operators to honor it leads to inconsistent tests.
Engineering practice. Define protocol methods on the class and expose ordinary injectable collaborators when per-instance customization is required.
equality and NotImplemented
Rich comparison methods should return NotImplemented for unsupported operand types so Python can try reflected behavior or apply fallback rules.
Returning False prematurely prevents the other operand from participating and can break symmetry across cooperating types.
Interview trap. NotImplemented is not the same as raising NotImplementedError and is truthy if misused directly.
Engineering practice. Compare meaningful fields for compatible types, return NotImplemented otherwise, and test symmetry and hash consistency.
object hashing
Objects equal under eq must have equal hashes, and mutable value objects usually should not be hashable.
Defining eq without a suitable hash normally makes instances unhashable, protecting dictionary and set invariants.
Interview trap. Hashing a tuple of mutable fields once does not make later mutation safe.
Engineering practice. Use immutable identity fields, frozen value objects with stable members, or explicit identity semantics according to the domain.
dataclass generated methods
@dataclass inspects annotated fields and can generate init, repr, and comparison methods without changing field values into a new storage model.
Generation flags interact: order requires equality, repr can omit fields, and user-defined methods may suppress or conflict with generation.
Interview trap. Treating dataclass as automatic validation or serialization attributes capabilities it does not provide.
Engineering practice. Use it for data-focused types, validate invariants explicitly, and review generated equality and representation for domain and secrecy needs.
dataclass field order
Generated init parameter order follows field definition order across inheritance, subject to default and keyword-only rules.
A non-default field cannot follow a default field in the effective field sequence, including fields inherited from base dataclasses.
Interview trap. A subclass can trigger a definition-time TypeError because a base default precedes its required positional field.
Engineering practice. Use keyword-only fields or reorganize hierarchy when evolution makes positional construction fragile.
default_factory
field(default_factory=...) calls a zero-argument factory for each new dataclass instance.
It creates independent mutable defaults and can also compute non-mutable defaults at construction time.
Interview trap. Writing default_factory=[] passes an object instead of a callable, while default=[] would share or be rejected as a mutable default.
Engineering practice. Pass list, dict, set, or a named factory and test that separate instances do not alias their defaults.
post_init
post_init runs after a generated dataclass init assigns fields and receives InitVar values when declared.
Generated initialization does not automatically invoke a non-dataclass base init, so post-init may be the place for explicit base setup.
Interview trap. Assuming post_init runs when a custom init replaces generation can leave invariants unchecked.
Engineering practice. Keep derived-field computation deterministic, validate cross-field invariants, and document side effects during construction.
InitVar and ClassVar
ClassVar marks class-level names excluded from dataclass fields, while InitVar adds initialization-only parameters passed to post_init.
Neither behaves like an ordinary stored field in fields(), equality, or generated representation.
Interview trap. Using an ordinary annotation for shared configuration accidentally makes it part of instance construction and equality.
Engineering practice. Mark semantic roles explicitly and avoid hiding required long-lived dependencies as initialization-only values.
frozen dataclasses
frozen=True adds guards against normal field assignment but does not make the entire reachable object graph deeply immutable.
Generated initialization uses low-level assignment, and a mutable list stored in a frozen field can still mutate.
Interview trap. Calling frozen instances thread-safe or automatically hashable regardless of member stability overclaims the feature.
Engineering practice. Use immutable field types for value objects and treat frozen as an API constraint, not a security boundary.
unsafe_hash
unsafe_hash=True forces dataclass hash generation and can violate hash-table invariants if equality-relevant state later changes.
The name signals that logical immutability must be guaranteed by the programmer even when the class is not structurally frozen.
Interview trap. Enabling unsafe_hash merely to silence an unhashable error can make dictionary entries unreachable after mutation.
Engineering practice. Redesign identity or immutability first; force hashing only with a documented invariant and mutation tests.
dataclass slots
dataclass(slots=True) returns a class with generated slots for fields, usually removing the per-instance dict unless inherited circumstances add one.
Slots can reduce instance memory and reject undeclared attributes, but inheritance, weak references, pickling, and zero-argument super deserve care.
Interview trap. Treating slots as a universal speed switch ignores class replacement and compatibility constraints.
Engineering practice. Use slots for many stable instances after measurement, and test inheritance, serialization, tooling, and monkey-patching assumptions.
manual slots
slots declares descriptors for named instance attributes and can prevent automatic dict and weakref creation.
Every class in an inheritance chain affects final layout; duplicate slot names and multiple slotted bases have constraints.
Interview trap. Listing a class variable with the same name as a slot conflicts with the generated descriptor and default-value expectations.
Engineering practice. Prefer dataclass slots for field-like types, and document layout constraints when manual descriptors or inheritance require custom slots.
properties and descriptors
A property is a data descriptor that can preserve attribute syntax while controlling reads, writes, validation, or computation.
Descriptor precedence explains why a property can override an entry in an instance dictionary and why methods bind automatically.
Interview trap. Hiding expensive I/O behind an innocent-looking property makes call sites misleading and repeated access costly.
Engineering practice. Use properties for attribute-like, unsurprising work; use explicit methods for expensive, fallible, or side-effecting operations.
protocol structural typing
typing.Protocol supports static structural subtyping: a type can satisfy an interface by providing required members without explicit inheritance.
Type checkers compare shape; @runtime_checkable enables limited isinstance checks that inspect member presence rather than full signatures or invariants.
Interview trap. Passing runtime protocol checks does not prove method signatures, semantics, or side-effect contracts.
Engineering practice. Keep protocols small and consumer-owned, test behavior separately, and use runtime checks only as coarse adaptation guards.
abstract base classes
Abstract base classes provide nominal relationships and abstract-method enforcement while also permitting registration and subclass hooks.
A class with unresolved abstract methods cannot be instantiated, though abstract methods may still contain cooperative implementations.
Interview trap. Registering a virtual subclass does not inject methods or place the ABC in that class's MRO.
Engineering practice. Use ABCs when runtime nominal identity or shared implementation matters; prefer protocols for decoupled static interfaces.
dataclass replacement
dataclasses.replace constructs a new instance through the dataclass initializer, so post_init and InitVar requirements still apply.
It is not a raw field clone, and init=False fields are handled according to documented restrictions rather than blindly copied.
Interview trap. Using replace as an unrestricted cloning primitive can recompute derived data or repeat construction side effects.
Engineering practice. Keep dataclass construction controlled and side effects minimal, or provide a domain-specific copy method with explicit semantics.
pattern matching and match_args
Dataclasses can generate match_args from non-keyword-only initializer parameters for positional class patterns.
Changing field order or keyword-only status can therefore change a public pattern-matching surface even when attribute names remain.
Interview trap. Treating field reordering as a harmless refactor can break downstream positional matches.
Engineering practice. Prefer keyword class patterns across package boundaries and set match_args deliberately for stable public types.
designing Python object models
A strong object model aligns identity, equality, mutability, construction, interface, and composition with domain invariants.
Dataclasses remove boilerplate; protocols describe shape; ABCs describe nominal contracts; none replaces thoughtful boundaries and behavior.
Interview trap. Choosing a decorator or hierarchy first can bake accidental storage details into a public API.
Engineering practice. Start from substitutability and lifecycle, expose the smallest protocol, prefer composition, and measure slots only when instance scale warrants it.
Worked example: a class-body list is one list
class Cart: items = [] then a = Cart(); b = Cart(); a.items.append("x"). b.items is also ["x"] because assignment to self.items never happened. field(default_factory=list) on a dataclass gives each instance its own list.
| step | a.items | b.items | id(a.items)==id(b.items) |
|---|---|---|---|
| a=Cart(); b=Cart() | [] | [] | true |
| a.items.append("x") | ["x"] | ["x"] | true |
| dataclass factory, then a.items.append("x") | ["x"] | [] | false |
That table is the interview: class attributes are shared until an instance shadows the name.
