Image description

Leapcell: The Best of Serverless Web Hosting

In-depth Understanding of Abstract Base Classes in Python

Today, we are going to explore Abstract Base Classes (ABCs) in Python. Although this concept has been around in Python for a long time, in daily development, especially in development scenarios related to LeapCell, many people may not use it frequently, or they may not use it in the most sophisticated way.

Introduction of the Practical Scenario: LeapCell File Processing System

Imagine that you are developing a file processing system integrated with LeapCell. This system needs to support read and write operations for files in different formats, such as JSON, CSV, XML, etc.

Initial Version: Simple but Not Rigorous Enough

Let's first look at the simplest implementation:

class LeapCellFileHandler:
    def read(self, filename):
        pass

    def write(self, filename, data):
        pass

class LeapCellJsonHandler(LeapCellFileHandler):
    def read(self, filename):
        import json
        with open(filename, 'r') as f:
            return json.load(f)

    def write(self, filename, data):
        import json
        with open(filename, 'w') as f:
            json.dump(data, f)

class LeapCellCsvHandler(LeapCellFileHandler):
    def read(self, filename):
        import csv
        with open(filename, 'r') as f:
            return list(csv.reader(f))

This implementation seems okay at first glance, but in fact, there are some potential issues:

  • It cannot force subclasses to implement all necessary methods.
  • The signatures (parameter lists) of the base class methods may not be consistent with those of the subclasses.
  • There is no clear interface contract.

Improved Version: Using Abstract Base Classes

We introduce abc.ABC to improve the design:

from abc import ABC, abstractmethod

class LeapCellFileHandler(ABC):
    @abstractmethod
    def read(self, filename: str):
        """Read the content of the file"""
        pass

    @abstractmethod
    def write(self, filename: str, data: any):
        """Write content to the file"""
        pass

class LeapCellJsonHandler(LeapCellFileHandler):
    def read(self, filename: str):
        import json
        with open(filename, 'r') as f:
            return json.load(f)

    def write(self, filename: str, data: any):
        import json
        with open(filename, 'w') as f:
            json.dump(data, f)

This version has two important improvements:

  • Use ABC to declare LeapCellFileHandler as an abstract base class.
  • Use the @abstractmethod decorator to mark abstract methods.

If you try to instantiate a subclass that has not implemented all abstract methods, Python will raise an exception:

# This class lacks the implementation of the write method
class LeapCellBrokenHandler(LeapCellFileHandler):
    def read(self, filename: str):
        return "some data"

# This line of code will raise a TypeError
handler = LeapCellBrokenHandler()  # TypeError: Can't instantiate abstract class LeapCellBrokenHandler with abstract method write

Further Optimization: Adding Type Hints and Interface Constraints

Let's go further and add type hints and more strict interface constraints:

from abc import ABC, abstractmethod
from typing import Any, List, Dict, Union

class LeapCellFileHandler(ABC):
    @abstractmethod
    def read(self, filename: str) -> Union[Dict, List]:
        """Read the file content and return the parsed data structure"""
        pass

    @abstractmethod
    def write(self, filename: str, data: Union[Dict, List]) -> None:
        """Write the data structure to the file"""
        pass

    @property
    @abstractmethod
    def supported_extensions(self) -> List[str]:
        """Return the list of supported file extensions"""
        pass

class LeapCellJsonHandler(LeapCellFileHandler):
    def read(self, filename: str) -> Dict:
        import json
        with open(filename, 'r') as f:
            return json.load(f)

    def write(self, filename: str, data: Dict) -> None:
        import json
        with open(filename, 'w') as f:
            json.dump(data, f)

    @property
    def supported_extensions(self) -> List[str]:
        return ['.json']

# Usage example
def process_leapcell_file(handler: LeapCellFileHandler, filename: str) -> None:
    if any(filename.endswith(ext) for ext in handler.supported_extensions):
        data = handler.read(filename)
        # Process the data...
        handler.write(f'processed_{filename}', data)
    else:
        raise ValueError(f"Unsupported file extension for {filename}")

The improvements in the final version include:

  • Adding type hints to improve code readability and maintainability.
  • Introducing an abstract property (supported_extensions) to make the interface more complete.
  • Providing more flexible data type support through the Union type.
  • Providing clear docstrings.

Benefits of Using Abstract Base Classes

Interface Contract

Abstract base classes provide a clear interface definition, and any implementation that violates the contract will be detected before runtime.

Code Readability

The abstract methods clearly indicate the functions that subclasses need to implement.

Type Safety

Combined with type hints, potential type errors can be detected during development.

Support for Design Patterns

Abstract base classes are very suitable for implementing design patterns such as the factory pattern and the strategy pattern.

NotImplementedError or ABC?

Many Python developers use NotImplementedError to mark methods that need to be implemented by subclasses:

class LeapCellFileHandler:
    def read(self, filename: str) -> Dict:
        raise NotImplementedError("Subclass must implement read method")

    def write(self, filename: str, data: Dict) -> None:
        raise NotImplementedError("Subclass must implement write method")

