The Clean Architecture I Use for Every Python Project

This is the architecture I now use for every Python project — whether it’s a CLI tool, web API, or machine learning pipeline.

The Clean Architecture I Use for Every Python Project
Photo by Priscilla Du Preez 🇨🇦 on Unsplash

Most Python projects start messy and stay that way. I’ve learned (the hard way) that a clean structure beats clever code every time.

The Clean Architecture I Use for Every Python Project

When I first started writing Python code, I was thrilled by how quickly I could get things done.

Toss a few functions in a file, spin up a Flask app, hit run — boom, working app. But then came the refactor.

And the feature requests. And the bugs. Suddenly, what once felt like a breeze turned into a tangled mess of imports, business logic hidden in routes, and duplicate code scattered everywhere.

That’s when I stumbled upon Clean Architecture, and it completely changed the way I build Python projects.

In this article, I’ll walk you through the Clean Architecture I now use for every Python project — from solo scripts to production-ready APIs.

Whether you’re building with Flask, FastAPI, or even plain scripts, this architecture will make your codebase scalable, testable, and future-proof.


What Is Clean Architecture?

Coined by Uncle Bob (Robert C. Martin), Clean Architecture is a way of organizing code to enforce a clear separation of concerns.

The idea is simple: dependencies flow inward, and the core business logic stays isolated from external frameworks, databases, or user interfaces.

Think of it like this:

[ UI / CLI / API ] 
        ↓ 
[ Application / Services ] 
        ↓ 
[ Domain / Entities ]

This layered approach allows you to swap out the web framework or the database without rewriting your business logic.

It’s not magic — just good engineering.

The Folder Structure I Use

Here’s my go-to folder structure:

my_project/ 
│ 
├── app/ 
│   ├── domain/         # Core business logic and entities 
│   ├── services/       # Use cases and application logic 
│   ├── adapters/       # Interfaces to external systems 
│   ├── api/            # FastAPI or Flask routes 
│   ├── config/         # Environment configs and settings 
│   └── __init__.py 
│ 
├── tests/              # Unit and integration tests 
├── requirements.txt 
├── pyproject.toml 
└── main.py             # Entry point

Let’s break it down.

1. domain/ — The Core of Your App

This is where your entities live. These are pure Python classes or dataclasses that represent the key objects in your system.

Example:

# domain/models.py 
 
@dataclass 
class User: 
    id: int 
    username: str 
    email: str

They contain no database code, no HTTP logic — just plain Python and rules that define how your system behaves.

2. services/ — Your Use Cases

The services/ directory contains your application logic — the workflows that orchestrate domain models.

# services/user_service.py 
 
class UserService: 
    def __init__(self, user_repo): 
        self.user_repo = user_repo 
 
    def register_user(self, username, email): 
        if self.user_repo.get_by_email(email): 
            raise ValueError("Email already exists") 
        return self.user_repo.create_user(username, email)

Notice how the service depends on an interface, not a concrete database. This makes testing and swapping implementations a breeze.

3. adapters/ — Gateways to the Outside World

Adapters are where your app talks to the real world: databases, APIs, queues, etc.

# adapters/sql_user_repo.py 
 
class SQLUserRepository: 
    def __init__(self, db): 
        self.db = db 
 
    def get_by_email(self, email): 
        return self.db.query(UserModel).filter_by(email=email).first() 
 
    def create_user(self, username, email): 
        user = UserModel(username=username, email=email) 
        self.db.add(user) 
        self.db.commit() 
        return user

These classes implement the interfaces expected by your services.

4. api/ — Web Layer (FastAPI / Flask)

This is where your framework-specific code lives — endpoints, serializers, request validation, etc.

# api/user_routes.py 
 
@router.post("/register") 
def register_user(user_data: UserCreateSchema, user_service: UserService = Depends()): 
    return user_service.register_user(user_data.username, user_data.email)

This layer should be as thin as possible. No business logic here — just delegation.

5. tests/ — Fast, Isolated, and Reliable

Because everything is modular and your services don’t depend on a real database, writing tests becomes dead simple.

# tests/test_user_service.py 
 
def test_register_user_success(): 
    fake_repo = InMemoryUserRepo() 
    service = UserService(fake_repo) 
 
    user = service.register_user("john", "john@example.com") 
 
    assert user.username == "john"

You don’t need to spin up a database to test your business logic.

Clean, fast, and deterministic.

Why This Architecture Works (for Me)

Here’s what I gained after switching to Clean Architecture:

Separation of concerns — Easy to locate logic, change behavior, or fix bugs.
Testability — I can test core logic without spinning up a server or database.
Flexibility — I’ve swapped databases, replaced FastAPI with Flask, even added GraphQL — with minimal changes.
Onboarding — New team members understand the flow quickly.
Maintenance — Less spaghetti, fewer regrets.

Tools That Fit Nicely

If you’re building modern Python apps, these tools work great with Clean Architecture:

FastAPI or Flask — Web frameworks
SQLAlchemy or Tortoise ORM — For database adapters
Pydantic — For request/response schemas
Poetry or uv— Dependency management
Pytest — Testing framework
Dependency Injector — For managing service dependencies

Tips to Make It Stick

  1. Resist shortcuts. Don’t put logic in route handlers. It adds up.
  2. Keep the domain layer pure. No imports from frameworks or libraries.
  3. Write interfaces first. Then plug in implementations.
  4. Start small. Even scripts can benefit from structure.
  5. Don’t over-engineer. Clean Architecture helps scale, but it shouldn’t feel heavy.

Final Thoughts

Clean Architecture isn’t just for enterprise apps or massive codebases. It’s for anyone who wants to write maintainable Python code that lasts.

The first time you refactor a messy feature and realize it only takes changing a single file — you’ll get it.

So next time you create a Python project, start clean.

You’ll thank yourself later.


If you found this helpful, leave a clap or share it with someone who’s tired of rewriting spaghetti Python.


Follow me for more Python architecture, tips, and developer wisdom.

Photo by Diego Gennaro on Unsplash