Polymorphism, Method Overriding, Overloading & Encapsulation

Module 3 β€’ Session 26 β€’ Dynamic Dispatch, Method Overriding, Overloading Strategies, Access Control & Properties

polymorphism method-overriding method-overloading duck-typing dunder-methods encapsulation

Polymorphism and Encapsulation represent core pillars of Object-Oriented Programming in Python. Polymorphism allows objects of different types to respond dynamically to uniform method invocations via Duck Typing, Method Overriding, Method Overloading strategies, and Operator Overloading. Encapsulation restricts direct modification of an object's internal state by bundling data with methods and controlling access via naming conventions, name mangling, and property descriptors.

1. Understanding Polymorphism in Python

The term polymorphism derives from Greek, meaning "many forms." In Python, polymorphism allows functions, operators, and methods to operate seamlessly across different classes without explicit type declarations.

A. Dynamic Duck Typing ("If it walks like a duck...")

Python uses dynamic typing where interface structure takes precedence over strict inheritance hierarchies. If an object responds to expected method calls (e.g., render() or export()), Python executes it regardless of class ancestry.

class PDFExporter: def export(self, data): return f"Rendering '{data}' as PDF document..." class CSVExporter: def export(self, data): return f"Formatting '{data}' as CSV rows..." class HTMLExporter: def export(self, data): return f"<div>{data}</div>" # Polymorphic worker function that relies purely on duck typing def publish_report(exporter, content): print(exporter.export(content)) publish_report(PDFExporter(), "Financial Statement Q3") publish_report(CSVExporter(), "User Metrics") publish_report(HTMLExporter(), "Dashboard Banner")

2. Method Overriding in Python

Method Overriding occurs when a subclass provides a specific implementation for a method that is already defined in its parent base class. When invoked on a child instance, the child's version overrides the base implementation.

A. Basic Method Overriding

Child classes redefine inherited methods to modify or expand behavior suited to specialized requirements:

class PaymentProcessor: def process_payment(self, amount): print(f"Processing generic transaction of ${amount:.2f}") class CreditCardProcessor(PaymentProcessor): def process_payment(self, amount): # Overriding base implementation with credit card specific behavior print(f"Authorizing credit card charge of ${amount:.2f} via Payment Gateway...") class CryptoProcessor(PaymentProcessor): def process_payment(self, amount): # Overriding base implementation with crypto wallet behavior print(f"Broadcasting blockchain transaction for ${amount:.2f} worth of ETH...") # Polymorphic invocation through method overriding processors = [CreditCardProcessor(), CryptoProcessor(), PaymentProcessor()] for proc in processors: proc.process_payment(250.00)

B. Extending Overridden Methods Using super()

Instead of completely replacing a parent method's behavior, a child class can reuse and extend base logic by calling super().method_name():

class Employee: def calculate_bonus(self, base_pay): return base_pay * 0.10 # Standard 10% bonus class Executive(Employee): def calculate_bonus(self, base_pay): # Extend parent logic by adding an executive stock grant bonus standard_bonus = super().calculate_bonus(base_pay) executive_stock_bonus = 5000.0 return standard_bonus + executive_stock_bonus emp = Employee() exec_user = Executive() print("Employee Bonus:", emp.calculate_bonus(50000)) # Output: 5000.0 print("Executive Bonus:", exec_user.calculate_bonus(50000)) # Output: 10000.0

3. Method Overloading in Python

In statically typed languages like Java or C++, Method Overloading allows multiple methods in the same class to share the same name provided they have different parameter signatures (different counts or data types).

Key Distinction: Python does NOT support traditional compile-time method overloading out of the box. If you define multiple methods with the exact same name inside a class, Python overwrites previous definitions with the last defined method.
# Demonstration of Python overwriting identical method names: class CalculatorDemo: def add(self, a, b): return a + b def add(self, a, b, c): # THIS OVERWRITES THE PREVIOUS add() METHOD! return a + b + c calc = CalculatorDemo() # print(calc.add(5, 10)) # Raises TypeError: add() missing 1 required positional argument: 'c' print(calc.add(5, 10, 15)) # Works: 30

How to Achieve Method Overloading in Python

Python developers use three core design patterns to simulate method overloading behavior:

Strategy 1: Using Default Arguments

class GeometryCalculator: def area(self, dimension1, dimension2=None): if dimension2 is not None: # Acts as rectangle area calculation return dimension1 * dimension2 # Acts as square area calculation return dimension1 * dimension1 geo = GeometryCalculator() print("Square Area (1 param):", geo.area(5)) # Output: 25 print("Rectangle Area (2 params):", geo.area(5, 8)) # Output: 40

