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.

pythonsetuptoolspackagingpypi

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.