Insightsโ†’Advance Python : Part 1
Python

Advance Python : Part 1

Gaurav ChopraGaurav ChopraยทNovember 15, 2025

Object-Oriented Python Mastery

Module 1: Why OOP Matters in Python

1.1 Problems with Procedural Code

Before diving into Object-Oriented Programming, let's understand the problems it solves. Consider a simple banking system written in procedural style:

โŒ Procedural Approach (Problems)
# Procedural banking system
# Global variables - hard to manage!
account_numbers = []
account_balances = []
account_holders = []def create_account(holder, initial_balance):
account_num = len(account_numbers) + 1
account_numbers.append(account_num)
account_holders.append(holder)
account_balances.append(initial_balance)
return account_numdef deposit(account_num, amount):
idx = account_numbers.index(account_num)
account_balances[idx] += amountdef withdraw(account_num, amount):
idx = account_numbers.index(account_num)
if account_balances[idx] >= amount:
account_balances[idx] -= amount
return True
return Falsedef get_balance(account_num):
idx = account_numbers.index(account_num)
return account_balances[idx]

Problems:

  • Multiple global lists that must stay synchronized
  • Easy to manipulate balances directly
  • Hard to add new account types
  • No clear data ownership
โœ… OOP Approach (Solution)
class BankAccount:
"""Encapsulates account data and behavior"""

def __init__(self, holder, initial_balance):
self.account_number = self._generate_account_number()
self.holder = holder
self._balance = initial_balance # Protected

def deposit(self, amount):
if amount > 0:
self._balance += amount
return True
return False

def withdraw(self, amount):
if 0 < amount <= self._balance:
self._balance -= amount
return True
return False

def get_balance(self):
return self._balance# Usage
account = BankAccount("Alice", 1000)
account.deposit(500)

Benefits:

  • All related data grouped together
  • Balance is protected from direct manipulation
  • Easy to create multiple accounts
  • Clear ownership and responsibilities

1.2 Benefits of OOP

๐Ÿ”’ Encapsulation

Bundle data and methods together, hiding internal implementation details. Control access through public interfaces.

๐Ÿงฉ Modularity

Organize code into logical, reusable components. Each class has a single, well-defined responsibility.

โ™ป๏ธ Reusability

Create once, use many times. Extend existing classes without modifying them through inheritance.

๐Ÿ”ง Maintainability

Changes in one class don't ripple across the entire codebase. Easy to locate and fix bugs.

Real-World Example: E-commerce System

class Product:
"""Encapsulates product information"""

def __init__(self, name, price, stock):
self.name = name
self._price = price # Protected from direct changes
self._stock = stock

def get_price(self):
"""Controlled access to price"""
return self._price

def apply_discount(self, percentage):
"""Business logic for discounts"""
if 0 < percentage < 100:
self._price *= (1 - percentage / 100)

def is_available(self, quantity=1):
return self._stock >= quantity

def reduce_stock(self, quantity):
if self.is_available(quantity):
self._stock -= quantity
return True
return Falseclass ShoppingCart:
"""Manages customer's shopping cart"""

def __init__(self):
self._items = {} # {product: quantity}

def add_item(self, product, quantity=1):
if product.is_available(quantity):
self._items[product] = self._items.get(product, 0) + quantity
return True
return False

def get_total(self):
return sum(product.get_price() * qty
for product, qty in self._items.items())

def checkout(self):
"""Process the order"""
for product, quantity in self._items.items():
if not product.reduce_stock(quantity):
return False, f"Insufficient stock for {product.name}"
self._items.clear()
return True, "Order placed successfully"# Usage
laptop = Product("Gaming Laptop", 1200, 5)
mouse = Product("Wireless Mouse", 25, 50)cart = ShoppingCart()
cart.add_item(laptop, 1)
cart.add_item(mouse, 2)print(f"Total: ${cart.get_total()}") # Total: $1250
success, message = cart.checkout()
print(message) # Order placed successfully

๐ŸŽฏ Key Takeaway

OOP helps you model real-world entities and their interactions naturally. Each class represents a "thing" with its own data (attributes) and behaviors (methods). This makes your code more intuitive and easier to reason about.

1.3 Python's OOP Philosophy

Python's approach to OOP is unique compared to languages like Java or C++:

"Everything is an Object"

In Python, literally everything is an object - even functions, classes, and modules!

