OOP: Classes, Inheritance & Polymorphism
Master Python Object-Oriented Programming – classes, objects, inheritance, polymorphism, encapsulation, abstraction, dunder methods, properties, and production‑grade OOP patterns with real‑world examples and practice problems.
📌 What you'll learn: Object-Oriented Programming (OOP) is a paradigm that organizes code around objects — bundles of data (attributes) and behavior (methods). Python's OOP system lets you model real‑world entities, promote code reuse through inheritance, and build maintainable systems through encapsulation and polymorphism. This page covers the full OOP stack: from basic class syntax to abstract base classes and production patterns.
🎯 Goal: Write clean, reusable, and extensible code using Python's OOP features — whether you're building a simple CLI app, a web framework, or an enterprise‑grade system with thousands of classes.
Encapsulation
Bundling data and methods inside a class; restricting direct access to internal state using conventions and properties.
Inheritance
Creating new classes from existing ones; reusing and extending functionality without rewriting code.
Polymorphism
Different classes implementing the same interface; writing code that works with any object that follows a contract.
Abstraction
Hiding complex implementation details behind simple interfaces; exposing only what's necessary.
Classes & Objects
Encapsulation
Inheritance
Polymorphism
Dunder Methods
Advanced OOP
Production
Why OOP Matters in the Real World
In procedural code, data and functions are separate — as the program grows, keeping track of which function modifies which data becomes a nightmare. OOP bundles data with the functions that operate on it, creating self‑contained units (objects) that are easier to reason about, test, and reuse.
Without OOP, a banking application with 100,000 lines would have global variables scattered everywhere — changing one could break dozens of functions. OOP's encapsulation protects data, inheritance eliminates duplication, and polymorphism lets you swap implementations without rewriting code.
Production systems use OOP to model domain entities (User, Order, Payment), implement design patterns (Factory, Strategy, Observer), and enforce contracts through abstract base classes. Frameworks like Django, FastAPI, and SQLAlchemy are built entirely on OOP — understanding it is essential for professional Python development.
1 Classes & Objects – The Blueprint and the Instance
A class is a blueprint for creating objects. An object (or instance) is a concrete realization of that blueprint with its own unique data. The __init__ method (constructor) initializes instance attributes, and self refers to the current instance.
class BankAccount:
"""A simple bank account class."""
bank_name = "National Bank" # class attribute (shared)
def __init__(self, owner: str, balance: float = 0.0):
self.owner = owner # instance attribute
self.balance = balance # instance attribute
def deposit(self, amount: float) -> float:
"""Add money to the account."""
self.balance += amount
return self.balance
def withdraw(self, amount: float) -> float:
"""Withdraw money if sufficient balance."""
if amount > self.balance:
raise ValueError("Insufficient funds")
self.balance -= amount
return self.balance
# Creating objects (instances)
alice_acc = BankAccount("Alice", 1000.0)
bob_acc = BankAccount("Bob", 500.0)
print(alice_acc.owner) # Alice
print(bob_acc.bank_name) # National Bank (shared)
alice_acc.deposit(250) # 1250.0
bob_acc.withdraw(100) # 400.0
bank_name is shared like a common label on all cookies.2 Encapsulation – Protecting Your Data
Encapsulation means restricting direct access to an object's internal state. In Python, we use conventions: a single underscore _ for protected (internal use) and double underscore __ for name mangling (harder to access accidentally). Use @property for controlled access with getters/setters.
class Temperature:
def __init__(self, celsius: float):
self._celsius = celsius # protected attribute (convention)
@property
def celsius(self) -> float:
"""Getter – allows read access."""
return self._celsius
@celsius.setter
def celsius(self, value: float):
"""Setter – validates before setting."""
if value < -273.15:
raise ValueError("Below absolute zero!")
self._celsius = value
@property
def fahrenheit(self) -> float:
"""Computed property – no setter needed."""
return self._celsius * 9/5 + 32
temp = Temperature(25)
print(temp.celsius) # 25 (via getter)
print(temp.fahrenheit) # 77.0 (computed)
temp.celsius = 100 # via setter (validation passes)
# temp.celsius = -500 # ❌ ValueError: Below absolute zero!
account.balance = -999999. Encapsulation lets you validate changes (e.g., balance can't go below minimum) and log every modification — critical for audit trails in production.3 Inheritance – Reuse & Extend
Inheritance lets a child class (subclass) inherit attributes and methods from a parent class (superclass). Use super() to call the parent's methods. This eliminates code duplication and creates logical hierarchies.
class Employee:
def __init__(self, name: str, salary: float):
self.name = name
self.salary = salary
def work(self) -> str:
return f"{self.name} is working."
def get_annual_bonus(self) -> float:
return self.salary * 0.10 # 10% bonus
class Developer(Employee):
def __init__(self, name: str, salary: float, language: str):
super().__init__(name, salary) # call parent constructor
self.language = language
def work(self) -> str: # override parent method
return f"{self.name} is coding in {self.language}."
def get_annual_bonus(self) -> float:
return self.salary * 0.15 # developers get 15%
class Manager(Employee):
def __init__(self, name: str, salary: float, team_size: int):
super().__init__(name, salary)
self.team_size = team_size
def work(self) -> str:
return f"{self.name} is managing a team of {self.team_size}."
dev = Developer("Alice", 80000, "Python")
mgr = Manager("Bob", 100000, 5)
print(dev.work()) # Alice is coding in Python.
print(mgr.get_annual_bonus()) # 10000.0
Product, DigitalProduct, and PhysicalProduct. All share name, price, but PhysicalProduct adds weight for shipping, while DigitalProduct adds download_link. Inheritance avoids repeating the common fields across all three classes.4 Polymorphism – One Interface, Many Forms
Polymorphism means "many shapes." In Python, it's achieved through duck typing: if an object has the required method, it can be used regardless of its class. This lets you write generic code that works with any object that follows a contract.
class PayPal:
def pay(self, amount: float) -> str:
return f"Paid ${amount:.2f} via PayPal."
class Stripe:
def pay(self, amount: float) -> str:
return f"Paid ${amount:.2f} via Stripe."
class CryptoWallet:
def pay(self, amount: float) -> str:
return f"Paid ${amount:.2f} via Crypto."
def process_payment(payment_method, amount: float):
"""Works with ANY object that has a pay() method."""
return payment_method.pay(amount)
# All three work — polymorphic behavior
print(process_payment(PayPal(), 99.99)) # Paid $99.99 via PayPal.
print(process_payment(Stripe(), 49.50)) # Paid $49.50 via Stripe.
print(process_payment(CryptoWallet(), 200)) # Paid $200.00 via Crypto.
5 Dunder Methods – Operator Overloading & More
Dunder (double underscore) methods like __str__, __repr__, __eq__, __add__, and __len__ let you define how your objects behave with built‑in Python operations — making them feel like native types.
from dataclasses import dataclass
class Vector:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
def __repr__(self) -> str: # unambiguous representation
return f"Vector({self.x}, {self.y})"
def __str__(self) -> str: # human‑readable
return f"({self.x}, {self.y})"
def __add__(self, other: 'Vector') -> 'Vector':
return Vector(self.x + other.x, self.y + other.y)
def __eq__(self, other: 'Vector') -> bool:
return self.x == other.x and self.y == other.y
def __abs__(self) -> float:
return (self.x**2 + self.y**2) ** 0.5
v1 = Vector(3, 4)
v2 = Vector(1, 2)
print(v1 + v2) # (4, 6)
print(abs(v1)) # 5.0
print(v1 == Vector(3, 4)) # True
| Dunder Method | Purpose | Example Usage |
|---|---|---|
__init__ | Constructor | obj = MyClass() |
__str__ | String for users | print(obj) |
__repr__ | String for developers | repr(obj) |
__eq__ | Equality comparison | obj1 == obj2 |
__add__ | Addition | obj1 + obj2 |
__len__ | Length | len(obj) |
__getitem__ | Indexing | obj[0] |
__enter__/__exit__ | Context manager | with obj as o: |
6 Advanced OOP – Dataclasses, ABCs & Mixins
Dataclasses auto‑generate __init__, __repr__, __eq__. Abstract Base Classes (ABCs) enforce that subclasses implement certain methods. Mixins add reusable functionality through multiple inheritance without creating a full parent class.
from dataclasses import dataclass, field
from abc import ABC, abstractmethod
# Dataclass – reduces boilerplate
@dataclass(frozen=True) # immutable
class Point:
x: float
y: float
label: str = "origin" # default value
# Abstract Base Class – enforces interface
class Shape(ABC):
@abstractmethod
def area(self) -> float:
pass
@abstractmethod
def perimeter(self) -> float:
pass
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
return 3.14159 * self.radius ** 2
def perimeter(self) -> float:
return 2 * 3.14159 * self.radius
# Mixin – reusable behavior
class LoggerMixin:
def log(self, message: str):
print(f"[{self.__class__.__name__}] {message}")
class DatabaseConnection(LoggerMixin):
def connect(self):
self.log("Connecting to database...")
# connection logic here
db = DatabaseConnection()
db.connect() # [DatabaseConnection] Connecting to database...
models.Model). Mixins power features like logging, serialization, and authentication that can be "mixed in" to any class.7 Production‑Grade OOP Patterns
Real‑world Python projects use OOP to implement design patterns that solve recurring problems. Here are three you'll encounter frequently.
# 1. FACTORY PATTERN – create objects without specifying exact class
class PaymentFactory:
@staticmethod
def create(method: str):
if method == "paypal":
return PayPal()
elif method == "stripe":
return Stripe()
raise ValueError(f"Unknown method: {method}")
# 2. SINGLETON PATTERN – ensure only one instance exists
class DatabasePool:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.connections = []
return cls._instance
# 3. DEPENDENCY INJECTION – pass dependencies instead of hardcoding
class EmailService:
def send(self, to: str, body: str):
print(f"Sending email to {to}: {body}")
class UserRegistration:
def __init__(self, email_service: EmailService):
self.email_service = email_service # injected dependency
def register(self, user_email: str):
# registration logic...
self.email_service.send(user_email, "Welcome!")
# Usage
email_svc = EmailService()
registration = UserRegistration(email_svc)
registration.register("alice@example.com")
EmailService in tests without sending real emails.Solved Practice Problems
🔰 Easy (3 problems)
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def greet(self):
return f"Hi, I'm {self.name}."
p = Person("Alice", 30)
print(p.greet()) # Hi, I'm Alice.class Rectangle:
def __init__(self, w, h):
self.w = w
self.h = h
def area(self):
return self.w * self.h
r = Rectangle(5, 3)
print(r.area()) # 15class Animal:
def speak(self): return "Some sound"
class Dog(Animal):
def speak(self): return "Woof!"
d = Dog()
print(d.speak()) # Woof!⚡ Intermediate (3 problems)
class BankAccount:
def __init__(self, balance=0):
self._balance = balance
@property
def balance(self): return self._balance
def deposit(self, amt):
self._balance += amt
acc = BankAccount(100)
acc.deposit(50)
print(acc.balance) # 150class Circle:
def __init__(self, r): self.r = r
def area(self): return 3.14 * self.r**2
class Square:
def __init__(self, s): self.s = s
def area(self): return self.s**2
def print_area(shape):
print(shape.area())
print_area(Circle(5)) # 78.5
print_area(Square(4)) # 16from dataclasses import dataclass
@dataclass
class Config:
host: str = "localhost"
port: int = 8080
debug: bool = False
cfg = Config(port=3000)
print(cfg) # Config(host='localhost', port=3000, debug=False)🚀 Advanced (3 problems)
from abc import ABC, abstractmethod
class Plugin(ABC):
@abstractmethod
def run(self, data): pass
class LogPlugin(Plugin):
def run(self, data):
print(f"Logging: {data}")
class AlertPlugin(Plugin):
def run(self, data):
print(f"Alert: {data}")
plugins = [LogPlugin(), AlertPlugin()]
for p in plugins:
p.run("system event")class DatabaseConnection:
def __init__(self, url): self.url = url
def __enter__(self):
print("Connecting..."); return self
def __exit__(self, *args):
print("Closing connection")
with DatabaseConnection("db://...") as conn:
print("Running query...")class MockEmailService:
def send(self, to, body):
print(f"Mock: Would send to {to}")
class UserRegistration:
def __init__(self, email_svc):
self.email_svc = email_svc
def register(self, email):
self.email_svc.send(email, "Welcome!")
# In tests, inject the mock
reg = UserRegistration(MockEmailService())
reg.register("test@test.com")Unsolved Practice Problems
🔰 Easy (3 problems)
1. Student Class
Create a Student class with name and grades (list). Add a method average() that returns the average grade.
2. Car Inherits from Vehicle
Create a Vehicle class with speed attribute. Create a Car subclass that adds a brand attribute and overrides a info() method.
3. Book Class with __str__
Define a Book class with title, author. Override __str__ to return "title by author".
⚡ Intermediate (3 problems)
4. Temperature Converter with @property
Create a Temperature class with a private _celsius attribute. Add properties for celsius (get/set with validation) and fahrenheit (computed, read‑only).
5. Polymorphic Payment System
Create CreditCard, UPI, and NetBanking classes — each with a pay(amount) method. Write a checkout(cart, payment_method) function that works with any of them.
6. Dataclass with Validation in __post_init__
Create a dataclass Email with fields address and subject. In __post_init__, validate that address contains '@'.
🚀 Advanced (3 problems)
7. Abstract Notification System
Create an abstract base class Notification with abstract method send(message). Implement EmailNotification, SMSNotification, and PushNotification subclasses.
8. Custom List Class with Dunder Methods
Create a CustomList class that wraps a list and implements __len__, __getitem__, __setitem__, __add__ (concatenation), and __eq__.
9. Factory for Database Connections
Implement a ConnectionFactory that returns different database connection objects (MySQLConnection, PostgresConnection, SQLiteConnection) based on a URL prefix — all implementing the same interface.