If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Tim Peters, Zen of Python
Before we talk about programming paradigms, we must first define what a program is: a set of instructions in a language that can be translated into machine code and executed by a computer. As long as we don't make any algorithmic mistakes and our instructions don't require unnecessary computation, it doesn't really matter how we construct these programs for the computer to execute.
However, since programs are (so far) written by humans, we need to structure our code and keep it in an understandable form so it can be maintained and extended by humans. As programs have become more complex, three basic programming paradigms have emerged: Procedural, Object-Oriented, and Functional Programming. These are ways of thinking about solving problems and structuring program code.
Some languages focus on one paradigm (such as Haskell for functional programming). Python, which we use here, is suitable for all three, so it is called a multi-paradigm programming language. What these paradigms share is that they modularize code, but they use different concepts to do so. This similarity is not surprising, because any large program will contain code that needs to be used multiple times. Writing that code repeatedly would be redundant, while it is more elegant to write a generic version once and call it from different places.
Before introducing the paradigms, we should distinguish two types of programming: imperative and declarative.
SELECT * FROM users). Functional
programming often leans towards this style.
Imperative programming is closer to how computers operate (changing state in memory), so it's easier for the machine to translate, but declarative code is often more expressive and easier to reason about.
Procedural programming is an imperative paradigm where code is organized into procedures (subroutines). In Python, procedures are often implemented as functions. For example, Jupyter Notebooks commonly employ procedural programming for data analysis (load data, clean data, plot data). Procedural programming is more intuitive for beginners, but purely procedural code can be harder to reuse across different projects compared to OOP because it often relies on shared global state.
# Managing a Flower Store: Procedural Approach
# Store flowers in a global dictionary
flowers = {}
def add_flower(flower_type: str, quantity: int):
if flower_type in flowers:
flowers[flower_type] += quantity
else:
flowers[flower_type] = quantity
def get_quantity(flower_type: str) -> int:
return flowers.get(flower_type, 0)
# Adding flowers
add_flower("Roses", 10)
add_flower("Lilies", 15)
# Getting quantity of a flower type
print(f"Roses available: {get_quantity('Roses')}")
Procedural programming focuses on writing a series of instructions (procedures) that act on data. In this
florist example, we keep a global dictionary flowers and define functions like
add_flower and get_quantity to operate on it. This
approach clarifies how tasks are carried out but can become unwieldy for large, complex systems.
Object-Oriented Programming (OOP) is based on the concept of "objects," which contain both data (attributes) and methods (functions) for manipulating that data. OOP organizes code into modular parts called Classes.
If we imagine a program as an organism, different tasks are handled by different organs. For instance, the
heart pumps blood, but the liver doesn't need to know the details of that process. This is analogous to
Encapsulation in OOP. In Python classes, the self keyword
refers to the specific instance of the object, allowing each object to maintain its own independent data.
# Managing a Flower Store: Object-Oriented Approach
class FlowerStore:
def __init__(self):
# Data is encapsulated inside the object (self)
self.flowers = {}
def add_flower(self, flower_type: str, quantity: int):
if flower_type in self.flowers:
self.flowers[flower_type] += quantity
else:
self.flowers[flower_type] = quantity
def get_quantity(self, flower_type: str) -> int:
return self.flowers.get(flower_type, 0)
# Creating a FlowerStore instance (Object)
flower_store = FlowerStore()
# Adding flowers
flower_store.add_flower("Roses", 10)
flower_store.add_flower("Lilies", 15)
# Getting quantity of Roses
print(f"Roses available: {flower_store.get_quantity('Roses')}")
In OOP, the florist software is modeled by a FlowerStore class that encapsulates
the inventory in self.flowers along with methods like
add_flower and get_quantity. This encapsulation
allows for more complex, real-world models and encourages code reuse through inheritance.
Functional programming follows the declarative approach and emphasizes pure functions—functions that rely only on their inputs and produce outputs, with no side effects. A pure function does not affect any external variable, ensuring it consistently yields the same result for a given input. This approach relies heavily on Immutability (never modifying data in place, but creating new versions of it).
# Managing a Flower Store: Functional Approach
def add_flower(flowers: dict, flower_type: str, quantity: int) -> dict:
# Create a COPY of the data rather than modifying the original
updated_flowers = flowers.copy()
updated_flowers[flower_type] = updated_flowers.get(flower_type, 0) + quantity
return updated_flowers
def get_quantity(flowers: dict, flower_type: str) -> int:
return flowers.get(flower_type, 0)
# Initial empty flower collection
flowers_state_0 = {}
# Adding flowers in an immutable way (Creating new states)
flowers_state_1 = add_flower(flowers_state_0, "Roses", 10)
flowers_state_2 = add_flower(flowers_state_1, "Lilies", 15)
# Getting quantity of Roses from the final state
print(f"Roses available: {get_quantity(flowers_state_2, 'Roses')}")
In this functional style, data is not mutated directly; rather, new data structures are derived from old
ones using pure functions. This florist example uses add_flower and
get_quantity functions that take a
flowers dictionary, process it, and return a new one. This
pattern avoids hidden state changes, simplifying reasoning and concurrency.
Different paradigms can be used to implement the same functionality. AI can assist in choosing a suitable paradigm, experimenting with different approaches, and coding in a particular style. Instead of asking broadly, "How do I structure my program?", you might say, "Show me how to build an inventory system in Python using procedural, object-oriented, and functional programming," which yields comparative implementations.
Example Prompt:Resulting AI-generated code:
class Book:
def __init__(self, title: str, author: str, year: int):
self.title = title
self.author = author
self.year = year
def __str__(self) -> str:
return f"{self.title} by {self.author} ({self.year})"
class Library:
def __init__(self):
self.books: list[Book] = []
def add_book(self, book: Book):
self.books.append(book)
def get_books(self) -> list[Book]:
return self.books
# Creating book instances
book1 = Book("1984", "George Orwell", 1949)
book2 = Book("To Kill a Mockingbird", "Harper Lee", 1960)
# Creating a Library instance
library = Library()
# Adding books to the library
library.add_book(book1)
library.add_book(book2)
# Getting all books in the library
for book in library.get_books():
print(book)