- What is an interface β a contract that defines what methods a class must have
- Duck typing β Python's informal approach to interfaces
- Abstract Base Classes (ABCs) β formal interfaces with runtime enforcement
- Protocols β modern structural interfaces for type checking
- ABC vs Protocol β when to use each approach
- Real-world use β practical examples you can use
What is an Interface?
An interface is like a contract. It says: "If you want to be a certain type of object, you must have these methods." It doesn't tell you how to implement them β it just tells you what you need to have.
Think of it like a job description. A job description says "must be able to drive" but doesn't tell you how. You could drive a car, a truck, or even a bus β as long as you can drive, you qualify. An interface works the same way β it says "must have a drive() method" but doesn't care how you implement it.
Unlike languages like Java or C# that have a special interface keyword, Python takes a different approach. It uses duck typing by default, but also provides tools like Abstract Base Classes (ABCs) and Protocols for more formal interfaces [citation:1][citation:7].
π‘ Key concept: An interface is a contract that defines what methods an object must have, not how they work. It's about "what" not "how."
Duck Typing β Python's Informal Interface
If It Walks Like a Duck...
Python follows the principle of duck typing: "If it walks like a duck and quacks like a duck, then it's a duck." In other words, Python doesn't care about the type of an object β it only cares about what methods it has [citation:1][citation:7].
This is Python's informal approach to interfaces. There's no formal contract β you just assume that an object has the methods you need. If it doesn't, Python will throw an error when you try to use it.
# Duck typing in action β Python's informal interface
class Dog:
def speak(self):
return "Woof!"
def move(self):
return "Runs on four legs"
class Cat:
def speak(self):
return "Meow!"
def move(self):
return "Walks silently"
class Robot:
def speak(self):
return "Beep boop!"
def move(self):
return "Rolls on wheels"
# This function doesn't care about the type β just that the object has speak()
def make_it_speak(animal):
return animal.speak()
# All these work because all have a speak() method
print(make_it_speak(Dog())) # Woof!
print(make_it_speak(Cat())) # Meow!
print(make_it_speak(Robot())) # Beep boop!
# The trade-off: no safety net
class Fish:
def swim(self):
return "Swimming"
# This would cause an error at runtime
# print(make_it_speak(Fish())) # AttributeError: 'Fish' object has no attribute 'speak'
print("\nβ
Duck typing: Python doesn't care what you are, only what you can do!")
print("β The trade-off: errors are only caught at runtime when you try to call a missing method.")
Duck typing key points:
- No formal contract β Python doesn't enforce anything
- Focus on behavior β what methods does the object have?
- Flexible β any class can be used as long as it has the right methods
- Runtime errors β problems are caught when you try to use the missing method
- EAFP principle β Easier to Ask for Forgiveness than Permission
Quick Check: What is duck typing? (Answer: Python focuses on what an object can do, not what it is)
Abstract Base Classes β Formal Interfaces
Creating Formal Interfaces with ABCs
When you need a formal interface that's enforced at runtime, you use Abstract Base Classes (ABCs). ABCs are classes that can't be instantiated β they exist only to be inherited from [citation:1][citation:3][citation:9].
When a class inherits from an ABC, it must implement all abstract methods. If it doesn't, Python throws an error when you try to create an object. This is like having a bouncer at the door β you can't get in unless you meet all the requirements [citation:3][citation:9].
# Abstract Base Classes β formal interfaces
from abc import ABC, abstractmethod
# Define a formal interface
class Printable(ABC):
"""Interface: anything that can be printed must have these methods"""
@abstractmethod
def print_document(self, document):
"""Print a document β must be implemented"""
pass
@abstractmethod
def get_status(self):
"""Get printer status β must be implemented"""
pass
# A class that implements the interface properly
class LaserPrinter(Printable):
def __init__(self):
self.toner_level = 100
def print_document(self, document):
self.toner_level -= 5
return f"Laser printing: {document} (toner: {self.toner_level}%)"
def get_status(self):
return f"Laser printer ready, toner: {self.toner_level}%"
# Another class that implements the interface
class InkjetPrinter(Printable):
def __init__(self):
self.ink_level = 80
def print_document(self, document):
self.ink_level -= 3
return f"Inkjet printing: {document} (ink: {self.ink_level}%)"
def get_status(self):
return f"Inkjet printer ready, ink: {self.ink_level}%"
# This would cause an error β missing abstract method
class BrokenPrinter(Printable):
def print_document(self, document):
return f"Printing: {document}"
# Missing get_status() β can't instantiate!
# Using the classes
print("=== LASER PRINTER ===")
laser = LaserPrinter()
print(laser.print_document("Report.pdf"))
print(laser.get_status())
print("\n=== INKJET PRINTER ===")
inkjet = InkjetPrinter()
print(inkjet.print_document("Photo.jpg"))
print(inkjet.get_status())
# This would cause a TypeError at instantiation:
# broken = BrokenPrinter() # TypeError: Can't instantiate abstract class BrokenPrinter
print("\nβ
ABCs enforce the contract at instantiation time!")
print("You can't create an object that doesn't implement all abstract methods.")
ABCs key points:
- Formal interface β defines what methods must exist
- Runtime enforcement β catches missing methods when you instantiate
- Uses @abstractmethod β from the abc module
- Child must implement all β or it remains abstract
- Can have concrete methods β shared behavior for all children
Quick Check: What happens if a class inherits from an ABC but doesn't implement all abstract methods? (Answer: You can't instantiate it β Python raises a TypeError)
Protocols β Modern Structural Interfaces
Structural Subtyping with Protocols
Protocols are a newer way to define interfaces in Python (introduced in Python 3.8). They use structural subtyping β a class doesn't need to explicitly inherit from the protocol. It just needs to have the right methods with the right signatures [citation:1][citation:7][citation:8].
Protocols are great for static type checking (with tools like mypy) and for creating loosely coupled code. The implementing class doesn't even need to know about the protocol β it just happens to have the right methods [citation:1][citation:8].
# Protocols β structural interfaces
from typing import Protocol
# Define a protocol
class Flyable(Protocol):
"""Anything that can fly must have a fly() method"""
def fly(self) -> str:
"""Return a string describing the flight"""
pass
# Classes that implement the protocol (without inheriting from it!)
class Bird:
def fly(self) -> str:
return "Flapping wings and soaring"
def sing(self) -> str:
return "Chirping"
class Airplane:
def fly(self) -> str:
return "Engines roaring, taking off"
def land(self) -> str:
return "Landing gear down"
class Drone:
def fly(self) -> str:
return "Humming and hovering"
def take_photo(self) -> str:
return "Photo taken"
# This class does NOT implement the protocol
class Fish:
def swim(self) -> str:
return "Swimming in water"
# Function that accepts any Flyable (using type hints)
def send_to_sky(flyer: Flyable) -> str:
return flyer.fly()
# All these work because they have fly()
print(send_to_sky(Bird())) # Flapping wings and soaring
print(send_to_sky(Airplane())) # Engines roaring, taking off
print(send_to_sky(Drone())) # Humming and hovering
# This would fail type checking (but run if ignored)
# print(send_to_sky(Fish())) # AttributeError: 'Fish' object has no attribute 'fly'
print("\nβ
Protocols: The class doesn't need to know about the interface!")
print("It just needs to have the right methods.")
print("This is called 'structural subtyping.'")
Protocols key points:
- Structural subtyping β classes don't need to inherit from the protocol
- Static type checking β works with mypy, pyright, etc.
- No runtime enforcement by default β but can use @runtime_checkable
- Decoupled β the implementing class doesn't depend on the protocol
- Modern approach β introduced in Python 3.8
Quick Check: What's the main difference between ABCs and Protocols? (Answer: ABCs require inheritance and enforce at runtime; Protocols use structural subtyping and are for static type checking)
ABC vs Protocol: Which to Choose?
Making the Right Choice
So when should you use ABCs and when should you use Protocols? The answer depends on your needs. Here's a simple way to think about it [citation:1][citation:8]:
# Decision Matrix: ABC vs Protocol
# ---------- USE ABC WHEN ----------
# 1. You need runtime enforcement
class Storage(ABC):
@abstractmethod
def save(self, data):
pass
@abstractmethod
def load(self):
pass
class FileStorage(Storage):
def save(self, data):
return f"Saving: {data}"
def load(self):
return "Loading data"
# β
Runtime check: can't create FileStorage without implementing both methods
# 2. You want to share default helper methods
class Logger(ABC):
@abstractmethod
def log(self, message):
pass
# Concrete method β shared by all children
def log_error(self, message):
self.log(f"ERROR: {message}")
def log_info(self, message):
self.log(f"INFO: {message}")
class ConsoleLogger(Logger):
def log(self, message):
print(f"Console: {message}")
# β
ConsoleLogger inherits log_error() and log_info() for free
# 3. You need isinstance() checks with inheritance
# β
ABCs support isinstance() by default
# ---------- USE PROTOCOL WHEN ----------
# 1. Your class shouldn't depend on the interface
from typing import Protocol
class Runner(Protocol):
def run(self) -> str: ...
# Some third-party class that we can't modify
class Athlete:
def run(self) -> str:
return "Running fast"
# β
Athlete doesn't need to know about Runner protocol
# 2. You want loose coupling
# β
The protocol can change without modifying implementers
# 3. You're working with static type checking
def make_them_run(runner: Runner) -> str:
return runner.run()
# β
mypy will check that Athlete satisfies Runner
# ---------- ABC vs Protocol Decision Table ----------
"""
| Use Case | Choose ABC | Choose Protocol |
|-----------------------------------|------------|-----------------|
| Need runtime enforcement | β
Yes | β No (unless @runtime_checkable) |
| Want to share helper methods | β
Yes | β No |
| Class shouldn't depend on you | β No | β
Yes |
| Working with third-party classes | β No | β
Yes |
| Need isinstance() support | β
Yes | β No |
| Static type checking | β
Yes | β
Yes |
| Multiple inheritance | β
Yes | β
Yes (no conflicts) |
| Library internal code | β
Yes | Sometimes |
| Public API for users | Sometimes | β
Preferred |
"""
print("β
Choose ABC when you need runtime enforcement and shared behavior")
print("β
Choose Protocol when you want loose coupling and structural typing")
ABC vs Protocol key points:
- ABC β runtime enforcement, shared methods, class depends on interface
- Protocol β static type checking, loose coupling, interface depends on class
- ABC for internal code β when you control all implementations
- Protocol for public APIs β when users should be able to plug in their own types
- Both can be used together β ABCs internally, Protocols externally [citation:8]
Quick Check: When should you use a Protocol instead of an ABC? (Answer: When you want loose coupling and the class shouldn't depend on your interface)
Real-World Examples
Seeing Interfaces in Action
# Real-world example: A Plugin System
from abc import ABC, abstractmethod
from typing import Protocol
import json
# ---- Part 1: Using ABC for Internal Plugins ----
class Plugin(ABC):
"""Abstract base class for all plugins"""
@abstractmethod
def name(self) -> str:
"""Return the plugin's name"""
pass
@abstractmethod
def process(self, data: dict) -> dict:
"""Process the data and return modified data"""
pass
@abstractmethod
def validate(self, data: dict) -> bool:
"""Validate that the data can be processed"""
pass
class LoggingPlugin(Plugin):
def name(self) -> str:
return "Logging Plugin"
def process(self, data: dict) -> dict:
print(f"[LOG] Processing: {data}")
data["logged"] = True
return data
def validate(self, data: dict) -> bool:
return isinstance(data, dict)
class EncryptionPlugin(Plugin):
def name(self) -> str:
return "Encryption Plugin"
def process(self, data: dict) -> dict:
data["encrypted"] = True
data["data"] = "π " + str(data.get("data", ""))
return data
def validate(self, data: dict) -> bool:
return "data" in data
# ---- Part 2: Using Protocol for External Plugins ----
class DataExporter(Protocol):
"""Protocol for data exporters (structural)"""
def export(self, data: list) -> str:
"""Export data to a string format"""
pass
def get_format_name(self) -> str:
"""Return the format name"""
pass
# These classes don't import or know about DataExporter protocol
class JSONExporter:
def export(self, data: list) -> str:
return json.dumps(data, indent=2)
def get_format_name(self) -> str:
return "JSON"
class CSVExporter:
def export(self, data: list) -> str:
if not data:
return ""
headers = data[0].keys()
lines = [",".join(headers)]
for item in data:
lines.append(",".join(str(item.get(h, "")) for h in headers))
return "\n".join(lines)
def get_format_name(self) -> str:
return "CSV"
# ---- Part 3: Using the System ----
class PluginManager:
"""Manages plugins (uses ABC-based plugins)"""
def __init__(self):
self.plugins = []
def register(self, plugin: Plugin):
self.plugins.append(plugin)
def process_data(self, data: dict) -> dict:
result = data.copy()
for plugin in self.plugins:
if plugin.validate(result):
result = plugin.process(result)
return result
def export_data(exporters: list, data: list):
"""Function that works with any exporter (structural)"""
for exporter in exporters:
print(f"--- {exporter.get_format_name()} ---")
print(exporter.export(data))
print()
# ---- Demo ----
print("=" * 50)
print("PLUGIN SYSTEM WITH INTERFACES")
print("=" * 50)
# Using ABC-based plugins
manager = PluginManager()
manager.register(LoggingPlugin())
manager.register(EncryptionPlugin())
print("\n1. PROCESSING WITH PLUGINS (ABC)")
data = {"user": "Alice", "data": "Hello World"}
print(f"Original data: {data}")
processed = manager.process_data(data)
print(f"Processed data: {processed}")
# Using Protocol-based exporters
print("\n2. EXPORTING DATA (PROTOCOL)")
sample_data = [
{"name": "Alice", "age": 30},
{"name": "Bob", "age": 25}
]
exporters = [JSONExporter(), CSVExporter()]
export_data(exporters, sample_data)
print("\nβ
ABCs for internal code with runtime enforcement")
print("β
Protocols for public API with loose coupling")
print("Both approaches work together in the same system!")
Real-world example key points:
- Plugin (ABC) β enforces that all plugins have name(), process(), validate()
- LoggingPlugin, EncryptionPlugin β implement the ABC interface
- DataExporter (Protocol) β defines export interface without requiring inheritance
- JSONExporter, CSVExporter β don't know about the protocol, just have the right methods
- Both approaches β work together in the same system
Quick Check: In the example, what's the advantage of using Protocol for exporters? (Answer: Exporters don't need to import or depend on the interface)
Best Practices for Interfaces
Using Interfaces Effectively
# Best practices for interfaces
from abc import ABC, abstractmethod
from typing import Protocol
# 1. Name interfaces clearly
# Good β descriptive names
class Database(ABC):
@abstractmethod
def connect(self):
pass
class FileReader(Protocol):
def read(self) -> str: ...
# Bad β vague names
class D(ABC):
pass
class FR(Protocol):
pass
# 2. Keep interfaces focused
# Good β single responsibility
class Logger(ABC):
@abstractmethod
def log(self, message):
pass
class Formatter(ABC):
@abstractmethod
def format(self, data):
pass
# Bad β too many responsibilities
class Utility(ABC):
@abstractmethod
def log(self, message):
pass
@abstractmethod
def format(self, data):
pass
@abstractmethod
def save(self, data):
pass
# 3. Use docstrings to document interfaces
class Payment(ABC):
@abstractmethod
def process(self, amount: float) -> str:
"""
Process a payment of the given amount.
Args:
amount: The amount to process
Returns:
str: Status message
"""
pass
# 4. For Protocols, keep them simple
class Runner(Protocol):
def run(self) -> str:
"""A simple protocol with one method"""
...
# 5. Use ABCs when you need to share code
class Writer(ABC):
@abstractmethod
def write(self, content):
pass
# Shared helper
def write_line(self, content):
return self.write(content + "\n")
# 6. Use Protocols for structural typing
class Processor(Protocol):
def process(self, data: dict) -> dict:
...
# 7. Combine both approaches
# Use ABCs internally, Protocols externally
class InternalPlugin(ABC):
"""Internal code β uses ABC"""
@abstractmethod
def execute(self):
pass
class PublicPlugin(Protocol):
"""Public API β uses Protocol"""
def execute(self) -> None: ...
# 8. Don't overcomplicate
# Simple duck typing is often enough for small projects
def print_message(obj):
"""Just call the method β duck typing works fine"""
return obj.message()
Best practices summary:
- Clear names β make it obvious it's an interface
- Keep it focused β each interface should have one purpose
- Document β explain what the interface requires
- ABC for code sharing β when you need default implementations
- Protocol for loose coupling β when classes shouldn't depend on your interface
- Combine approaches β ABCs internally, Protocols externally
- Don't overcomplicate β duck typing is fine for simple cases
Quick Check: What's a good reason to use Protocols over ABCs? (Answer: When you want loose coupling and the implementing class shouldn't depend on your interface)
Try It Yourself
Experiment with interfaces in the editor below.
INTERFACES PRACTICE
========================================
1. DEFINING AN ABC INTERFACE
2. IMPLEMENTING THE ABC
3. DEFINING A PROTOCOL
4. SATISFYING THE PROTOCOL
5. USING THE CLASSES
=== MEDIA PLAYERS (ABC) ===
π΅ Playing music
βΈοΈ Music paused
Volume: 50%
π¬ Playing video
βΉοΈ Video stopped
=== RENDERERS (PROTOCOL) ===
Rendering: π Rendering PDF
Rendering: π Rendering HTML
Rendering: πΌοΈ Rendering Image
Interfaces practice complete!
You've Got It!
You now understand interfaces in Python. You know how to use duck typing, create formal interfaces with ABCs, and use Protocols for structural typing.
Quick Quiz
Test what you've learned:
Frequently Asked Questions
Does Python have an interface keyword like Java?
interface keyword like Java or C#. Instead, Python uses duck typing by default, and provides Abstract Base Classes (ABCs) and Protocols for more formal interface-like behavior [citation:1][citation:7].
Can I use isinstance() with Protocols?
isinstance(). You need to use the @runtime_checkable decorator to enable this. However, even then, runtime-checkable protocols only check that methods exist β not that they have the correct signatures [citation:1][citation:8].
What's the difference between an abstract class and an interface?
What's a common interview question about interfaces?
Should I use ABCs or Protocols in my code?
What are the built-in ABCs in Python?
collections.abc module provides many useful ABCs including: Container, Iterable, Sequence, MutableSequence, Set, MutableSet, Mapping, MutableMapping, and more [citation:2][citation:5]. These let you check if a class implements a particular interface.
Where to Go From Here
Now that you understand interfaces in Python, check out these related topics:
Abstract Class vs Interface
Learn the key differences between these two concepts.
Learn More βAbstract Methods
Learn more about methods that must be implemented.
Learn More βPolymorphism
Learn how interfaces enable polymorphic behavior.
Learn More β