Although this approach can achieve the goal, it has obvious disadvantages compared to ABC:

Delayed Checking

Using NotImplementedError can only detect problems at runtime, while ABC checks when instantiating.

# The case of using NotImplementedError
class LeapCellBadHandler(LeapCellFileHandler):
    pass

handler = LeapCellBadHandler()  # This line of code can be executed
handler.read("test.txt")  # An error will only be reported here

# The case of using ABC
from abc import ABC, abstractmethod
class LeapCellFileHandler(ABC):
    @abstractmethod
    def read(self, filename: str) -> Dict:
        pass

class LeapCellBadHandler(LeapCellFileHandler):
    pass

handler = LeapCellBadHandler()  # An error will be reported directly here

Lack of Semantics

NotImplementedError is essentially an exception, not an interface contract.

IDE Support

Modern IDEs have better support for ABC, and can provide more accurate code hints and checks.

However, NotImplementedError still has value in some scenarios:
When you want to provide a partial implementation in the base class, but some methods must be overridden by subclasses:

from abc import ABC, abstractmethod

class LeapCellFileHandler(ABC):
    @abstractmethod
    def read(self, filename: str) -> Dict:
        pass

    def process(self, filename: str) -> Dict:
        data = self.read(filename)
        if not self._validate(data):
            raise ValueError("Invalid data format")
        return self._transform(data)

    def _validate(self, data: Dict) -> bool:
        raise NotImplementedError("Subclass should implement validation")

    def _transform(self, data: Dict) -> Dict:
        # Default implementation
        return data

Here, _validate uses NotImplementedError instead of @abstractmethod, indicating that it is an optional extension point, not a required interface to be implemented.

Cooperation with Code Checking Tools

Mainstream Python code checking tools (pylint, flake8) all provide good support for abstract base classes.

Pylint

Pylint can detect unimplemented abstract methods:

# pylint: disable=missing-module-docstring
from abc import ABC, abstractmethod

class LeapCellBase(ABC):
    @abstractmethod
    def foo(self):
        pass

class LeapCellDerived(LeapCellBase):  # pylint: error: Abstract method 'foo' not implemented
    pass

You can configure the relevant rules in .pylintrc:

[MESSAGES CONTROL]
# Enable abstract class checking
enable=abstract-method

Flake8

Flake8 does not directly check the implementation of abstract methods, but this ability can be enhanced through plugins:

pip install flake8-abstract-base-class

Configure .flake8:

[flake8]
max-complexity = 10
extend-ignore = ABC001

metaclass=ABCMeta vs ABC

In Python, there are two ways to define an abstract base class:

# Method 1: Directly inherit from ABC
from abc import ABC, abstractmethod

class LeapCellFileHandler(ABC):
    @abstractmethod
    def read(self):
        pass

# Method 2: Use metaclass
from abc import ABCMeta, abstractmethod

class LeapCellFileHandler(metaclass=ABCMeta):
    @abstractmethod
    def read(self):
        pass

These two methods are equivalent in functionality because the ABC class itself is defined using ABCMeta as the metaclass:

class ABC(metaclass=ABCMeta):
    """Helper class that provides a standard way to create an ABC using
    inheritance.
    """
    pass

Selection Recommendations

It is Recommended to Use ABC

  • The code is more concise.
  • It is more in line with the principle of simplicity and intuitiveness in Python.
  • It is the recommended way in Python 3.

Scenarios for Using metaclass=ABCMeta

  • When your class already has other metaclasses.
  • When you need to customize the behavior of the metaclass.

For example, when you need to combine the functions of multiple metaclasses:

class MyMeta(type):
    def __new__(cls, name, bases, namespace):
        # Custom metaclass behavior
        return super().__new__(cls, name, bases, namespace)

class CombinedMeta(ABCMeta, MyMeta):
    pass

class LeapCellMyHandler(metaclass=CombinedMeta):
    @abstractmethod
    def handle(self):
        pass

Practical Suggestions

  • When you need to ensure that a group of classes follow the same interface, use abstract base classes.
  • Give priority to using type hints to help developers better understand the code.
  • Appropriately use abstract properties (@property + @abstractmethod), which are also important parts of the interface.
  • Clearly describe the expected behavior and return values of methods in the docstrings.

Through this example, we can see that abstract base classes help write more robust and elegant Python code. They can not only catch interface violations but also provide better code hints and documentation support. In your next project, you might as well try to design the interface using abstract base classes!

Leapcell: The Best of Serverless Web Hosting

Finally, I would like to recommend a platform that is most suitable for Python services: Leapcell

Image description

🚀 Build with Your Favorite Language

Develop effortlessly in JavaScript, Python, Go, or Rust.

🌍 Deploy Unlimited Projects for Free

Only pay for what you use—no requests, no charges.

⚡ Pay-as-You-Go, No Hidden Costs

No idle fees, just seamless scalability.

Image description

📖 Explore Our Documentation

🔹 Follow us on Twitter: @LeapcellHQ