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)
- 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
- OOP in interview data structures
- Abstract base classes (preview)
- 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)#
__new__(cls, ...)— allocates object (rarely overridden)__init__(self, ...)— initializes instance- 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#
class Demo:
x = "class"
def __init__(self):
self.x = "instance"
Demo().x # "instance" — instance shadows class
Interview trap — mutable class attributes
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 |
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:
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#
- What is the difference between class and instance attributes?
- When should you use composition over inheritance?
- What does
super()actually do in multiple inheritance? - Why shouldn't mutable objects be dict keys?
- 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 |