Code of the Day
AdvancedPlugin Systems

Entry points as extensions

How pyproject.toml entry-points register plugins at install time and how importlib.metadata discovers them at runtime.

UtilitiesAdvanced6 min read
By the end of this lesson you will be able to:
  • Explain how [project.entry-points."group"] in pyproject.toml registers a plugin at install time
  • Use importlib.metadata.entry_points(group=...) to discover all installed plugins for a group
  • Describe how pytest, Sphinx, and Click use this same mechanism

A plugin system lets other packages extend your tool without modifying its source code. The question is how the host tool discovers extensions it has never seen. The answer is already built into Python's packaging infrastructure: entry points.

What entry points are

An entry point is metadata a package writes into its wheel at build time. When pip installs the package, that metadata lands in the dist-info directory alongside the code. It is not loaded — it is just a structured record that says "this package exposes the dotted name mypackage.processors:UppercaseProcessor under the group mytool.processors with the name uppercase."

The declaration lives in pyproject.toml:

[project.entry-points."mytool.processors"]
uppercase = "mypackage.processors:UppercaseProcessor"
filter-empty = "mypackage.processors:FilterEmptyProcessor"

The key on the left (uppercase) is the plugin name. The value (mypackage.processors:UppercaseProcessor) is a dotted import path followed by a colon and the attribute to load. No file scanning, no import at install time — just metadata.

Discovering plugins at runtime

importlib.metadata reads that installed metadata and hands back a list of entry point objects:

from importlib.metadata import entry_points

plugins = entry_points(group="mytool.processors")
for ep in plugins:
    print(ep.name, ep.value)
    cls = ep.load()   # imports the module and returns UppercaseProcessor
    instance = cls()

ep.load() does the actual import on demand. If a plugin package is broken, only that plugin fails — the rest load normally.

The group string is a namespace that belongs to the tool author. Choose something specific enough to avoid collisions: mytool.processors rather than just processors.

Pytest, Sphinx, and Click all ship their own plugin systems on top of entry points, because the pattern works at any scale:

  • pytest discovers plugins registered under pytest11. Every package that ships a conftest or hook simply declares [project.entry-points.pytest11] in its pyproject.toml. Running pytest --co loads them all without any configuration from the user.
  • Sphinx uses sphinx.builders and sphinx.roles groups. Third-party themes and extensions register their components here.
  • Click plugin groups let CLI suites split commands across separate installable packages that merge into one top-level command.

The common thread: the host tool calls entry_points(group=...) once at startup and gets back every registered plugin, regardless of who wrote it or when it was installed.

Entry points require the plugin package to be installed — they are not discovered by scanning directories or by adding paths to sys.path. This is a feature, not a limitation. It means plugins are explicit (installed with pip), versioned, and removable without touching the host tool's files.

What this gives you

The entry point mechanism separates three concerns that are often tangled:

  1. Authorship — a third party writes the plugin in their own repository.
  2. Distribution — they publish it to PyPI with the entry point declaration.
  3. Discovery — your tool finds it automatically after pip install.

No configuration files to edit, no import hooks to register, no directory conventions to document. Install the package and it just works.

Where to go next

Next: designing a plugin API — defining a stable interface using Protocol so that every plugin your tool discovers is guaranteed to be callable in the same way.

Finished reading? Mark it complete to track your progress.

On this page