Python Setuptools Packaging — Core Concepts
Why this topic matters
If you’ve written reusable Python code — a library, a CLI tool, a framework — you need a way to package it so others can install it. Setuptools has been the dominant packaging tool since 2004 and remains the default build backend for most Python projects. Understanding it is essential for publishing to PyPI or distributing code within an organization.
How it works
Modern setuptools uses pyproject.toml as its configuration file. Here’s a minimal example:
[build-system]
requires = ["setuptools>=68.0", "setuptools-scm"]
build-backend = "setuptools.build_meta"
[project]
name = "my-awesome-lib"
version = "1.0.0"
description = "A library that does awesome things"
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.9"
dependencies = [
"requests>=2.28",
"click>=8.0",
]
[project.optional-dependencies]
dev = ["pytest", "black", "mypy"]
[project.scripts]
awesome-cli = "my_awesome_lib.cli:main"
Build the package:
pip install build
python -m build
This creates two files in dist/: a source distribution (.tar.gz) and a wheel (.whl).
Key concepts
Project layout
Setuptools supports two common layouts:
src layout (recommended):
my-project/
├── pyproject.toml
├── src/
│ └── my_awesome_lib/
│ ├── __init__.py
│ └── core.py
└── tests/
└── test_core.py
Flat layout:
my-project/
├── pyproject.toml
├── my_awesome_lib/
│ ├── __init__.py
│ └── core.py
└── tests/
└── test_core.py
The src layout prevents accidental imports from the local directory during testing — you’re always testing the installed version.
Package discovery
Setuptools automatically finds Python packages (directories with __init__.py). You can customize this:
[tool.setuptools.packages.find]
where = ["src"]
include = ["my_awesome_lib*"]
exclude = ["tests*"]
Entry points
Entry points let your package register CLI commands or plugins:
[project.scripts]
my-tool = "my_awesome_lib.cli:main"
[project.gui-scripts]
my-gui = "my_awesome_lib.gui:launch"
[project.entry-points."myapp.plugins"]
json-reader = "my_awesome_lib.plugins:JsonReader"
csv-reader = "my_awesome_lib.plugins:CsvReader"
After installation, my-tool becomes available as a command, and other packages can discover your plugins via the entry point group.
Including non-Python files
By default, setuptools only includes Python files. For templates, config files, or data:
[tool.setuptools.package-data]
my_awesome_lib = ["templates/*.html", "data/*.json"]
Or use a MANIFEST.in for source distributions:
include LICENSE
recursive-include src/my_awesome_lib/templates *.html
recursive-include src/my_awesome_lib/data *.json
Dynamic metadata
Some fields can be computed rather than hard-coded:
[project]
dynamic = ["version"]
[tool.setuptools.dynamic]
version = {attr = "my_awesome_lib.__version__"}
Or with setuptools-scm for Git-based versioning:
[project]
dynamic = ["version"]
[tool.setuptools-scm]
This derives the version from Git tags — v1.2.3 tag becomes version 1.2.3.
Publishing to PyPI
pip install twine
python -m build
twine check dist/*
twine upload dist/*
For testing, upload to TestPyPI first:
twine upload --repository testpypi dist/*
pip install --index-url https://test.pypi.org/simple/ my-awesome-lib
Common misconception
“setup.py is the only way.” The setup.py file was the traditional approach, but modern Python packaging prefers pyproject.toml. Setuptools fully supports pyproject.toml since version 61.0, and the Python Packaging Authority recommends it as the standard configuration format. You can still use setup.py for complex build customization, but most projects don’t need it.
One thing to remember
Setuptools transforms your Python project directory into an installable package. Define your metadata in pyproject.toml, run python -m build, and your code is ready for pip installation — locally, from a private index, or from PyPI.
See Also
- Python Black Formatter Understand Black Formatter through a practical analogy so your Python decisions become faster and clearer.
- Python Bumpversion Release Change your software's version number in every file at once with a single command — no more find-and-replace mistakes.
- Python Changelog Automation Let your git commits write the changelog so you never forget what changed in a release.
- Python Ci Cd Python Understand CI CD Python through a practical analogy so your Python decisions become faster and clearer.
- Python Cicd Pipelines Use Python CI/CD pipelines to remove setup chaos so Python projects stay predictable for every teammate.