Abstract Classes in Python: Enforcing Contracts with the abc Module
When you design a class hierarchy, sometimes you need to define what every subclass must implement — without providing the implementation yourself. That is the role of abstract classes. Python handles this through the abc module (Abstract Base Classes), which lets you define methods that subclasses are required to override.
The Problem Abstract Classes Solve
Without any enforcement, a subclass might “forget” to implement a method:
class Shape: def area(self): raise NotImplementedError("Subclasses must implement area()")
class Circle(Shape): def __init__(self, radius): self.radius = radius # forgot to implement area()
c = Circle(5)c.area() # RuntimeError — but only at call time, not at creation timeThe NotImplementedError approach works, but the error surfaces only when the method is actually called. You could create a Circle object, pass it around, and not discover the missing method until later.
Abstract classes catch this earlier — at instantiation time:
from abc import ABC, abstractmethod
class Shape(ABC): @abstractmethod def area(self) -> float: """Return the area of this shape."""
class Circle(Shape): def __init__(self, radius): self.radius = radius # still forgot area()
c = Circle(5) # TypeError: Can't instantiate abstract class Circle # with abstract method areaThe error happens the moment you try to create the object, not somewhere down the line.
Basic Usage
Inherit from ABC and mark abstract methods with @abstractmethod:
from abc import ABC, abstractmethodimport math
class Shape(ABC): @abstractmethod def area(self) -> float: """Return the area."""
@abstractmethod def perimeter(self) -> float: """Return the perimeter."""
def describe(self): """Concrete method — shared by all subclasses.""" return (f"{type(self).__name__}: " f"area={self.area():.2f}, perimeter={self.perimeter():.2f}")
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(5), Rectangle(4, 6)]for shape in shapes: print(shape.describe())# Circle: area=78.54, perimeter=31.42# Rectangle: area=24.00, perimeter=20.00Notice that Shape has a concrete method (describe) alongside abstract methods. Abstract classes can mix both — abstract methods define the contract, concrete methods provide shared behaviour.
Abstract Properties
Use @property combined with @abstractmethod to require subclasses to expose particular attributes through a property interface.
from abc import ABC, abstractmethod
class DatabaseAdapter(ABC): @property @abstractmethod def connection_string(self) -> str: """Must return a valid connection string."""
@abstractmethod def connect(self): """Establish the connection."""
@abstractmethod def disconnect(self): """Close the connection."""
class PostgreSQLAdapter(DatabaseAdapter): def __init__(self, host, port, dbname): self.host = host self.port = port self.dbname = dbname self._conn = None
@property def connection_string(self): return f"postgresql://{self.host}:{self.port}/{self.dbname}"
def connect(self): print(f"Connecting: {self.connection_string}") # self._conn = psycopg2.connect(...)
def disconnect(self): print("Disconnecting from PostgreSQL") # self._conn.close()
db = PostgreSQLAdapter("localhost", 5432, "myapp")print(db.connection_string)db.connect()Simulating Interfaces
Python has no interface keyword. An interface in Python is simply an abstract class with only abstract methods and no concrete implementation.
from abc import ABC, abstractmethod
class Serialisable(ABC): """Interface: any class implementing this can be serialised."""
@abstractmethod def to_dict(self) -> dict: """Convert object to a dictionary."""
@abstractmethod def from_dict(self, data: dict): """Populate object from a dictionary."""
class JSONSerialisable(ABC): """Interface: any class implementing this can be serialised to JSON."""
@abstractmethod def to_json(self) -> str: """Return a JSON string representation."""
class UserProfile(Serialisable, JSONSerialisable): def __init__(self, username, email): self.username = username self.email = email
def to_dict(self): return {"username": self.username, "email": self.email}
def from_dict(self, data): self.username = data["username"] self.email = data["email"]
def to_json(self): import json return json.dumps(self.to_dict())
profile = UserProfile("alice", "alice@example.com")print(profile.to_json()) # {"username": "alice", "email": "alice@example.com"}A class can inherit from multiple abstract classes (and therefore satisfy multiple “interface contracts”). Python’s MRO handles the resolution just as with regular classes.
Registering Virtual Subclasses
The abc module lets you register a class as a virtual subclass — it passes isinstance() checks without actually inheriting from the abstract class.
from abc import ABC
class Drawable(ABC): pass
class ExternalWidget: # from a third-party library, can't modify it def draw(self): print("Drawing widget")
Drawable.register(ExternalWidget)
w = ExternalWidget()print(isinstance(w, Drawable)) # TrueThis is useful for integrating with third-party code where you cannot modify the class hierarchy.
When to Use Abstract Classes
Use abstract classes when:
- Multiple subclasses share some common behaviour (concrete methods) but each must implement specific parts differently.
- You are designing a framework where users subclass your classes and you want to ensure required methods exist.
- You want the error to appear at instantiation time, not at call time.
Do not use abstract classes when:
- You just want duck typing — if callers will always pass the right kind of object, you may not need the enforcement.
- A simple
Protocol(fromtyping) would be clearer — especially when working with type checkers. - The hierarchy is simple and a
NotImplementedErrorin a concrete base class is sufficient.
Common Mistakes
Using ABC without importing it. ABC must come from abc, not defined manually.
Forgetting @abstractmethod. A method defined inside an ABC subclass without @abstractmethod is treated as a regular concrete method. The subclass is not required to override it.
Implementing only some abstract methods. A subclass must implement every abstract method from all parent abstract classes. Missing even one keeps the class abstract, and trying to instantiate it raises TypeError.
Putting too much logic in the abstract class. Abstract classes work best as thin contracts. If you find yourself writing extensive logic in an abstract class, consider whether a mixin or a separate utility class would be clearer.