# Functions are objects
def greet():
return "Hello!"print(type(greet)) # <class 'function'>
print(greet.__name__) # greet# Numbers are objects
x = 5
print(x.__add__(3)) # 8 - same as x + 3# Classes are objects
class MyClass:
passprint(type(MyClass)) # <class 'type'>

๐Ÿ’ก Python vs Java/C++

  • No mandatory access modifiers: Python uses conventions (_private, __really_private) instead of strict enforcement
  • Duck typing over interfaces: "If it walks like a duck..." - we'll cover this in Module 2
  • Multiple inheritance: Python supports it cleanly (unlike Java)
  • Flexible and pragmatic: You can mix OOP with procedural and functional styles

๐Ÿ‹๏ธ Lab Exercise: Refactor Procedural to OOP

Task: Refactor the following procedural library management system into an OOP design.

# Procedural code - refactor this!
books = []
borrowed_books = {}def add_book(title, author, isbn):
books.append({'title': title, 'author': author, 'isbn': isbn, 'available': True})def borrow_book(isbn, member_name):
for book in books:
if book['isbn'] == isbn and book['available']:
book['available'] = False
borrowed_books[isbn] = member_name
return True
return Falsedef return_book(isbn):
for book in books:
if book['isbn'] == isbn:
book['available'] = True
del borrowed_books[isbn]
return True
return False

Requirements:

  • Create Book, Member, and Library classes
  • Encapsulate data properly (use private attributes where appropriate)
  • Add methods for common operations
  • Bonus: Add a method to list overdue books
from datetime import datetime, timedeltaclass Book:
"""Represents a book in the library"""

def __init__(self, title, author, isbn):
self.title = title
self.author = author
self.isbn = isbn
self._is_available = True
self._borrowed_by = None
self._due_date = None

def borrow(self, member, days=14):
if self._is_available:
self._is_available = False
self._borrowed_by = member
self._due_date = datetime.now() + timedelta(days=days)
return True
return False

def return_book(self):
self._is_available = True
self._borrowed_by = None
self._due_date = None

def is_available(self):
return self._is_available

def is_overdue(self):
if self._due_date:
return datetime.now() > self._due_date
return False

def __str__(self):
return f"{self.title} by {self.author}"class Member:
"""Represents a library member"""

def __init__(self, name, member_id):
self.name = name
self.member_id = member_id
self._borrowed_books = []

def borrow_book(self, book):
if book.borrow(self):
self._borrowed_books.append(book)
return True
return False

def return_book(self, book):
if book in self._borrowed_books:
book.return_book()
self._borrowed_books.remove(book)
return True
return False

def get_borrowed_books(self):
return self._borrowed_books[:]class Library:
"""Manages the library's book collection"""

def __init__(self, name):
self.name = name
self._books = []
self._members = []

def add_book(self, book):
self._books.append(book)

def register_member(self, member):
self._members.append(member)

def find_book(self, isbn):
for book in self._books:
if book.isbn == isbn:
return book
return None

def get_available_books(self):
return [book for book in self._books if book.is_available()]

def get_overdue_books(self):
return [book for book in self._books if book.is_overdue()]# Usage
library = Library("City Library")# Add books
book1 = Book("1984", "George Orwell", "123456")
book2 = Book("To Kill a Mockingbird", "Harper Lee", "789012")
library.add_book(book1)
library.add_book(book2)# Register member
alice = Member("Alice", "M001")
library.register_member(alice)# Borrow book
alice.borrow_book(book1)
print(f"Available books: {len(library.get_available_books())}")

Module 2: Duck Typing - Python's Superpower

๐Ÿฆ† "If it walks like a duck and quacks like a duck, then it must be a duck"

Duck typing is Python's philosophy: we don't care what an object is, we care what it can do. If an object has the methods we need, we can use it, regardless of its actual type.

2.1 Understanding Duck Typing

Example: The Power of Duck Typing

# We don't need to define a formal interface!
def process_payment(payment_method, amount):
"""
This function works with ANY object that has a charge() method
It doesn't care if it's CreditCard, PayPal, or Bitcoin
"""
result = payment_method.charge(amount)
return result# Different payment implementations
class CreditCard:
def __init__(self, card_number):
self.card_number = card_number

def charge(self, amount):
return f"Charged ${amount} to card ****{self.card_number[-4:]}"class PayPal:
def __init__(self, email):
self.email = email

def charge(self, amount):
return f"Charged ${amount} to PayPal account {self.email}"class Bitcoin:
def __init__(self, wallet_address):
self.wallet_address = wallet_address

