Backend Development 13 min read

Using Pluggy and Stevedore for Python Plugin Development

This article explains Python plugin architecture, introducing pluggy and stevedore frameworks, showing how to define hook specifications, implement plugins, register them via entry points, and use driver or extension managers to load and invoke plugins in backend applications.

Python Programming Learning Circle
Python Programming Learning Circle
Python Programming Learning Circle
Using Pluggy and Stevedore for Python Plugin Development

In computing, a plug‑in is a software component that adds a specific feature to an existing program. Python’s dynamic nature makes plugin implementation especially flexible, often using namespace packages and dynamic imports.

Pluggy is a plugin tool derived from pytest . It provides hook specifications and implementations via markers:

<code>hookspec = pluggy.HookspecMarker("eggsample")
hookimpl = pluggy.HookimplMarker("eggsample")</code>

Example hook specifications:

<code>import pluggy
hookspec = pluggy.HookspecMarker("eggsample")

@hookspec
def eggsample_add_ingredients(ingredients: tuple):
    """Return a list of ingredients."""

@hookspec
def eggsample_prep_condiments(condiments: dict):
    """Reorganize the condiments tray."""
</code>

Corresponding implementation:

<code>import pluggy
hookimpl = pluggy.HookimplMarker("eggsample")

class ExamplePluggy:
    @hookimpl
    def eggsample_add_ingredients(self):
        spices = ["salt", "pepper"]
        eggs = ["egg", "egg"]
        return spices + eggs

    @hookimpl
    def eggsample_prep_condiments(self, condiments):
        condiments["mint sauce"] = 1
</code>

To discover and load external plugins, pluggy uses load_setuptools_entrypoints which scans the entry_points registered under the same namespace:

<code>import itertools, random, pluggy

def get_plugin_manager():
    pm = pluggy.PluginManager("eggsample")
    pm.add_hookspecs(hookspecs)
    pm.load_setuptools_entrypoints("eggsample")
    pm.register(ExamplePluggy)
    return pm
</code>

Plugins are invoked via the manager’s hook, e.g., pm.hook.eggsample_add_ingredients(ingredients) .

External plugins follow the same specification:

<code>import eggsample

@eggsample.hookimpl
def eggsample_add_ingredients(ingredients):
    if "egg" in ingredients:
        return ["lovely spam", "wonderous spam"]
    else:
        return ["splendiferous spam", "magnificent spam"]
</code>

Packaging registers the plugin in setup.py using the same entry‑point namespace:

<code>from setuptools import setup
setup(
    name="eggsample-spam",
    install_requires="eggsample",
    entry_points={"eggsample": ["spam = eggsample_spam"]},
    py_modules=["eggsample_spam"],
)
</code>

Stevedore , maintained by OpenStack, offers a similar plugin system with a class‑based interface. A base formatter class is defined:

<code>import abc
class FormatterBase(metaclass=abc.ABCMeta):
    """Base class for example plugins."""
    def __init__(self, max_width=60):
        self.max_width = max_width
    @abc.abstractmethod
    def format(self, data):
        """Format the data and return text."""
</code>

A simple formatter implementation:

<code>from stevedore.example import base
class Simple(base.FormatterBase):
    """A very basic formatter."""
    def format(self, data):
        for name, value in sorted(data.items()):
            line = f"{name} = {value}\n"
            yield line
</code>

The package is bundled with an entry_points section that registers the formatter under the stevedore.example.formatter namespace.

<code>setup(
    name='stevedore-examples',
    entry_points={
        'stevedore.example.formatter': [
            'simple = stevedore.example.simple:Simple',
            'plain = stevedore.example.simple:Simple',
        ],
    },
    ...
)
</code>

Plugins can be loaded as drivers:

<code>import argparse
from stevedore import driver

parser = argparse.ArgumentParser()
parser.add_argument('format', nargs='?', default='simple')
parser.add_argument('--width', default=60, type=int)
args = parser.parse_args()

data = {'a': 'A', 'b': 'B', 'long': 'word ' * 80}

mgr = driver.DriverManager(
    namespace='stevedore.example.formatter',
    name=args.format,
    invoke_on_load=True,
    invoke_args=(args.width,)
)
for chunk in mgr.driver.format(data):
    print(chunk, end='')
</code>

Or as extensions:

<code>import argparse
from stevedore import extension

parser = argparse.ArgumentParser()
parser.add_argument('--width', default=60, type=int)
args = parser.parse_args()

data = {'a': 'A', 'b': 'B', 'long': 'word ' * 80}

mgr = extension.ExtensionManager(
    namespace='stevedore.example.formatter',
    invoke_on_load=True,
    invoke_args=(args.width,)
)

def format_data(ext, data):
    return (ext.name, ext.obj.format(data))

results = mgr.map(format_data, data)
for name, result in results:
    print(f'Formatter: {name}')
    for chunk in result:
        print(chunk, end='')
    print()
</code>

Practical recommendation : based on development experience, stevedore is preferred for its clear base‑class interface, flexible invocation, and lower complexity.

Define interfaces via a base class.

Plugins are more flexible to call.

Stevedore offers superior ease of use.

software architecturepythonBackend DevelopmentpluginPluggystevedore
Python Programming Learning Circle
Written by

Python Programming Learning Circle

A global community of Chinese Python developers offering technical articles, columns, original video tutorials, and problem sets. Topics include web full‑stack development, web scraping, data analysis, natural language processing, image processing, machine learning, automated testing, DevOps automation, and big data.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.