How to Apply the Clean Architecture Dependency Rule in Python

Demonstrates the dependency rule with a Protocol repository, a use case, and a presenter wired together at a composition root.

Medium Python 3.8+ Aug 9, 2026 System design patterns 13 views 0 copies

Python code

38 lines
Python 3.8+
from dataclasses import dataclass
from typing import List, Protocol

class Repository(Protocol):
    def get_items(self) -> List[str]:
        ...

@dataclass
class InMemoryRepository:
    items: List[str]

    def get_items(self) -> List[str]:
        return self.items

class UseCase:
    """Application layer depends on interfaces, not implementations."""
    def __init__(self, repository: Repository):
        self.repository = repository

    def execute(self) -> List[str]:
        return [item.upper() for item in self.repository.get_items()]

class ConsolePresenter:
    """Interface layer depends on application and domain layers."""
    def present(self, items: List[str]) -> None:
        print(f"Items: {', '.join(items)}")

def main():
    # Composition root: wires dependencies
    repository = InMemoryRepository(items=["apple", "banana", "cherry"])
    usecase = UseCase(repository)
    presenter = ConsolePresenter()

    # Presenter calls usecase, usecase calls repository
    presenter.present(usecase.execute())

if __name__ == "__main__":
    main()

Output

stdout
Items: APPLE, BANANA, CHERRY

How it works

The dependency rule says source code dependencies point inward: the application layer (UseCase) depends only on abstractions, not concrete implementations. Here Repository is a Protocol, so UseCase never imports InMemoryRepository — the concrete type is injected at runtime. ConsolePresenter lives in the interface layer and depends on the application layer by calling usecase.execute(). The main() function acts as the composition root, wiring all concrete objects together. This keeps the use case testable and lets you swap repositories or presenters without changing application logic.

Common mistakes

  • Making the use case import the concrete repository instead of a Protocol or abstract base class
  • Placing the composition root inside the application layer instead of at the app's entry point
  • Letting the domain layer depend on the framework or interface layer
  • Using concrete types in constructor hints instead of duck-typed Protocols

Variations

  1. Use `abc.ABC` and `@abstractmethod` instead of `Protocol` for explicit abstract classes
  2. Inject a presenter into the use case for read-model style responses rather than returning data

Real-world use cases

  • Structuring a Flask or FastAPI service so controllers call use cases that depend on database repositories through interfaces.
  • Swapping a production SQL repository for an in-memory fake in unit tests without touching the use case code.
  • Building a CLI tool where the presenter writes to stdout today and to a file or API later.

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from System design patterns

Related tutorials and quizzes for this topic.