No history yet

Separating Concerns Pythonically

Beyond Scripts: Structuring Python Projects

You already know that software architecture patterns like Model-View-Controller (MVC) are about separating concerns. But how do you translate that theory into a real Python project? It's not about a specific library; it's about structure. The goal is to move from a single, long script to a well-organized collection of modules and packages.

Python's own structure gives us the tools we need. A directory containing an __init__.py file is a package, and each .py file within it is a module. By creating separate directories for models, views, and controllers, we can physically enforce the separation of logic. This isn't just for tidiness. It creates boundaries that make the code easier to reason about, test, and scale.

Managing Dependencies

A well-structured project manages its dependencies carefully. The controller is the orchestrator. It imports from both the model and the view, but the model and view should never import from each other. The model handles data and business rules, ignorant of how it will be displayed. The view handles the presentation, ignorant of where the data comes from. The controller acts as the intermediary, fetching data from the model and passing it to the view. This creates a one-way flow of dependencies, which prevents circular imports and makes each component more reusable.

Model → Controller ← View. The model and view should be strangers to each other.

Imagine a controller file, user_controller.py. It would contain code like this:

# In controllers/user_controller.py

# Import from the other layers
from models.user_model import User
from views.user_view import UserView

class UserController:
    def __init__(self):
        self.model = User()
        self.view = UserView()

    def show_user(self, user_id):
        # 1. Controller gets data from the Model
        user_data = self.model.get_user(user_id)

        # 2. Controller gives data to the View
        if user_data:
            self.view.show_user_details(user_data)
        else:
            self.view.show_error()

Notice how the controller is the only component with knowledge of the other two. This is key to and makes it possible to, for example, swap out a command-line view for a graphical one without touching the model at all.

Encapsulation in the Model

The model's job is to represent data and enforce business logic. This means protecting the data's integrity. We want to prevent other parts of the application from putting the model into an invalid state, for example, by setting a user's age to a negative number. This is where encapsulation comes in.

Python gives us tools beyond simple attributes to achieve this. Properties, for instance, let you wrap getters and setters around an attribute, allowing you to run validation logic whenever a value is changed. For more advanced or reusable validation, we can use a powerful, behind-the-scenes protocol that lets an object customize attribute access.

Here is a simple example of a descriptor that ensures an attribute is a positive number:

# In models/validators.py

class PositiveNumber:
    def __set_name__(self, owner, name):
        self.private_name = '_' + name

    def __get__(self, obj, objtype=None):
        return getattr(obj, self.private_name)

    def __set__(self, obj, value):
        if value <= 0:
            raise ValueError("Value must be positive.")
        setattr(obj, self.private_name, value)

# In models/product_model.py
from .validators import PositiveNumber

class Product:
    price = PositiveNumber()

    def __init__(self, price):
        self.price = price # The descriptor's __set__ is called here

By using a descriptor, the validation logic for a positive price is contained in its own reusable class, keeping the Product model clean and focused on its core purpose. The controller doesn't need to know how the model validates data, only that it does. This separation defines a clear interface between components, which is the ultimate goal of any good architecture.

Now, let's test your understanding of how these Pythonic structures support the MVC pattern.

Quiz Questions 1/5

What is the primary mechanism in Python for grouping related modules into a single hierarchical structure?

Quiz Questions 2/5

In a well-structured MVC application, which of the following import statements would be considered a violation of the pattern's dependency flow if found within a model file like models/product.py?

By organizing your code into distinct layers and using Python's features to enforce boundaries, you create applications that are more robust, testable, and easier to maintain as they grow.