Back to Blog
Python

Python Abstract Class: Defining Contracts for Subclasses

python abstract class: Learn how to define and use 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.

python abstract class requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

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 Methods and Their 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 compiles but will fail when area is called with the expected argument:

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

The Shape contract expects area(self), but Circle defines area(self, radius). The class is still instantiable because area exists. The mismatch only surfaces when code calls circle.area() without arguments. 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 decorator has no effect. The class becomes instantiable, and the abstract method is treated as a regular method. Always inherit from ABC or use the ABCMeta metaclass explicitly.

Another mistake is calling super().abstract_method() inside an override. Abstract methods often have no implementation, so calling super() raises AttributeError unless the base class provides a default implementation. If you need a default behavior, implement the method in the base class and do not mark it as abstract. Alternatively, you can call super() only if the base method has a body.

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. 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 compiler or 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