Abstract Classes & ABC

intermediate ABC abstractmethod abstract-class interface Protocol

An abstract class is a class that can’t be instantiated directly. It exists only to define a contract — “if you’re my subclass, you must implement these methods.” Think of it like a job description — it says what needs to be done, but doesn’t do the work itself.

Why Do We Need Abstract Classes?

Without them, we have no way to enforce that subclasses implement certain methods. We’d only find out at runtime when calling a missing method.

class Shape:
    def area(self):
        pass  # subclasses "should" override this, but nothing forces them

class Circle(Shape):
    pass  # oops, forgot to implement area()

c = Circle()
c.area()  # returns None — no error, just a silent bug

Abstract classes fix this by raising an error at instantiation time, not when the method is called.

abc.ABC and @abstractmethod

We use the abc module (Abstract Base Classes) to create abstract classes.

from abc import ABC, abstractmethod

class Shape(ABC):  # inherit from ABC
    @abstractmethod
    def area(self):
        """Subclasses must implement this."""
        pass

    @abstractmethod
    def perimeter(self):
        pass

# Shape()  # TypeError: Can't instantiate abstract class Shape

Now any subclass must implement both area() and perimeter(), or it’s also abstract and can’t be instantiated.

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14159 * self.radius ** 2

    def perimeter(self):
        return 2 * 3.14159 * self.radius

c = Circle(5)  # works — all abstract methods are implemented
print(c.area())  # 78.53975

If we forget one:

class BadCircle(Shape):
    def area(self):
        return 0
    # forgot perimeter!

# BadCircle()  # TypeError: Can't instantiate abstract class BadCircle
#              # with abstract method perimeter

The error message tells us exactly which methods we missed. Very helpful.

Abstract Properties

We can combine @abstractmethod with @property to require subclasses to implement properties.

from abc import ABC, abstractmethod

class Vehicle(ABC):
    @property
    @abstractmethod
    def fuel_type(self):
        pass

class Car(Vehicle):
    @property
    def fuel_type(self):
        return "Gasoline"

print(Car().fuel_type)  # Gasoline

The order matters: @property goes above @abstractmethod.

Concrete Methods in Abstract Classes

Abstract classes can have regular (concrete) methods too. These provide shared behavior that subclasses inherit for free.

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

    def describe(self):  # concrete — all subclasses get this
        return f"I'm a shape with area {self.area():.2f}"

class Square(Shape):
    def __init__(self, side):
        self.side = side

    def area(self):
        return self.side ** 2

print(Square(4).describe())  # I'm a shape with area 16.00

ABCMeta — The Metaclass Behind ABC

ABC is just a convenience class that uses ABCMeta as its metaclass. These two are equivalent:

# Style 1: inherit from ABC
class Shape(ABC):
    pass

# Style 2: use ABCMeta directly
class Shape(metaclass=ABCMeta):
    pass

We almost always use ABC unless we need to combine with another metaclass.

Virtual Subclasses with register()

Sometimes we want to say “this class satisfies the interface” without actual inheritance. register() creates a virtual subclass.

from abc import ABC, abstractmethod

class Drawable(ABC):
    @abstractmethod
    def draw(self):
        pass

class ThirdPartyWidget:  # we can't modify this class
    def draw(self):
        print("Drawing widget")

Drawable.register(ThirdPartyWidget)

print(isinstance(ThirdPartyWidget(), Drawable))  # True
print(issubclass(ThirdPartyWidget, Drawable))     # True

But be careful — register() doesn’t actually check that ThirdPartyWidget implements draw(). It’s a declaration of intent, not enforcement.

ABC vs Duck Typing vs Protocol

Python gives us three ways to define “interfaces”:

ABC (nominal): “You must inherit from me.” Strict, explicit, enforced at instantiation. Good for frameworks where we need guarantees.

Duck typing: “If it has the right methods, it works.” No formal contract. The simplest approach, and the most Pythonic for day-to-day code.

typing.Protocol (structural): “If it has the right methods, the type checker is happy.” Combines duck typing’s flexibility with static analysis. No inheritance needed.

from typing import Protocol

class Drawable(Protocol):
    def draw(self) -> None: ...  # just the signature

class Circle:
    def draw(self) -> None:  # no inheritance needed
        print("Drawing circle")

def render(obj: Drawable) -> None:  # type checker validates this
    obj.draw()

render(Circle())  # works — Circle matches the protocol structurally

Protocols are covered in detail in the Protocols & Structural Subtyping topic.

When to Use ABC

  • When we’re building a framework and need to guarantee subclasses implement certain methods.
  • When we want early failure — errors at instantiation, not deep in runtime.
  • When using isinstance() checks against a shared base class.

For most application code, duck typing or Protocol is simpler and more flexible. ABCs shine in library and framework design.

In simple language, abstract classes are contracts that say “implement these methods or you can’t be instantiated.” Python’s abc.ABC and @abstractmethod enforce this contract at creation time. For lighter-weight contracts, duck typing and typing.Protocol are often better fits.