def charge(self, amount):
btc_amount = amount / 50000 # Simplified conversion
return f"Charged {btc_amount:.4f} BTC to {self.wallet_address[:10]}..."# All work with the same function!
card = CreditCard("1234567890123456")
paypal = PayPal("user@example.com")
bitcoin = Bitcoin("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa")print(process_payment(card, 100))
print(process_payment(paypal, 100))
print(process_payment(bitcoin, 100))
Charged $100 to card ****3456 Charged $100 to PayPal account user@example.com Charged 0.0020 BTC to 1A1zP1eP5Q...

๐Ÿ’ก Contrast with Java

In Java, you'd need to define an interface first:

// Java requires explicit interface
interface PaymentMethod {
String charge(double amount);
}class CreditCard implements PaymentMethod { ... }

Python doesn't require this! Any object with a charge() method will work. This is more flexible but requires more testing.

2.2 Protocols and Implicit Interfaces

Python has many built-in "protocols" - sets of special methods that give objects certain behaviors:

Example: Making Objects "Iterable"

class Playlist:
"""A custom iterable container"""

def __init__(self):
self._songs = []

def add_song(self, song):
self._songs.append(song)

# Implement the iterator protocol
def __iter__(self):
return iter(self._songs)

# Implement the sized protocol
def __len__(self):
return len(self._songs)

# Implement the container protocol
def __contains__(self, song):
return song in self._songs# Now our Playlist behaves like a built-in collection!
playlist = Playlist()
playlist.add_song("Bohemian Rhapsody")
playlist.add_song("Stairway to Heaven")
playlist.add_song("Hotel California")# We can iterate over it
for song in playlist:
print(song)# We can check its length
print(f"Playlist has {len(playlist)} songs")# We can use 'in' operator
if "Bohemian Rhapsody" in playlist:
print("Found it!")

๐Ÿ”ง Common Python Protocols

Protocol Methods Enables
Container __contains__ item in obj
Sized __len__ len(obj)
Iterable __iter__ for item in obj
Sequence __getitem__, __len__ obj[index], slicing
Callable __call__ obj(args)
Context Manager __enter__, __exit__ with obj:

2.3 Writing Generic, Adaptable Code

๐ŸŽฏ EAFP vs LBYL

EAFP: "Easier to Ask Forgiveness than Permission" - Try it and handle errors
LBYL: "Look Before You Leap" - Check conditions before trying

Python favors EAFP because it's more flexible and often faster!

LBYL (Not Pythonic)
def save_to_file(data, file_obj):
# Check if it has write method
if hasattr(file_obj, 'write'):
if callable(file_obj.write):
file_obj.write(data)
else:
raise TypeError("write is not callable")
else:
raise TypeError("No write method")
EAFP (Pythonic)
def save_to_file(data, file_obj):
# Just try it!
try:
file_obj.write(data)
except AttributeError:
raise TypeError(
f"{type(file_obj)} doesn't support write()"
)

Example: File-like Objects

This is why Python's approach is powerful - any object that "quacks like a file" will work:

import iodef process_data(file_like):
"""Works with any object that has read() method"""
data = file_like.read()
return data.upper()# Real file
with open('data.txt', 'r') as f:
result = process_data(f)# String buffer (in-memory file)
string_buffer = io.StringIO("hello world")
result = process_data(string_buffer)# Network stream
import urllib.request
with urllib.request.urlopen('http://example.com') as response:
result = process_data(response)# Custom class
class DatabaseReader:
def read(self):
return "data from database"db_reader = DatabaseReader()
result = process_data(db_reader) # Works!

2.4 Type Hints and Duck Typing

Python 3.8+ introduced the Protocol class for structural typing - giving you the best of both worlds:

Example: Using Protocol for Type Safety

from typing import Protocol, runtime_checkable# Define a protocol (structural type)
@runtime_checkable
class Drawable(Protocol):
"""Anything with a draw() method is Drawable"""
def draw(self) -> str:
...def render(obj: Drawable) -> None:
"""Type checkers will verify obj has draw() method"""
print(obj.draw())# These classes don't explicitly inherit from Drawable
class Circle:
def draw(self):
return "โ—‹"class Square:
def draw(self):
return "โ–ก"class Text:
def draw(self):
return "ABC"# All work because they have draw() method!
render(Circle()) # โ—‹
render(Square()) # โ–ก
render(Text()) # ABC# Runtime check
print(isinstance(Circle(), Drawable)) # True!

