No history yet

Modular Software Design

Beyond the Single Script

When you first start coding, projects are often contained in a single file. It’s simple and effective for small tasks. But as projects grow, or when you bring in an AI assistant to write code, that single file can become a tangled mess. This is where modular design comes in. It’s the practice of breaking a large program into smaller, independent, and interchangeable components called modules.

Think of it like building with LEGO bricks instead of carving a statue from a single block of marble. Each brick has a clear purpose and connects to others in a predictable way. This makes the overall structure easier to build, modify, and repair.

This approach is crucial when working with AI. You can direct your AI assistant to generate a specific function or class—a single LEGO brick—while you, the architect, decide how all the bricks fit together. This prevents the AI from generating

When you first start coding, projects are often contained in a single file. It’s simple and effective for small tasks. But as projects grow, or when you bring in an AI assistant to write code, that single file can become a tangled mess. This is where modular design comes in. It’s the practice of breaking a large program into smaller, independent, and interchangeable components called modules.

Think of it like building with LEGO bricks instead of carving a statue from a single block of marble. Each brick has a clear purpose and connects to others in a predictable way. This makes the overall structure easier to build, modify, and repair.

This approach is crucial when working with AI. You can direct your AI assistant to generate a specific function or class—a single LEGO brick—while you, the architect, decide how all the bricks fit together. This prevents the AI from generating , a nightmare of tangled logic that's nearly impossible to debug or extend.

One Job, One Module

The guiding light of modular design is the (SRP). It’s a simple idea with powerful implications: every module, class, or function in a program should have responsibility over a single part of that program's functionality.

Let's say you're building an application to manage a library. A single script might handle fetching book data from a database, displaying it to the user, and processing new book checkouts. This violates SRP.

A modular approach would separate these concerns:

ModuleResponsibility
database_manager.pyHandles all communication with the database (connecting, fetching, updating).
user_interface.pyManages how information is displayed to the user and captures their input.
business_logic.pyContains the core rules, like how to process a book checkout or check for overdue items.
main.pyOrchestrates the other modules, telling them what to do and when.

This separation is called separation of concerns. Now, if you want to change the database from SQL to something else, you only need to modify database_manager.py. The user interface doesn't need to know or care. You can ask your AI assistant to rewrite the database module without worrying it will break the entire application.

Structuring a Python Project

In Python, this modularity is achieved through packages and modules. A module is simply a .py file containing Python definitions and statements. A package is a way of structuring Python’s module namespace by using

In Python, this modularity is achieved through packages and modules. A module is simply a .py file containing Python definitions and statements. A package is a way of structuring Python’s module namespace by using "dotted module names". For example, the module name A.B designates a submodule named B in a package named A.

Let’s organize our library project into a proper package.

library_project/
├── library/
│   ├── __init__.py
│   ├── database_manager.py
│   ├── user_interface.py
│   └── business_logic.py
└── main.py

The library/ directory is our package. The key file here is . This file can be empty, but its presence tells Python that the library directory should be treated as a package. This allows you to import modules from within it.

For instance, inside main.py, you could now write:

# main.py
from library.database_manager import get_all_books
from library.user_interface import display_books

books = get_all_books()
display_books(books)

This structure creates clear boundaries. The code in main.py doesn't know how get_all_books works; it only knows that it can call it and expect a list of books in return. This is the essence of a clear interface. The function signature def get_all_books(): is a contract. As long as the contract is honored, the implementation inside database_manager.py can change completely.

From Script to Class

As modules grow, you might find yourself with a collection of related functions and data. This is a good time to refactor them into a class. A class bundles data (attributes) and functions that operate on that data (methods) into a single object.

Let's refactor our database_manager.py. Instead of loose functions, we can create a DatabaseManager class.

# library/database_manager.py

class DatabaseManager:
    def __init__(self, connection_string):
        # Code to connect to the database
        self.connection = self._connect(connection_string)
        print("Database connected.")

    def _connect(self, conn_str):
        # Private method to handle connection logic
        # ... returns a connection object
        pass

    def get_all_books(self):
        # Logic to query the database for all books
        # using self.connection
        print("Fetching all books...")
        return [{"title": "Moby Dick"}, {"title": "War and Peace"}]

    def get_book_by_id(self, book_id):
        # Logic to fetch a single book
        print(f"Fetching book {book_id}...")
        return {"title": "Moby Dick"}

Using this class in main.py now looks cleaner. We create an instance of the manager and then call its methods.

# main.py
from library.database_manager import DatabaseManager
from library.user_interface import display_books

# Create an instance of the manager
db_manager = DatabaseManager("my_db_connection_details")

# Use the instance to get data
all_books = db_manager.get_all_books()
display_books(all_books)

This is a perfect task for an AI assistant. You can provide the existing functions and ask it to refactor them into a class, specifying the desired methods. You remain in control of the high-level design, using the AI for the more mechanical task of boilerplate code generation. This lets you focus on building a robust, maintainable system, not just a script that works for now.

Quiz Questions 1/5

What is the primary purpose of the Single Responsibility Principle (SRP) in software design?

Quiz Questions 2/5

In Python, what is the purpose of the __init__.py file inside a directory?