W3docs

Python Abstract Base Classes (ABC)

Learn how Python abstract base classes (ABC) enforce a common interface across subclasses using @abstractmethod, abstract properties, and the abc module.

An abstract class is a class that cannot be instantiated on its own. It exists purely to define a shared interface — a set of methods that every concrete subclass must implement. Python provides this through the built-in abc module (Abstract Base Classes).

This chapter covers:

  • Why abstract classes exist and when to use them
  • The abc module — ABC and abstractmethod
  • Enforcing method contracts with @abstractmethod
  • Abstract properties
  • Concrete methods inside an abstract class
  • Abstract classes vs. raising NotImplementedError
  • A real-world example

Before reading this chapter, make sure you are comfortable with Python classes and objects and inheritance.

Why Use Abstract Classes?

As you add more subclasses to a hierarchy, it becomes easy to forget to implement a required method. The problem only surfaces at runtime, deep inside your code, which makes debugging painful.

Abstract base classes solve this by moving the error to the moment a class is instantiated — not the moment a missing method is called. They act as a contract: every concrete subclass must implement each abstract method, or Python will refuse to create an object from it.

Abstract classes vs. raising NotImplementedError

Before the abc module existed, the common pattern was to raise NotImplementedError in a base class method:

class Shape:
    def area(self):
        raise NotImplementedError("Subclasses must implement area()")

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

c = Circle()   # No error yet — Python lets this through
c.area()       # NotImplementedError only here, at call time

The weakness: Python happily creates the object. The error is invisible until the method is actually called, which might be in a completely different part of the program.

With an abstract class, the error happens immediately:

from abc import ABC, abstractmethod

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

class Circle(Shape):
    pass  # still missing area()

c = Circle()
# TypeError: Can't instantiate abstract class Circle
#            with abstract method area

The class is broken at the point of creation — the exact place where the contract was violated.

The abc Module

Python's abc module provides two tools you will use constantly:

NameWhat it is
ABCA helper base class. Inherit from it to make your class abstract.
abstractmethodA decorator that marks a method as abstract.

Import them like this:

from abc import ABC, abstractmethod

Creating an abstract class

Inherit from ABC and decorate at least one method with @abstractmethod:

from abc import ABC, abstractmethod

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

    @abstractmethod
    def perimeter(self):
        pass

Shape now declares that anything calling itself a shape must provide area() and perimeter(). You cannot create a Shape directly:

s = Shape()
# TypeError: Can't instantiate abstract class Shape
#            with abstract methods area, perimeter

Implementing an Abstract Class

A concrete class is one that inherits from the abstract class and implements every abstract method. Only then can you create instances:

Implementing an abstract class with Circle and Rectangle

from abc import ABC, abstractmethod
import math

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

    @abstractmethod
    def perimeter(self):
        pass

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

    def area(self):
        return math.pi * self.radius ** 2

    def perimeter(self):
        return 2 * math.pi * self.radius

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

    def perimeter(self):
        return 2 * (self.width + self.height)

c = Circle(5)
r = Rectangle(4, 6)

print(round(c.area(), 4))       # 78.5398
print(round(c.perimeter(), 4))  # 31.4159
print(r.area())                 # 24
print(r.perimeter())            # 20

Both Circle and Rectangle implement every abstract method, so Python allows them to be instantiated.

What happens when you miss a method?

If a subclass implements only some abstract methods, it is still abstract — instantiating it raises a TypeError that names the missing method:

from abc import ABC, abstractmethod

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

    @abstractmethod
    def perimeter(self):
        pass

class IncompleteShape(Shape):
    def area(self):
        return 0
    # perimeter() not implemented

s = IncompleteShape()
# TypeError: Can't instantiate abstract class IncompleteShape
#            with abstract method perimeter

The error message tells you exactly which method you forgot — far more useful than a generic call-time crash.

Abstract Properties

You can also make a property abstract. This forces subclasses to provide a property (or attribute computed from a property), not just a plain method. Use @property and @abstractmethod together, with @property on top:

Using abstract properties

from abc import ABC, abstractmethod

class Animal(ABC):
    @property
    @abstractmethod
    def sound(self):
        pass

    def describe(self):
        return f"I make the sound: {self.sound}"

class Dog(Animal):
    @property
    def sound(self):
        return "Woof"

class Cat(Animal):
    @property
    def sound(self):
        return "Meow"

dog = Dog()
cat = Cat()

