Skip to content

Calling conventions

Once plugins are registered, calling a hook runs a small dispatch engine inside HookCaller. This page explains the rules that engine follows; each rule has a dedicated mechanism page with a runnable demo.

Typed calls: pm.caller

pm.caller(spec) returns a caller whose result type is derived from the spec's dispatch mode and checked by mypy and pyright - no hand annotations:

ingredients = pm.caller(Specs.add_ingredients)(base=["banana"])  # list[list[str]]
cup = pm.caller(Specs.choose_cup)(size="small")                  # str | None

pm.hook.<name>(...) works too and is more concise, but it is untyped (returns Any). Use pm.hook for quick scripts and pm.caller when you want the type checker's help; both share one PluginManager, so you never need a manager per spec.

Arguments

Calls are typically made with keyword arguments, though positional ones bind to the spec's parameters in order (matching the typed caller's signature):

pm.caller(Specs.add_ingredients)(base=["banana"])   # keyword (clearest)
pm.caller(Specs.add_ingredients)(["banana"])        # positional also works

Keyword form is what lets each implementation declare only the arguments it cares about. The caller passes the full set of kwargs; each implementation receives the subset matching its own signature, computed once at registration with inspect.signature.

@extension
def add_ingredients(self):          # ignores base entirely
    return ["honey"]

@extension
def add_ingredients(self, base):    # receives base
    return [] if "berry" in base else ["blueberry"]

Collecting vs first result

By default a hook is collecting: every implementation runs and the non-None results are returned as a list.

pm.caller(Specs.add_ingredients)(base=["banana"])
# [['blueberry', 'strawberry'], ['spinach', 'kale']]   # typed list[list[str]]

A spec marked firstresult stops at the first implementation that returns a non-None value and returns that value directly - not a list (typed R | None):

pm.caller(Specs.choose_cup)(size="small")   # '8oz paper cup'

None results are skipped in both modes, so an implementation can abstain simply by returning None.

A third mode, pipeline, threads the result of each implementation into the next and returns the final value - a fold/middleware chain.

Order

Within a hook, implementations run in this order:

  1. everything marked tryfirst,
  2. then normal implementations,
  3. then everything marked trylast.

Inside each of those three buckets, order is registration order (first registered runs first). See Ordering for the full story and the difference from pluggy.

Wrappers

An implementation marked wrapper=True is a generator that wraps the call: code before its yield runs first, the value sent back to the yield is the result of the inner (non-wrapper) implementations, and whatever the generator returns replaces that result. Wrappers also observe exceptions. See Wrappers.

Historic calls

A historic hook is called through call_historic instead of a plain call. The caller remembers the call and replays it to any plugin registered afterwards, so late-loading plugins still see one-off startup events. See Historic hooks.