๐Ÿ’ก When to Use Protocols

  • Large codebases: Type hints help catch bugs early
  • Team projects: Make expectations explicit
  • Library code: Document what interfaces you expect
  • But remember: Python's power is in flexibility - don't over-specify!

๐Ÿ‹๏ธ Lab Exercise: Payment Processing System

Task: Build a flexible payment processing system using duck typing that accepts any object with a charge() method.

Requirements:

  • Create at least 3 different payment method classes (CreditCard, PayPal, Cryptocurrency)
  • Create a ShoppingCart class with a checkout(payment_method) method
  • The system should work with ANY payment method that has charge(amount)
  • Add error handling for invalid payment methods
  • Bonus: Create a Protocol for type hints
from typing import Protocol, runtime_checkable
from datetime import datetime# Define protocol for type safety (optional)
@runtime_checkable
class PaymentMethod(Protocol):
def charge(self, amount: float) -> dict:
"""Charge the payment method and return transaction details"""
...class CreditCard:
def __init__(self, card_number, cvv, expiry):
self.card_number = card_number
self.cvv = cvv
self.expiry = expiry

def charge(self, amount):
# Simulate card validation
if len(self.cvv) != 3:
return {'success': False, 'error': 'Invalid CVV'}

return {
'success': True,
'transaction_id': f'CC-{datetime.now().timestamp()}',
'amount': amount,
'method': f'Card ****{self.card_number[-4:]}'
}class PayPal:
def __init__(self, email, password):
self.email = email
self._password = password # Private
self._balance = 1000 # Simulate account balance

def charge(self, amount):
if amount > self._balance:
return {'success': False, 'error': 'Insufficient funds'}

self._balance -= amount
return {
'success': True,
'transaction_id': f'PP-{datetime.now().timestamp()}',
'amount': amount,
'method': f'PayPal ({self.email})'
}class Cryptocurrency:
def __init__(self, wallet_address, crypto_type="BTC"):
self.wallet_address = wallet_address
self.crypto_type = crypto_type
self._conversion_rates = {"BTC": 50000, "ETH": 3000}

def charge(self, amount):
crypto_amount = amount / self._conversion_rates[self.crypto_type]

return {
'success': True,
'transaction_id': f'{self.crypto_type}-{datetime.now().timestamp()}',
'amount': amount,
'crypto_amount': f'{crypto_amount:.6f} {self.crypto_type}',
'method': f'{self.crypto_type} ({self.wallet_address[:10]}...)'
}class ShoppingCart:
def __init__(self):
self._items = []

def add_item(self, name, price):
self._items.append({'name': name, 'price': price})

def get_total(self):
return sum(item['price'] for item in self._items)

def checkout(self, payment_method):
"""
Duck typing magic! Works with ANY object that has charge() method
"""
if not self._items:
return {'success': False, 'error': 'Cart is empty'}

total = self.get_total()

# EAFP approach - just try to charge!
try:
result = payment_method.charge(total)

if result.get('success'):
self._items.clear() # Clear cart on success

return result

except AttributeError:
return {
'success': False,
'error': f'{type(payment_method).__name__} is not a valid payment method'
}
except Exception as e:
return {'success': False, 'error': str(e)}# Usage Example
cart = ShoppingCart()
cart.add_item("Laptop", 1200)
cart.add_item("Mouse", 25)
cart.add_item("Keyboard", 75)print(f"Total: ${cart.get_total()}")# Try different payment methods
credit_card = CreditCard("1234567890123456", "123", "12/25")
result = cart.checkout(credit_card)
print(f"Payment result: {result}")# Add items again
cart.add_item("Monitor", 300)
paypal = PayPal("user@example.com", "secret")
result = cart.checkout(paypal)
print(f"Payment result: {result}")# Try with crypto
cart.add_item("Headphones", 150)
btc = Cryptocurrency("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa")
result = cart.checkout(btc)
print(f"Payment result: {result}")# Try with invalid payment method
cart.add_item("Cable", 10)
result = cart.checkout("not a payment method")
print(f"Payment result: {result}")

Module 3: Inheritance and Code Reuse

Inheritance allows you to create new classes based on existing ones, inheriting their attributes and methods while adding or overriding functionality.

3.1 Single Inheritance Patterns

Example: Employee Hierarchy

class Employee:
"""Base class for all employees"""

def __init__(self, name, employee_id, salary):
self.name = name
self.employee_id = employee_id
self._salary = salary

