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
pip install pluggy
Output: (none — exits 0 on success)
uv add pluggy
Output: dependency resolved + added to pyproject.toml
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.xline as of mid-2026 — has been1.xsince 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-devGitHub 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
| Package | Trade-off |
|---|---|
entrypoints / importlib.metadata | Standard library entry-point lookup with no hook model. Fine for "load all plugins" patterns; pluggy adds the call protocol. |
stevedore | OpenStack's plugin loader. More opinionated, heavier, integrates with setup.cfg extras. Use when you also need OpenStack-style namespaces. |
pkgutil/importlib directly | Roll-your-own. Fine for one or two hooks; pluggy starts paying off around five. |
| Manual subclass registry | Works 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.
# 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.
# 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.
# myplugin/__init__.py
import pluggy
hookimpl = pluggy.HookimplMarker("myapp")
@hookimpl
def myapp_process_item(item: dict) -> dict:
return {**item, "processed": True}
# 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.
@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.
@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.
@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.
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
PluginManagerper process. Building it scans entry-points, which touches the filesystem. Once-per-process, not once-per-call. - Prefer
firstresult=Trueover 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_itemis 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+) addedhistorichooks 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.x—implprefix=removed; useHookimplMarker(project_name)instead.multicallAPI renamed.1.0 → 1.3—hookwrappersemantics tightened; theoutcome.force_result()API stabilized.1.3 → 1.5— added thehistoric=Trueflag on hookspecs for replay-to-late-plugins behavior.
# 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.txtfor 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 installadds code to your runtime. For multi-tenant services, prefer explicitpm.register(plugin)calls with a vetted allow-list. - Expose a
--no-pluginsflag. 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 undermyapp. Treat it like loading a random.pyfrom 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'simportside effects still ran. To actually prevent loading, filter the entry-point list before callingload_setuptools_entrypointsor usepm.registerexplicitly.- 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
hookimplis normal Python; same hardening rules apply.
Testing & CI integration
# 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— everyconftest.pyis a plugin; built-ins like--lfand--covintegration ride the same plugin pathway.tox—tox-uv,tox-pyenv,tox-condaall 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.coverageusespluggyindirectly through pytest's plugin scaffolding forpytest-cov.
If you're building a tool with "plugin" anywhere in its tagline, pluggy is the obvious starting point.
Troubleshooting common errors
| Error / Symptom | Likely cause | Fix |
|---|---|---|
PluginValidationError: unknown hook 'xxx' in plugin Y | Plugin uses a hookimpl name that the host never declared | Define a matching hookspec on the host, or rename in the plugin. |
| Plugin not picked up | Entry-point group name mismatch | Group must match the string passed to PluginManager(project_name) and the [project.entry-points."<name>"] table. |
pm.hook.foo() returns [] even with plugin installed | Plugin module raised at import | pm.load_setuptools_entrypoints swallows import errors — check with pm.list_plugin_distinfo() or watch logs. |
tryfirst/trylast annotations ignored | Used with firstresult=True — only the first impl matters | Drop one of the two. |
Hookwrapper outcome.get_result() raises | The wrapped impls raised | Catch via try/except around yield, or use outcome.excinfo. |
RecursionError from hookwrapper | Wrapper calls back into the same hook | Set 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
| Python | pluggy line | Notes |
|---|---|---|
| 3.7 | 0.13, 1.0 | Drop floor. |
| 3.8 | 1.x | Minimum for current series. |
| 3.9–3.12 | 1.x | Fully supported. |
| 3.13 | 1.5+ | Free-threaded build works. |
See also
- Python: pytest — the original pluggy host
- Packages: pip-pytest — pytest as a package