Packaging with pyproject.toml — From Script to Installable Package
Intermediatepyproject.toml is the single modern config for a Python package: metadata, dependencies, and console scripts in one file — pip install -e . for development, python -m build + twine to publish.
Overview
The moment your code is imported by another project — or by your own tests — it should become a package. Modern packaging is one file, pyproject.toml, which replaced the old setup.py: it declares the name, version, dependencies, and build backend (hatchling or setuptools). The src/ layout keeps import mistakes honest, pip install -e . installs your package in editable mode so edits apply instantly, and [project.scripts] turns a function into a real command-line tool. Publishing to PyPI is two commands away, but the bigger everyday win is internal: shared utilities installed as versioned packages instead of copy-pasted files.
Layout + pyproject.toml
The src/ layout prevents accidentally importing the local folder instead of the installed package. One TOML file declares everything a build tool or pip needs to know.
# Project layout (src/ layout — the recommended one)
# campus-utils/
# ├── pyproject.toml
# ├── README.md
# └── src/
# └── campus_utils/
# ├── __init__.py
# ├── cli.py
# └── marks.py
# ── pyproject.toml ──────────────────────────────
[project]
name = "campus-utils"
version = "0.1.0"
description = "Marksheet and placement helpers"
requires-python = ">=3.10"
dependencies = [
"requests>=2.31",
"pydantic>=2.0",
]
[project.optional-dependencies]
dev = ["pytest", "ruff"]
[project.scripts]
campus = "campus_utils.cli:main" # installs a real 'campus' command
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"Editable Install, Build & Publish
pip install -e . links the source into your venv — imports and the console script work immediately, and every edit is live without reinstalling. python -m build produces the wheel; twine uploads it.
# Development: editable install into the active venv
pip install -e ".[dev]" # package + dev extras (pytest, ruff)
python -c "from campus_utils.marks import percentage; print(percentage(432, 500))"
campus --help # the [project.scripts] entry point works
# ── src/campus_utils/cli.py ─────────────────────
def main():
import sys
print(f"campus-utils CLI, args: {sys.argv[1:]}")
# Build distributable artifacts
pip install build twine
python -m build # → dist/campus_utils-0.1.0-py3-none-any.whl
# dist/campus_utils-0.1.0.tar.gz
# Publish (TestPyPI first, always)
twine upload --repository testpypi dist/*
twine upload dist/* # the real PyPI
# Versioning: bump [project] version, rebuild, re-upload —
# PyPI never accepts the same version twice.Key Points to Remember
- 1pyproject.toml is the modern standard — setup.py is legacy
- 2src/ layout prevents "works locally, breaks installed" import bugs
- 3pip install -e . = editable mode: edits apply without reinstalling
- 4[project.scripts] creates console commands; build + twine publish to PyPI
Interview Questions
Sign in to ask AriaWhat problems does pyproject.toml solve over setup.py?
What does pip install -e . actually do, and when do you use it?
Why is the src/ layout recommended over a flat package directory?
Ask Aria about Packaging with pyproject.toml — From Script to Installable Package
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.