Skip to content

Introduction to OOPs#

Object-oriented programming (OOP) models data and behavior together in classes. Interviews use OOP when problems define entities with state (trees, linked lists, LRU cache, Trie) or when the platform expects a Solution class. This page covers classes, inheritance, polymorphism, dunder methods, and multiple design approaches.

How to use this page

Learn __init__, instance vs class attributes, and basic inheritance. Advanced OOP: Object Oriented Programming.

At a glance
Track Python Basics
Sections 11 major topics
Outline Use the right-hand TOC to jump

Topics: OOP core concepts mapped to Python · Classes and objects · Instance vs class attributes · Methods — instance, class, static · Encapsulation — conventions and @property · Inheritance and super() · Composition vs inheritance · Dunder (magic) methods — essential subset · … (+3 more)

  1. OOP core concepts mapped to Python
  2. Classes and objects
  3. Instance vs class attributes
  4. Methods — instance, class, static
  5. Encapsulation — conventions and @property
  6. Inheritance and super()
  7. Composition vs inheritance
  8. Dunder (magic) methods — essential subset
  9. OOP in interview data structures
  10. Abstract base classes (preview)
  11. When to use OOP vs functions

OOP core concepts mapped to Python#

Concept Python manifestation
Encapsulation Naming conventions (_private), @property
Inheritance class Child(Parent):
Polymorphism Duck typing + method override
Abstraction ABCs (abc.ABC) — advanced
Composition Objects as attributes (self.engine)

Python prioritizes duck typing: "If it walks like a duck and quacks like a duck, it's a duck." Types matter less than the methods an object supports.


Classes and objects#

class Point:
    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y

    def distance_from_origin(self) -> float:
        return (self.x ** 2 + self.y ** 2) ** 0.5

    def __repr__(self) -> str:
        return f"Point({self.x}, {self.y})"

p = Point(3, 4)
p.distance_from_origin()   # 5.0
Term Meaning
Class Blueprint — defines attributes and methods
Instance Concrete object created from class
__init__ Initializer — configures instance after allocation
self Reference to current instance
Method Function bound to instance
Attribute Data on instance (self.x) or class

Object creation steps (CPython)#

  1. __new__(cls, ...) — allocates object (rarely overridden)
  2. __init__(self, ...) — initializes instance
  3. Reference returned to caller
class Singleton:
    _instance = None
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

Rare in interviews except explicit design questions.


Instance vs class attributes#

class Counter:
    total_created = 0          # class attribute — shared by class

    def __init__(self):
        Counter.total_created += 1
        self.count = 0         # instance attribute — per object

a = Counter()
b = Counter()
a.count += 1
print(a.count)                # 1
print(b.count)                # 0
print(Counter.total_created)  # 2

Attribute lookup order#

instance.__dict__ → class.__dict__ → parent classes (MRO) → AttributeError
class Demo:
    x = "class"
    def __init__(self):
        self.x = "instance"

Demo().x   # "instance" — instance shadows class

Interview trap — mutable class attributes

class Bad:
    items = []    # ONE list shared by ALL instances

a, b = Bad(), Bad()
a.items.append(1)
print(b.items)    # [1]

# Fix
class Good:
    def __init__(self):
        self.items = []

Methods — instance, class, static#

class BankAccount:
    interest_rate = 0.02       # class attribute

    def __init__(self, balance: float = 0):
        self.balance = balance

    def deposit(self, amount: float) -> None:      # instance method
        if amount <= 0:
            raise ValueError("amount must be positive")
        self.balance += amount

    @classmethod
    def from_string(cls, s: str) -> "BankAccount":  # alternative constructor
        balance = float(s)
        return cls(balance)

    @staticmethod
    def validate_amount(amount: float) -> bool:     # no self/cls
        return amount > 0
Type First arg Typical use
Instance method self Read/write instance state
@classmethod cls Factory methods, alternate constructors
@staticmethod none Utility in class namespace
acc = BankAccount(100)
acc2 = BankAccount.from_string("250.50")
BankAccount.validate_amount(50)

Details: Class Methods and Static Methods.


Encapsulation — conventions and @property#

Python has no true private fields:

Convention Meaning Example access
name Public obj.name
_name Internal — "don't touch" Still accessible
__name Name mangled to _ClassName__name Harder accidental access
class User:
    def __init__(self, email: str):
        self._email = email

    @property
    def email(self) -> str:
        return self._email

    @email.setter
    def email(self, value: str) -> None:
        if "@" not in value:
            raise ValueError("invalid email")
        self._email = value

Use @property when validation or computed attributes are needed — not for every field in interviews.


Inheritance and super()#

class Animal:
    def __init__(self, name: str):
        self.name = name

    def speak(self) -> str:
        return "..."

class Dog(Animal):
    def speak(self) -> str:
        return "woof"

class GuideDog(Dog):
    def __init__(self, name: str, owner: str):
        super().__init__(name)   # call Dog/Animal __init__
        self.owner = owner

Method Resolution Order (MRO)#

Python uses C3 linearization — ClassName.__mro__ or help(ClassName):

class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass

D.__mro__   # (D, B, C, A, object)

super() follows MRO — not necessarily "parent class" in simple terms. Deep dive: Method Resolution Order.

Polymorphism#

