Code of the Day
AdvancedPlugin Systems

Dynamic loading

Discover installed plugins with importlib.metadata.entry_points, import them on demand, and skip incompatible versions gracefully.

UtilitiesAdvanced10 min read
Recommended first
By the end of this lesson you will be able to:
  • 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 the pyproject.toml entry 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:

Python — editable, runs in your browser

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}")
    continue

Logging 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.

Finished reading? Mark it complete to track your progress.

On this page