| name | setuptools |
| description | [Applies to: **/*.py] This guide defines best practices for configuring Python packages using setuptools, emphasizing modern, declarative `pyproject.toml` workflows for robust and maintainable projects. |
| source | cursor_mdc |
setuptools Best Practices
setuptools is the de-facto standard for Python packaging. This guide outlines the definitive, modern approach to configuring your Python projects, prioritizing declarative pyproject.toml files, automated versioning, and clear dependency management. Adhere to these rules for consistent, maintainable, and standards-compliant package distributions.
1. Code Organization and Structure
Always use the src/ layout for your package source code. This clearly separates your package's importable code from project-level files (like pyproject.toml, README.md, tests/).
❌ BAD: Flat layout
my_package/
├── __init__.py
├── module.py
├── pyproject.toml
└── tests/
✅ GOOD: src/ layout
my_package/
├── src/
│ └── my_package/
│ ├── __init__.py
│ └── module.py
├── pyproject.toml
├── README.md
└── tests/
2. Declarative Configuration in pyproject.toml
Configure all project metadata and build settings declaratively in pyproject.toml. This aligns with PEP 517/518, enables isolated builds, and makes your package metadata machine-readable without executing code. Avoid setup.py for metadata.
Always include [build-system] and [project] tables.
[build-system]
requires = ["setuptools>=61", "setuptools-scm>=8"]
build-backend = "setuptools.build_meta"
[project]
name = "my-awesome-package"
dynamic = ["version"]
description = "A short description of my awesome package."
readme = "README.md"
requires-python = ">=3.8"
license = { file = "LICENSE" }
authors = [
{ name = "Your Name", email = "your.email@example.com" },
]
keywords = ["python", "utility", "example"]
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
[project.urls]
Homepage = "https://github.com/yourusername/my-awesome-package"
Documentation = "https://my-awesome-package.readthedocs.io/"
Repository = "https://github.com/yourusername/my-awesome-package"
[tool.setuptools]
package-dir = {"" = "src"}
[tool.setuptools_scm]
write_to = "src/my_package/_version.py"
3. Version Management with setuptools-scm
Derive your package version directly from VCS (Git) tags using setuptools-scm. This eliminates manual version bumps and ensures your package version always reflects your repository state.
- Declare
setuptools-scm in build-system.requires.
- Set
dynamic = ["version"] in [project].
- Configure
[tool.setuptools_scm] (optional, but recommended for write_to).
❌ BAD: Manual version in pyproject.toml or __init__.py
[project]
version = "0.1.0"
✅ GOOD: Dynamic versioning with setuptools-scm
[build-system]
requires = ["setuptools>=61", "setuptools-scm>=8"]
build-backend = "setuptools.build_meta"
[project]
name = "my-package"
dynamic = ["version"]
[tool.setuptools_scm]
write_to = "src/my_package/_version.py"
4. Dependency Declaration
Manage all dependencies declaratively in pyproject.toml.
dependencies: For runtime dependencies.
optional-dependencies: For optional dependencies (e.g., dev, test, docs).
❌ BAD: dependency_links or requirements.txt for runtime dependencies
[project]
✅ GOOD: dependencies and optional-dependencies
[project]
dependencies = [
"requests~=2.30",
"rich>=13.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"pytest-cov",
"black",
"isort",
"mypy",
"setuptools-scm[toml]>=8",
]
docs = [
"sphinx",
"furo",
]
5. Entry Points for Scripts and Plugins
Expose console scripts, GUI entry points, or plugin APIs using the [project.scripts] and [project.entry-points] tables. This is the standard way to create discoverable extensions and command-line tools.
[project.scripts]
my-cli = "my_package.cli:main"
[project.gui-scripts]
my-gui = "my_package.gui:start_app"
[project.entry-points."my_package.plugins"]
plugin_a = "my_package.plugins:PluginA"
plugin_b = "my_package.plugins:PluginB"
6. Resource Files and Namespace Packages
Bundle non-code assets (e.g., templates, data files) cleanly using package_data. For larger projects, use native namespace package support (PEP 420).
[tool.setuptools.package-data]
"my_package" = ["data/*.json", "templates/*.html"]
For namespace packages, simply omit __init__.py from the parent directory and ensure setuptools is configured to find them (e.g., via package-dir for src/ layout).
src/
└── my_namespace/
├── my_package_a/
│ └── __init__.py
└── my_package_b/
└── __init__.py
7. Development Workflow
Always use a virtual environment. For local development, install your package in "editable" mode to reflect source changes instantly.
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
Pitfall: Entry points (scripts, plugins) are only discoverable after the package is installed (even in editable mode). If you add a new entry point, you might need to re-run pip install -e . for it to register.
8. Building and Publishing
Generate standard distribution packages (wheels and source distributions) using build, and upload them securely with twine.
pip install build twine
rm -rf dist/ build/ *.egg-info/
python -m build
twine upload --repository testpypi dist/*
twine upload dist/*
9. Type Hints
While not setuptools-specific, consistently apply type hints throughout your Python codebase. This improves code readability, maintainability, and enables static analysis tools to catch errors early. setuptools itself does not dictate type hint usage, but a well-packaged project is a well-typed project.
def calculate_sum(a: int, b: int) -> int:
return a + b
10. Custom Build Logic (When Absolutely Necessary)
If you must perform custom build steps (e.g., compiling C extensions, generating code), setup.py is still the place for imperative logic. However, keep it minimal and ensure it only handles build-time operations, not metadata.
from setuptools import setup, Extension
my_extension = Extension(
'my_package.my_c_module',
sources=['src/my_package/my_c_module.c'],
)
setup(
ext_modules=[my_extension],
)
Note: Even for extensions, much can be configured in pyproject.toml directly with setuptools's [tool.setuptools.extensions] table. Only resort to setup.py if truly complex, imperative logic is required.