Interfaces in Python: Informal & Formal Contracts
An interface is a contract that defines the methods a class must implement. In Python, any object that has the required methods conforms to that interface, regardless of inheritance—a concept called duck typing. Unlike languages such as Java or C++, Python offers two ways to define interfaces: informal interfaces using conventions and duck typing, and formal interfaces using Abstract Base Classes (ABC) from the abc module. Informal interfaces provide Pythonic flexibility; formal interfaces provide runtime enforcement.
Understanding Interfaces and Contracts in Python
An interface specifies a set of methods that conforming classes must provide. It is a behavioral contract: any class that implements the required methods satisfies the interface, regardless of whether it explicitly inherits from an interface base. This allows different implementations (CSV exporters, JSON exporters, API clients) to work interchangeably because they share the same method signatures.
In Python's duck-typing philosophy—"if it walks like a duck and quacks like a duck, it is a duck"—an object conforms to an interface if it implements the required methods, without needing explicit interface inheritance.
# Interface contract example
class DataParser:
"""Contract: any data parser must have a parse() method."""
def parse(self, data):
raise NotImplementedError("Subclasses must implement parse()")
Informal Interfaces: Duck Typing in Practice
Informal interfaces are the most Pythonic approach. They rely on documentation, convention, and duck typing rather than strict language enforcement. Any class that implements the required methods conforms to the interface, even if it doesn't inherit from a base class.
Creating an Informal Interface
To signal that a method should be implemented by subclasses, raise NotImplementedError:
class FileExporter:
"""Informal interface for file exporters.
Any exporter must have an export(data, filepath) method.
"""
def export(self, data, filepath):
raise NotImplementedError(
"Subclasses must implement export(data, filepath)"
)
Implementing Informal Interfaces
Concrete classes inherit from the interface base and implement the required methods:
class CSVExporter(FileExporter):
"""Exports data to CSV format."""
def export(self, data, filepath):
print(f"Exporting to CSV: {filepath}")
# Real implementation would write CSV
class JSONExporter(FileExporter):
"""Exports data to JSON format."""
def export(self, data, filepath):
print(f"Exporting to JSON: {filepath}")
# Real implementation would write JSON
class XMLExporter(FileExporter):
"""Exports data to XML format."""
def export(self, data, filepath):
print(f"Exporting to XML: {filepath}")
# Real implementation would write XML
Using Informal Interfaces Polymorphically
Because all exporters follow the same interface, client code treats them identically:
def save_report(exporter, data, path):
"""Works with any exporter following the interface."""
exporter.export(data, path)
data = [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]
# Same function works with all implementations
save_report(CSVExporter(), data, "report.csv")
save_report(JSONExporter(), data, "report.json")
save_report(XMLExporter(), data, "report.xml")
# Output:
# Exporting to CSV: report.csv
# Exporting to JSON: report.json
# Exporting to XML: report.xml
Advantages and Disadvantages of Informal Interfaces
Advantages:
- Simple and lightweight; no extra imports or decorators required.
- Leverages Python's natural strength: duck typing.
- Allows any class (even outside your control) to conform to an interface by implementing the methods.
- Ideal for small-to-medium projects and Pythonic code.
Disadvantages:
- No runtime enforcement; a subclass can forget to implement required methods, raising
NotImplementedErroronly when that method is called. - Less explicit intent; readers must examine code or docstrings to understand the interface contract.
- Harder to catch bugs early in development.
Formal Interfaces: Abstract Base Classes
For larger applications, frameworks, or codebases where strict contracts are essential, use the abc (Abstract Base Classes) module. ABCs define abstract methods that subclasses must implement; instantiation fails immediately if any abstract method is missing.
Creating a Formal Interface with ABC
The ABC class and @abstractmethod decorator enforce the contract:
from abc import ABC, abstractmethod
class DataSource(ABC):
"""Formal interface for data sources.
Any subclass must implement get_data() and close().
"""
@abstractmethod
def get_data(self):
"""Retrieve and return data."""
pass
@abstractmethod
def close(self):
"""Close the data source connection."""
pass
Implementing Formal Interfaces
Concrete subclasses must implement all abstract methods:
class DatabaseSource(DataSource):
def __init__(self, connection_string):
self.conn = connection_string
def get_data(self):
print(f"Fetching data from {self.conn}...")
return {"source": "database", "records": 100}
def close(self):
print("Closing database connection")
class APISource(DataSource):
def __init__(self, endpoint):
self.endpoint = endpoint
def get_data(self):
print(f"Fetching from {self.endpoint}...")
return {"source": "api", "records": 50}
def close(self):
print("Closing API connection")
Incomplete Implementation Results in TypeError
Attempting to instantiate a class that doesn't implement all abstract methods raises TypeError:
class IncompleteSource(DataSource):
# Only implements get_data(), not close()
def get_data(self):
return {"source": "incomplete"}
try:
source = IncompleteSource()
except TypeError as error:
print(f"Error: {error}")
# Output: Can't instantiate abstract class IncompleteSource
# with abstract method close
The error message explicitly states which methods are missing, aiding rapid debugging.
Using Formal Interfaces Polymorphically
Client code accepts any DataSource, confident all abstract methods are implemented:
def fetch_and_process(source: DataSource):
"""Works with any DataSource because ABC guarantees methods exist."""
data = source.get_data()
print(f"Processing: {data}")
source.close()
# Both implementations work; TypeError would be raised if methods were missing
db_source = DatabaseSource("postgresql://localhost/mydb")
fetch_and_process(db_source)
api_source = APISource("https://api.example.com/data")
fetch_and_process(api_source)
# Output:
# Fetching data from postgresql://localhost/mydb...
# Processing: {'source': 'database', 'records': 100}
# Closing database connection
# Fetching from https://api.example.com/data...
# Processing: {'source': 'api', 'records': 50}
# Closing API connection
Multiple Abstract Methods
ABCs can define multiple abstract methods, ensuring comprehensive contracts:
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
"""Formal interface for payment processors."""
@abstractmethod
def validate_payment(self, amount):
pass
@abstractmethod
def process_payment(self, amount):
pass
@abstractmethod
def get_transaction_status(self, transaction_id):
pass
class CreditCardProcessor(PaymentProcessor):
def validate_payment(self, amount):
print(f"Validating card for ${amount}")
return True
def process_payment(self, amount):
print(f"Processing card payment: ${amount}")
return "TXN123"
def get_transaction_status(self, transaction_id):
print(f"Checking status of {transaction_id}")
return "completed"
When to Use Informal vs. Formal Interfaces
Use Informal Interfaces when:
- Building small-to-medium personal projects or scripts.
- Working in a dynamic, exploratory codebase.
- You want maximum flexibility and minimal boilerplate.
- External classes might need to conform without explicit inheritance.
Use Formal Interfaces when:
- Building large frameworks or libraries where users extend your code.
- You need to guarantee implementations are complete before runtime.
- Your team values explicit contracts and early error detection.
- You require type checking with tools like
mypyfor static verification.
Abstract Methods with Implementation Defaults
Sometimes abstract methods have a default implementation that subclasses can call via super():
from abc import ABC, abstractmethod
class Logger(ABC):
@abstractmethod
def log(self, message):
"""Log message. Subclasses must call super()."""
print(f"[LOG] {message}") # Default behavior
class FileLogger(Logger):
def log(self, message):
super().log(message) # Call parent implementation
with open("app.log", "a") as f:
f.write(message + "\n")
class ConsoleLogger(Logger):
def log(self, message):
super().log(message) # Call parent implementation
print(f"CONSOLE: {message}")
Key Takeaways
- An interface is a contract defining the methods a class must implement; in Python, any object with the required methods conforms to the interface, regardless of explicit inheritance (duck typing).
- Informal interfaces use conventions and
NotImplementedError, relying on duck typing for flexibility; they are simple but lack runtime enforcement. - Formal interfaces use the
ABCclass and@abstractmethoddecorator from theabcmodule; they enforce the contract at instantiation time and are essential for large frameworks. - Informal interfaces suit small-to-medium projects where flexibility is prioritized; formal interfaces suit large systems where early error detection and explicit contracts are critical.
- Both approaches enable polymorphism: client code can accept any object conforming to an interface and call its methods uniformly.
- Abstract methods in ABCs can include default implementations that subclasses extend using
super().
Frequently Asked Questions
Can a class inherit from multiple ABCs?
Yes. A class can inherit from multiple ABCs and must implement all abstract methods from all parent ABCs. Multiple inheritance can lead to diamond problems, so use it carefully and prefer composition when possible.
What is the difference between @abstractmethod and raising NotImplementedError?
@abstractmethod prevents instantiation of incomplete subclasses; NotImplementedError is a runtime error raised only when a method is called. @abstractmethod provides earlier, compile-time-like checking.
Can I create an instance of an ABC?
No. Attempting to instantiate an ABC directly raises TypeError. ABCs are meant to be inherited; only concrete subclasses with all abstract methods implemented can be instantiated.
How do I type-hint that a function accepts any class conforming to an interface?
Use the ABC (or parent class) as the type hint: def process(source: DataSource):. This signals that any DataSource subclass is acceptable and enables type checkers like mypy to verify correctness.
Is duck typing safe without ABCs?
Duck typing is safe if your codebase is small, well-tested, and your team communicates interface contracts clearly. For large codebases or libraries, ABCs provide valuable safety and documentation.