def get_details(self):
return f"{self.name} (ID: {self.employee_id})"

def calculate_bonus(self):
"""Base bonus calculation - 5% of salary"""
return self._salary * 0.05class Developer(Employee):
"""Developer extends Employee with programming skills"""

def __init__(self, name, employee_id, salary, programming_languages):
# Call parent constructor
super().__init__(name, employee_id, salary)
self.programming_languages = programming_languages

def get_details(self):
"""Override to add language info"""
base_details = super().get_details()
langs = ", ".join(self.programming_languages)
return f"{base_details} | Languages: {langs}"

def code_review(self, code):
"""New method specific to developers"""
return f"{self.name} is reviewing code..."class Manager(Employee):
"""Manager extends Employee with team management"""

def __init__(self, name, employee_id, salary, team_size):
super().__init__(name, employee_id, salary)
self.team_size = team_size
self._team_members = []

def calculate_bonus(self):
"""Managers get higher bonus based on team size"""
base_bonus = super().calculate_bonus()
team_bonus = self.team_size * 1000
return base_bonus + team_bonus

def add_team_member(self, employee):
self._team_members.append(employee)

def get_details(self):
base_details = super().get_details()
return f"{base_details} | Team Size: {self.team_size}"# Usage
dev = Developer("Alice", "D001", 80000, ["Python", "JavaScript"])
mgr = Manager("Bob", "M001", 100000, 5)print(dev.get_details())
print(f"Developer bonus: ${dev.calculate_bonus()}")print(mgr.get_details())
print(f"Manager bonus: ${mgr.calculate_bonus()}")
Alice (ID: D001) | Languages: Python, JavaScript Developer bonus: $4000.0 Bob (ID: M001) | Team Size: 5 Manager bonus: $10000.0

๐ŸŽฏ The super() Function

super() gives you access to the parent class without naming it explicitly. This is crucial for:

  • Calling parent constructors: super().__init__(...)
  • Extending parent methods: Get parent's result, then add to it
  • Multiple inheritance: Follows Method Resolution Order (MRO)

3.2 Multiple Inheritance

Python supports multiple inheritance - a class can inherit from multiple parent classes. This is powerful but can be complex!

Example: Mixins for Reusable Functionality

# Mixins provide reusable functionality
class LoggerMixin:
"""Adds logging capability to any class"""

def log(self, message):
print(f"[LOG] {self.__class__.__name__}: {message}")class TimestampMixin:
"""Adds timestamp tracking"""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
from datetime import datetime
self.created_at = datetime.now()

def get_age(self):
from datetime import datetime
return (datetime.now() - self.created_at).secondsclass SerializableMixin:
"""Adds JSON serialization"""

def to_dict(self):
"""Convert object to dictionary"""
return {
key: value for key, value in self.__dict__.items()
if not key.startswith('_')
}# Now combine them!
class User(LoggerMixin, TimestampMixin, SerializableMixin):
"""User class with logging, timestamps, and serialization"""

def __init__(self, username, email):
super().__init__() # Calls TimestampMixin.__init__
self.username = username
self.email = email
self.log(f"Created user: {username}")

def update_email(self, new_email):
old_email = self.email
self.email = new_email
self.log(f"Email changed from {old_email} to {new_email}")# Usage
user = User("alice", "alice@example.com")
user.update_email("alice.smith@example.com")print(f"User data: {user.to_dict()}")
print(f"User age: {user.get_age()} seconds")
[LOG] User: Created user: alice [LOG] User: Email changed from alice@example.com to alice.smith@example.com User data: {'username': 'alice', 'email': 'alice.smith@example.com', 'created_at': datetime.datetime(...)} User age: 0 seconds

โš ๏ธ The Diamond Problem

What happens when two parent classes have the same method?

class A:
def method(self):
return "A"class B(A):
def method(self):
return "B"class C(A):
def method(self):
return "C"class D(B, C):
passd = D()
print(d.method()) # Which one? "B"!
print(D.__mro__) # Method Resolution Order

Python uses C3 Linearization to determine method resolution order (MRO). The MRO is: D โ†’ B โ†’ C โ†’ A โ†’ object

3.3 Composition vs Inheritance

๐ŸŽฏ "Favor Composition Over Inheritance"

Inheritance: "IS-A" relationship (a Dog IS-A Animal)
Composition: "HAS-A" relationship (a Car HAS-A Engine)

Composition is often more flexible and easier to maintain!

