Interfaces #
Python has no interface keyword like Java or Go. But Python has something more flexible: three different ways to define contracts between components — duck typing, Abstract Base Classes (ABC), and Protocol. All three answer the same question: “How do I make sure an object has the methods I need?” — but with different levels of strictness and expressiveness. Knowing when to use each is the key to designing clean, extensible Python code.
Duck Typing — Implicit Interfaces #
Before discussing formal mechanisms, it’s important to understand Python’s core philosophy: “If it walks like a duck and quacks like a duck, then it’s a duck.” Python doesn’t care about an object’s type explicitly — what matters is that the object has the methods you need.
# No interface declaration — Python just calls the methods
class Dog:
def make_sound(self):
return "Woof!"
class Cat:
def make_sound(self):
return "Meow!"
class Duck:
def make_sound(self):
return "Quack!"
# This function works for any object with a make_sound() method
# No inheritance or interface declaration needed
def make_noise(animal_list):
for animal in animal_list:
print(animal.make_sound()) # Python doesn't check types — it just calls
animals = [Dog(), Cat(), Duck()]
make_noise(animals)
# → Woof!
# → Meow!
# → Quack!
Duck typing is very powerful and flexible, but it has a weakness: there’s no built-in mechanism ensuring an object really has the required methods before runtime. Errors only surface when the method is called — not when the object is created.
Abstract Base Classes (ABC) #
ABCs are the formal way to define interfaces in Python. A class inheriting from an ABC with abstract methods must implement all of those abstract methods — otherwise Python raises a TypeError when you try to create an instance.
Basic ABC #
from abc import ABC, abstractmethod
class Vehicle(ABC):
"""Interface for all kinds of vehicles."""
@abstractmethod
def move(self) -> str:
"""Returns a description of how the vehicle moves."""
...
@abstractmethod
def stop(self) -> None:
"""Stops the vehicle."""
...
@abstractmethod
def max_speed(self) -> float:
"""Returns the maximum speed in km/h."""
...
# Concrete method in the ABC — available to all subclasses
def info(self) -> str:
return f"{self.__class__.__name__}: max speed {self.max_speed()} km/h"
# ANTI-PATTERN: a subclass that doesn't implement all abstract methods
class Bicycle(Vehicle):
def move(self) -> str:
return "pedaling"
# stop() and max_speed() are not implemented!
# bicycle = Bicycle()
# → TypeError: Can't instantiate abstract class Bicycle
# with abstract methods max_speed, stop
# CORRECT: implement all abstract methods
class Car(Vehicle):
def __init__(self, brand: str, top_speed: float):
self.brand = brand
self._top_speed = top_speed
self._running = False
def move(self) -> str:
self._running = True
return f"{self.brand} starts moving"
def stop(self) -> None:
self._running = False
def max_speed(self) -> float:
return self._top_speed
class Bicycle(Vehicle):
def move(self) -> str:
return "Pedaling"
def stop(self) -> None:
print("Pulling the brakes")
def max_speed(self) -> float:
return 30.0
class Train(Vehicle):
def move(self) -> str:
return "Riding the rails"
def stop(self) -> None:
print("Pneumatic brakes engaged")
def max_speed(self) -> float:
return 300.0
# You can't instantiate an ABC directly
# vehicle = Vehicle() # → TypeError
car = Car("Toyota", 180.0)
bicycle = Bicycle()
train = Train()
print(car.move()) # → Toyota starts moving
print(car.info()) # → Car: max speed 180.0 km/h
print(bicycle.info()) # → Bicycle: max speed 30.0 km/h
print(train.info()) # → Train: max speed 300.0 km/h
# Polymorphism — the function works on every Vehicle
def race(vehicle_list: list[Vehicle]) -> None:
for v in sorted(vehicle_list, key=lambda x: x.max_speed(), reverse=True):
print(f"{v.__class__.__name__}: {v.move()} ({v.max_speed()} km/h)")
race([car, bicycle, train])
# → Train: Riding the rails (300.0 km/h)
# → Car: Toyota starts moving (180.0 km/h)
# → Bicycle: Pedaling (30.0 km/h)
Abstract Properties #
ABCs also support abstract properties — attributes that subclasses must implement:
from abc import ABC, abstractmethod
class Shape(ABC):
"""Interface for 2D geometric shapes."""
@property
@abstractmethod
def area(self) -> float:
"""The shape's area in square units."""
...
@property
@abstractmethod
def perimeter(self) -> float:
"""The shape's perimeter."""
...
def description(self) -> str:
return (
f"{self.__class__.__name__}: "
f"area={self.area:.2f}, perimeter={self.perimeter:.2f}"
)
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
@property
def area(self) -> float:
import math
return math.pi * self.radius ** 2
@property
def perimeter(self) -> float:
import math
return 2 * math.pi * self.radius
class Square(Shape):
def __init__(self, side: float):
self.side = side
@property
def area(self) -> float:
return self.side ** 2
@property
def perimeter(self) -> float:
return 4 * self.side
class Triangle(Shape):
def __init__(self, base: float, height: float, side_a: float, side_b: float, side_c: float):
self.base = base
self.height = height
self._sides = (side_a, side_b, side_c)
@property
def area(self) -> float:
return 0.5 * self.base * self.height
@property
def perimeter(self) -> float:
return sum(self._sides)
shape_list: list[Shape] = [
Circle(7),
Square(5),
Triangle(6, 4, 6, 5, 5),
]
for shape in shape_list:
print(shape.description())
# → Circle: area=153.94, perimeter=43.98
# → Square: area=25.00, perimeter=20.00
# → Triangle: area=12.00, perimeter=16.00
ABCs with Default Implementations (Template Method Pattern) #
An ABC doesn’t have to contain only abstract methods — it can provide default implementations that subclasses may override:
from abc import ABC, abstractmethod
class Report(ABC):
"""Template for generating reports — the Template Method pattern."""
def generate(self) -> str:
"""The step order is fixed; the details are implemented by subclasses."""
sections = [
self._header(),
self._body(),
self._footer(),
]
return "\n".join(sections)
@abstractmethod
def _header(self) -> str:
...
@abstractmethod
def _body(self) -> str:
...
def _footer(self) -> str:
# Default implementation — subclasses may override it or not
return "--- End of Report ---"
class SalesReport(Report):
def __init__(self, month: str, total: float):
self.month = month
self.total = total
def _header(self) -> str:
return f"=== SALES REPORT {self.month.upper()} ==="
def _body(self) -> str:
return f"Total sales: Rp{self.total:,.0f}"
class InventoryReport(Report):
def __init__(self, product_list: list):
self.product_list = product_list
def _header(self) -> str:
return "=== INVENTORY REPORT ==="
def _body(self) -> str:
lines = [f"- {p['name']}: {p['stock']} units" for p in self.product_list]
return "\n".join(lines)
def _footer(self) -> str:
# Override the footer for inventory reports
total_stock = sum(p['stock'] for p in self.product_list)
return f"Total stock overall: {total_stock} units"
sales_report = SalesReport("March", 125_500_000)
print(sales_report.generate())
# → === SALES REPORT MARCH ===
# → Total sales: Rp125,500,000
# → --- End of Report ---
Virtual Subclasses — Register Without Inheriting #
ABCs also support virtual subclasses — classes considered implementations of an ABC without directly inheriting from it. Useful for integrating third-party classes into an existing type system.
from abc import ABC, abstractmethod
class Serializable(ABC):
@abstractmethod
def serialize(self) -> str:
...
@abstractmethod
def deserialize(self, data: str) -> None:
...
# A third-party class that doesn't inherit from Serializable
class AmazingClass:
def serialize(self) -> str:
return '{"data": "ok"}'
def deserialize(self, data: str) -> None:
pass
# Register it as a virtual subclass
Serializable.register(AmazingClass)
obj = AmazingClass()
print(isinstance(obj, Serializable)) # → True (even though it doesn't inherit)
print(issubclass(AmazingClass, Serializable)) # → True
Protocol — Structural Subtyping (Formalized Duck Typing)
#
Protocol (introduced in Python 3.8 via PEP 544) is a way to define interfaces based on structure — not inheritance. A class is considered to implement a Protocol as long as it has the required methods and attributes, without needing to inherit or register itself explicitly.
from typing import Protocol, runtime_checkable
class Drawable(Protocol):
"""Protocol for drawable objects."""
def draw(self) -> None:
...
def resize(self, factor: float) -> None:
...
# The following classes do NOT inherit from Drawable — but they're considered
# to implement it because they have draw() and resize() methods
class Circle:
def __init__(self, r: float):
self.r = r
def draw(self) -> None:
print(f"Drawing circle r={self.r}")
def resize(self, factor: float) -> None:
self.r *= factor
class Box:
def __init__(self, w: float, h: float):
self.w = w
self.h = h
def draw(self) -> None:
print(f"Drawing box {self.w}x{self.h}")
def resize(self, factor: float) -> None:
self.w *= factor
self.h *= factor
# A type checker (mypy) accepts both as Drawable
def render_all(shapes: list[Drawable]) -> None:
for shape in shapes:
shape.draw()
shapes = [Circle(5), Box(10, 8)]
render_all(shapes)
# → Drawing circle r=5
# → Drawing box 10x8
@runtime_checkable — Protocols Checkable with isinstance
#
from typing import Protocol, runtime_checkable
@runtime_checkable
class Closable(Protocol):
def close(self) -> None:
...
class DatabaseConnection:
def close(self) -> None:
print("Closing database connection")
class FileHandler:
def close(self) -> None:
print("Closing file")
class Timer:
def start(self) -> None: # no close()
pass
db = DatabaseConnection()
fh = FileHandler()
tm = Timer()
print(isinstance(db, Closable)) # → True
print(isinstance(fh, Closable)) # → True
print(isinstance(tm, Closable)) # → False
# Useful for resource cleanup
def close_if_possible(obj: object) -> None:
if isinstance(obj, Closable):
obj.close()
close_if_possible(db) # → Closing database connection
close_if_possible(tm) # → (no output — Timer has no close())
ABC vs Protocol — When to Use Which #
Use ABC when:
✓ You want to force method implementation when the class is created (not when it's used)
✓ The ABC provides default implementations shared by subclasses
✓ A clear, explicit IS-A relationship is required
✓ You want to use the Template Method pattern
✓ You need virtual subclasses for third-party classes
Use Protocol when:
✓ You want documented duck typing that a type checker can verify
✓ The implementation classes already exist and can't be changed (can't add inheritance)
✓ No default implementations needed — only defining a contract
✓ You want a lighter interface without coupling to a class hierarchy
✓ Working with third-party libraries that can't be modified
To clarify the conceptual difference between Nominal Subtyping (based on name and explicit inheritance) and Structural Subtyping (based on structural similarity without inheritance), look at the comparison class diagram below:
classDiagram
class ABC_Model {
<<Abstract>>
+save(key, value)
+get(key)
}
class RedisStorage_ABC {
+save(key, value)
+get(key)
}
ABC_Model <|-- RedisStorage_ABC : Explicit Inheritance (Nominal)
class Protocol_Model {
<<Protocol>>
+save(key, value)
+get(key)
}
class RedisStorage_Protocol {
+save(key, value)
+get(key)
}
Protocol_Model <.. RedisStorage_Protocol : Structural Similarity (Structural)In the ABC (Nominal) model, the RedisStorage class must explicitly inherit from StorageABC. Meanwhile, in the Protocol (Structural) model, RedisStorage is automatically considered to implement StorageProtocol as long as it has methods with the same structure/signature, without inheriting from any class.
Head-to-Head: ABC vs Protocol for the Same Interface #
# === With ABC ===
from abc import ABC, abstractmethod
class StorageABC(ABC):
@abstractmethod
def save(self, key: str, value: str) -> None: ...
@abstractmethod
def get(self, key: str) -> str | None: ...
class RedisStorage(StorageABC): # MUST inherit from StorageABC
def save(self, key, value): ...
def get(self, key): ...
# === With Protocol ===
from typing import Protocol
class StorageProtocol(Protocol):
def save(self, key: str, value: str) -> None: ...
def get(self, key: str) -> str | None: ...
class RedisStorage: # no declaration needed at all
def save(self, key, value): ...
def get(self, key): ...
# RedisStorage is already compatible with StorageProtocol
# without inheriting anything — a type checker verifies its structure
Summary #
- Duck typing is Python’s default — if an object has the methods you need, it can be used without any formal declaration.
- ABCs provide an explicit contract enforced when the class is created — a subclass that doesn’t implement abstract methods raises
TypeErrorat instantiation, not when the method is called.@abstractmethodmarks methods that must be implemented.@property+@abstractmethodmarks required attributes.- ABCs may have default implementations — subclasses can inherit or override them. This pattern is called Template Method.
- Virtual subclasses (
ABC.register()) let third-party classes count as ABC implementations without modifying their code.Protocoldefines interfaces by structure, not inheritance — any class with the required methods is considered compatible.@runtime_checkablemakesisinstance()work on Protocols — useful for resource cleanup and dynamic dispatch.- Choose ABC if you need enforcement at class definition time or shared default implementations. Choose Protocol if you need a lighter interface and don’t want coupling to a class hierarchy.