Interfaces and abstract classes — the basics

How to guarantee that every subclass actually implements certain behavior, instead of just hoping they remember to — the mechanism behind polymorphism actually being safe to rely on.

Beginner

3 min read

The problem: nothing stops a subclass from forgetting a method

class Shape:
    def area(self):
        pass   # every Shape is "supposed to" implement this... but nothing enforces it
 
class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius
    def area(self):
        return 3.14159 * self.radius ** 2
 
class Square(Shape):
    def __init__(self, side):
        self.side = side
    # oops — forgot to define area()
 
Square(4).area()   # returns None (from Shape's pass), silently wrong — no error at all

The polymorphism lesson relies on every subtype correctly implementing the methods calling code expects — but a plain base class with a pass body doesn't actually enforce that a subclass implements it. Square compiles fine, runs fine, and silently returns the wrong thing, with no error pointing at the actual problem: a missing area() implementation.

ABC and abstractmethod: making the requirement enforced

from abc import ABC, abstractmethod
 
class Shape(ABC):
    @abstractmethod
    def area(self):
        ...
 
class Square(Shape):
    def __init__(self, side):
        self.side = side
    # still forgot area()
 
Square(4)   # TypeError: Can't instantiate abstract class Square with abstract method area

Inheriting from ABC (Abstract Base Class) and marking a method @abstractmethod changes the failure from "silently wrong at runtime" to "can't even create the object in the first place." Shape itself can never be instantiated directly (Shape() raises the same error) — it exists purely to define a contract; only a subclass that actually implements every @abstractmethod can be instantiated. This turns a class of "forgot to implement something" bugs into an error you get immediately, at the exact moment the mistake happens, rather than later when something calls the missing method on real data.

What "interface" means, as a concept

An interface — the general OOP concept, not tied to any specific Python syntax — is a promise about what a type can do, with no commitment to how. Python doesn't have a dedicated interface keyword the way some languages do; an ABC where every method is abstract (no real implementation at all, just the required method signatures) is Python's version of a pure interface — purely a contract, with each subclass providing 100% of the actual behavior. An ABC that provides some real implementation alongside abstract methods is a genuine abstract class rather than a pure interface — a middle ground between "plain base class with real code" and "pure contract with none."

Why this matters for polymorphism specifically

def total_area(shapes: list[Shape]) -> float:
    return sum(shape.area() for shape in shapes)

This function trusts that every Shape in the list actually has a working area() method — the entire polymorphism idea depends on that trust being justified. An ABC with @abstractmethod is what makes that trust enforced rather than just hoped for: it's structurally impossible to create a Shape subclass instance that's missing area(), so total_area() can rely on every item genuinely supporting the call, without a defensive hasattr(shape, "area") check anywhere.

When this is worth the extra structure, and when it isn't

For a small script with two or three closely related classes, a plain base class (no ABC) is often simpler and entirely sufficient — the extra ceremony of ABC/abstractmethod earns its keep specifically when a codebase has many different subclasses (written by different people, or added over time), where "did every one of them remember to implement this" stops being something you can just double-check by eye.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. What actually happens if a subclass forgets to implement a plain (non-abstract) method the base class defines with just `pass`?

2. What changes when you mark a method @abstractmethod on a class inheriting from ABC?

3. What does 'interface' mean as a general OOP concept?

4. Why does an ABC make polymorphic code like `sum(shape.area() for shape in shapes)` more trustworthy?