Inheritance (Rigid)
class Car:
def start(self):
return "Vroom!"class ElectricCar(Car):
def start(self):
return "Whirr..."class HybridCar(Car):
# How do we combine both?
# This gets messy!
pass

Problems:

  • Hard to add new engine types
  • Can't change engine at runtime
  • Tight coupling
Composition (Flexible)
class Engine:
def start(self):
return "Vroom!"class ElectricEngine:
def start(self):
return "Whirr..."class Car:
def __init__(self, engine):
self.engine = engine

def start(self):
return self.engine.start()# Easy to swap engines!
car1 = Car(Engine())
car2 = Car(ElectricEngine())

Benefits:

  • Easy to add new engines
  • Can swap engines dynamically
  • Loose coupling

Real-World Example: Game Character

# Components (composition)
class HealthComponent:
def __init__(self, max_health):
self.max_health = max_health
self.current_health = max_health

def take_damage(self, amount):
self.current_health = max(0, self.current_health - amount)

def heal(self, amount):
self.current_health = min(self.max_health, self.current_health + amount)

def is_alive(self):
return self.current_health > 0class InventoryComponent:
def __init__(self, capacity):
self.capacity = capacity
self.items = []

def add_item(self, item):
if len(self.items) < self.capacity:
self.items.append(item)
return True
return False

def remove_item(self, item):
if item in self.items:
self.items.remove(item)class CombatComponent:
def __init__(self, attack_power, defense):
self.attack_power = attack_power
self.defense = defense

def attack(self, target):
damage = self.attack_power - target.combat.defense
target.health.take_damage(max(0, damage))
return damage# Character composed of components
class Character:
def __init__(self, name, health, inventory_size, attack, defense):
self.name = name
self.health = HealthComponent(health)
self.inventory = InventoryComponent(inventory_size)
self.combat = CombatComponent(attack, defense)

def get_status(self):
return f"{self.name}: {self.health.current_health}/{self.health.max_health} HP"# Different character types with same components
warrior = Character("Conan", health=150, inventory_size=10, attack=25, defense=10)
mage = Character("Gandalf", health=80, inventory_size=15, attack=30, defense=5)# Easy to add/remove components!
warrior.inventory.add_item("Sword")
warrior.combat.attack(mage)print(warrior.get_status())
print(mage.get_status())

๐Ÿ‹๏ธ Lab Exercise: Employee Management System

Task: Build an employee hierarchy using inheritance.

Requirements:

  • Create base Employee class with name, ID, and salary
  • Create Developer, Designer, and Manager subclasses
  • Each subclass should have unique attributes and methods
  • Override calculate_bonus() differently for each type
  • Add a mixin for email notifications
  • Bonus: Demonstrate composition by adding a Department class

Solution provided in the downloadable materials. Try implementing it yourself first!

Module 4: Polymorphism and Extensibility

Polymorphism means "many forms" - the ability to use different objects interchangeably if they share a common interface.

4.1 Understanding Polymorphism

Example: Shape Hierarchy

import mathclass Shape:
"""Base class for all shapes"""

def area(self):
raise NotImplementedError("Subclasses must implement area()")

def perimeter(self):
raise NotImplementedError("Subclasses must implement perimeter()")class Circle(Shape):
def __init__(self, radius):
self.radius = radius

def area(self):
return math.pi * self.radius ** 2

def perimeter(self):
return 2 * math.pi * self.radiusclass Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height

def area(self):
return self.width * self.height

def perimeter(self):
return 2 * (self.width + self.height)class Triangle(Shape):
def __init__(self, a, b, c):
self.a = a
self.b = b
self.c = c

def area(self):
# Heron's formula
s = (self.a + self.b + self.c) / 2
return math.sqrt(s * (s - self.a) * (s - self.b) * (s - self.c))

def perimeter(self):
return self.a + self.b + self.c# Polymorphism in action!
def print_shape_info(shape):
"""Works with ANY shape - that's polymorphism!"""
print(f"{shape.__class__.__name__}:")
print(f" Area: {shape.area():.2f}")
print(f" Perimeter: {shape.perimeter():.2f}")# Different shapes, same interface
shapes = [
Circle(5),
Rectangle(4, 6),
Triangle(3, 4, 5)
]for shape in shapes:
print_shape_info(shape)
print()
Circle: Area: 78.54 Perimeter: 31.42Rectangle: Area: 24.00 Perimeter: 20.00Triangle: Area: 6.00 Perimeter: 12.00

