A programming paradigm is a fundamental style or approach to programming. It defines how you structure your code, organize logic, and think about problem-solving.
Key paradigms:
Important: Most modern languages support multiple paradigms (multi-paradigm languages).
Model the problem domain as a collection of objects that contain both data (attributes) and behavior (methods). Objects interact through well-defined interfaces.
Bundle data and methods that operate on that data within a single unit (class). Hide internal implementation details.
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.__balance = balance # Private attribute (name mangling)
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)
print(account.get_balance()) # 1500
# Direct access prevented (by convention/name mangling)
# account.__balance # AttributeError
Benefits:
Create new classes based on existing classes, inheriting their attributes and methods. Promotes code reuse.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
raise NotImplementedError("Subclass must implement")
def sleep(self):
return f"{self.name} is sleeping"
class Dog(Animal): # Inherit from Animal
def speak(self):
return f"{self.name} says Woof!"
class Cat(Animal):
def speak(self):
return f"{self.name} says Meow!"
# Usage
dog = Dog("Buddy")
cat = Cat("Whiskers")
print(dog.speak()) # Buddy says Woof!
print(cat.sleep()) # Whiskers is sleeping (inherited)
Types of Inheritance:
"Many forms" - Same interface, different implementations. Allows treating objects of different types uniformly.
# Method overriding (runtime polymorphism)
class Shape:
def area(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
# Polymorphic behavior
shapes = [Rectangle(5, 10), Circle(7), Rectangle(3, 4)]
for shape in shapes:
print(f"Area: {shape.area()}") # Same method, different behavior
# Works because all shapes implement area()
# Don't need to know specific type
Types:
Hide complex implementation details, expose only essential features. Focus on "what" an object does, not "how".
from abc import ABC, abstractmethod
class PaymentProcessor(ABC): # Abstract Base Class
@abstractmethod
def process_payment(self, amount):
"""Process payment - must be implemented by subclasses"""
pass
@abstractmethod
def refund(self, transaction_id, amount):
"""Process refund - must be implemented by subclasses"""
pass
class StripePayment(PaymentProcessor):
def process_payment(self, amount):
# Stripe-specific implementation
return f"Processing ${amount} via Stripe"
def refund(self, transaction_id, amount):
return f"Refunding ${amount} via Stripe"
class PayPalPayment(PaymentProcessor):
def process_payment(self, amount):
# PayPal-specific implementation
return f"Processing ${amount} via PayPal"
def refund(self, transaction_id, amount):
return f"Refunding ${amount} via PayPal"
# Client code works with abstraction
def checkout(processor: PaymentProcessor, amount):
result = processor.process_payment(amount)
print(result)
# Can swap implementations easily
checkout(StripePayment(), 100)
checkout(PayPalPayment(), 100)
Five design principles for maintainable OOP code:
A class should have one, and only one, reason to change.
# Bad: Multiple responsibilities
class User:
def __init__(self, name):
self.name = name
def save_to_database(self): # Database concern
pass
def send_email(self): # Email concern
pass
# Good: Separate concerns
class User:
def __init__(self, name):
self.name = name
class UserRepository:
def save(self, user):
pass
class EmailService:
def send(self, user, message):
pass
Classes should be open for extension, closed for modification.
# Bad: Must modify class to add new discount type
class DiscountCalculator:
def calculate(self, order, discount_type):
if discount_type == "percentage":
return order.total * 0.9
elif discount_type == "fixed":
return order.total - 10
# Good: Extend without modifying
class Discount(ABC):
@abstractmethod
def apply(self, total):
pass
class PercentageDiscount(Discount):
def apply(self, total):
return total * 0.9
class FixedDiscount(Discount):
def apply(self, total):
return total - 10
Subclasses should be substitutable for their base classes without breaking functionality.
# Bad: Violates LSP
class Bird:
def fly(self):
return "Flying"
class Penguin(Bird):
def fly(self):
raise Exception("Penguins can't fly!") # Breaks contract
# Good: Design hierarchy correctly
class Bird:
pass
class FlyingBird(Bird):
def fly(self):
return "Flying"
class Penguin(Bird):
def swim(self):
return "Swimming"
Clients shouldn't be forced to depend on interfaces they don't use.
# Bad: Fat interface
class Worker(ABC):
@abstractmethod
def work(self): pass
@abstractmethod
def eat(self): pass
class Robot(Worker):
def work(self): return "Working"
def eat(self): pass # Robots don't eat! Forced to implement
# Good: Segregated interfaces
class Workable(ABC):
@abstractmethod
def work(self): pass
class Eatable(ABC):
@abstractmethod
def eat(self): pass
class Human(Workable, Eatable):
def work(self): return "Working"
def eat(self): return "Eating"
class Robot(Workable):
def work(self): return "Working"
Depend on abstractions, not concretions. High-level modules shouldn't depend on low-level modules.
# Bad: High-level depends on low-level
class MySQLDatabase:
def save(self, data): pass
class UserService:
def __init__(self):
self.db = MySQLDatabase() # Tightly coupled!
# Good: Depend on abstraction
class Database(ABC):
@abstractmethod
def save(self, data): pass
class MySQLDatabase(Database):
def save(self, data): pass
class UserService:
def __init__(self, database: Database):
self.db = database # Can inject any Database implementation
class Engine:
def start(self):
return "Engine starting"
class Car(Engine): # Car IS-A Engine?
pass
# Problems:
# - Tight coupling
# - Can't change at runtime
# - Fragile base class problem
class Engine:
def start(self):
return "Engine starting"
class Car:
def __init__(self):
self.engine = Engine() # Car HAS-A Engine
def start(self):
return self.engine.start()
# Benefits:
# - Loose coupling
# - Can change at runtime
# - More flexible
Treat computation as the evaluation of mathematical functions. Avoid changing state and mutable data. Functions are first-class citizens.
Same input always produces same output. No side effects (doesn't modify external state).
# Impure function (side effects)
total = 0
def add_to_total(x):
global total
total += x # Modifies external state!
return total
# Pure function (no side effects)
def add(x, y):
return x + y # Only depends on inputs, no state change
# Benefits of pure functions:
# - Testable (no setup needed)
# - Cacheable (memoization)
# - Parallelizable (no shared state)
# - Predictable (referential transparency)
Data cannot be changed after creation. Create new data instead of modifying existing.
# Mutable (imperative style)
numbers = [1, 2, 3]
numbers.append(4) # Modifies original list
# Immutable (functional style)
numbers = (1, 2, 3) # Tuple (immutable)
new_numbers = numbers + (4,) # Creates new tuple
# Python immutable types: int, float, str, tuple, frozenset
# Mutable types: list, dict, set
# Immutability benefits:
# - Thread-safe (no race conditions)
# - Easier to reason about
# - Can share safely
Functions are values - can be assigned to variables, passed as arguments, returned from other functions.
# Assign function to variable
def greet(name):
return f"Hello, {name}"
say_hello = greet
print(say_hello("Alice")) # Hello, Alice
# Pass function as argument
def apply_twice(func, x):
return func(func(x))
def double(x):
return x * 2
print(apply_twice(double, 5)) # 20
# Return function from function
def create_multiplier(n):
def multiplier(x):
return x * n
return multiplier
times_three = create_multiplier(3)
print(times_three(10)) # 30
Functions that take functions as arguments or return functions.
# Map: Apply function to each element
numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x**2, numbers))
# [1, 4, 9, 16, 25]
# Filter: Keep elements matching predicate
evens = list(filter(lambda x: x % 2 == 0, numbers))
# [2, 4]
# Reduce: Combine elements into single value
from functools import reduce
total = reduce(lambda acc, x: acc + x, numbers, 0)
# 15
# List comprehensions (Pythonic alternative)
squared = [x**2 for x in numbers]
evens = [x for x in numbers if x % 2 == 0]
Function that captures variables from enclosing scope.
def make_counter():
count = 0 # Captured by closure
def increment():
nonlocal count
count += 1
return count
return increment
counter1 = make_counter()
counter2 = make_counter()
print(counter1()) # 1
print(counter1()) # 2
print(counter2()) # 1 (separate closure)
Combine simple functions to build complex behavior.
def compose(f, g):
return lambda x: f(g(x))
def add_one(x):
return x + 1
def double(x):
return x * 2
# Compose functions
add_then_double = compose(double, add_one)
print(add_then_double(5)) # (5 + 1) * 2 = 12
# Pipe operator (compose in reverse)
def pipe(*funcs):
def piped(x):
result = x
for func in funcs:
result = func(result)
return result
return piped
process = pipe(add_one, double, lambda x: x - 3)
print(process(5)) # ((5 + 1) * 2) - 3 = 9
Function calls itself. Replaces loops in functional programming.
# Imperative (loop)
def factorial_loop(n):
result = 1
for i in range(1, n + 1):
result *= i
return result
# Functional (recursion)
def factorial_recursive(n):
if n <= 1:
return 1
return n * factorial_recursive(n - 1)
# Tail recursion (optimizable)
def factorial_tail(n, acc=1):
if n <= 1:
return acc
return factorial_tail(n - 1, n * acc)
print(factorial_recursive(5)) # 120
| Benefit | Explanation |
|---|---|
| Easier Testing | Pure functions = no mocks, no setup, deterministic |
| Concurrency | Immutability eliminates race conditions |
| Reasoning | No hidden state changes, easier to understand |
| Modularity | Small, composable functions |
| Debugging | No temporal coupling, easier to reproduce bugs |
Organize code into procedures (functions/subroutines) that operate on data. Focus on step-by-step instructions.
// Procedural style - data and functions separate
struct BankAccount {
char owner[50];
double balance;
};
void deposit(struct BankAccount* account, double amount) {
if (amount > 0) {
account->balance += amount;
}
}
void withdraw(struct BankAccount* account, double amount) {
if (amount > 0 && amount <= account->balance) {
account->balance -= amount;
}
}
// Usage
struct BankAccount account = {"Alice", 1000.0};
deposit(&account, 500);
withdraw(&account, 200);
Python Procedural Style:
# Data and functions separate
bank_accounts = {}
def create_account(owner, initial_balance=0):
bank_accounts[owner] = initial_balance
def deposit(owner, amount):
if owner in bank_accounts and amount > 0:
bank_accounts[owner] += amount
def withdraw(owner, amount):
if owner in bank_accounts and 0 < amount <= bank_accounts[owner]:
bank_accounts[owner] -= amount
# Usage
create_account("Alice", 1000)
deposit("Alice", 500)
withdraw("Alice", 200)
| Aspect | Procedural | OOP |
|---|---|---|
| Organization | Around functions | Around objects (data + behavior) |
| Data/Functions | Separate | Encapsulated together |
| Access | Data often global or passed explicitly | Data hidden, accessed through methods |
| Reuse | Function reuse | Inheritance, polymorphism |
| Best For | Small programs, scripts, algorithms | Large, complex systems |
"How to do it" - Explicit step-by-step instructions to achieve a goal.
# Imperative: Describe HOW to filter and transform
numbers = [1, 2, 3, 4, 5, 6]
result = []
for num in numbers:
if num % 2 == 0: # Filter even numbers
result.append(num * 2) # Double them
print(result) # [4, 8, 12]
# You control the flow: loop, if, append
"What you want" - Describe the desired result, not the steps.
# Declarative: Describe WHAT you want
numbers = [1, 2, 3, 4, 5, 6]
result = [num * 2 for num in numbers if num % 2 == 0]
print(result) # [4, 8, 12]
# Don't specify how to iterate, just what to produce
SQL (Declarative):
-- Declarative: What data you want, not how to get it
SELECT name, salary * 1.1 AS new_salary
FROM employees
WHERE department = 'Engineering'
ORDER BY salary DESC;
-- Database engine figures out HOW to execute efficiently
HTML (Declarative):
<!-- Describe what you want displayed, not how to render -->
<div class="card">
<h2>Title</h2>
<p>Content</p>
</div>
<!-- Browser handles rendering details -->
React (Declarative UI):
// Declarative: Describe UI based on state
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
// vs Imperative (manual DOM manipulation):
// document.getElementById('count').textContent = count;
// document.getElementById('button').addEventListener('click', ...)
| Aspect | Imperative | Declarative |
|---|---|---|
| Focus | How (control flow) | What (desired result) |
| Abstraction | Low-level | High-level |
| Examples | C, Java, Python (imperative parts) | SQL, HTML, CSS, React |
| Control | Explicit loops, conditionals | Hidden, handled by framework |
| Best For | Performance-critical code, algorithms | Business logic, UI, queries |
Express logic in terms of relations and rules. Program is a set of logical facts and rules.
% Facts
parent(tom, bob).
parent(tom, liz).
parent(bob, ann).
parent(bob, pat).
% Rules
grandparent(X, Y) :- parent(X, Z), parent(Z, Y).
% Query
?- grandparent(tom, ann). % true
?- grandparent(tom, Who). % Who = ann; Who = pat
Program flow determined by events (user actions, sensor outputs, messages).
// Register event handlers
button.addEventListener('click', () => {
console.log('Button clicked!');
});
window.addEventListener('load', () => {
console.log('Page loaded!');
});
// Flow controlled by user interactions, not linear execution
Asynchronous data streams and propagation of change.
// Stream of click events
const clicks = fromEvent(button, 'click');
// Transform stream
clicks
.pipe(
debounceTime(300),
map(event => event.clientX)
)
.subscribe(x => console.log(x));
| Paradigm | Best For | Examples |
|---|---|---|
| OOP |
- Large, complex systems - Modeling real-world entities - Code reuse through inheritance - Team collaboration |
Enterprise apps, games, GUIs |
| Functional |
- Data transformations - Concurrent/parallel processing - Mathematical computations - Avoiding side effects |
Data pipelines, compilers, parsers |
| Procedural |
- Small programs - Scripts and utilities - Performance-critical code - Simple algorithms |
System utilities, embedded systems |
| Declarative |
- Database queries - UI descriptions - Configuration - Business rules |
SQL queries, HTML/CSS, React |
Modern best practice: Use the paradigm that best fits each problem within the same codebase.
# OOP for domain models
class Order:
def __init__(self, items):
self.items = items
self.status = "pending"
def total(self):
# Functional approach for calculation
return sum(item.price for item in self.items)
# Functional for data transformation
def apply_discount(orders, discount_rate):
return [
Order([item for item in order.items])
for order in orders
if order.total() > 100
]
# Procedural for script-like tasks
def main():
orders = load_orders()
discounted = apply_discount(orders, 0.1)
save_orders(discounted)
if __name__ == "__main__":
main()
When asked about paradigms: