🌐 Detecting your location…
📢 Advertisement — Configure AdSense in Appearance → Customize → AdSense Settings

Python OOP Complete Guide 2026: Classes, Inheritance and Design Patterns

⏱️7 min read  ·  1,322 words

Object-Oriented Programming (OOP) in Python is powerful, elegant, and essential for building large-scale applications. This complete guide covers classes, inheritance, polymorphism, magic methods, properties, and design patterns that every Python developer should know in 2026.

Classes and Instances

class BankAccount:
    # Class variable (shared by all instances)
    interest_rate = 0.05
    _total_accounts = 0

    def __init__(self, owner: str, initial_balance: float = 0.0):
        self.owner = owner          # instance variable
        self._balance = initial_balance  # convention: protected
        self.__id = BankAccount._total_accounts  # private (name-mangled)
        BankAccount._total_accounts += 1

    @property
    def balance(self) -> float:
        return self._balance

    @balance.setter
    def balance(self, amount: float) -> None:
        if amount < 0:
            raise ValueError("Balance cannot be negative")
        self._balance = amount

    def deposit(self, amount: float) -> "BankAccount":
        if amount <= 0:
            raise ValueError(f"Deposit amount must be positive, got {amount}")
        self._balance += amount
        return self  # enables method chaining

    def withdraw(self, amount: float) -> "BankAccount":
        if amount > self._balance:
            raise ValueError("Insufficient funds")
        self._balance -= amount
        return self

    def apply_interest(self) -> "BankAccount":
        self._balance *= (1 + self.interest_rate)
        return self

    @classmethod
    def get_total_accounts(cls) -> int:
        return cls._total_accounts

    @staticmethod
    def is_valid_amount(amount: float) -> bool:
        return amount > 0

    def __str__(self) -> str:
        return f"BankAccount(owner={self.owner!r}, balance={self._balance:.2f})"

    def __repr__(self) -> str:
        return f"BankAccount(owner={self.owner!r}, balance={self._balance!r})"

# Usage
account = BankAccount("Alice", 1000.0)
account.deposit(500).apply_interest().withdraw(200)  # method chaining
print(account)  # BankAccount(owner='Alice', balance=1365.00)
print(BankAccount.get_total_accounts())  # 1

Inheritance and Polymorphism

from abc import ABC, abstractmethod
from typing import Protocol

# Abstract base class
class Animal(ABC):
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age

    @abstractmethod
    def speak(self) -> str:
        pass

    @abstractmethod
    def move(self) -> str:
        pass

    def __str__(self) -> str:
        return f"{type(self).__name__}(name={self.name!r})"

class Dog(Animal):
    def speak(self) -> str:
        return f"{self.name} says: Woof!"

    def move(self) -> str:
        return f"{self.name} runs"

    def fetch(self, item: str) -> str:
        return f"{self.name} fetched the {item}!"

class Cat(Animal):
    def speak(self) -> str:
        return f"{self.name} says: Meow!"

    def move(self) -> str:
        return f"{self.name} slinks"

# Polymorphism
animals: list[Animal] = [Dog("Rex", 3), Cat("Whiskers", 5), Dog("Buddy", 2)]

for animal in animals:
    print(animal.speak())   # each calls its own speak()
    print(animal.move())

# isinstance checks
for animal in animals:
    if isinstance(animal, Dog):
        print(animal.fetch("ball"))

# Multiple inheritance with MRO
class Flyable:
    def fly(self) -> str:
        return f"{self.name} flies"  # type: ignore

class FlyingDog(Dog, Flyable):
    def move(self) -> str:
        return f"{self.name} runs and flies!"

super_dog = FlyingDog("Krypto", 4)
print(super_dog.fly())  # from Flyable
print(FlyingDog.__mro__)  # Method Resolution Order

Magic Methods (Dunder Methods)

from functools import total_ordering

@total_ordering  # auto-generates __le__, __lt__, __ge__, __gt__ from __eq__ and __lt__
class Temperature:
    def __init__(self, celsius: float):
        self._celsius = celsius

    @classmethod
    def from_fahrenheit(cls, f: float) -> "Temperature":
        return cls((f - 32) * 5 / 9)

    @property
    def celsius(self) -> float:
        return self._celsius

    @property
    def fahrenheit(self) -> float:
        return self._celsius * 9 / 5 + 32

    # String representation
    def __str__(self) -> str:
        return f"{self._celsius:.1f}C"

    def __repr__(self) -> str:
        return f"Temperature({self._celsius!r})"

    # Comparison
    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Temperature):
            return NotImplemented
        return self._celsius == other._celsius

    def __lt__(self, other: "Temperature") -> bool:
        return self._celsius < other._celsius

    # Arithmetic
    def __add__(self, other: "Temperature") -> "Temperature":
        return Temperature(self._celsius + other._celsius)

    def __mul__(self, factor: float) -> "Temperature":
        return Temperature(self._celsius * factor)

    # Container protocol
    def __len__(self) -> int:
        return 1

    # Context manager protocol
    def __enter__(self) -> "Temperature":
        print(f"Measuring temperature: {self}")
        return self

    def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
        print("Done measuring")
        return False  # don't suppress exceptions