๐ŸŽฏ Operator Overloading

You can define how operators (+, -, *, ==, etc.) work with your objects:

Example: Custom Vector Class

class Vector:
def __init__(self, x, y):
self.x = x
self.y = y

# Addition: v1 + v2
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)

# Subtraction: v1 - v2
def __sub__(self, other):
return Vector(self.x - other.x, self.y - other.y)

# Multiplication by scalar: v * 2
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)

# Equality: v1 == v2
def __eq__(self, other):
return self.x == other.x and self.y == other.y

# String representation: str(v)
def __str__(self):
return f"Vector({self.x}, {self.y})"

# Representation: repr(v)
def __repr__(self):
return f"Vector({self.x}, {self.y})"

# Length: abs(v)
def __abs__(self):
return math.sqrt(self.x**2 + self.y**2)# Now we can use natural syntax!
v1 = Vector(2, 3)
v2 = Vector(1, 1)v3 = v1 + v2 # Vector(3, 4)
v4 = v1 * 2 # Vector(4, 6)
print(v1 == v2) # False
print(abs(v1)) # 3.605...
print(v3) # Vector(3, 4)

4.2 Abstract Base Classes (ABC)

ABCs let you define interfaces and ensure subclasses implement required methods:

Example: Data Serializer Interface

from abc import ABC, abstractmethod
import json
import csv
import xml.etree.ElementTree as ETclass DataSerializer(ABC):
"""Abstract base class for data serializers"""

@abstractmethod
def serialize(self, data):
"""Convert data to string format"""
pass

@abstractmethod
def deserialize(self, string):
"""Convert string back to data"""
passclass JSONSerializer(DataSerializer):
def serialize(self, data):
return json.dumps(data, indent=2)

def deserialize(self, string):
return json.loads(string)class CSVSerializer(DataSerializer):
def serialize(self, data):
"""data should be a list of dictionaries"""
if not data:
return ""

import io
output = io.StringIO()
writer = csv.DictWriter(output, fieldnames=data[0].keys())
writer.writeheader()
writer.writerows(data)
return output.getvalue()

def deserialize(self, string):
import io
reader = csv.DictReader(io.StringIO(string))
return list(reader)class XMLSerializer(DataSerializer):
def serialize(self, data):
"""data should be a dict"""
root = ET.Element("root")
for key, value in data.items():
child = ET.SubElement(root, key)
child.text = str(value)
return ET.tostring(root, encoding='unicode')

def deserialize(self, string):
root = ET.fromstring(string)
return {child.tag: child.text for child in root}# Polymorphic function works with ANY serializer
def save_and_load(serializer, data):
"""Test serialization round-trip"""
print(f"\n{serializer.__class__.__name__}:")

# Serialize
serialized = serializer.serialize(data)
print(f"Serialized:\n{serialized}")

# Deserialize
deserialized = serializer.deserialize(serialized)
print(f"Deserialized: {deserialized}")# Test with different serializers
data = {"name": "Alice", "age": 30, "city": "NYC"}serializers = [
JSONSerializer(),
XMLSerializer()
]for serializer in serializers:
save_and_load(serializer, data)

๐Ÿ’ก When to Use ABC

  • Defining clear contracts: When you want to enforce that subclasses implement certain methods
  • Framework/library code: When others will extend your classes
  • Complex hierarchies: To prevent bugs from missing implementations
  • But remember: Python's duck typing often makes ABCs unnecessary!

4.3 Building Extensible Systems

Example: Plugin Architecture

from abc import ABC, abstractmethodclass Plugin(ABC):
"""Base class for all plugins"""

@abstractmethod
def execute(self, data):
"""Process the data"""
pass

@property
@abstractmethod
def name(self):
"""Plugin name"""
passclass PluginManager:
"""Manages and executes plugins"""

def __init__(self):
self._plugins = {}

def register(self, plugin):
"""Register a new plugin"""
if not isinstance(plugin, Plugin):
raise TypeError("Must be a Plugin instance")
self._plugins[plugin.name] = plugin
print(f"Registered plugin: {plugin.name}")

def execute_all(self, data):
"""Execute all plugins in sequence"""
result = data
for plugin in self._plugins.values():
print(f"Executing {plugin.name}...")
result = plugin.execute(result)
return result# Example plugins
class UppercasePlugin(Plugin):
@property
def name(self):
return "uppercase"

def execute(self, data):
return data.upper()class RemoveSpacesPlugin(Plugin):
@property
def name(self):
return "remove_spaces"

