cheat sheet

pluggy

Package-level reference for pluggy on PyPI — install, version policy, alternatives, and the hookspec/hookimpl model that powers pytest, tox, and devpi.

pluggy

What it is

pluggy is the plugin manager extracted from pytest in 2015. It implements a hook specification / hook implementation model: a host application defines named hook specs (function signatures), plugins provide impls with matching signatures, and pluggy calls every registered impl in order when the host fires the hook. The host gets to control the ordering, decide whether to collect all results or stop at the first, and route impls through entry-points so plugins install via plain pip install.

Reach for pluggy when you're building a library or CLI that you want third parties to extend without monkey-patching. It's the plugin system underlying half the Python tooling ecosystem — pytest, tox, devpi, datasette, pre-commit's internals, and many more all use it. If you've ever written a conftest.py fixture, you've used pluggy without knowing.

Install

bash
pip install pluggy

Output: (none — exits 0 on success)

bash
uv add pluggy

Output: dependency resolved + added to pyproject.toml

bash
poetry add pluggy

Output: updated lockfile + virtualenv install

There are no extras. pluggy is pure Python with zero required dependencies.

Versioning & Python support

  • Stable 1.x line as of mid-2026 — has been 1.x since 2022. Semver is honored.
  • Supports Python 3.8+ on recent releases.
  • API has been remarkably stable; most of the surface (HookspecMarker, HookimplMarker, PluginManager) hasn't shifted in years. Newer minors add diagnostics and historic-hook plumbing rather than breaking changes.

Package metadata

  • Maintainer: pytest core team (Bruno Oliveira, Ronny Pfannschmidt et al.) under the pytest-dev GitHub org
  • Project home: github.com/pytest-dev/pluggy
  • Docs: pluggy.readthedocs.io
  • PyPI: pypi.org/project/pluggy
  • License: MIT
  • First released: 2015 (extracted from pytest)
  • Downloads: hundreds of millions per month — pulled in by every pytest install, plus standalone use by tox, devpi, datasette, and more

Optional dependencies & extras

  • pluggy[dev] — development tooling (pytest, sphinx). Only useful when hacking on pluggy itself.
  • pluggy[testing] — pytest + pre-commit for the test suite. Same caveat.

No runtime extras. Pluggy has no compiled code and no transitive runtime dependencies — adding it to a project costs effectively nothing.

Alternatives

PackageTrade-off
entrypoints / importlib.metadataStandard library entry-point lookup with no hook model. Fine for "load all plugins" patterns; pluggy adds the call protocol.
stevedoreOpenStack's plugin loader. More opinionated, heavier, integrates with setup.cfg extras. Use when you also need OpenStack-style namespaces.
pkgutil/importlib directlyRoll-your-own. Fine for one or two hooks; pluggy starts paying off around five.
Manual subclass registryWorks for class-based plugins. Loses entry-point discovery and the firstresult/ordering knobs.

Real-world recipes

The pluggy mental model: the host owns the HookspecMarker and PluginManager; plugins own the HookimplMarker and register via entry-points. Examples below show both sides.

Recipe 1 — Define a hookspec on the host side.

python
# myapp/hookspecs.py
import pluggy

hookspec = pluggy.HookspecMarker("myapp")

class MyAppSpec:
    @hookspec
    def myapp_process_item(self, item: dict) -> dict | None:
        """Transform item; return None to leave it unchanged."""

    @hookspec(firstresult=True)
    def myapp_choose_renderer(self, item: dict) -> str | None:
        """Return a renderer name; first non-None wins."""

Output: specs registered as callables that do nothing yet — they're signatures.

Recipe 2 — Wire up the PluginManager and load entry-point plugins.

python
# myapp/core.py
import pluggy
from myapp.hookspecs import MyAppSpec

def get_plugin_manager() -> pluggy.PluginManager:
    pm = pluggy.PluginManager("myapp")
    pm.add_hookspecs(MyAppSpec)
    pm.load_setuptools_entrypoints("myapp")    # any pip-installed entry-point under "myapp"
    return pm

pm = get_plugin_manager()
results = pm.hook.myapp_process_item(item={"name": "x"})

