⏱️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.
📋 Table of Contents
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.
📚 You might also like
🔗 Share this article




✍️ Leave a Comment