Classes and OOP Basics#
This page deepens Introdicion to OOPs with production patterns: @property, inheritance design, dataclass, abstract interfaces, and the trade-offs between classes, dataclasses, and named tuples.
How to use this page
Read after Basics OOP. Advanced: Inheritance and Multiple Inheritance, Dunder or Magic Methods.
At a glance
| Track | Python Intermediate |
| Sections | 11 major topics |
| Outline | Use the right-hand TOC to jump |
Topics: When OOP earns its complexity · Class anatomy — intermediate detail · @property — computed and validated attributes · @classmethod vs @staticmethod · Inheritance — design patterns · Multiple inheritance and mixins · Abstract Base Classes (ABCs) · dataclass vs namedtuple vs plain class · … (+3 more)
- When OOP earns its complexity
- Class anatomy — intermediate detail
@property— computed and validated attributes@classmethodvs@staticmethod- Inheritance — design patterns
- Multiple inheritance and mixins
- Abstract Base Classes (ABCs)
dataclassvs namedtuple vs plain class- Equality, hashing, and collections
- Interview data structure classes
- Encapsulation without Java-style privacy
When OOP earns its complexity#
| Use a class | Use a function or dataclass |
|---|---|
| Multiple methods sharing mutable state | One-shot transformation |
| Design problem API (LRU, Trie) | Pure algorithm on inputs |
Platform requires class Solution |
LeetCode-style unless template given |
| Invariants enforced across methods | Simple record with fields |
| Polymorphic family of types | Single implementation |
Default in DSA: functions unless the problem defines a class contract or persistent state.
Class anatomy — intermediate detail#
class Account:
bank_name = "Example Bank" # class attribute
def __init__(self, owner: str, balance: float = 0):
self.owner = owner # instance attribute
self._balance = balance # convention: internal
def deposit(self, amount: float) -> None:
self._validate(amount)
self._balance += amount
def _validate(self, amount: float) -> None:
if amount <= 0:
raise ValueError("amount must be positive")
@property
def balance(self) -> float:
return self._balance
def __repr__(self) -> str:
return f"Account(owner={self.owner!r}, balance={self.balance})"
__repr__ vs __str__#
| Method | Audience | Goal |
|---|---|---|
__repr__ |
Developers | Unambiguous, ideally eval-able |
__str__ |
Users | Readable |
Define __repr__ at minimum — debugging depends on it.
@property — computed and validated attributes#
class Rectangle:
def __init__(self, width: float, height: float):
self.width = width
self.height = height
@property
def area(self) -> float:
return self.width * self.height
class Celsius:
def __init__(self, temp: float):
self._temp = temp
@property
def temp(self) -> float:
return self._temp
@temp.setter
def temp(self, value: float) -> None:
if value < -273.15:
raise ValueError("below absolute zero")
self._temp = value
Use @property when:
- Validation on assignment is required
- Value is computed from other fields
- You may add logic later without breaking callers
Don't wrap every field — public attributes are fine when no invariant exists.
Details: @property Decorators.
@classmethod vs @staticmethod#
class Date:
def __init__(self, year: int, month: int, day: int):
self.year, self.month, self.day = year, month, day
@classmethod
def from_iso(cls, s: str) -> "Date":
year, month, day = map(int, s.split("-"))
return cls(year, month, day)
@staticmethod
def is_leap_year(year: int) -> bool:
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
d = Date.from_iso("2026-07-07")
Date.is_leap_year(2024)
| Decorator | Receives | Use |
|---|---|---|
@classmethod |
cls |
Alternative constructors, factory methods |
@staticmethod |
nothing | Utility grouped with class namespace |
@classmethod can be overridden in subclasses — cls refers to the actual class called.
Inheritance — design patterns#
Template method (outline in base, detail in subclass)#
class Parser:
def parse(self, text: str) -> list:
tokens = self.tokenize(text)
return self.build(tokens)
def tokenize(self, text: str) -> list[str]:
raise NotImplementedError
def build(self, tokens: list[str]) -> list:
raise NotImplementedError
class CSVParser(Parser):
def tokenize(self, text: str) -> list[str]:
return text.split(",")
def build(self, tokens: list[str]) -> list:
return tokens
Composition over inheritance#
class Logger:
def log(self, msg: str) -> None: ...
class Service:
def __init__(self, logger: Logger):
self._logger = logger # has-a
def run(self):
self._logger.log("starting")
Prefer has-a when you combine behaviors; is-a when subtype substitutability is real.
Multiple inheritance and mixins#
class JSONMixin:
def to_json(self) -> str:
import json
return json.dumps(self.__dict__)
class TimestampMixin:
created_at: float
def touch(self) -> None:
import time
self.created_at = time.time()
class Record(JSONMixin, TimestampMixin):
def __init__(self, name: str):
self.name = name
self.touch()
Python resolves methods via MRO (Method Resolution Order):
Use super() in cooperative multiple inheritance — see Method Resolution Order.
Abstract Base Classes (ABCs)#
Enforce interface without implementation:
from abc import ABC, abstractmethod
class Storage(ABC):
@abstractmethod
def get(self, key: str) -> str: ...
@abstractmethod
def put(self, key: str, value: str) -> None: ...
class MemoryStorage(Storage):
def __init__(self):
self._data: dict[str, str] = {}
def get(self, key: str) -> str:
return self._data[key]
def put(self, key: str, value: str) -> None:
self._data[key] = value
# Storage() # TypeError — can't instantiate ABC
Instantiating a subclass that doesn't implement all abstract methods also fails at instantiation time.
Details: Abstract Base Classes.
dataclass vs namedtuple vs plain class#
| Feature | @dataclass |
namedtuple |
Plain class |
|---|---|---|---|
| Boilerplate | Low | Low | High |
| Mutable | Yes (default) | No | Yes |
| Methods | Yes | Limited | Yes |
| Defaults | Yes + field() |
No (use _replace) |
Yes |
| Inheritance | Yes | Awkward | Yes |
__slots__ |
Optional (3.10+) | Implicit | Manual |
from dataclasses import dataclass
from collections import namedtuple
@dataclass(frozen=True, order=True)
class Point:
x: int
y: int
Point2 = namedtuple("Point2", "x y")
class Point3:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x, self.y = x, y
Interview guidance:
dataclass— structured records, config objects, return bundlesnamedtuple— lightweight immutable rows- Plain class — behavior-heavy types (LRU, Trie, custom iterators)
__slots__— memory optimization when creating millions of instances
Syntax details: Intermediate Syntax and Structures.
Equality, hashing, and collections#
If instances go in set or as dict keys:
@dataclass(frozen=True)
class Key:
row: int
col: int
cache: dict[Key, int] = {}
cache[Key(0, 0)] = 1
Mutable objects must not be dict keys. If you define custom __eq__, ensure hash consistency:
Interview data structure classes#
Linked list#
class ListNode:
def __init__(self, val: int = 0, next: "ListNode | None" = None):
self.val = val
self.next = next
Binary tree#
class TreeNode:
def __init__(
self,
val: int = 0,
left: "TreeNode | None" = None,
right: "TreeNode | None" = None,
):
self.val = val
self.left = left
self.right = right
Union-Find (Disjoint Set)#
class UnionFind:
def __init__(self, n: int):
self.parent = list(range(n))
self.rank = [0] * n
def find(self, x: int) -> int:
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, a: int, b: int) -> bool:
ra, rb = self.find(a), self.find(b)
if ra == rb:
return False
if self.rank[ra] < self.rank[rb]:
ra, rb = rb, ra
self.parent[rb] = ra
if self.rank[ra] == self.rank[rb]:
self.rank[ra] += 1
return True
See Graph.
Encapsulation without Java-style privacy#
Python trusts conventions:
class Wallet:
def __init__(self):
self._balance = 0 # internal
self.__pin = "1234" # mangled to _Wallet__pin
def __getattr__(self, name: str):
if name == "legacy_api":
return self._deprecated_method
raise AttributeError(name)
Name mangling (__attr) prevents accidental access in subclasses — not security.
Details: Encapsulation and Access Modifiers.
Interview traps (quick reference)#
| Trap | What goes wrong | Safe approach |
|---|---|---|
| Mutable class attribute | Shared across instances | Init in __init__ |
Missing super().__init__ |
Parent not initialized | Call super in MRO chain |
@property without setter |
AttributeError on assign | Add setter or use public field |
| Mutable object as dict key | TypeError or silent bugs | frozen dataclass or tuple key |
| Deep inheritance tree | Fragile MRO | Composition / mixins sparingly |
| Class for one function | Over-engineering | Use function |
Mental model checklist#
- When should you use
@propertyvs a public attribute? - What is the difference between
@classmethodand@staticmethod? - When is composition preferred over inheritance?
- Why must equal objects have equal hashes?
- What problem does an ABC solve?
What's next#
| Topic | Page |
|---|---|
| Basics OOP intro | Introdicion to OOPs |
| Dunder methods | Dunder or Magic Methods |
| Polymorphism | Polymorphism |
| Design patterns | Design Patterns |