Pluggy: A Practical Look at Python Plugin Infrastructure
Pluggy, a Python library extracted from the pytest project, provides a reusable foundation for building plugin systems. Instead of rolling your own, it offers a proven framework for defining hooks, discovering plugins, and managing their execution. Here's how it works in practice, using a toy htmlize tool with plugins as a case study.
Defining and Implementing Hooks
Pluggy centers on hooks: functions that host applications expose and plugins implement. A host creates a decorator via pluggy.HookspecMarker to define a hook spec; plugins use a decorator returned from pluggy.HookimplMarker to attach their implementations. The host and its plugins must use the same project name when creating these markers.
Our htmlize host defines two hooks to support custom roles and arbitrary text processing:
import pluggy
hookspec = pluggy.HookspecMarker("htmlize")
@hookspec(firstresult=True)
def htmlize_role_handler(role_name):
"""Return a function accepting role contents.
The function will be called with a single argument - the role contents, and
should return what the role gets replaced with.
"""
pass
@hookspec
def htmlize_contents(post, db):
"""Return a function accepting full document contents.
The function will be called with a single argument - the document contents
(after paragraph splitting and role processing), and should return the
transformed contents.
"""
pass
For plugins to attach, the host also exports an implementation marker:
hookimpl = pluggy.HookimplMarker("htmlize")
Hooks are permissive about parameters and return types. In this example, the hooks return functions, preserving the original htmlize contract.
Plugin Discovery and Registration
Pluggy leaves most discovery logic to the host application. Its simplest path is the register method on PluginManager, but it also ships with a built-in mechanism based on setuptools entry points. This is what pytest and many other projects use.
The host loads plugins at startup like so:
pm = pluggy.PluginManager("htmlize")
pm.add_hookspecs(hookspecs)
pm.load_setuptools_entrypoints("htmlize")
load_setuptools_entrypoints relies on importlib.metadata to find plugins that were installed into the same environment via pip. Plugins signal themselves by declaring entry points in their packaging metadata:
[project.entry-points.htmlize] tt = "tt"
These entry points, defined in a project's pyproject.toml, pair a plugin name (like tt) with the host's ID (htmlize). Using this packaging-based approach makes plugins easy to distribute and auto-discover, though any custom discovery method is still perfectly viable.
Invoking Plugin Logic
Once registered, the host invokes a hook and collects results from all attached implementations, receiving a list of return values. The default call order is LIFO, but plugins can influence it with options like tryfirst and trylast. Here is how htmlize triggers its contents hook:
# Build full contents back again, and ask plugins to act on
# contents.
contents = ''.join(parts)
for handler in plugin_manager.hook.htmlize_contents(post=post, db=db):
contents = handler(contents)
return contents
A plugin attaching to this hook might look like this:
import htmlize
@htmlize.hookimpl
def htmlize_contents(post, db):
repl = f'<b>I ({post.author})</b>'
def hook(contents):
return re.sub(r'\bI\b', repl, contents)
return hook
This plugin imports the hookimpl marker directly from the installed host package. This import also serves as the host's API exposure to the plugin, along with the parameters passed at call time. The function it returns conforms to the ABI defined by the hook spec.
Evaluating Pluggy Against Core Plugin Concepts
As a library for building plugin systems, Pluggy is a meta-case study for fundamental plugin architecture principles.
Discovery
Discovery is largely the host's responsibility. Pluggy's register method accepts plugins, and discovery can follow any scheme the host choses. The built-in entry point mechanism is a significant convenience, provided both host and plugins are installed via standard Python packaging tools.
Registration
With the entry point workflow, plugins register themselves by adding a [project.entry-points.<HOST-ID>] section in their pyproject.tomlfiles. For other setups, hosts can devise their own registration logic.
Hooks and API Exposure
Pluggy's term for extension points—"hooks"—matches the foundational concept directly. Its decorator-based approach is elegant and keeps the mechanism explicit in plugin code. SincePluggy serves Python hosts and plugins, exposing an application programming interface is trivial: plugins simply import the host modules, and any data structures passed as hook parameters provide direct, typed access to host data.
Is Pluggy Worth Adopting?
Plugin frameworks are a shallow API: easy to build from scratch, with a small amount of unique functionality relative to their footprint. That said, Pluggy bundles useful features for advanced scenarios that would be tedious to implement yourself:
- Automatic discovery through packaging entry points
- Signature validation for hook calls
- Consistent result collection from multiple plugin attachments
- Explicit ordering controls such as
firstresult,tryfirst, andtrylast - Special "wrapper" hooks for narrowing use cases
The decision ultimately hinges on the tradeoff between external dependencies and project effort. For a simple tool, a small custom dispatcher may be cleaner. For a project aiming to grow a rich ecosystem of third-party extensions, Pluggy's maturity and features tilt the balance in its favor.



