Back to Blog
Python

Python Abstract Class: Defining Contracts for Subclasses

Learn how to define abstract classes in Python with the abc module, enforce method contracts, and avoid common pitfalls.

abstract classabc moduleabstractmethodOOPPython
Illustration of a Python abstract class blueprint with subclasses implementing required methods.

In Python, an abstract class is a class that cannot be instantiated directly and is meant to be subclassed. It defines a contract for its subclasses by declaring abstract methods that must be implemented. The standard way to create one is with the abc module, which provides the ABC base class and the abstractmethod decorator. This article explains how to define abstract classes, what happens when they are used incorrectly, and how to decide between an abstract class and other Python mechanisms like protocols.

Defining an Abstract Class with ABC

To create an abstract class, inherit from abc.ABC and decorate at least one method with @abstractmethod. Any class that inherits from ABC and has at least one abstract method cannot be instantiated. Here is a minimal example:

from abc import ABC, abstractmethod class Shape(ABC): @abstractmethod def area(self) -> float: """Return the area of the shape.""" pass @abstractmethod def perimeter(self) -> float: """Return the perimeter of the shape.""" pass

The Shape class defines two abstract methods. Any concrete subclass must provide implementations for both. If a subclass fails to do so, it remains abstract and cannot be instantiated either.

What Happens When You Try to Instantiate an Abstract Class

Attempting to create an instance of Shape raises TypeError. The error message lists the abstract methods that are not implemented:

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

This behavior is enforced at runtime. It prevents accidental use of an incomplete class and makes the contract explicit. The same error appears if you try to instantiate a subclass that does not implement all abstract methods.

Abstract Method Signatures

@abstractmethod only checks that the method exists on the subclass; it does not enforce the method signature. You can override an abstract method with a different number of arguments, and Python will not complain at class definition time. However, this can lead to runtime errors when the method is called through the base class interface. If you need to enforce a specific signature, you have to rely on type hints and static analysis tools, because the abc module does not validate signatures.

For example, this subclass satisfies the required method names but changes their signatures:

class Circle(Shape): def area(self, radius: float) -> float: return 3.14159 * radius ** 2 def perimeter(self, radius: float) -> float: return 2 * 3.14159 * radius

The Shape contract expects area(self) and perimeter(self), but Circle defines area(self, radius) and perimeter(self, radius). The class is instantiable because both required methods exist; Python's abc module does not compare signatures. The mismatch only surfaces when code calls circle.area() without arguments, which raises a TypeError because radius is missing. To avoid this, keep abstract method signatures consistent across all implementations and use type hints to document the expected parameters.

Using Abstract Properties and Setters

Abstract classes are not limited to methods. You can also declare abstract properties and abstract setters using @property combined with @abstractmethod. This is useful when subclasses must expose a specific attribute or enforce a setter contract.

from abc import ABC, abstractmethod class Document(ABC): @property @abstractmethod def size(self) -> int: """Size of the document in bytes.""" pass @size.setter @abstractmethod def size(self, value: int) -> None: """Set the size, must be non-negative.""" pass

A concrete subclass must implement both the getter and the setter. If only the getter is implemented, the class remains abstract because the setter is still missing. This pattern is useful when you want to enforce validation logic in the setter across all subclasses.

Practical Example: A Plugin Interface

A common use case for abstract classes is defining a plugin interface where each plugin must implement a fixed set of methods. Consider a logging backend:

from abc import ABC, abstractmethod class Logger(ABC): @abstractmethod def log(self, message: str, level: str) -> None: """Write a log message at the given level.""" pass @abstractmethod def close(self) -> None: """Release any resources held by the logger.""" pass class FileLogger(Logger): def __init__(self, path: str): self.path = path self._file = open(path, "a") def log(self, message: str, level: str) -> None: self._file.write(f"[{level}] {message}\n") def close(self) -> None: self._file.close() class StdoutLogger(Logger): def log(self, message: str, level: str) -> None: print(f"[{level}] {message}") def close(self) -> None: pass

Here, the abstract class guarantees that every logger has log and close methods. Code that consumes a Logger can rely on that interface without knowing which concrete implementation is used. This makes the system easier to extend and test.

Common Mistakes and How to Avoid Them

One frequent mistake is forgetting to inherit from ABC. If you define a class with @abstractmethod but do not inherit from ABC, the abstractness is not enforced: the class is instantiable and the method behaves like an ordinary method. Always inherit from ABC or use the ABCMeta metaclass explicitly.

Another common mistake is calling super().method() inside an override when the base method is only a stub. The call does execute that stub and returns None; it raises no error by itself. If you need shared default behavior, implement it in a normal base-class method instead of marking the method abstract. If an abstract method has a real body, subclasses can call super().method() to reuse that behavior.

A third issue is overusing abstract classes. Not every interface needs to be an abstract class. If you only need to define a set of methods without any shared implementation, a typing.Protocol may be more flexible because it supports structural subtyping and does not force inheritance.

When to Use an Abstract Class vs. a Protocol

typing.Protocol (available from Python 3.8) allows you to define a structural interface without requiring inheritance. At the type-checking level, a class that implements the required methods is considered a subtype of the protocol even if it does not inherit from it. Abstract classes, on the other hand, enforce a nominal relationship: a subclass must explicitly inherit from the abstract class.

CriterionAbstract ClassProtocol
Inheritance requiredYesNo
Can provide default implementationsYesNo (only method stubs)
Runtime instantiation checkYes (TypeError)No (only static type check)
Use for shared codeGoodNot suitable
Use for duck-typingLess flexibleMore flexible

Use an abstract class when you need to share implementation code among subclasses or when you want to enforce that all subclasses are instances of the same base type at runtime. Use a protocol when you are designing for static type checking and want to allow unrelated classes to satisfy the interface.

Maintainability Considerations

Abstract classes can improve maintainability by centralizing the contract and reducing duplication. When a new subclass is added, the runtime forces the developer to implement all required methods, which reduces the chance of missing functionality. However, they also introduce coupling: every subclass is tied to the base class hierarchy, which can make refactoring harder if the contract changes. Keep abstract classes small and focused on a single responsibility. If an abstract class grows too large, consider splitting it into multiple smaller interfaces or protocols.

Another maintainability point is that abstract methods are checked at instantiation time, not at class definition time. This means a subclass that is missing a method will not fail until you try to create an instance. In large codebases, this can delay error detection. Static type checkers like mypy can catch missing implementations earlier if you use them in your CI pipeline.

Python Abstract Class: Practical Usage and Code Examples | RYUSLOG DEV