print(dog.describe())   # I make the sound: Woof
print(cat.describe())   # I make the sound: Meow

The sound property in each subclass behaves like a read-only attribute from the caller's perspective, while the abstract class guarantees that every concrete subclass provides one.

Concrete Methods in an Abstract Class

Abstract classes are not limited to abstract methods. They can contain fully implemented (concrete) methods that all subclasses share automatically. This is the key difference between abstract classes and plain interfaces in other languages — you can put shared logic in the abstract class:

Sharing common logic via a concrete method

from abc import ABC, abstractmethod

class Logger(ABC):
    def log(self, message):
        """Concrete method — shared by all subclasses."""
        formatted = self.format_message(message)
        self.write(formatted)

    def format_message(self, message):
        return f"[LOG] {message}"

    @abstractmethod
    def write(self, message):
        """Abstract — each subclass decides how to output."""
        pass

class ConsoleLogger(Logger):
    def write(self, message):
        print(message)

class FileLogger(Logger):
    def __init__(self):
        self.entries = []

    def write(self, message):
        self.entries.append(message)

console = ConsoleLogger()
console.log("Server started")      # prints: [LOG] Server started

file_log = FileLogger()
file_log.log("Database connected")
print(file_log.entries)            # ['[LOG] Database connected']

Here log() and format_message() are concrete shared methods. Only write() — the part that differs between backends — is abstract. Subclasses never have to re-implement the formatting logic.

isinstance() and Abstract Classes

An object that is an instance of a concrete subclass is also considered an instance of the abstract base class. This lets you write functions that accept any object conforming to the interface, without caring about the specific type:

from abc import ABC, abstractmethod
import math

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

    @abstractmethod
    def perimeter(self):
        pass

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

    def area(self):
        return math.pi * self.radius ** 2

    def perimeter(self):
        return 2 * math.pi * self.radius

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

    def perimeter(self):
        return 2 * (self.width + self.height)

shapes = [Circle(3), Rectangle(4, 5)]

for shape in shapes:
    print(isinstance(shape, Shape))   # True for both

def total_area(shapes):
    return sum(s.area() for s in shapes)

print(round(total_area(shapes), 4))  # 48.2743

This is the core benefit: total_area() works with any Shape — past, present, or future — as long as the object implements the interface.

Real-World Example: Payment Processors

Abstract classes shine when you need multiple implementations of the same concept. Consider a payment system that must support several payment providers:

Abstract class for payment processing

from abc import ABC, abstractmethod

class PaymentProcessor(ABC):
    @abstractmethod
    def charge(self, amount):
        pass

    @abstractmethod
    def refund(self, amount):
        pass

    def process(self, amount):
        """Concrete method — shared processing logic."""
        print(f"Processing payment of ${amount}")
        self.charge(amount)

class StripeProcessor(PaymentProcessor):
    def charge(self, amount):
        print(f"Stripe: charged ${amount}")

    def refund(self, amount):
        print(f"Stripe: refunded ${amount}")

class PayPalProcessor(PaymentProcessor):
    def charge(self, amount):
        print(f"PayPal: charged ${amount}")

    def refund(self, amount):
        print(f"PayPal: refunded ${amount}")

processors = [StripeProcessor(), PayPalProcessor()]

for p in processors:
    p.process(100)
# Processing payment of $100
# Stripe: charged $100
# Processing payment of $100
# PayPal: charged $100

Adding a third payment provider — say BraintreeProcessor — only requires implementing charge() and refund(). The process() logic and any isinstance checks continue to work without modification.

When to Use Abstract Classes

Use an abstract class when:

  • You have a family of related classes that must all provide certain methods.
  • You want to share some code (concrete methods) while enforcing other methods to be overridden.
  • You want Python to raise an error immediately if a subclass is incomplete, rather than waiting for a call-time crash.

You probably do not need an abstract class when:

  • You have only one or two subclasses and the hierarchy is unlikely to grow.
  • The shared interface is already enforced through other means (e.g., Python dataclasses or duck typing).
  • You just want to prevent direct instantiation — a simpler solution is a __init_subclass__ hook or a plain comment.

For closely related concepts, see Python inheritance and Python polymorphism.

Practice

Practice
What happens when you try to instantiate a class that inherits from ABC but does not implement all abstract methods?
What happens when you try to instantiate a class that inherits from ABC but does not implement all abstract methods?
Was this page helpful?