Skip to content

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)

  1. When OOP earns its complexity
  2. Class anatomy — intermediate detail
  3. @property — computed and validated attributes
  4. @classmethod vs @staticmethod
  5. Inheritance — design patterns
  6. Multiple inheritance and mixins
  7. Abstract Base Classes (ABCs)
  8. dataclass vs namedtuple vs plain class
  9. Equality, hashing, and collections
  10. Interview data structure classes
  11. 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
print(account)       # __str__ if defined, else __repr__
repr(account)        # __repr__

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):

Record.__mro__
# (Record, JSONMixin, TimestampMixin, object)

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 bundles
  • namedtuple — 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:

def __eq__(self, other): ...
def __hash__(self): ...
# a == b  →  hash(a) == hash(b)

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#

  1. When should you use @property vs a public attribute?
  2. What is the difference between @classmethod and @staticmethod?
  3. When is composition preferred over inheritance?
  4. Why must equal objects have equal hashes?
  5. 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