Python Full-Stack Interview Questions 26–30 (Dataclasses advanced, attrs vs pydantic, Abstract vs Interface, Polymorphism, Mixins)
This lesson covers five focused Python interview topics: advanced dataclass features (frozen, order, kw_only, post_init, field defaults), how dataclasses compare with attrs and validation libraries like pydantic, the distinction between abstract classes and interfaces in Python, how polymorphism is achieved, and what mixins are. Each question includes beginner-friendly explanations, real-world analogies, runnable code examples, and expected terminal output so you can try them in your editor.
26. Explain dataclasses advanced features: frozen, order, kw_only, post_init, and using field() defaults.
Dataclasses provide a concise way to declare classes primarily used to store data. Beyond the basics, they offer features that change mutability, comparison behavior, argument style, and lifecycle hooks. Think of a dataclass as a ready-made filing box: the options let you choose whether the lid locks (frozen), how you compare boxes (order), whether you must name fields when putting things in the box (kw_only), and a short checklist you run after packing (post_init). `field()` customizes defaults and factory functions.
- frozen=True — makes instances immutable (like a locked box).
- order=True — generates comparison methods (`__lt__`, `__le__`, etc.) based on field order.
- kw_only=True — requires keyword-only initialization for fields declared after it (helps clarity).
- __post_init__ — a method that runs after the generated
__init__, useful for validation or derived attributes. - field() — used for defaults, default_factory (for mutable defaults), and metadata or repr control.
Example showing each feature and expected output:
from dataclasses import dataclass, field
@dataclass(order=True, frozen=True)
class Point:
x: int
y: int = field(default=0)
label: str = field(default="origin", compare=False)
def __post_init__(self):
# Even though frozen=True, __post_init__ can set attributes using object.__setattr__
if not isinstance(self.x, int) or not isinstance(self.y, int):
raise TypeError("x and y must be integers")
# Normalize label to lowercase
object.__setattr__(self, "label", self.label.lower())
p1 = Point(1, 2, label="A")
p2 = Point(2, 3)
print("p1:", p1)
print("p2:", p2)
print("p1 < p2:", p1 < p2)
# Attempt to mutate (will raise)
try:
p1.x = 10
except Exception as e:
print("Mutation error:", type(e).__name__, str(e))Expected output when run:
# Output:
p1: Point(x=1, y=2, label='a')
p2: Point(x=2, y=3, label='origin')
p1 < p2: True
Mutation error: FrozenInstanceError cannot assign to field 'x' Notes and tips:
- Use
default_factoryinfield()for mutable defaults (e.g., lists). compare=Falseexcludes a field from generated comparison methods.- With
frozen, useobject.__setattr__inside__post_init__for controlled initialization logic.
27. Compare dataclasses with attrs and with validation libraries like pydantic.
Dataclasses, attrs, and pydantic overlap but serve slightly different needs. Imagine three carpentry tools: dataclasses are a solid hammer (built-in, simple), attrs is a power-tool set with plugins (more features, flexible), and pydantic is a specialized tool that validates and transforms inputs automatically (great for external data). Choose based on simplicity, flexibility, and validation needs.
- dataclasses: part of the standard library (Python 3.7+), minimal boilerplate, good for simple data containers.
- attrs: third-party library that predates dataclasses, offers richer customization, converters, validators, and better performance in some cases.
- pydantic: focuses on validation and parsing (especially useful for user input, APIs). It performs type coercion, deep validation, and useful error messages.
Example illustrating the difference with simple validation of input that may be a string:
# dataclass: manual validation
from dataclasses import dataclass
@dataclass
class Manual:
age: int
def __post_init__(self):
if not isinstance(self.age, int):
raise TypeError("age must be int")
# pydantic: automatic coercion and validation (conceptual snippet)
# from pydantic import BaseModel
# class Person(BaseModel):
# age: int
#
# Person(age="30") # pydantic converts "30" -> 30 automatically and validates
# attrs: built-in validators and converters (conceptual snippet)
# import attr
#
# @attr.s
# class User:
# age = attr.ib(converter=int, validator=attr.validators.instance_of(int))
Expected dataclass run (for the manual example):
# Output:
# Creating Manual(age=30) succeeds if age is int; Manual(age="30") raises TypeError in **post_init** Guidance:
- Use dataclasses for simple, internal data containers.
- Use attrs if you need advanced features, succinct validators, or performance tuning.
- Use pydantic when accepting external data (APIs, config files) that needs parsing/coercion and clear validation errors.
28. What is the difference between an abstract class and an interface in Python?
Python does not have a separate language-level "interface" keyword like Java, but the concept exists. Use the Abstract Base Classes (ABCs) from the abc module to define abstract classes and interface-like contracts. Think of an abstract class as a blueprint house that may include some built-in fixtures (concrete methods), while an interface is a contract listing only required methods. In Python, ABCs can act as either.
- Abstract class (ABC): can declare abstract methods (must be overridden) and provide concrete methods and attributes.
- Interface (concept): a pure contract of method signatures; in Python you can implement this via ABCs with only abstract methods, or via duck typing without formal inheritance.
- Python favors duck typing: satisfying method names can be enough without explicit inheritance.
Example showing ABC acting like an interface:
from abc import ABC, abstractmethod
class Serializer(ABC):
@abstractmethod
def serialize(self, obj) -> str:
pass
@abstractmethod
def deserialize(self, data: str):
pass
class JsonSerializer(Serializer):
def serialize(self, obj) -> str:
import json
return json.dumps(obj)
def deserialize(self, data: str):
import json
return json.loads(data)
js = JsonSerializer()
print(js.serialize({"a": 1}))
print(js.deserialize('{"a":1}')) Expected output:
# Output:
{"a": 1}
{'a': 1} 29. How does Python achieve polymorphism?
Polymorphism means "many forms": the same operation works on different types. Python achieves polymorphism primarily through duck typing and inheritance. In practice you write code that expects a behavior (methods/attributes), not a specific class. A real-world analogy: multiple payment methods (cash, card, mobile) — you call pay() and each method handles it.
- Duck typing: objects with required methods are usable regardless of their class.
- Inheritance and method overriding: subclasses provide specialized implementations for base-class methods.
- Protocols (structural typing): static typing tools (like typing.Protocol) formalize polymorphic behavior.
Example demonstrating polymorphism via duck typing and inheritance:
class Dog:
def speak(self):
return "woof"
class Cat:
def speak(self):
return "meow"
def animal_sound(a):
# We only require that 'a' has a .speak() method
print(a.speak())
animal_sound(Dog())
animal_sound(Cat())
# Inheritance example
class Animal:
def speak(self):
return "..."
class Cow(Animal):
def speak(self):
return "moo"
print(Cow().speak()) Expected output:
# Output:
woof
meow
moo 30. What are Python mixins?
Mixins are small classes intended to provide a focused piece of behavior that you can "mix in" with other classes via multiple inheritance. Think of mixins as accessory tools you can add to a basic device (like adding a camera or GPS to a smartphone). Mixins usually do not represent a standalone entity and are not meant to be instantiated on their own.
- Mixins should be narrowly focused (e.g., logging, serialization, caching).
- Prefer composition for complex behavior; use mixins when behavior naturally composes and is reusable across classes.
- Be mindful of method resolution order (MRO) when combining multiple mixins.
Example: a LoggingMixin that adds a simple log() method to several classes:
class LoggingMixin:
def log(self, message):
print(f"[{self.__class__.__name__}] {message}")
class Service(LoggingMixin):
def run(self):
self.log("service started")
class Worker(LoggingMixin):
def work(self):
self.log("working...")
s = Service()
s.run()
w = Worker()
w.work()Expected output:
# Output:
[Service] service started
[Worker] working... Final notes: In interviews, be explicit about trade-offs: dataclasses are convenient but limited for rich validation (use attrs or pydantic when needed); ABCs provide formal contracts but Python's duck typing is often enough; polymorphism is natural and idiomatic in Python; and mixins are a lightweight way to share behavior — use them carefully to avoid complex MRO issues. Practice writing small examples like the ones above and walk an interviewer through the expected outputs.