Output: results is a list — one entry per registered plugin's impl, in LIFO order by default.

Recipe 3 — Implement a hook in a plugin package.

python
# myplugin/__init__.py
import pluggy

hookimpl = pluggy.HookimplMarker("myapp")

@hookimpl
def myapp_process_item(item: dict) -> dict:
    return {**item, "processed": True}
toml
# pyproject.toml
[project.entry-points."myapp"]
myplugin = "myplugin"

Output: after pip install ./myplugin, the next get_plugin_manager() call picks up the plugin automatically.

Recipe 4 — firstresult=True for "first match wins" semantics.

python
@hookspec(firstresult=True)
def myapp_choose_renderer(self, item): ...

@hookimpl
def myapp_choose_renderer(item):
    if item.get("type") == "image":
        return "png-renderer"
    return None      # let later impls decide

renderer = pm.hook.myapp_choose_renderer(item={"type": "image"})
print(renderer)

Output: png-renderer. firstresult stops calling impls after the first non-None return — the canonical way to express "the plugin that knows wins".

Recipe 5 — Ordering with tryfirst / trylast.

python
@hookimpl(tryfirst=True)
def myapp_process_item(item):
    return {**item, "stamp": "early"}

@hookimpl(trylast=True)
def myapp_process_item(item):
    return {**item, "stamp": "late"}

Output: the tryfirst impl runs before any other; trylast runs after all others. Use sparingly — most ordering should come from plugin design, not annotations.

Recipe 6 — hookwrapper=True to wrap every other impl in a context.

python
@hookimpl(hookwrapper=True)
def myapp_process_item(item):
    print("before all impls")
    outcome = yield                  # other impls run here
    result = outcome.get_result()
    print(f"after; result={result}")

Output: the wrapper sees aggregated outcome — handy for tracing, timing, error capture, or undoing on exception.

Recipe 7 — Block a plugin from a specific call.

python
pm.set_blocked("myplugin")
results = pm.hook.myapp_process_item(item={"name": "x"})

Output: myplugin's impls are silently excluded. Useful for selective testing and for emergency disable in production.

Performance tuning

pluggy's overhead is one Python-level dispatch per impl per hook call — measured in single-digit microseconds. The knobs are mostly about avoiding unnecessary dispatch.

  • Cache the PluginManager per process. Building it scans entry-points, which touches the filesystem. Once-per-process, not once-per-call.
  • Prefer firstresult=True over filtering after the fact. It stops dispatch early.
  • Avoid wrapping every hook in hookwrapper. Wrappers run for every impl call; in tight loops they add 2-3× overhead.
  • For perf-critical paths, sidestep pluggy. pm.hook.myapp_process_item is fine for setup-time fan-out; if you're calling it per request in a hot loop, cache the resolved impl list and call directly.
  • Newer pluggy (1.5+) added historic hooks that replay to late-registered plugins. Historic hooks store every call — use only when needed; memory grows with call count.

Version migration guide

pluggy 1.x has been steady. The two real breaks were 0.x → 1.x and 1.0 → 1.3, both in 2022.

  • 0.x → 1.ximplprefix= removed; use HookimplMarker(project_name) instead. multicall API renamed.
  • 1.0 → 1.3hookwrapper semantics tightened; the outcome.force_result() API stabilized.
  • 1.3 → 1.5 — added the historic=True flag on hookspecs for replay-to-late-plugins behavior.
python
# Before (0.x)
hookimpl = pluggy.HookimplMarker(implprefix="myapp_")

# After (1.x)
hookimpl = pluggy.HookimplMarker("myapp")

Output: identical behavior in 1.x; the project name now lives in the marker, not the prefix.

Production deployment notes

pluggy itself is a library — there is no deployment artifact. But a host application that uses it benefits from a few conventions in production.

  • Lock pluggy. A patch bump to pluggy could subtly change ordering. Pin it in requirements.txt for reproducible runs.
  • Log loaded plugins on startup. pm.list_name_plugin() returns the list; emit it once at boot. This makes plugin-related incidents debuggable without re-running.
  • Don't auto-load entry-points in security-sensitive contexts. Any pip install adds code to your runtime. For multi-tenant services, prefer explicit pm.register(plugin) calls with a vetted allow-list.
  • Expose a --no-plugins flag. When something breaks, the operator wants to isolate "is it core or a plugin?" in one step.
  • Use tracing.PluginTracer (built into pluggy) for hook timing in production. Cheap; emits to a callback you supply.

