Of course! Let's dive deep into Python entry points. This is a crucial concept for creating well-behaved, installable Python packages.

What are Entry Points?
In simple terms, entry points are bridges between your installed Python package and the outside world (like the command line or other Python applications).
They allow you to:
- Create command-line tools that users can run directly (e.g.,
my-cli-tool --help). - Register plugins for other applications (e.g., a static site generator that can load plugins from various installed packages).
Without entry points, a user would have to manually figure out where your package's script was installed (e.g., in a bin/ directory inside their virtual environment) and run it from there. Entry points make this process seamless and standard.
The Two Main Types of Entry Points
There are two primary types you'll define in your pyproject.toml file:

- Console Scripts: These create command-line executables.
- Entry Points (for plugins): These register Python functions or classes that other applications can discover and use at runtime.
How to Define Entry Points
Since the adoption of the modern Python packaging standard (PEP 517/518), entry points are defined in the [project.scripts] and [project.entry-points] tables inside your pyproject.toml file.
Let's look at a practical example.
Project Structure
Imagine we have a simple project structure like this:
my_cli_app/
├── pyproject.toml
├── src/
│ └── my_cli_app/
│ ├── __init__.py
│ └── cli.py
└── README.md
The Python Code (src/my_cli_app/cli.py)
First, we need a function in our package that will be the entry point for our command-line tool. A common convention is to name it main().
# src/my_cli_app/cli.py
import argparse
import sys
def main():
"""Main entry point for the application."""
parser = argparse.ArgumentParser(description="A simple CLI tool made with Python entry points.")
parser.add_argument("name", help="The name to greet.")
parser.add_argument("--count", type=int, default=1, help="Number of times to greet.")
args = parser.parse_args()
for _ in range(args.count):
print(f"Hello, {args.name}!")
# It's good practice to exit with a status code
sys.exit(0)
The Configuration (pyproject.toml)
This is where the magic happens. We tell Python's packaging tools (like setuptools, flit, or hatch) to create a console script that points to our main() function.
# pyproject.toml
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "my-cli-app"
version = "0.1.0"
authors = [
{ name="Your Name", email="you@example.com" },
]
description = "A small example package demonstrating entry points"
readme = "README.md"
requires-python = ">=3.8"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
# --- THIS IS THE IMPORTANT PART ---
[project.scripts]
# The key is the command name the user will type.
# The value is a path to the function: <module_name>.<function_name>
my-cli-tool = "my_cli_app.cli:main"
Breaking down [project.scripts] entry:
my-cli-tool: This is the name of the command that will be available in the user's terminal after they install your package.my_cli_app.cli:main: This is the "path" to your function.my_cli_app: The name of your Python package (the directorysrc/my_cli_app).cli: The name of the module (the filesrc/my_cli_app/cli.py).main: The name of the function to call when the command is executed.
Installation and Usage
Now, let's see it in action.
-
Build and Install the Package:
Navigate to the root directory of your project (
my_cli_app/) and run:# Build a "wheel" (a distributable package format) python -m build # Install the package in "editable" mode for development pip install -e .
python -m buildis the modern standard for building packages.pip install -e .installs your package. The-eor--editableflag means it's installed in "editable" mode, so changes you make to the source code are immediately reflected without needing to reinstall.
-
Run the Command:
Now, you can run your tool from anywhere in your terminal, not just from the project directory!
# On macOS/Linux my-cli-tool --help # On Windows, it might be lowercase my-cli-tool --help
Expected Output:
usage: my-cli-tool [-h] [--count COUNT] name A simple CLI tool made with Python entry points. positional arguments: name The name to greet. options: -h, --help show this help message and exit --count COUNT Number of times to greet.And with arguments:
my-cli-tool Alice --count 3
Expected Output:
Hello, Alice! Hello, Alice! Hello, Alice!
Entry Points for Plugins (The "Other" Type)
This is a more advanced but extremely powerful use case. It allows your application to be extensible by other packages.
Let's imagine we have a simple application that can process data. We want to allow other developers to write their own "processors" as separate packages, and our main application can find and use them.
The Main Application (my_app/main.py)
This application will look for all registered plugins under a specific "group" name.
# my_app/main.py
import importlib
import pkg_resources
# This is the "entry point group" name. All plugins will register under this.
# It's good practice to use a reverse domain name.
ENTRY_POINT_GROUP = "my_app.processors"
def load_processors():
"""Discovers and loads all processors registered under the entry point group."""
processors = {}
for entry_point in pkg_resources.iter_entry_points(group=ENTRY_POINT_GROUP):
try:
# The entry point's name is the key (e.g., 'uppercase')
# The entry point's load() method calls the function (e.g., 'get_processor')
processor_class = entry_point.load()
processors[entry_point.name] = processor_class()
print(f"Loaded processor: '{entry_point.name}'")
except Exception as e:
print(f"Failed to load processor '{entry_point.name}': {e}")
return processors
def main():
print("Discovering processors...")
processors = load_processors()
if not processors:
print("No processors found. Exiting.")
return
print("\nRunning processors...")
# Example data
data = "hello world from my app"
for name, processor in processors.items():
print(f"\n--- Running '{name}' processor ---")
result = processor.process(data)
print(f"Input: '{data}'")
print(f"Output: '{result}'")
if __name__ == "__main__":
main()
The Plugin Package (my_plugin_one/pyproject.toml)
Now, let's create a separate package that provides a plugin.
# my_plugin_one/pyproject.toml [build-system] requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [project] name = "my-plugin-one" version = "0.1.0" description = "A plugin for my_app" # --- THIS IS THE PLUGIN DEFINITION --- [project.entry-points."my_app.processors"] # The key is the name of the plugin as it will be known to the main app. # The value is the path to the plugin's class/function. uppercase = "my_plugin_one.processors:UppercaseProcessor"
And the plugin code itself (my_plugin_one/my_plugin_one/processors.py):
# my_plugin_one/my_plugin_one/processors.py
class UppercaseProcessor:
def __init__(self):
print("UppercaseProcessor initialized.")
def process(self, text: str) -> str:
"""Converts text to uppercase."""
return text.upper()
How It Works
- You would install both
my-appandmy-plugin-onein the same environment.pip install -e ./my_app pip install -e ./my_plugin_one
- When you run
python -m my_app.main, thepkg_resources.iter_entry_points()function scans all installed packages. - It finds the entry point definition under the group
my_app.processorsinmy-plugin-one. - It successfully loads the
UppercaseProcessorclass. - Your main application can now use this processor without knowing anything about the
my-plugin-onepackage beforehand.
This is the exact mechanism used by frameworks like pytest (for discovering test functions), Flask (for extensions), and many others.
Key Takeaways
- Standardization: Entry points are the standard, declarative way to define command-line tools and plugins.
pyproject.toml: Define them in the[project.scripts](for CLI) and[project.entry-points](for plugins) tables.- Syntax: The format is
<command_name> = <module.path:function_or_class_name>. - Installation: Users install your package with
pip, and the entry points are automatically registered. - Discovery: Plugins are discovered at runtime using
pkg_resources.iter_entry_points()(the classic way) or the newerimportlib.metadata(the modern way, preferred for Python 3.8+). - Power: They are the foundation of the rich Python ecosystem of pluggable applications.