Strategy 2: Flexible Dispatch with *args and Type Checking

class DataFormatter: def format_input(self, *args): if len(args) == 1 and isinstance(args[0], str): return f"String Output: {args[0].upper()}" elif len(args) == 1 and isinstance(args[0], (int, float)): return f"Numeric Currency: ${args[0]:,.2f}" elif len(args) == 2: return f"Key-Value Pair: {args[0]} => {args[1]}" else: raise TypeError("Unsupported argument combination") fmt = DataFormatter() print(fmt.format_input("python masterclass")) # String Output: PYTHON MASTERCLASS print(fmt.format_input(1250.75)) # Numeric Currency: $1,250.75 print(fmt.format_input("UserCount", 450)) # Key-Value Pair: UserCount => 450

Strategy 3: Dispatching via multipledispatch Decorators

For explicit, type-based method overloading syntax, Python supports the external multipledispatch library using the @dispatch decorator:

from multipledispatch import dispatch class MathOperations: @dispatch(int, int) def multiply(self, x, y): print("Multiplying two Integers:") return x * y @dispatch(float, float) def multiply(self, x, y): print("Multiplying two Floats:") return x * y @dispatch(int, int, int) def multiply(self, x, y, z): print("Multiplying three Integers:") return x * y * z math_op = MathOperations() print(math_op.multiply(4, 5)) # Triggers @dispatch(int, int) print(math_op.multiply(2.5, 4.0)) # Triggers @dispatch(float, float) print(math_op.multiply(2, 3, 4)) # Triggers @dispatch(int, int, int)

4. Operator Overloading via Magic/Dunder Methods

Built-in operators (+, -, *, ==, <, etc.) call double-underscore special methods under the hood. Overriding these special methods enables custom classes to participate seamlessly in Python's operator expressions.

Operator Dunder Method Behavior Description
+ __add__(self, other) Custom addition logic (e.g., adding vectors or money values).
== __eq__(self, other) Value equality check between two custom instances.
str() / print() __str__(self) User-friendly string representation.
repr() __repr__(self) Developer unambiguous string representation for debugging.
len() __len__(self) Returns custom collection length integer.
class Money: def __init__(self, amount, currency="USD"): self.amount = amount self.currency = currency def __add__(self, other): if self.currency != other.currency: raise ValueError("Currency mismatch! Cannot add different currencies.") return Money(self.amount + other.amount, self.currency) def __eq__(self, other): return self.amount == other.amount and self.currency == other.currency def __str__(self): return f"${self.amount:.2f} {self.currency}" def __repr__(self): return f"Money(amount={self.amount}, currency='{self.currency}')" m1 = Money(100.50) m2 = Money(49.50) m3 = m1 + m2 print(m3) # Output: $150.00 USD (calls __str__) print(repr(m1)) # Output: Money(amount=100.5, currency='USD') (calls __repr__) print(m1 == m2) # Output: False (calls __eq__)

5. Encapsulation & Access Control

Encapsulation wraps state data and operational logic into single units (classes) while hiding internal state details from accidental external mutation.

Access Modifiers in Python: Public, Protected, and Private

Unlike languages with strict keywords like public or private, Python uses naming conventions to signal attribute visibility:

  • name (Public): Accessible everywhere without restriction.
  • _name (Protected): Convention signaling that the attribute is intended for internal or subclass use only.
  • __name (Private): Enables Name Mangling, transforming the internal name to _ClassName__name to prevent direct accidental access or override.
class BankAccount: def __init__(self, owner, balance): self.owner = owner # Public self._account_type = "Check"# Protected (convention) self.__balance = balance # Private (Name Mangled) def deposit(self, amount): if amount > 0: self.__balance += amount return True return False def get_balance(self): return self.__balance acc = BankAccount("Snoop", 5000) print(acc.owner) # Works: Snoop print(acc._account_type) # Works (by convention should be avoided outside class) # Direct access to private variable fails with AttributeError: try: print(acc.__balance) except AttributeError as e: print("AttributeError Caught:", e) # Accessing via name mangling (strictly for debugging, avoid in production): print("Mangled access:", acc._BankAccount__balance) # Prints: 5000
Security Note: Python's name mangling (__private) is designed to avoid namespace collisions in inheritance chainsβ€”it is not an encryption or security barrier.

6. Controlled State Management with `@property` Decorators

Python's @property decorator allows methods to be accessed like standard attributes while executing underlying getter, setter, and deleter logic. This gives you validation control without breaking clean attribute syntax.

