Dynamic loading
Discover installed plugins with importlib.metadata.entry_points, import them on demand, and skip incompatible versions gracefully.
- Use importlib.metadata.entry_points to enumerate all installed plugins for a group
- Call ep.load() to import a plugin class on demand
- Check api_version at load time and skip incompatible plugins with a warning
Entry point metadata tells you what plugins exist. importlib.metadata reads
that metadata and hands you back callable objects. The loading step — going from
a string record in dist-info to a live Python class — happens with a single
method call, and the import is deferred until that moment.
The discovery loop
The core pattern is short:
from importlib.metadata import entry_points
for ep in entry_points(group="mytool.processors"):
cls = ep.load()
instance = cls()
result = instance.process(lines)entry_points(group=...) returns an iterable of EntryPoint objects. Each has:
ep.name— the key from thepyproject.tomlentry point declaration.ep.value— the dotted import path (mypackage.processors:UppercaseProcessor).ep.load()— performs the actual import and returns the named attribute.
The import happens at ep.load(), not at entry_points(). Calling
entry_points() is fast and safe; only ep.load() can raise ImportError or
AttributeError.
Adding version checking
Wrap the load in a helper that enforces the API version contract:
Run this and notice that old-plugin is skipped with a clear warning. The
remaining two processors run in discovery order: filter-empty removes blank
lines, then uppercase capitalises what remains.
Handling import errors
A plugin package can declare an entry point but have a broken import — a missing
dependency, a syntax error, a renamed attribute. Catch Exception broadly around
ep.load() so one broken plugin does not prevent the others from loading:
try:
cls = ep.load()
except Exception as exc:
print(f"WARNING: could not load '{ep.name}': {exc}")
continueLogging the ep.name alongside the error is essential. Without it, a broken
plugin produces a traceback with no indication of which package caused it.
Discovery order
entry_points() does not guarantee a stable ordering across Python versions and
platforms. If the order in which processors run matters for your tool, make it
explicit — either by adding a priority attribute to the Protocol and sorting on
it, or by accepting a user-configured list in a config file.
For testing, you do not need to install a real package to exercise the discovery
code. Inject FakeEntryPoint objects as shown above, or use importlib.metadata's
SelectableGroups fixture if you want to test against the real machinery. Keep
the loading logic in a standalone function so tests can call it directly.
Where to go next
Next: lab — plugin system — wire everything together by defining a Processor
Protocol, writing two built-in processors, and registering them via entry points so
the discovery loop finds them automatically.
Designing a plugin API
Use Protocol or ABC to define a stable plugin interface, version it to avoid breaking changes, and document the contract every plugin must fulfill.
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.