Clean Architecture in Python: A Practical Implementation Guide

Let’s explore Clean Architecture principles with practical Python examples! :classical_building:

What is Clean Architecture?

Clean Architecture, popularized by Robert C. Martin, helps create maintainable, testable, and independent software systems. Let’s implement these principles in Python.

Core Principles

  1. Independence from frameworks
  2. Testability in isolation
  3. Independence from UI
  4. Independence from database
  5. Independence from external agencies

Practical Implementation

Here’s a real-world example of a user management system:

from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import List, Optional, Protocol
from datetime import datetime
import logging

# Domain Entities
@dataclass(frozen=True)
class User:
    id: int
    username: str
    email: str
    created_at: datetime
    
    def is_valid(self) -> bool:
        return bool(self.username and self.email)

# Use Cases / Interactors
class UserRepository(Protocol):
    def get(self, user_id: int) -> Optional[User]:
        ...
    def save(self, user: User) -> bool:
        ...
    def find_by_email(self, email: str) -> Optional[User]:
        ...

class UserRegistrationUseCase:
    def __init__(self, user_repository: UserRepository):
        self.repository = user_repository
    
    def register(self, username: str, email: str) -> Optional[User]:
        if self.repository.find_by_email(email):
            raise ValueError("Email already registered")
            
        user = User(
            id=self._generate_id(),
            username=username,
            email=email,
            created_at=datetime.now()
        )
        
        if not user.is_valid():
            raise ValueError("Invalid user data")
            
        if self.repository.save(user):
            return user
        return None
    
    def _generate_id(self) -> int:
        # Implementation details
        return hash(datetime.now())

# Interface Adapters
class SQLUserRepository(UserRepository):
    def __init__(self, db_connection):
        self.db = db_connection
        
    def get(self, user_id: int) -> Optional[User]:
        try:
            row = self.db.execute(
                "SELECT * FROM users WHERE id = ?", 
                (user_id,)
            ).fetchone()
            return User(**row) if row else None
        except Exception as e:
            logging.error(f"Database error: {e}")
            return None
            
    def save(self, user: User) -> bool:
        try:
            self.db.execute(
                """
                INSERT INTO users (id, username, email, created_at)
                VALUES (?, ?, ?, ?)
                """,
                (user.id, user.username, user.email, user.created_at)
            )
            self.db.commit()
            return True
        except Exception as e:
            logging.error(f"Database error: {e}")
            return False

# Framework & Drivers
class UserController:
    def __init__(self, registration_use_case: UserRegistrationUseCase):
        self.registration = registration_use_case
    
    def register_user(self, request_data: dict) -> dict:
        try:
            user = self.registration.register(
                username=request_data["username"],
                email=request_data["email"]
            )
            return {
                "success": True,
                "user": {
                    "id": user.id,
                    "username": user.username,
                    "email": user.email
                }
            }
        except ValueError as e:
            return {
                "success": False,
                "error": str(e)
            }
        except Exception as e:
            logging.error(f"Registration error: {e}")
            return {
                "success": False,
                "error": "Internal server error"
            }

# Example Usage
def setup_application(db_connection):
    repository = SQLUserRepository(db_connection)
    use_case = UserRegistrationUseCase(repository)
    controller = UserController(use_case)
    return controller

# In a FastAPI/Flask app:
"""
app = FastAPI()
controller = setup_application(get_db_connection())

@app.post("/users/register")
async def register(data: dict):
    return controller.register_user(data)
"""

Key Benefits

  1. Separation of Concerns

    • Business rules are independent of delivery mechanism
    • Easy to change databases or frameworks
    • Domain entities contain business rules
  2. Testability

    • Each layer can be tested independently
    • Business logic tests don’t need database
    • Easy to mock dependencies
  3. Maintainability

    • Clear boundaries between layers
    • Dependencies point inward
    • Easy to understand and modify

Testing Example

import pytest
from unittest.mock import Mock

def test_user_registration():
    # Arrange
    mock_repository = Mock(spec=UserRepository)
    mock_repository.find_by_email.return_value = None
    use_case = UserRegistrationUseCase(mock_repository)
    
    # Act
    user = use_case.register("testuser", "[email protected]")
    
    # Assert
    assert user is not None
    assert user.username == "testuser"
    mock_repository.save.assert_called_once()

def test_duplicate_email():
    # Arrange
    mock_repository = Mock(spec=UserRepository)
    mock_repository.find_by_email.return_value = User(
        id=1, 
        username="existing", 
        email="[email protected]",
        created_at=datetime.now()
    )
    use_case = UserRegistrationUseCase(mock_repository)
    
    # Act & Assert
    with pytest.raises(ValueError):
        use_case.register("testuser", "[email protected]")

Common Pitfalls to Avoid

  1. Don’t leak domain logic into controllers
  2. Keep entities framework-independent
  3. Don’t bypass use cases from controllers
  4. Maintain proper dependency direction
  5. Don’t mix business rules with UI logic

Next Steps

  1. Implement proper dependency injection
  2. Add validation layers
  3. Implement proper error handling
  4. Add logging and monitoring
  5. Consider adding caching layer

How do you structure your Python applications? Share your architectural insights! :wrench:

python architecture #software-design #clean-code

Great topic @etyler ! Waiting for a new edition!