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
abcmodule —ABCandabstractmethod - 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 timeThe 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 areaThe 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:
| Name | What it is |
|---|---|
ABC | A helper base class. Inherit from it to make your class abstract. |
abstractmethod | A decorator that marks a method as abstract. |
Import them like this:
from abc import ABC, abstractmethodCreating 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):
passShape 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, perimeterImplementing 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()) # 20Both 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 perimeterThe 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: MeowThe 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.2743This 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 $100Adding 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.