ABCs and Protocols

Python 3.14 · ✓ verified by execution on 2026-07-29

Python is famous for its dynamic “duck typing” — if it walks like a duck and quacks like a duck, it’s a duck! But as Python codebases grow, relying purely on runtime behavior can lead to unpredictable errors.

To solve this, Python gives us two powerful ways to define and enforce interfaces: Abstract Base Classes (ABCs) for nominal typing, and Protocols for structural subtyping.

Let’s explore how they differ and when to use each.

Abstract Base Classes (ABCs)

The abc module provides the infrastructure for defining abstract base classes (ABCs) in Python.

An ABC allows you to define an interface that other classes must implement. You can define an abstract method using the @abstractmethod decorator.

python
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

class Circle(Shape):
    def area(self):
        return 3.14

c = Circle()
print(c.area())
Output
3.14

What happens if a subclass forgets to implement an abstract method? A class that has a metaclass derived from ABCMeta cannot be instantiated unless all of its abstract methods and properties are overridden.

python
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

try:
    s = Shape()
except TypeError as e:
    print(str(e))
Output
Can't instantiate abstract class Shape without an implementation for abstract method 'area'

Misconception: Abstract methods cannot have an implementation An abstract method can have an implementation (which can be called using super() by subclasses), but subclasses are still forced to override it.

python · visualize
from abc import ABC, abstractmethod
class Shape(ABC):
    @abstractmethod
    def area(self): pass
class Circle(Shape):
    def area(self): return 3.14
c = Circle()

Protocols (Structural Subtyping)

While ABCs require explicit inheritance (nominal typing), Protocols allow you to define structural subtyping (static duck-typing).

Introduced in Python 3.8 via the typing module, such classes are primarily used with static type checkers that recognize structural subtyping (static duck-typing).

A class is considered a subtype of a Protocol if it implements all the protocol’s methods, even without inheriting from it.

python
from typing import Protocol

class SupportsFly(Protocol):
    def fly(self) -> str:
        ...

class Bird:
    def fly(self) -> str:
        return 'Flap flap'

def make_it_fly(f: SupportsFly):
    print(f.fly())

make_it_fly(Bird())
Output
Flap flap

Notice that Bird never explicitly inherits from SupportsFly. The type checker simply looks at the structure: does Bird have a fly() method? Yes!

When to use Protocols? Protocols are perfect when you want to type-check objects that you didn’t write yourself (like standard library classes or third-party objects) that already implement the methods you need, but don’t inherit from your ABC.

Checking Interfaces at Runtime

By default, Protocols are only for static type checking (like mypy). If you try to use isinstance() with a regular Protocol, it will raise a TypeError.

To use isinstance() with Protocols at runtime, you must decorate the Protocol with @runtime_checkable.

python
from typing import Protocol, runtime_checkable

@runtime_checkable
class SupportsQuack(Protocol):
    def quack(self) -> str:
        ...

class Duck:
    def quack(self) -> str:
        return 'Quack!'

print(isinstance(Duck(), SupportsQuack))
Output
True

If the object lacks the required methods, isinstance() will safely return False:

python
from typing import Protocol, runtime_checkable

@runtime_checkable
class SupportsQuack(Protocol):
    def quack(self):
        pass

class Dog:
    def bark(self):
        pass

print(isinstance(Dog(), SupportsQuack))
Output
False

Runtime Checking Limitations Using @runtime_checkable only checks for the presence of the method names at runtime, not their full type signatures.

python · visualize
from typing import Protocol
class SupportsFly(Protocol):
    def fly(self) -> str: ...
class Bird:
    def fly(self) -> str: return 'Flap flap'
b = Bird()

Summary

Check yourself

Your function needs any object with a .read() method. Which approach avoids forcing callers to inherit from your class?

Reveal answer

A Protocol — structural subtyping matches on methods, not ancestry — Protocols use structural subtyping: any class with the right methods satisfies it, with no inheritance relationship required.

Can an @abstractmethod contain a real implementation?

Reveal answer

Yes, and subclasses are still required to override it — An abstract method may carry a usable implementation (reachable via super()), but the subclass is still forced to override it.

What does inheriting from an ABC actually do at runtime?

Reveal answer

Refuses instantiation until every abstract method is implemented — ABCs are a design and type-checking tool. The runtime effect is blocking instantiation of an incomplete subclass, not performance.

Challenges

Challenge 1 +50 XP

Create an Abstract Base Class named `Vehicle` with an abstract method `start()`. Then create a `Car` subclass that implements `start()` by returning 'Vroom'. Instantiate a `Car` and print the result of `start()`.

python
  • Test 1 — expects "Vroom"
Need a hint? (−25% XP)

Use the ABC class and the @abstractmethod decorator.

Show solution (0 XP)
from abc import ABC, abstractmethod

class Vehicle(ABC):
    @abstractmethod
    def start(self):
        pass

class Car(Vehicle):
    def start(self):
        return 'Vroom'

c = Car()
print(c.start())

Challenge 2 +50 XP

Define a `Drawable` Protocol that specifies a `draw(self) -> str` method. Decorate it with `@runtime_checkable`. Create a `Circle` class that implements `draw` returning 'Drawing Circle'. Print whether a `Circle` instance is an instance of `Drawable` using `isinstance()`.

python
  • Test 1 — expects "True"
Need a hint? (−25% XP)

Use @runtime_checkable on the Drawable Protocol so you can use isinstance().

Show solution (0 XP)
from typing import Protocol, runtime_checkable

@runtime_checkable
class Drawable(Protocol):
    def draw(self) -> str:
        ...

class Circle:
    def draw(self) -> str:
        return 'Drawing Circle'

print(isinstance(Circle(), Drawable))