Security considerations

  • Entry-point loading is code execution. pm.load_setuptools_entrypoints("myapp") imports every plugin module from every distribution that registers under myapp. Treat it like loading a random .py from disk.
  • Plugin sandbox is none. Plugins run with full host privileges. If you cannot trust the supply chain, don't auto-load.
  • pm.set_blocked() is advisory, not security. A blocked plugin's import side effects still ran. To actually prevent loading, filter the entry-point list before calling load_setuptools_entrypoints or use pm.register explicitly.
  • Argument validation is the host's job. Pluggy passes arguments through unchanged. If a hook returns a value the host trusts (path, command), validate it.
  • Avoid eval/exec inside hookimpls. A plugin's hookimpl is normal Python; same hardening rules apply.

Testing & CI integration

python
# pip install pytest
import pluggy
from myapp.hookspecs import MyAppSpec
from myapp.core import get_plugin_manager

def test_plugin_runs():
    pm = pluggy.PluginManager("myapp")
    pm.add_hookspecs(MyAppSpec)

    class FakePlugin:
        @pluggy.HookimplMarker("myapp")
        def myapp_process_item(self, item):
            return {**item, "tested": True}

    pm.register(FakePlugin())
    out = pm.hook.myapp_process_item(item={"x": 1})
    assert out[0] == {"x": 1, "tested": True}

Output: test passes — register fake plugins inline, no entry-point indirection needed.

The pattern: in tests, use pm.register(instance) directly rather than load_setuptools_entrypoints. Faster, deterministic, no install needed.

Ecosystem integrations

Hosts using pluggy:

  • pytest — every conftest.py is a plugin; built-ins like --lf and --cov integration ride the same plugin pathway.
  • toxtox-uv, tox-pyenv, tox-conda all attach via pluggy.
  • devpi — the index server's auth, indexes, and webhooks are pluggy hooks.
  • datasette — the entire Datasette UI is composed of pluggy plugins.
  • pre-commit — internal hook orchestration.
  • coverage uses pluggy indirectly through pytest's plugin scaffolding for pytest-cov.

If you're building a tool with "plugin" anywhere in its tagline, pluggy is the obvious starting point.

Troubleshooting common errors

Error / SymptomLikely causeFix
PluginValidationError: unknown hook 'xxx' in plugin YPlugin uses a hookimpl name that the host never declaredDefine a matching hookspec on the host, or rename in the plugin.
Plugin not picked upEntry-point group name mismatchGroup must match the string passed to PluginManager(project_name) and the [project.entry-points."<name>"] table.
pm.hook.foo() returns [] even with plugin installedPlugin module raised at importpm.load_setuptools_entrypoints swallows import errors — check with pm.list_plugin_distinfo() or watch logs.
tryfirst/trylast annotations ignoredUsed with firstresult=True — only the first impl mattersDrop one of the two.
Hookwrapper outcome.get_result() raisesThe wrapped impls raisedCatch via try/except around yield, or use outcome.excinfo.
RecursionError from hookwrapperWrapper calls back into the same hookSet a sentinel or use a historic=True spec.

When NOT to use this

  • A single extension point. Don't pull in pluggy for one callback — define a function in your config.
  • You want runtime isolation, not just orchestration. Plugins run in the same process with full access. Use subprocess/IPC if you need a sandbox.
  • Class-hierarchy extensibility works. If your design naturally extends through subclassing, an abstract base class plus a registry is lighter.
  • Cython-fast inner loop. Pluggy dispatch is microseconds per call — fine for setup, slow for million-call inner loops.

Compatibility matrix

Pythonpluggy lineNotes
3.70.13, 1.0Drop floor.
3.81.xMinimum for current series.
3.9–3.121.xFully supported.
3.131.5+Free-threaded build works.

See also