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 TypeError at instantiation, not when the method is called.
  • @abstractmethod marks methods that must be implemented. @property + @abstractmethod marks 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.
  • Protocol defines interfaces by structure, not inheritance — any class with the required methods is considered compatible.
  • @runtime_checkable makes isinstance() 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.

← Previous: Classes   Next: Exceptions →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact