Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
310: The Protocol Class for Structural Typing
You've likely spent a lot of time using Abstract Base Classes (ABCs) to define interfaces in Python. It feels like the "correct" way—you define a base class, you mark some methods as @abstractmethod, and you force every subclass to implement them. It gives you a sense of security knowing that if a class inherits from PaymentProcessor, it definitely has a process_payment method. But there's a hidden cost to this approach that usually bites you once your project grows or you start integrating third-party libraries.
The Friction of Nominal Inheritance
The problem with ABCs is that they rely on nominal typing. In plain English: Python checks the name of the class (and its parents) to see if it matches. If it doesn't explicitly inherit from your base class, Python's type checker doesn't care if the class has all the right methods—it still considers it the wrong type.
Imagine you've built a system to handle payments. You might start with something like this:
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@abstractmethod
def process_payment(self, amount: float) -> bool:
pass
def execute_transaction(processor: PaymentProcessor, amount: float):
return processor.process_payment(amount)
class InternalProcessor(PaymentProcessor):
def process_payment(self, amount: float) -> bool:
print(f"Processing ${amount} internally...")
return True
# This works great
execute_transaction(InternalProcessor(), 100.0)
This works perfectly until you decide to use a third-party library, like a hypothetical StripeSDK. The SDK provides a StripeClient class that has a process_payment method, but since the SDK authors didn't know about your PaymentProcessor ABC, StripeClient doesn't inherit from it. Even though the StripeClient provides exactly what execute_transaction needs, Mypy or Pyright will scream at you, telling you that StripeClient is not a PaymentProcessor.
I've seen developers solve this by writing "wrapper" or "adapter" classes—essentially creating a new class just to wrap the SDK client and inherit from the ABC. It's a lot of boilerplate code that exists solely to satisfy the type checker, not to add actual value to the logic.
Static Duck Typing with Protocols
This is where typing.Protocol comes in. Protocols implement structural typing. Instead of asking "Who is your parent?", the type checker asks "What can you do?". If a class has the methods defined in the Protocol, it is considered a subtype of that Protocol, regardless of its inheritance tree. I call this "static duck typing" because it gives you the flexibility of Python's dynamic nature but catches the errors before you ever run the code.
Here is how we refactor the payment example to be decoupled:
from typing import Protocol
class PaymentProcessor(Protocol):
def process_payment(self, amount: float) -> bool:
...
def execute_transaction(processor: PaymentProcessor, amount: float):
return processor.process_payment(amount)
# This class knows NOTHING about PaymentProcessor
class StripeClient:
def process_payment(self, amount: float) -> bool:
print(f"Stripe is processing ${amount}...")
return True
# This now passes type checking!
execute_transaction(StripeClient(), 250.0)
Notice a few things here. First, we use ... (the ellipsis) instead of pass in the Protocol methods. Second, StripeClient doesn't inherit from PaymentProcessor. It just happens to have the right signature. The coupling is gone.
When to stick with ABCs
You might be wondering if you should just delete all your ABCs. Not necessarily. Protocols are fantastic for defining interfaces for things you don't control (like libraries) or for creating highly flexible plugins. However, ABCs are still useful when you want to provide shared logic. Since a Protocol cannot contain actual implementation code (it only defines the shape), an ABC is still the right tool if you have a bunch of subclasses that should all share a common helper method or a default behavior.
The trade-off is simple: use ABCs when you want to provide a template and shared state; use Protocols when you want to define a contract that any class, anywhere, can satisfy simply by having the right methods.
📋 Practical Task
Implementing a Decoupled File Export System
You are building a system that exports data to various formats. Some exporters are written internally, but others come from an external LegacyExportLib that you cannot modify. Your goal is to create a system where any object with an export(data: str) -> bool method can be used, without forcing them to inherit from a specific base class.
Requirements:
- Define a
ExporterProtocol that requires anexportmethod taking a string and returning a boolean. - Create an internal
JsonExporterclass that implements this method. - Assume a class
LegacyCSVExporterexists (you can just define it normally without inheriting from anything) that also has anexportmethod. - Write a function
run_export_pipeline(exporter: Exporter, data: str)that calls the export method. - Demonstrate that
run_export_pipelineaccepts both theJsonExporterand theLegacyCSVExporter.
There are no comments for now.