class Employee: def __init__(self, name, salary): self.name = name self._salary = salary # Internal backing field @property def salary(self): """Getter for salary.""" return self._salary @salary.setter def salary(self, value): """Setter with validation logic.""" if value < 0: raise ValueError("Salary cannot be negative!") self._salary = value @salary.deleter def salary(self): """Deleter routine.""" print(f"Deleting salary record for {self.name}...") del self._salary emp = Employee("Elena", 75000) # Attribute access triggers @property getter method transparently print(f"{emp.name} Salary:", emp.salary) # Assignment triggers @salary.setter with validation emp.salary = 82000 print("Updated Salary:", emp.salary) try: emp.salary = -5000 # Raises ValueError except ValueError as err: print("Validation Error:", err)

7. Feature Comparison Matrix: OOP Concepts

OOP Concept Primary Objective Python Implementation Strategy
Polymorphism Provide uniform interfaces for different underlying data types. Duck Typing, Method Overriding, Method Overloading strategies, Dunder Operator Overloading (e.g., __add__).
Method Overriding Redefine a base class method in a child subclass with custom behavior. Re-declaring the method with identical name in child class; calling super() to preserve base behavior.
Method Overloading Execute different logic based on varying parameter counts/types. Simulated using default argument values, *args dynamic checks, or multipledispatch library.
Encapsulation Bundle state with behavior and control internal variable visibility. Naming conventions (_protected), Name Mangling (__private), and @property decorators.

8. Real-World Enterprise Scenario: Secure Wallet Engine

This example combines polymorphism (duck typing for transaction processors), method overriding (customizing payment rules), and encapsulation (protected balances and property setters for risk limits).

class CryptoWallet: def __init__(self, wallet_id, initial_balance=0.0): self.wallet_id = wallet_id self._balance = float(initial_balance) self._daily_limit = 1000.0 @property def balance(self): return self._balance @property def daily_limit(self): return self._daily_limit @daily_limit.setter def daily_limit(self, limit): if limit <= 0: raise ValueError("Daily limit must be positive.") self._daily_limit = float(limit) def withdraw(self, amount): if amount > self._daily_limit: print(f"Transaction rejected: Exceeds daily limit of ${self._daily_limit:.2f}") return False if amount > self._balance: print("Transaction rejected: Insufficient wallet balance.") return False self._balance -= amount print(f"Successfully withdrew ${amount:.2f}. Remaining balance: ${self._balance:.2f}") return True # Subclass overriding method to bypass daily limits for VIP Wallets class VIPCryptoWallet(CryptoWallet): def withdraw(self, amount): print("[VIP ACCOUNT DETECTED] Bypassing daily limit checks...") if amount > self._balance: print("Transaction rejected: Insufficient wallet balance.") return False self._balance -= amount print(f"VIP payout authorized: ${amount:.2f}. Remaining: ${self._balance:.2f}") return True # Polymorphic processor expecting any object with a .withdraw(amount) interface def execute_vault_payout(wallet_object, payout_amount): print(f"\n[VAULT EXECUTION] Requesting payout of ${payout_amount:.2f}...") wallet_object.withdraw(payout_amount) standard_wallet = CryptoWallet("0x9F...A32", 2500.0) vip_wallet = VIPCryptoWallet("0xVIP...001", 10000.0) execute_vault_payout(standard_wallet, 1500.0) # Rejection due to daily limit execute_vault_payout(vip_wallet, 1500.0) # Success due to method overriding

πŸ‹οΈ Try It Yourself: Practice Challenges

Challenge 1: Method Overriding in Shape Hierarchy

Create a base class Shape with a method area() that returns 0. Create subclasses Rectangle and Circle that override area() with their corresponding geometric formulas.

Challenge 2: Simulated Method Overloading

Write a class AreaCalculator with a method compute() that uses default arguments or *args to calculate area for either a circle (1 argument: radius) or a rectangle (2 arguments: length, width).

Challenge 3: Encapsulation with Temperature Converter

Design a Celsius class that stores temperature in _celsius. Add a fahrenheit property getter and setter that automatically calculates conversion when updated.

⚑ Interactive Sandbox (Overriding, Overloading & Encapsulation)
Console Output:
Click "Run Code" above to execute scripts...

πŸ“ Knowledge Check Quiz

1. What happens if you define two methods with the same name in a single Python class without external libraries?
2. What is the key characteristic of Method Overriding?
3. Which decorator allows a method to act like a readable attribute while preserving encapsulated getter logic?
Advertisement
Horizontal Ad Banner Slot (728x90 / Responsive)