Deep Engineering #38: Steven Lott on Practical Object-Oriented Design in Python
Review can catch defects, but not weak design. This issue looks at dependency injection, protocols, and the architectural choices that shape reliable Python code.
Safely Refactor Production Codebases with AI
AI can refactor your code in seconds, but it can introduce subtle breakages just as quickly. Learn how to combine AI-assisted reasoning with AST-based structural guarantees to refactor code safely.
Online: March 14 | 10:00 AM EST
✓ 2-for-1 deal: Bring a colleague, get 2 passes for $48
✓ Includes 300+ page Mastering ast-grep ebook
✍️ From the editor’s desk,
Welcome to the 38th issue of Deep Engineering!
The rise of AI-generated code is putting pressure on a familiar engineering constraint: review does not scale as easily as output. Anthropic’s own recent research on Claude Code points in the same direction. It found that longer autonomous coding sessions are becoming more common, and that experienced users increasingly shift from step-by-step approval to letting the tool run and intervening when needed. But, if more code is being written with less direct review, how do engineers stop poor design from shipping in the first place?
Anthropic’s new Code Review system for Claude Code is one answer at the downstream end. It runs five specialized reviewers in parallel, scores findings on a confidence scale, and only posts high-confidence comments back to GitHub. That is a useful safeguard. But it also clarifies the limit of review itself. The more teams rely on layered automation to inspect pull requests, the more important the original design decisions become. Review can catch defects. It cannot compensate for weak boundaries, poor abstractions, or code that was hard to reason about from the start.
That is the context for this week’s Expert Insight from Steven F. Lott. Lott has been programming since the era when computers were large, expensive, and rare, and has worked with Python since the 1990s. He is the co-author of the newly published fifth edition of Python Object-Oriented Programming, updated for Python 3.13 with added material on areas such as type hints, testing, and professional software engineering practice. In this issue, his piece looks at object-oriented design as a practical engineering discipline: dependency injection, protocols, and duck typing as choices that shape coupling, change, and maintainability in production systems. We have also included Chapter 1, “Object-Oriented Design,” from the new edition for readers who want the full foundation.
Let’s get started.
PulseMCP Newsletter
Our friends at PulseMCP recently wrote up a great post on the rise of agentic engineering. It highlights both the very public, high profile case studies of top tier engineering organizations adopting the practice, and gets into the weeds of what’s working in the PulseMCP team’s personal coaching of engineering teams trying to make the transition from “AI pair programming” to “autonomous agents”.
🧠Expert Insight
Part 2: Getting Started with Object-Oriented Programming in Python
by Steven Lott
Python provides three primary ways to structure immutable data: dataclass with frozen=True, NamedTuple, and TypedDict. Each carries different trade-offs that matter in production systems.
A dataclass with frozen=True provides immutability through runtime enforcement. When you attempt to modify a frozen dataclass instance, Python raises an exception. This makes frozen dataclasses useful when you need immutability guarantees but also need features like inheritance, custom methods, or post-initialization processing. The runtime check adds minimal overhead and catches mutation attempts immediately in development and testing.
NamedTuple provides immutability through tuple semantics. Because NamedTuple instances are actual tuples under the hood, they inherit tuple’s immutability at the language level. This makes NamedTuple marginally faster than frozen dataclass and ensures that mutation is structurally impossible rather than runtime-prevented. NamedTuple works well for simple data containers that do not require inheritance or complex initialization logic.
TypedDict does not provide immutability at all. TypedDict exists purely for type checking and documentation. At runtime, a TypedDict is just a regular dictionary with no special behavior or constraints. This makes TypedDict appropriate when you need to type-hint dictionary structures for API contracts or configuration, but you do not control the actual dictionary creation or mutation.
The choice between these three depends on what guarantees your system actually requires. If you need inheritance or custom methods, use frozen dataclass. If you need maximum performance for simple data structures, use NamedTuple. If you need to type-hint existing dictionary-based interfaces without changing runtime behavior, use TypedDict.
Hashability and when it matters
Immutable objects in Python can be hashable, which means they can be used as dictionary keys or stored in sets. This is not just a convenience feature. Hashability enables certain architectural patterns that would otherwise require significantly more complex code.
When you need to deduplicate objects, track which objects have been processed, or use objects as cache keys, hashability becomes a structural requirement rather than a nice-to-have feature. A frozen dataclass or NamedTuple containing only hashable fields is automatically hashable. A regular mutable dataclass is not hashable because its contents could change after being added to a set or used as a dictionary key, which would break the fundamental contract of hash-based collections.
SOLID principles in Python
The SOLID principles originated in statically-typed object-oriented languages like Java and C++, but they translate to Python with some adjustments for Python’s dynamic nature and duck typing.
Single Responsibility Principle states that a class should have one reason to change. In Python, this often means keeping data structures separate from business logic, keeping validation logic separate from transformation logic, and keeping I/O operations separate from computation. When a class starts accumulating methods that could logically belong to different concerns, it is time to split it.
Open/Closed Principle states that software entities should be open for extension but closed for modification. In Python, this is typically achieved through composition and protocols rather than deep inheritance hierarchies. Instead of modifying existing classes to add behavior, you compose new classes that delegate to existing ones or define protocol-based interfaces that new classes can implement without touching existing code.
Liskov Substitution Principle states that subtypes must be substitutable for their base types. In Python, this means that if your code expects a file-like object, any object that implements the file protocol (read, write, close) should work correctly. The principle prevents surprising behavior when substituting one implementation for another.
Interface Segregation Principle states that clients should not depend on interfaces they do not use. In Python, this translates to keeping protocols narrow and focused rather than creating large monolithic base classes with dozens of methods that most implementations leave empty or raise NotImplementedError.
Dependency Inversion Principle states that high-level modules should not depend on low-level modules; both should depend on abstractions. In Python, this is achieved through dependency injection and protocols. Instead of instantiating concrete dependencies inside a class, you pass dependencies in as constructor arguments, and instead of depending on concrete types, you depend on protocols that define the interface you actually need.
Dependency injection without frameworks
Dependency injection in Python does not require a framework. The pattern is simple: instead of creating dependencies inside a class, pass them in as constructor arguments.
python
# Without dependency injection
class OrderProcessor:
def __init__(self):
self.payment_gateway = StripeGateway() # Hard dependency
self.email_service = SendGridService() # Hard dependency
def process(self, order):
self.payment_gateway.charge(order.amount)
self.email_service.send_confirmation(order.email)
# With dependency injection
class OrderProcessor:
def __init__(self, payment_gateway, email_service):
self.payment_gateway = payment_gateway
self.email_service = email_service
def process(self, order):
self.payment_gateway.charge(order.amount)
self.email_service.send_confirmation(order.email)The second version allows you to swap payment gateways or email services without modifying OrderProcessor. It allows you to test OrderProcessor with mock implementations that do not actually charge credit cards or send emails. It makes the dependencies explicit in the constructor signature rather than hidden inside the implementation.
Protocols and duck typing
Python’s protocols allow you to define interfaces without requiring inheritance. A protocol specifies what methods an object must have without requiring the object to inherit from a specific base class.
python
from typing import Protocol
class PaymentGateway(Protocol):
def charge(self, amount: float) -> bool: ...
class StripeGateway:
def charge(self, amount: float) -> bool:
# Stripe implementation
return True
class PayPalGateway:
def charge(self, amount: float) -> bool:
# PayPal implementation
return TrueBoth StripeGateway and PayPalGateway satisfy the PaymentGateway protocol without inheriting from it. Type checkers like mypy will verify that any object passed where a PaymentGateway is expected actually implements the charge method with the correct signature.
This is duck typing with type safety. You get the flexibility of duck typing (if it walks like a duck and quacks like a duck, it is a duck) combined with static verification that the duck actually has the methods you are going to call on it.
Part 2: Getting Started with Object-Oriented Programming in Python
Previously, we looked at some of the reasons OO programming is hard. And, we pitched a few strategies for getting started quickly:
Chapter 1: Object-Oriented Design
The complete foundation chapter from Object-Oriented Python by Steven Lott, from Python Object-Oriented Programming (Fourth Edition) that teaches how to build robust and maintainable object-oriented Python applications and libraries.
🔍In case you missed it…
🛠️ Tool of the Week
mypy — static type checker for Python
Highlights:
Catches type errors before runtime: Analyzes Python code statically to detect type inconsistencies, function signature mismatches, and attribute errors before the code ever runs, preventing entire classes of bugs from reaching production.
First-class support for OOP patterns: Understands inheritance hierarchies, protocols, generics, and abstract base classes, making it especially valuable for object-oriented codebases where polymorphism and interface contracts matter.
Gradual typing for existing projects: Works alongside untyped code, allowing you to add type hints incrementally to legacy projects without requiring a full rewrite, and can be configured to enforce strict typing only where needed.
📎 Tech Briefs
Python 3.15.0a7 (March 10, 2026) — The seventh alpha for Python 3.15 gives developers an early look at changes that matter to Python design work, including a built-in
frozendict, typed extra items forTypedDict,TypeForm, and further JIT performance gains.Python 3.12.13, 3.11.15, and 3.10.20 security releases (March 3, 2026) — Python shipped source-only security releases for 3.10–3.12 that fix header-injection issues, control-character handling bugs in HTTP and cookies, and bundled
libexpatvulnerabilities.Copilot code review now runs on an agentic architecture (March 5, 2026) — GitHub moved Copilot code review to an agentic tool-calling architecture that pulls broader repository context so review comments are higher-signal, lower-noise, and more grounded in architectural intent.
60 million Copilot code reviews and counting (March 5, 2026) — GitHub says Copilot code review usage has grown 10x to more than one in five code reviews on GitHub, and says its current system is optimized around accuracy, signal, and speed rather than comment volume.
Request Copilot code review from GitHub CLI (March 11, 2026) — GitHub now lets developers request Copilot review directly from the terminal with
gh pr editandgh pr create, pulling AI review more tightly into the existing pull-request workflow.
That’s all for today. Thank you for reading this issue of Deep Engineering.
We’ll be back next week with more expert-led content.
Stay awesome,
Saqib Jan
Editor-in-Chief, Deep Engineering
If your company is interested in reaching an audience of senior developers, software engineers, and technical decision-makers, you may want to advertise with us.







