Lab: Plugin system
Add a plugin system to a CLI by defining a Processor Protocol, writing two built-in processors, registering them via entry points, and chaining them at runtime.
- Define a Processor Protocol with a process(lines) method and an api_version attribute
- Write FilterEmpty and ToUppercase processors that satisfy the Protocol
- Register both processors in pyproject.toml under a shared entry-points group
- Write a discovery function that loads all registered processors and chains them
This lab guides you through adding a real plugin system to a minimal CLI tool. You will define the interface, write two built-in processors, register them as entry points, and verify the discovery-and-chaining pipeline works end-to-end. By the end, any third-party package that declares the right entry point will automatically be picked up — without touching your tool's source code.
The runnable cells below simulate the discovery step without actual package
installation. The pyproject.toml snippets are the configuration you would
use in a real project — copy them when you build this locally.
Step 1 — Define the Processor Protocol
The Protocol is the public contract. Keep it minimal: one method and one versioning attribute.
@runtime_checkable is what makes isinstance() work here. Without it, Protocol
is only used for static type-checking by tools like mypy or pyright and cannot be
checked at runtime.
Step 2 — Write the built-in processors
The two built-in processors live in the tool's own package and are registered as entry points alongside any third-party ones.
Step 3 — Register via pyproject.toml
In your real project, add this section to pyproject.toml:
[project.entry-points."mytool.processors"]
filter-empty = "mytool.processors:FilterEmpty"
to-uppercase = "mytool.processors:ToUppercase"After pip install -e . (or pip install mytool), these two processors will
appear when importlib.metadata.entry_points(group="mytool.processors") is
called.
Third-party authors add their own packages with the same declaration:
[project.entry-points."mytool.processors"]
deduplicate = "myplugin.processors:DeduplicateProcessor"No changes to mytool's source code — just installing the third-party package
is enough.
Step 4 — Discovery and chaining
The output should be ["HELLO", "WORLD", "FOO"]. Blank and whitespace-only lines
are removed first, then every remaining line is uppercased.
Step 5 — Verify a third-party processor integrates cleanly
Add a processor that is not in the built-in list and confirm it slots in without any changes to the discovery code:
Three processors, three transformations, zero changes to discovery code. The
PrefixLineNumber processor is treated identically to the built-in ones because
it satisfies the same Protocol and declares the same api_version.
What you built
You assembled a plugin system from its constituent parts:
- A
Protocolthat is the complete, versioned public contract. - Two built-in processors that satisfy it.
- A
pyproject.tomlentry point declaration that registers them. - A discovery function that loads, version-checks, and instantiates everything.
- A pipeline runner that chains processors in order.
The same pattern scales from two built-in processors to dozens of community- maintained ones. The only coordination required between plugin authors and the tool is the Protocol definition and the entry point group name.
Where to go next
Next module: Cross-Platform — making Python tools work correctly on Linux, macOS, and Windows without platform-specific branches.