def greet(pet: Animal):
    print(f"{pet.name} says {pet.speak()}")

greet(Dog("Rex"))      # woof
greet(GuideDog("Max", "Alice"))  # woof — inherited unless overridden

No interface keyword — duck typing:

def process(file_like):
    data = file_like.read()   # anything with .read()

Composition vs inheritance#

Relationship Use Example
is-a Inheritance Dog(Animal)
has-a Composition Car has Engine
# Inheritance — when subtype truly is-a parent
class SortedList(list):
    def append(self, item):
        super().append(item)
        self.sort()

# Composition — usually preferred for flexibility
class Engine:
    def start(self): ...

class Car:
    def __init__(self):
        self.engine = Engine()

    def start(self):
        self.engine.start()

Rule: favor composition when reuse is about combining behaviors, not substitutability.


Dunder (magic) methods — essential subset#

Special methods integrate with Python syntax:

Method Triggered by Purpose
__init__ Class(...) Initialize
__repr__ repr(obj), interactive Developer-facing unambiguous
__str__ str(obj), print User-facing readable
__eq__ == Equality
__hash__ hash(obj), set/dict key Must be consistent with __eq__
__lt__ etc. <, sorting Ordering
__len__ len(obj) Size
__getitem__ obj[key] Indexing
__setitem__ obj[key] = val Assignment
__iter__ for x in obj Iteration
__contains__ x in obj Membership
__call__ obj() Callable instances
class Interval:
    def __init__(self, start: int, end: int):
        self.start = start
        self.end = end

    def __repr__(self) -> str:
        return f"Interval({self.start}, {self.end})"

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Interval):
            return NotImplemented
        return self.start == other.start and self.end == other.end

    def __lt__(self, other: "Interval") -> bool:
        return self.start < other.start

    def __len__(self) -> int:
        return self.end - self.start

__eq__ and __hash__ rule#

If you define __eq__ and want objects in sets/dicts:

  • Equal objects must have equal hash values
  • If __eq__ is defined, default hash may be disabled (__hash__ = None) unless you define __hash__
  • Mutable objects should not be dict keys

Full reference: Dunder or Magic Methods.


OOP in interview data structures#

Linked list node#

class ListNode:
    def __init__(self, val: int = 0, next: "ListNode | None" = None):
        self.val = val
        self.next = next

# Build: 1 -> 2 -> 3
head = ListNode(1, ListNode(2, ListNode(3)))

Tree node#

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

Graph adjacency — class vs dict#

# Approach 1: dict (most common in interviews)
graph: dict[int, list[int]] = defaultdict(list)

# Approach 2: class wrapper
class Graph:
    def __init__(self):
        self.adj: dict[int, list[int]] = defaultdict(list)

    def add_edge(self, u: int, v: int) -> None:
        self.adj[u].append(v)

LRU Cache — multiple implementations#

# Approach 1: OrderedDict
from collections import OrderedDict

class LRUCache:
    def __init__(self, capacity: int):
        self.capacity = capacity
        self.cache: OrderedDict[int, int] = OrderedDict()

    def get(self, key: int) -> int:
        if key not in self.cache:
            return -1
        self.cache.move_to_end(key)
        return self.cache[key]

    def put(self, key: int, value: int) -> None:
        if key in self.cache:
            self.cache.move_to_end(key)
        self.cache[key] = value
        if len(self.cache) > self.capacity:
            self.cache.popitem(last=False)
# Approach 2: dict + doubly linked list (full O(1) — design interview)
# See Design Patterns / LRU discussions

Trie node#

class TrieNode:
    def __init__(self):
        self.children: dict[str, TrieNode] = {}
        self.is_end = False

class Trie:
    def __init__(self):
        self.root = TrieNode()

    def insert(self, word: str) -> None:
        node = self.root
        for ch in word:
            node = node.children.setdefault(ch, TrieNode())
        node.is_end = True

See Tries.


Abstract base classes (preview)#

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 = {}
    def get(self, key): return self._data[key]
    def put(self, key, value): self._data[key] = value

Details: Abstract Base Classes.


When to use OOP vs functions#

Use classes Use functions
Platform template (class Solution) Single algorithm, no persistent state
Design questions (LRU, Trie, Union-Find) Array/string/graph traversals
Multiple methods sharing state One-off transformations
Provided node types (ListNode) Utility helpers
Stateful object across calls Pure functions

Default to functions unless the problem defines a class API or requires persistent mutable state.


Interview traps (quick reference)#

Trap What goes wrong Safe approach
Mutable class attribute Shared list across instances Init in __init__
Forgetting self NameError First param always self
Missing super().__init__ Parent state uninitialized Call super() in child
Over-engineering Class for one function Keep simple
__eq__ without __hash__ Can't use in set/dict Define both or neither
Shallow copy in object Shared nested state copy.deepcopy when needed
Wrong MRO assumption super() surprises Know inheritance chain

Mental model checklist#

  1. What is the difference between class and instance attributes?
  2. When should you use composition over inheritance?
  3. What does super() actually do in multiple inheritance?
  4. Why shouldn't mutable objects be dict keys?
  5. When is a class justified vs a plain function?

What's next#

Topic Page
Intermediate OOP Classes and OOP Basics
Inheritance depth Inheritance and Multiple Inheritance
Custom containers Custom Containers
Design patterns Design Patterns