backend / theory foundations / 03_programming_paradigms.md

Programming Paradigms in Python

3 interview angles 4 min read source

Programming Paradigms in Python

Introduction

Python is a versatile programming language that supports multiple programming paradigms. This guide explores the main programming paradigms available in Python with practical examples for each.

1. Object-Oriented Programming (OOP)

Object-Oriented Programming organizes code into objects that contain both data and behavior.

Key Concepts:

  • Encapsulation
  • Inheritance
  • Polymorphism
  • Abstraction
class Animal:
    def __init__(self, name):
        self._name = name  # Encapsulation with protected attribute
    
    def make_sound(self):  # Abstract method
        pass

class Dog(Animal):  # Inheritance
    def __init__(self, name, breed):
        super().__init__(name)
        self.breed = breed
    
    def make_sound(self):  # Polymorphism
        return "Woof!"

class Cat(Animal):
    def __init__(self, name, color):
        super().__init__(name)
        self.color = color
    
    def make_sound(self):
        return "Meow!"

# Usage
dog = Dog("Buddy", "Golden Retriever")
cat = Cat("Whiskers", "Orange")

animals = [dog, cat]
for animal in animals:
    print(animal.make_sound())  # Polymorphic behavior

2. Functional Programming

Functional programming treats computation as the evaluation of mathematical functions and avoids changing state and mutable data.

Key Concepts:

  • Pure Functions
  • Immutability
  • First-Class Functions
  • Higher-Order Functions
from functools import reduce
from typing import List, Callable

# Pure function
def multiply_by_two(x: int) -> int:
    return x * 2

# Higher-order function
def compose(f: Callable, g: Callable) -> Callable:
    return lambda x: f(g(x))

# First-class functions
def apply_operations(numbers: List[int], operations: List[Callable]) -> List[int]:
    return [reduce(lambda x, f: f(x), operations, num) for num in numbers]

# Usage
numbers = [1, 2, 3, 4, 5]
operations = [multiply_by_two, lambda x: x + 1]
result = apply_operations(numbers, operations)
print(result)  # [3, 5, 7, 9, 11]

# List comprehension (functional approach)
squares = [x * x for x in range(10)]

# Using map and filter (functional style)
even_squares = list(map(lambda x: x * x, filter(lambda x: x % 2 == 0, range(10))))

3. Procedural Programming

Procedural programming organizes code into procedures or functions that operate on data.

def calculate_area(length: float, width: float) -> float:
    return length * width

def calculate_perimeter(length: float, width: float) -> float:
    return 2 * (length + width)

def print_rectangle_info(length: float, width: float) -> None:
    area = calculate_area(length, width)
    perimeter = calculate_perimeter(length, width)
    
    print(f"Rectangle dimensions: {length} x {width}")
    print(f"Area: {area}")
    print(f"Perimeter: {perimeter}")

# Usage
length = 5
width = 3
print_rectangle_info(length, width)

4. Aspect-Oriented Programming

Aspect-Oriented Programming (AOP) allows separation of cross-cutting concerns.

from functools import wraps
import time
import logging

# Decorator for logging (aspect)
def log_execution(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        logging.info(f"Executing {func.__name__}")
        result = func(*args, **kwargs)
        logging.info(f"Finished {func.__name__}")
        return result
    return wrapper

# Decorator for timing (aspect)
def measure_time(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} took {end - start:.2f} seconds")
        return result
    return wrapper

# Using aspects
@log_execution
@measure_time
def complex_calculation(n: int) -> int:
    return sum(i * i for i in range(n))

# Usage
result = complex_calculation(1000000)

5. Event-Driven Programming

Event-Driven Programming is based on events and event handlers.

from typing import Callable, Dict, List

class EventEmitter:
    def __init__(self):
        self._events: Dict[str, List[Callable]] = {}
    
    def on(self, event: str, callback: Callable):
        if event not in self._events:
            self._events[event] = []
        self._events[event].append(callback)
    
    def emit(self, event: str, *args, **kwargs):
        if event in self._events:
            for callback in self._events[event]:
                callback(*args, **kwargs)

# Usage
class Button(EventEmitter):
    def __init__(self, label: str):
        super().__init__()
        self.label = label
    
    def click(self):
        self.emit('click', self.label)

# Event handler
def handle_click(label: str):
    print(f"Button {label} was clicked!")

# Creating and using a button
button = Button("Submit")
button.on('click', handle_click)
button.click()  # Outputs: Button Submit was clicked!

6. Meta-programming

Meta-programming involves writing code that manipulates code.

# Class decorator
def singleton(cls):
    instances = {}
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return get_instance

# Metaclass
class LoggedMeta(type):
    def __new__(cls, name, bases, attrs):
        # Wrap all methods with logging
        for key, value in attrs.items():
            if callable(value):
                attrs[key] = log_execution(value)
        return super().__new__(cls, name, bases, attrs)

# Using metaclass
class API(metaclass=LoggedMeta):
    def get_data(self):
        return "data"
    
    def process_data(self, data):
        return f"processed {data}"

# Using class decorator
@singleton
class Configuration:
    def __init__(self):
        self.settings = {}

Best Practices

  1. Choose the Right Paradigm

    • Use OOP for complex systems with clear object hierarchies
    • Use functional programming for data processing and parallel execution
    • Use procedural programming for simple, straightforward scripts
    • Mix paradigms when it makes sense
  2. Design Principles

    • Keep it simple (KISS)
    • Don’t repeat yourself (DRY)
    • Single responsibility principle (SRP)
    • Open/closed principle (OCP)
  3. Code Organization

    • Group related functionality
    • Separate concerns
    • Use meaningful names
    • Document your code

Common Pitfalls

  1. Overcomplicating

    # Bad: Overusing OOP
    class Number:
        def __init__(self, value):
            self.value = value
        
        def add(self, other):
            return Number(self.value + other.value)
    
    # Better: Simple procedural approach
    def add(a, b):
        return a + b
  2. Mixing Paradigms Inappropriately

    # Bad: Mixing functional and OOP unnecessarily
    class ListProcessor:
        @staticmethod
        def process(lst):
            return list(map(lambda x: x * 2, lst))
    
    # Better: Choose one approach
    def process_list(lst):
        return [x * 2 for x in lst]
  3. Ignoring Python’s Built-in Features

    # Bad: Reinventing the wheel
    def get_unique(lst):
        result = []
        for item in lst:
            if item not in result:
                result.append(item)
        return result
    
    # Better: Using built-in set
    def get_unique(lst):
        return list(set(lst))

Interview angle

  • “Which paradigm does Python support?” - all of them, and idiomatic Python mixes them: objects for stateful components, functions and comprehensions for transformations, and decorators as a functional pattern over an object-oriented base.
  • “What does functional style buy you in practice?” - pure functions are trivially testable and safe to reuse, and immutable data removes a class of aliasing bugs. Applied dogmatically in Python it fights the language; applied to data transformation pipelines it’s a clear win.
  • “When is object orientation the right tool?” - when state and the behaviour operating on it genuinely belong together, and when you need polymorphic substitution. Classes that hold no state and expose one method should have been functions.