Python • Object-Oriented Programming

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.

Topics
1
2
3
4
5
6
7

📌 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.

01

Classes & Objects

class__init__self
02

Encapsulation

_protected@property
03

Inheritance

super()override
04

Polymorphism

duck typinginterface
05

Dunder Methods

__str____eq____add__
06

Advanced OOP

@dataclassABCMixin
07

Production

factoryDISOLID

Why OOP Matters in the Real World

What problems does OOP solve?

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.

Why is OOP required in large projects?

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.

How OOP helps in production

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.

classes_objects.py
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
🔍 Real‑World Analogy: A class is like a cookie cutter — it defines the shape. Each cookie (object) made from it has the same shape but can have different toppings (attribute values). The class attribute 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.

encapsulation.py
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!
💡 Why encapsulation? In a real banking system, you wouldn't want anyone to directly do 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.

inheritance.py
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
🔍 Real‑World Use: Imagine an e‑commerce platform with 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.

polymorphism.py
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.
💡 Production Impact: Polymorphism is the backbone of dependency injection and strategy patterns. You can swap payment gateways, logging backends, or database drivers without changing the code that uses them — just pass a different object that follows the same interface.
🧠 Duck Typing in Python: "If it walks like a duck and quacks like a duck, it's a duck." Python doesn't check the type — it checks whether the object has the required method. This makes Python OOP flexible and less verbose than languages that require explicit interfaces or abstract classes.

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.

dunder_methods.py
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 MethodPurposeExample Usage
__init__Constructorobj = MyClass()
__str__String for usersprint(obj)
__repr__String for developersrepr(obj)
__eq__Equality comparisonobj1 == obj2
__add__Additionobj1 + obj2
__len__Lengthlen(obj)
__getitem__Indexingobj[0]
__enter__/__exit__Context managerwith 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.

advanced_oop.py
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...
💡 Production Use: Dataclasses are perfect for DTOs (Data Transfer Objects) and configuration. ABCs are used extensively in frameworks (e.g., Django's 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.

production_patterns.py
# 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")
🔍 Why these patterns? Factory centralizes object creation (easy to add new payment methods). Singleton ensures one database connection pool. Dependency Injection makes code testable — you can pass a mock EmailService in tests without sending real emails.

Solved Practice Problems

🔰 Easy (3 problems)

1. Create a Person Class
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.
2. Rectangle Class with Area Method
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())  # 15
3. Dog Inherits from Animal
class Animal:
    def speak(self): return "Some sound"
class Dog(Animal):
    def speak(self): return "Woof!"
d = Dog()
print(d.speak())  # Woof!

⚡ Intermediate (3 problems)

4. BankAccount with Encapsulation
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)  # 150
5. Polymorphic Shape Area Calculation
class 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))  # 16
6. Dataclass for Configuration
from 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)

7. Abstract Base Class for Plugin System
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")
8. Custom Context Manager for Database
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...")
9. Dependency Injection for Testing
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.