Protocol: Structural Typing — Duck Typing with a Type Checker Behind It
Learn structural typing with Protocol: duck typing checked by the type checker. When to use Protocol vs ABC for payment processors and third-party library integration.
Okay, so I just got feedback on my pull request. Kai said "use Protocol, not ABC here." I nodded and closed the review, but honestly I have no idea what that means.
That's a very honest moment. What was the code doing?
I had a payment processor — Stripe, PayPal, and a manual fallback. All three take a charge and refund. I made them inherit from an abstract base class so they'd all have the same interface.
Okay, so the ABC worked. The type checker was happy. Your code ran. Why would Kai ask for Protocol instead?
That's what I don't get. If ABC works, why change it?
Here's the difference, and it's subtle but important. An ABC is a membership card. You inherit from it and you're saying "I am a member of this club." A Protocol is a skills test. You're saying "I can do these things — these methods with these signatures." You don't need anyone's permission. You don't need to inherit. You just need to have the right methods.
from typing import Protocol
from abc import ABC, abstractmethod
# ABC approach — Stripe MUST inherit from this
class PaymentProcessorABC(ABC):
@abstractmethod
def charge(self, amount: float, currency: str) -> dict: ...
@abstractmethod
def refund(self, transaction_id: str) -> dict: ...
# Protocol approach — Stripe just needs the right methods
class PaymentProcessor(Protocol):
def charge(self, amount: float, currency: str) -> dict: ...
def refund(self, transaction_id: str) -> dict: ...
# With Protocol: StripeProcessor doesn't inherit from anything
class StripeProcessor:
def charge(self, amount: float, currency: str) -> dict:
return {"success": True, "transaction_id": f"stripe_{amount}_{currency}"}
def refund(self, transaction_id: str) -> dict:
return {"success": True, "refunded_amount": 100.0}
# Type checker accepts StripeProcessor as a PaymentProcessor — it has the methods
def process(p: PaymentProcessor) -> dict:
return p.charge(99.99, "USD")
Wait. So you're saying Stripe could be a Protocol-compatible payment processor without ever knowing about the Protocol?
Exactly. Code a Protocol for what a payment processor should do. Then write your Stripe class however you want — add methods, inherit from something else, whatever. As long as your Stripe class has charge() and refund() with the right signatures, the type checker will accept it as a PaymentProcessor.
But that's duck typing. "If it quacks, it's a duck." Why would a type checker care about duck typing?
Because Python 3.8+ has structural typing — the type checker looks at what methods and attributes you have, not your inheritance chain. A Protocol is how you declare that structural contract to the type checker so it can check it.
Oh! So when Kai saw my ABC code, they thought "this enforces an inheritance relationship, but the real constraint is just the interface." Protocol says the interface is the only thing that matters?
You just explained it better than I would have.
So how does the type checker know if my Stripe class actually matches the Protocol? Does it run some kind of check?
The type checker is static — it never runs your code. It reads your type hints and your code and checks: does this class have charge() with this exact signature? Does it have refund() with this signature? If yes, it's compatible. The type checker doesn't care about inheritance.
What if I accidentally write charge() with the wrong signature?
The type checker will complain. Let's say your Protocol expects charge(amount: float, currency: str) -> bool. But your Stripe class has charge(amount: float) -> None. Type checker says no — you don't satisfy the Protocol.
So the Protocol is like a legal contract. "Sign here if you promise to have these methods with these signatures." And the type checker is the lawyer verifying you actually signed it.
That's perfect. And the type checker's check happens before your code runs — no runtime overhead.
But you can also check at runtime, right? I've seen isinstance() used on types.
Yes, but it requires a special decorator. By default, isinstance() won't work with Protocols — they're abstract ideas, not real classes. You add @runtime_checkable to the Protocol, and then isinstance() becomes a runtime test: "Does this object actually have these methods?" That's slower and only useful if you really need it.
When would you need the runtime check?
When you're loading classes dynamically — plugins, third-party code you don't control. You load a plugin and want to verify it's actually a valid PaymentProcessor before using it. Otherwise, stick with static type checking and let the type checker do the work at development time.
Okay, let me picture this. I have a function that accepts any payment processor and calls charge() on it. It takes a Protocol as a parameter, not an ABC. What changes?
Nothing, from the function's perspective. You call processor.charge(...) the same way. But the type checker is stricter: it will accept any class with the right methods, not just classes that inherit from your ABC. That's more flexible.
So if someone else writes a payment processor in a completely different library, and it happens to have charge() and refund() with the right signatures, I can use it without them knowing my Protocol exists?
Yes. That's the power of structural typing. The Stripe library could have been written 10 years ago, before you even thought about your Protocol. But if it has the right methods, it's compatible. No inheritance needed.
Versus ABC, where they'd have to explicitly inherit from your base class or it wouldn't work?
Right. ABC is nominal — names matter. "Are you a PaymentProcessor ABC?" Protocol is structural — structure matters. "Do you have the methods of a PaymentProcessor?"
One edge case: what if I intentionally don't want to accept just any class with charge() and refund()? What if I only want actual PaymentProcessors I've approved?
That's a good question. Then ABC is the right tool. ABC gives you control — only subclasses of your ABC are accepted. Protocol says "anything with the right structure is fine." Pick based on your intent.
So for our payment system, which is better?
Protocol. Stripe, PayPal, and your manual processor are from different sources. They don't all inherit from your code. But they all do the same things. Protocol captures that contract perfectly.
And if someone writes a new payment processor, they don't have to know my code exists. They just write charge() and refund(), and suddenly it works?
That's how you design systems for extension without modification.
Alright, so on the code side: I define a Protocol, and my classes just implement the methods?
You don't even have to explicitly say they implement the Protocol. The type checker figures it out. You can optionally inherit from the Protocol class to be explicit, but it's not required.
Why would I inherit if I don't have to?
For clarity and documentation. Saying class StripeProcessor(PaymentProcessor): tells the next person reading your code "this is intentionally a PaymentProcessor." Without inheritance, it's implicit — "happens to have the right methods." Sometimes explicit is better.
Okay, I think I get it. Protocol = structural typing, ABC = nominal typing. Use Protocol when the structure is the contract, ABC when membership matters.
That's the whole idea. And now when the next code review says "use Protocol," you'll know exactly why.
And I can explain it back to Kai.
Even better. Speaking of explaining things — next lesson we're going a layer deeper: TypedDict, Literal, and NewType. They're all advanced type hints that let you capture even more structure in your type system. You'll be combining these ideas.
Practice your skills
Sign up to write and run code in this lesson.
Protocol: Structural Typing — Duck Typing with a Type Checker Behind It
Learn structural typing with Protocol: duck typing checked by the type checker. When to use Protocol vs ABC for payment processors and third-party library integration.
Okay, so I just got feedback on my pull request. Kai said "use Protocol, not ABC here." I nodded and closed the review, but honestly I have no idea what that means.
That's a very honest moment. What was the code doing?
I had a payment processor — Stripe, PayPal, and a manual fallback. All three take a charge and refund. I made them inherit from an abstract base class so they'd all have the same interface.
Okay, so the ABC worked. The type checker was happy. Your code ran. Why would Kai ask for Protocol instead?
That's what I don't get. If ABC works, why change it?
Here's the difference, and it's subtle but important. An ABC is a membership card. You inherit from it and you're saying "I am a member of this club." A Protocol is a skills test. You're saying "I can do these things — these methods with these signatures." You don't need anyone's permission. You don't need to inherit. You just need to have the right methods.
from typing import Protocol
from abc import ABC, abstractmethod
# ABC approach — Stripe MUST inherit from this
class PaymentProcessorABC(ABC):
@abstractmethod
def charge(self, amount: float, currency: str) -> dict: ...
@abstractmethod
def refund(self, transaction_id: str) -> dict: ...
# Protocol approach — Stripe just needs the right methods
class PaymentProcessor(Protocol):
def charge(self, amount: float, currency: str) -> dict: ...
def refund(self, transaction_id: str) -> dict: ...
# With Protocol: StripeProcessor doesn't inherit from anything
class StripeProcessor:
def charge(self, amount: float, currency: str) -> dict:
return {"success": True, "transaction_id": f"stripe_{amount}_{currency}"}
def refund(self, transaction_id: str) -> dict:
return {"success": True, "refunded_amount": 100.0}
# Type checker accepts StripeProcessor as a PaymentProcessor — it has the methods
def process(p: PaymentProcessor) -> dict:
return p.charge(99.99, "USD")
Wait. So you're saying Stripe could be a Protocol-compatible payment processor without ever knowing about the Protocol?
Exactly. Code a Protocol for what a payment processor should do. Then write your Stripe class however you want — add methods, inherit from something else, whatever. As long as your Stripe class has charge() and refund() with the right signatures, the type checker will accept it as a PaymentProcessor.
But that's duck typing. "If it quacks, it's a duck." Why would a type checker care about duck typing?
Because Python 3.8+ has structural typing — the type checker looks at what methods and attributes you have, not your inheritance chain. A Protocol is how you declare that structural contract to the type checker so it can check it.
Oh! So when Kai saw my ABC code, they thought "this enforces an inheritance relationship, but the real constraint is just the interface." Protocol says the interface is the only thing that matters?
You just explained it better than I would have.
So how does the type checker know if my Stripe class actually matches the Protocol? Does it run some kind of check?
The type checker is static — it never runs your code. It reads your type hints and your code and checks: does this class have charge() with this exact signature? Does it have refund() with this signature? If yes, it's compatible. The type checker doesn't care about inheritance.
What if I accidentally write charge() with the wrong signature?
The type checker will complain. Let's say your Protocol expects charge(amount: float, currency: str) -> bool. But your Stripe class has charge(amount: float) -> None. Type checker says no — you don't satisfy the Protocol.
So the Protocol is like a legal contract. "Sign here if you promise to have these methods with these signatures." And the type checker is the lawyer verifying you actually signed it.
That's perfect. And the type checker's check happens before your code runs — no runtime overhead.
But you can also check at runtime, right? I've seen isinstance() used on types.
Yes, but it requires a special decorator. By default, isinstance() won't work with Protocols — they're abstract ideas, not real classes. You add @runtime_checkable to the Protocol, and then isinstance() becomes a runtime test: "Does this object actually have these methods?" That's slower and only useful if you really need it.
When would you need the runtime check?
When you're loading classes dynamically — plugins, third-party code you don't control. You load a plugin and want to verify it's actually a valid PaymentProcessor before using it. Otherwise, stick with static type checking and let the type checker do the work at development time.
Okay, let me picture this. I have a function that accepts any payment processor and calls charge() on it. It takes a Protocol as a parameter, not an ABC. What changes?
Nothing, from the function's perspective. You call processor.charge(...) the same way. But the type checker is stricter: it will accept any class with the right methods, not just classes that inherit from your ABC. That's more flexible.
So if someone else writes a payment processor in a completely different library, and it happens to have charge() and refund() with the right signatures, I can use it without them knowing my Protocol exists?
Yes. That's the power of structural typing. The Stripe library could have been written 10 years ago, before you even thought about your Protocol. But if it has the right methods, it's compatible. No inheritance needed.
Versus ABC, where they'd have to explicitly inherit from your base class or it wouldn't work?
Right. ABC is nominal — names matter. "Are you a PaymentProcessor ABC?" Protocol is structural — structure matters. "Do you have the methods of a PaymentProcessor?"
One edge case: what if I intentionally don't want to accept just any class with charge() and refund()? What if I only want actual PaymentProcessors I've approved?
That's a good question. Then ABC is the right tool. ABC gives you control — only subclasses of your ABC are accepted. Protocol says "anything with the right structure is fine." Pick based on your intent.
So for our payment system, which is better?
Protocol. Stripe, PayPal, and your manual processor are from different sources. They don't all inherit from your code. But they all do the same things. Protocol captures that contract perfectly.
And if someone writes a new payment processor, they don't have to know my code exists. They just write charge() and refund(), and suddenly it works?
That's how you design systems for extension without modification.
Alright, so on the code side: I define a Protocol, and my classes just implement the methods?
You don't even have to explicitly say they implement the Protocol. The type checker figures it out. You can optionally inherit from the Protocol class to be explicit, but it's not required.
Why would I inherit if I don't have to?
For clarity and documentation. Saying class StripeProcessor(PaymentProcessor): tells the next person reading your code "this is intentionally a PaymentProcessor." Without inheritance, it's implicit — "happens to have the right methods." Sometimes explicit is better.
Okay, I think I get it. Protocol = structural typing, ABC = nominal typing. Use Protocol when the structure is the contract, ABC when membership matters.
That's the whole idea. And now when the next code review says "use Protocol," you'll know exactly why.
And I can explain it back to Kai.
Even better. Speaking of explaining things — next lesson we're going a layer deeper: TypedDict, Literal, and NewType. They're all advanced type hints that let you capture even more structure in your type system. You'll be combining these ideas.