Entry Points & Console Scripts
Entry points register CLI commands and plugin hooks at install time. [project.scripts] creates console commands; [project.entry-points] enables plugin discovery.
Recipe
[project.scripts]
myapp = "myapp.cli:main"
[project.entry-points."myapp.plugins"]
json = "myapp.plugins.json:JsonPlugin"When to reach for this:
- Shipping a CLI with your package
- Building a plugin system other packages extend
- Registering tools discoverable by frameworks
Working Example
[project]
name = "invoice-cli"
version = "0.1.0"
[project.scripts]
invoice = "invoice_cli.main:cli"
[project.entry-points."invoice.exporters"]
csv = "invoice_cli.exporters:CSVExporter"
pdf = "invoice_cli.exporters:PDFExporter"# src/invoice_cli/main.py
import click
from importlib.metadata import entry_points
@click.group()
def cli():
"""Invoice management CLI."""
pass
@cli.command()
@click.option("--format", default="csv")
def export(format: str):
eps = entry_points(group="invoice.exporters")
exporter = eps[format].load()
exporter().run()
def main():
cli()uv pip install -e .
invoice export --format csvWhat this demonstrates:
project.scriptsinstallsinvoicecommand to PATH- Entry point groups enable plugin discovery via
importlib.metadata - Third-party packages can add exporters by registering their own entry points
clickortyperhandles CLI parsing insidemain
Deep Dive
Entry Point Types
| Type | Config | Result |
|---|---|---|
| Console scripts | [project.scripts] | Executable in bin/ |
| Plugin groups | [project.entry-points."group"] | importlib discovery |
| GUI scripts | [project.gui-scripts] | GUI launcher (rare) |
Gotchas
- Entry point to non-callable - install succeeds, run fails. Fix: point to a function, not a module.
- Name collision with existing CLI - overwrites user commands. Fix: unique, namespaced command names.
- Loading all plugins at import - slow startup. Fix: lazy-load plugins when subcommand is invoked.
- Missing
main()guard - fine for entry points but needed forpython -m. Fix: addif __name__ == "__main__"for module execution. - Editable install not updating scripts - stale entry points. Fix: reinstall after changing entry point config.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
python -m myapp | No install needed | Global CLI expected |
| pipx | Isolated CLI install | Library + CLI combo |
| Shell script wrapper | Legacy deploy | Cross-platform package |
FAQs
scripts vs entry-points?
scripts = CLI commands. entry-points = plugin discovery groups.
How do I test entry points?
from importlib.metadata import entry_points; ep.load()() in pytest.
Can third parties add plugins?
Yes. They register entry points in their pyproject.toml under your group name.
How does pipx use entry points?
pipx installs the package and exposes console_scripts to an isolated venv on PATH.
Console script naming?
Use short, unique names. invoice not cli.
Entry points and uv?
uv respects PEP 621 entry points identically to pip.
How do I list installed entry points?
importlib.metadata.entry_points(group="console_scripts").
Dynamic entry points?
Not standard. Plugins register statically in their own packages.
setuptools legacy entry_points?
[project.entry-points] replaces [options.entry_points] in setup.cfg.
Multiple CLI commands?
Multiple lines in [project.scripts], one per command.
Related
- CLI Basics - CLI design
- Packaging & Distributing CLIs - pipx distribution
- Packaging Basics - build and install
- Click - CLI framework
Stack versions: This page was written for Python 3.14.0, FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.