Deep Engineering #18: Sam Keen on Clean Architecture with Python
Pragmatic boundaries for Python at scale—the dependency rule in practice, a compact example, and a full chapter extract.
Join us on October 22, 2025 at DevSecCon25 - Securing the Shift to AI Native
This one-day event brings together AI, App, & Security pros to discuss securing the shift to AI Native SDLCs via DevSecOps.
The summit features speakers like Nnenna Ndukwe & Bob Remeika, plus an AI Lab with hands-on workshops, AI Security Research, and a Product Track!
✍️From the editor’s desk,
Welcome to the eighteenth issue of Deep Engineering.
Python’s flexibility is a gift; at scale, it’s also how coupling sneaks in and change gets expensive. What does it take to keep business rules independent of tools, so you can move fast without fragility?
To answer this question, we spoke to —a software engineering leader with 25+ years’ experience and author or Clean Architecture with Python (Packt, 2025). He’s a polyglot who’s used Python from early-stage startups to large-scale systems at AWS, Lululemon, and Nike. At Lululemon, he led the company’s first cloud-native development team, setting foundational standards for distributed architecture. Today, as a Principal Engineer at Pluralsight, he focuses on generative-AI enablement for software teams—building tools that amplify developer productivity while preserving architectural integrity. He also writes AlteredCraft, where his weekly Delta Notes distills what’s shifting in AI and software for practitioners.
If you’re navigating legacy refactors or standing up a new service, this issue is about strategies for reducing risk per change.
In this issue:
Feature Article: Clean Architecture in Python: Keeping the Core Clean in a Dynamic Language with Sam Keen
Clean Architecture Essentials: Transforming Python Development: The complete “Chapter 1” from the book Clean Architecture with Python by Sam Keen
Pragmatic Clean Architecture in Python: A Conversation with Sam Keen: Q&A covering applying DDD and the dependency rule in real-world Python—keeping frameworks at the edge, modeling with dataclasses/Pydantic, and using AI without breaking clean boundaries.
P.S. Before you dive into today’s issue: In just two days, we’re hosting Deep Engineering’s first live event — C++ Memory Management Masterclass with Patrice Roy (ISO C++ Committee member and veteran educator).
When: Sat, Sep 20, 2025, 8:30 AM – Sun, Sep 21, 2025, 2:00 PM PT
Offer: 50% off Standard and Premium passes with code SPECIAL50.
If C++ performance and safety are on your roadmap, this is the practical, hands-on session you are looking for. I hope to see you there.
Clean Architecture in Python: Keeping the Core Clean in a Dynamic Language with Sam Keen
Python’s greatest strength—its flexibility—can become its biggest liability as systems grow. A simple CRUD API can turn into a sprawling mesh of dependencies almost overnight. Without a clear structure, change becomes perilous, and testing collapses under the weight of hidden couplings. Clean Architecture, the layered design style popularized by Robert C. Martin, offers a way to prevent that entropy. It promises to keep business rules insulated from frameworks, databases, and delivery mechanisms—the volatile parts of our systems.
But Python poses a unique challenge. The language’s dynamism makes rigid architectures feel alien. Many Python teams have historically dismissed Clean Architecture as “too Java-esque.” Sam Keen has spent years proving otherwise.
“I wanted to take an approach and see: can we take from clean architecture the aspects that help us maintain larger Python codebases—without trying to turn it into Java?”
Keen argues that Clean Architecture can work precisely because of Python’s ethos of explicitness. Clean Architecture is operating discipline for Python: a few explicit boundaries that keep business logic independent of frameworks, making change safer and cheaper.
Boundaries Before Frameworks
At the heart of Clean Architecture lies the Dependency Rule:
“The principle is that dependencies all face inward. The domain layer shouldn’t be aware of anything above it. The application layer shouldn’t be aware of anything above it. They should only depend on what’s inside.
In Python, just like in other languages, you can check this. One way is pragmatic—make sure the team understands the principle and why it matters. Another way is structural—use a folder structure: a folder for domain objects, a folder for application objects, and so on. That way, each file is bound by the rules of its layer.
You can also get precise with automation. For example, in the book we give a simple fitness function test. It runs linting across import statements and checks them against the known directory structure of the layers. If it finds that a use case in the application layer is importing from, say, a driver, it fails the build. That shifts knowledge of violations left, so developers can correct them quickly.”
Keen recommends enforcing this through a hierarchy of guardrails: start with shared understanding, reinforce it with folder structure, and finally automate it. He often structures projects into domain
, application
, interfaces
, and frameworks
directories. That hierarchy acts as a map. If a developer tries to import a FastAPI router from within a domain entity, the linter blocks it.
This separation brings testability almost for free. Unit tests can target the domain layer in isolation. Application-layer tests can swap real implementations for mocks that conform to the same interface. End-to-end tests are left for validating the glue—not compensating for weak internal boundaries.
Entities, Value Objects, and Pythonic Modeling
Domain-driven design underpins Keen’s approach. Python’s dataclasses
make it straightforward to model entities and value objects idiomatically.
“For entities, which have an identity, I use a thin base entity class that every entity extends. A Task, a User, any entity extends this base entity class. That gives you a primary ID field—a reserved field for all entities. Anything in the system that knows it’s dealing with an entity knows that ID field is there and that it’s universally unique.
In Python specifically, in that entity class you’d also implement the
__hash__
and__eq__
methods. That keeps you in line with the concept of an entity having identity. You can change all the attributes of that class, but it will always represent the same person or the same task—it’s just that its attributes have changed.A value object is different. It doesn’t have an ID field; it’s defined solely by its properties. Again, a dataclass works well, but here you’d set
frozen=True
to make it immutable.”
This clarity matters. An entity represents identity that persists as attributes change; a value object represents a snapshot of values that must never mutate. These simple conventions help keep the domain layer conceptually coherent.
Keen warns against letting frameworks leak into these core layers. An anti-pattern is mixing SQLAlchemy models directly into domain entities. Instead, he advocates the repository pattern: the domain depends on an abstract interface like UserRepository
, and the actual SQLAlchemy implementation lives in the outer frameworks
layer.
A Glimpse of Clean Architecture in Code
Chapter 1 of Keen’s book (Clean Architecture with Python) illustrates these principles with a simple notification system. At the core is an abstract base class; the outer layer provides concrete implementations:
from abc import ABC, abstractmethod
class Notifier(ABC):
@abstractmethod
def send_notification(self, message: str) -> None:
pass
class EmailNotifier(Notifier):
def send_notification(self, message: str) -> None:
print(f"Sending email: {message}")
class NotificationService:
def __init__(self, notifier: Notifier):
self.notifier = notifier
def notify(self, message: str) -> None:
self.notifier.send_notification(message)
email_service = NotificationService(EmailNotifier())
email_service.notify("Hello via email")
The inner Notifier
abstraction knows nothing about EmailNotifier
or NotificationService
. Dependencies point inward, preserving flexibility. Swapping EmailNotifier
for SMSNotifier
doesn’t touch the service or the domain logicChapter.
Tooling and Practice in 2025
Recent projects have shown how to operationalize these ideas. At AddWeb Solutions, engineers refactored a legacy Python monolith into a cleanly layered system. By isolating business logic from frameworks and introducing repository interfaces, they cut deployment times from 45 minutes to under 10 and saved an estimated $3 million in technical debt within a year.
Static analysis is making enforcement tractable. import-linter lets teams define “contracts” (e.g. no imports from frameworks
into domain
) and fail builds on violations. Newer tools like Deply add structural checks beyond imports, detecting couplings through class hierarchies and decorators.
Meanwhile, mypy has become standard for enforcing interface contracts in dynamic code, giving Python a hint of compiler-like safety. Dependency injection frameworks such as Dependency Injector help keep wiring logic outside the domain, enabling test doubles to slot in cleanly.
AI and the Case for Structure
Even AI-driven development strengthens the case for Clean Architecture. As Keen says:
“AI is the definition of disruptive…
There are a couple of dimensions:
If you’re thinking of AI as a feature you would add to an application—integrating an LLM into an application—nothing really changes. That’s a driver—a framework driver. So the knowledge of, say, LangChain or LlamaIndex—really common frameworks—that’s all going to stay in the outer layers. Same playbook, and then you integrate that down to your pure domain objects. So that part doesn’t change.
The other part is using AI to build with—coding tools and these sorts of things. It helps. We talked about writing tests—AIs are great at writing unit tests, so you can definitely leverage it there. That’s where you mitigate hallucination—you’re writing tests to validate, so you can know the AI is doing the right thing. Another example: we have that User that needs to be saved to a database, and it has an interface contract. You can use AI to build the concrete class against that interface—at least get a start on it. Some might think of that as boilerplate, but it can do that.
Overall, the advantage is this idea of “context engineering.” The AI’s ability to help is only as good as the context you give it. If you let it index legacy, tightly coupled systems, it’s not really sure what the plan was—there kind of wasn’t one—so the AI will continue to build against that codebase without much of a plan. Whereas, if you’re explicit about your approach and you have these four folders with easily defined rationale of what goes into each folder, all that context goes to the LLM—since it’s helping a human. Using clean architecture and having that playbook for how to build out your application helps AIs do the right thing and not go off the rails. It’s exciting.”
In Keen’s view, clean boundaries are a form of context engineering. They make systems more comprehensible not just to humans but to AI tooling tasked with generating tests, scaffolding classes, or suggesting refactors.
To wrap up, Clean Architecture isn’t about heavyweight ceremony. As Keen puts it, you don’t want to build a skyscraper if all you need is a cottage. It’s about protecting what matters—the core business rules—from the churn of frameworks and infrastructure.
Python’s flexibility makes it easy to blur boundaries. Clean Architecture makes it easy to draw them again.
Key Takeaways
Protect the core. Keep business logic independent of frameworks and I/O. Enforce the Dependency Rule so dependencies face inward; swap details at the edge without touching the domain.
Make boundaries visible. Use a clear folder layout (
domain
,application
,interfaces
,frameworks
) plus interfaces/repositories to prevent leakage. Let structure guide behavior.Operationalise with tooling. Encode rules in CI: import-linter/Deply for layer contracts, mypy for interface guarantees, DI for wiring, and fast unit tests that don’t boot infrastructure.
Be pragmatic. Start simple for CRUD; evolve to fuller layering as complexity grows. Document intentional deviations in ADRs; avoid big-bang rewrites—strangle and slice.
Design for leverage (humans and AI). Clean seams and types improve comprehension and enable AI assistance to generate and refactor safely—raising delivery speed without raising risk.
🧠Expert Insight
Clean Architecture Essentials: Transforming Python Development
The complete “Chapter 1: Clean Architecture Essentials: Transforming Python Development” from the book Clean Architecture with Python by Sam Keen (Packt, June 2025).
Join us Saturday, Sept 27 (online) for AI-Powered Platform Engineering, a 5-hour workshop with George Hantzaras on designing golden paths, building smarter developer portals, and bringing AI into ops/observability—with practical patterns, real examples, and a 90-day implementation roadmap.
seats are limited—reserve your spot today at 30% off (Deep Engineering exclusive | No code necessary). Get tickets here.
🛠️Tool of the Week
Production-proven static typing for Python. The 1.18/1.18.1 line lands major performance work—~40% faster than 1.17 when type-checking mypy itself (with extreme cases up to 10×)—making CI/type gates far cheaper on large codebases.
📎Tech Briefs
Pydantic 2.11.8 & 2.11.9 — compatibility fix for mypy 1.18: Fresh releases fix the mypy plugin for 1.18 and backport stability changes; helps teams keep strict models/DTOs without breaking type checks.
Import Linter 2.5 — new “protected” contract type
Architecture linting gets sharper: Define protected modules/layers to prevent inward leaks; ideal for enforcing domain→application→interfaces→frameworks seams.Poetry 2.2.0— PEP 735 dependency groups, Python 3.14 support
Cleaner environment and dependency boundaries: Nested groups and PEP 735 help encode service/module scopes; good hygiene for modular monoliths and services.OpenTelemetry Python SDK 1.37.0 (Sept 11, 2025) — fresh SDK release
Observability keeps pace: Up-to-date telemetry SDK for tracing/metrics/logs supports boundary-aware instrumentation across layers.Pyright 1.1.405 (released yesterday): precision fixes for static typing
Bug fixes and diagnostics tweaks (e.g., “possibly unbound” false positives) improve reliability of strict typing gates in large Python repos—useful for enforcing clean interfaces at layer boundaries.
That’s all for today. Thank you for reading this issue of Deep Engineering. We’re just getting started, and your feedback will help shape what comes next. Do take a moment to fill out this short survey we run monthly—as a thank-you, we’ll add one Packt credit to your account, redeemable for any book of your choice.
We’ll be back next week with more expert-led content.
Stay awesome,
Divya Anne Selvaraj
Editor-in-Chief, Deep Engineering
If your company is interested in reaching an audience of developers, software engineers, and tech decision makers, you may want to advertise with us.