def execute(self, data):
return data.replace(" ", "")class ReversePlugin(Plugin):
@property
def name(self):
return "reverse"

def execute(self, data):
return data[::-1]# Usage
manager = PluginManager()
manager.register(UppercasePlugin())
manager.register(RemoveSpacesPlugin())
manager.register(ReversePlugin())result = manager.execute_all("hello world")
print(f"Final result: {result}")
Registered plugin: uppercase Registered plugin: remove_spaces Registered plugin: reverse Executing uppercase... Executing remove_spaces... Executing reverse... Final result: DLROWOLLEH

๐Ÿ‹๏ธ Lab Exercise: Data Serializer with Polymorphism

Task: Create a content management system that demonstrates polymorphism.

Requirements:

  • Create an ABC called DataSerializer with serialize() and deserialize() methods
  • Implement at least 3 concrete serializers: JSON, XML, CSV
  • Create a DataManager class that can work with any serializer
  • Demonstrate operator overloading by creating a custom data container class
  • Bonus: Add a plugin system for custom data transformers

Hint: Your solution structure should look like:

  • DataSerializer(ABC) - abstract base
  • JSONSerializer(DataSerializer)
  • XMLSerializer(DataSerializer)
  • CSVSerializer(DataSerializer)
  • DataManager - uses any serializer polymorphically
  • DataContainer - with operator overloading

๐ŸŽ“ Part 1 Capstone Project: Content Management System

Build a complete CMS that demonstrates all OOP concepts from Part 1!

Project Requirements:

  1. Content Types (Inheritance & Polymorphism)
    • Base Content class
    • Subclasses: Article, Video, Image, Podcast
    • Each type has unique attributes and behaviors
    • Common interface for rendering and validation
  2. Storage Backend (Duck Typing)
    • Works with any object that has save() and load() methods
    • Implement: FileStorage, DatabaseStorage (simulated)
    • No explicit interface required - duck typing!
  3. Content Processors (Plugin System)
    • ABC for content filters/processors
    • Plugins: spell checker, image optimizer, video transcoder
    • Dynamic plugin registration
  4. Mixins for Cross-Cutting Concerns
    • TimestampMixin - track creation/modification
    • TaggableMixin - add tags to content
    • SearchableMixin - make content searchable
  5. Operator Overloading
    • Content comparison (==, <, >)
    • String representation (__str__, __repr__)
    • Container protocol (__len__, __contains__)

Example Usage:

cms = CMS(storage=FileStorage())article = Article(
title="Python OOP Guide",
content="...",
author="Alice"
)cms.add_content(article)
cms.register_plugin(SpellCheckPlugin())
cms.process_all_content()results = cms.search("Python")
print(results)

Evaluation Criteria:

  • โœ… Proper use of inheritance hierarchies
  • โœ… Duck typing for flexible backends
  • โœ… Polymorphic behavior across content types
  • โœ… Clean separation of concerns
  • โœ… Extensible plugin architecture
  • โœ… Comprehensive error handling
  • โœ… Well-documented code with docstrings

Bonus Challenges:

  • ๐ŸŒŸ Add user authentication and permissions
  • ๐ŸŒŸ Implement content versioning
  • ๐ŸŒŸ Create a CLI interface
  • ๐ŸŒŸ Add unit tests for all components

๐Ÿ“š Part 1 Summary

What You've Learned:

  • Module 1: Why OOP matters, benefits of encapsulation and modularity
  • Module 2: Duck typing, protocols, and writing generic adaptable code
  • Module 3: Inheritance patterns, multiple inheritance, composition vs inheritance
  • Module 4: Polymorphism, operator overloading, ABCs, and building extensible systems

Key Takeaways:

  1. Encapsulation protects data and hides complexity
  2. Duck typing makes Python flexible - focus on behavior, not types
  3. Inheritance enables code reuse, but composition is often better
  4. Polymorphism lets you write code that works with multiple types
  5. ABCs define contracts when you need explicit interfaces

Next Steps:

Ready to continue? Move on to:

  • Part 2: Python Decorators - Elegant Code Enhancement
  • Part 3: Asynchronous Programming with async/await
Gaurav Chopra
Gaurav Chopra

Gaurav is a Co-Founder of Eightgen AI

Work with us

Found this useful? Let's talk about your build.

We write about what we build. If any of this resonates with a challenge you're facing, book a free 30-minute call โ€” no prep needed.