# Usage
t1 = Temperature(20)
t2 = Temperature.from_fahrenheit(98.6)
print(sorted([t2, t1]))   # uses __lt__
print(t1 + t2)            # uses __add__
print(t1 < t2)            # True (20 < 37)

with Temperature(37.0) as body_temp:
    print(f"Normal? {body_temp.celsius == 37.0}")

Properties and Descriptors

class Validator:
    def __set_name__(self, owner, name: str):
        self.name = name
        self.private_name = f"_{name}"

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return getattr(obj, self.private_name, None)

    def __set__(self, obj, value):
        self.validate(value)
        setattr(obj, self.private_name, value)

    def validate(self, value):
        pass  # override in subclasses

class PositiveNumber(Validator):
    def validate(self, value):
        if not isinstance(value, (int, float)) or value <= 0:
            raise ValueError(f"{self.name} must be a positive number, got {value!r}")

class NonEmptyString(Validator):
    def validate(self, value):
        if not isinstance(value, str) or not value.strip():
            raise ValueError(f"{self.name} must be a non-empty string, got {value!r}")

class Product:
    name = NonEmptyString()
    price = PositiveNumber()
    quantity = PositiveNumber()

    def __init__(self, name: str, price: float, quantity: int):
        self.name = name     # triggers NonEmptyString.__set__
        self.price = price   # triggers PositiveNumber.__set__
        self.quantity = quantity

    @property
    def total_value(self) -> float:
        return self.price * self.quantity

# Usage
product = Product("Widget", 9.99, 100)
print(product.total_value)  # 999.0
product.price = -5  # raises ValueError

Design Patterns in Python

# Observer pattern
from __future__ import annotations
from typing import Protocol

class Observer(Protocol):
    def update(self, event: str, data: dict) -> None: ...

class EventEmitter:
    def __init__(self):
        self._listeners: dict[str, list[Observer]] = {}

    def on(self, event: str, listener: Observer) -> None:
        self._listeners.setdefault(event, []).append(listener)

    def off(self, event: str, listener: Observer) -> None:
        self._listeners.get(event, []).remove(listener)

    def emit(self, event: str, data: dict = {}) -> None:
        for listener in self._listeners.get(event, []):
            listener.update(event, data)

# Strategy pattern
class SortStrategy(Protocol):
    def sort(self, data: list) -> list: ...

class BubbleSort:
    def sort(self, data: list) -> list:
        data = data.copy()
        n = len(data)
        for i in range(n):
            for j in range(0, n-i-1):
                if data[j] > data[j+1]:
                    data[j], data[j+1] = data[j+1], data[j]
        return data

class Sorter:
    def __init__(self, strategy: SortStrategy):
        self.strategy = strategy

    def sort(self, data: list) -> list:
        return self.strategy.sort(data)

sorter = Sorter(BubbleSort())
print(sorter.sort([3, 1, 4, 1, 5, 9]))

Dataclasses — Modern Python OOP

from dataclasses import dataclass, field
from typing import ClassVar

@dataclass
class Point:
    x: float
    y: float

    def distance_to(self, other: "Point") -> float:
        return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5

    def __add__(self, other: "Point") -> "Point":
        return Point(self.x + other.x, self.y + other.y)

@dataclass(order=True, frozen=True)  # immutable + comparable
class Version:
    major: int
    minor: int
    patch: int = 0
    VERSION_PATTERN: ClassVar[str] = r"(\d+)\.(\d+)\.?(\d+)?"

    def __str__(self) -> str:
        return f"{self.major}.{self.minor}.{self.patch}"

p1, p2 = Point(0, 0), Point(3, 4)
print(p1.distance_to(p2))  # 5.0

v1, v2 = Version(1, 2, 3), Version(2, 0, 0)
print(sorted([v2, v1]))  # [Version(1,2,3), Version(2,0,0)]

Python OOP in 2026 combines traditional class-based patterns with modern dataclasses, protocols for structural typing, and descriptors for reusable validation. Master these patterns and you’ll write cleaner, more maintainable Python at any scale.

✍️ Leave a Comment

Your email address will not be published. Required fields are marked *

🌐 Read in:🇬🇧 English🇩🇪 Deutsch🇧🇷 Português🇸🇦 العربية🇮🇳 हिन्दी🇧🇩 বাংলা