Plugin Registry with Class Decorators and setuptools Entry-Point Discovery

Shared by codesnips Jul 2026
4 tabs
import abc
from typing import IO


class Exporter(abc.ABC):
    name: str = ""

    @abc.abstractmethod
    def export(self, rows: list[dict], out: IO[str]) -> None:
        """Write rows to the given text stream."""
        raise NotImplementedError
4 files · python, ini Explain with highlit

This snippet shows a small but complete plugin system for a data-export tool, built from a decorator-based in-process registry plus lazy discovery of third-party plugins through packaging entry points. The pattern solves a recurring extensibility problem: a host application needs to accept new implementations of some interface (exporters, codecs, transports) without hard-coding imports of every implementation, and ideally without knowing about plugins shipped in separate packages at all.

In registry.py, ExporterRegistry holds a dict mapping a string key to an Exporter subclass. The register classmethod returns a decorator so a class can be added at definition time with @ExporterRegistry.register("csv"). The decorator validates that the decorated class is actually a subclass of Exporter and rejects duplicate names, which turns silent overwrites into a loud RegistryError at import time. Registering the class object rather than an instance keeps construction lazy: the host decides when and how to instantiate, passing whatever constructor arguments a given call needs.

The interesting part is discover_entry_points, which pulls in plugins from other installed distributions. It uses importlib.metadata.entry_points to look up every entry point advertised under the myapp.exporters group. Each EntryPoint is only imported when ep.load() is called, so packages that expose plugins pay no import cost until discovery runs. A guard (_discovered) makes the method idempotent, and a try/except around ep.load() means one broken plugin package logs a warning instead of taking down the whole application. This is the same mechanism pytest, flake8, and many CLIs use for their plugin ecosystems.

The Exporter base class in base.py defines the contract: a name attribute and an export method that subclasses must implement. Making it an abc.ABC with an @abstractmethod means a half-finished plugin fails fast at instantiation rather than at the point of use.

plugins.py demonstrates first-party plugins. JsonExporter and CsvExporter register themselves simply by being imported, thanks to the decorator running at class-definition time. This is why the host must import the module for local plugins to appear, while external plugins arrive purely through entry points.

Finally, pyproject.toml shows the packaging side. The [project.entry-points."myapp.exporters"] table is where a downstream package would advertise parquet = mypkg.exporters:ParquetExporter; the string on the right is exactly what ep.load() imports and returns. The trade-offs are worth noting: entry-point discovery adds a small startup cost and depends on correct package metadata, and the loose string keys mean typos surface at runtime. In exchange the host stays completely decoupled from concrete implementations, and third parties extend it without editing its source. A developer reaches for this when building anything meant to be extended by plugins across package boundaries.

Share this code

Here's the card — post it anywhere.

Plugin Registry with Class Decorators and setuptools Entry-Point Discovery — share card
Link copied