Home/Learn/Python A–Z/Packaging with pyproject.toml — From Script to Installable Package

Packaging with pyproject.toml — From Script to Installable Package

Intermediate
Packaging & venv

pyproject.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.

One file replaces setup.py, setup.cfg and MANIFEST.in
# 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.

pip install -e . while developing; build + twine to ship
# 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 Aria
1

What problems does pyproject.toml solve over setup.py?

MediumAtlassian
2

What does pip install -e . actually do, and when do you use it?

MediumPhonePe
3

Why is the src/ layout recommended over a flat package directory?

HardGoogle

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.

Loading discussion…