Sharing code — internally or on PyPI — and installing your own project reliably requires proper packaging.
A modern Python project is described by one file, pyproject.toml. Its [build-system] table names the build backend (hatchling, setuptools, flit-core, pdm-backend, uv_build...) that turns your source into installable artifacts. The [project] table holds standard metadata (PEP 621): name, version, requires-python, dependencies, optional dependency groups, and entry points such as console scripts.
The recommended src layout puts your package at src/yourpkg/. It forces tests to run against the installed package rather than whatever happens to be in the current directory, catching missing-file bugs before your users do.
Development workflow:
pip install -e .makes an editable install: the venv points at your source, so changes take effect without reinstallingpython -m build(from thebuildpackage) creates an sdist (.tar.gzof the source) and a wheel (.whl, a ready-to-install archive) indist/twine upload dist/*publishes to PyPI (use TestPyPI first, and prefer trusted publishing from CI)
Dependency specifiers in dependencies should be ranges for libraries ("httpx>=0.27"), so your package can coexist with others. Exact pins belong in an application's lock file, not in a library's metadata.
[project.scripts] generates command-line executables on install: mytool = "mytool.cli:main" creates a mytool command that calls main(). Tools also read their configuration from the same file ([tool.pytest.ini_options], [tool.ruff], [tool.mypy]).
uv, Poetry, PDM and Hatch wrap these steps into one CLI, but all of them produce the same standard pyproject metadata and wheels.
Example
# pyproject.toml layout: src/greetkit/__init__.py, src/greetkit/cli.py
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "greetkit"
version = "0.3.0"
description = "Friendly greetings for the command line"
readme = "README.md"
requires-python = ">=3.11"
dependencies = ["rich>=13"]
[project.optional-dependencies]
dev = ["pytest>=8", "mypy"]
[project.scripts]
greet = "greetkit.cli:main"
[tool.pytest.ini_options]
testpaths = ["tests"]
# pip install -e ".[dev]" -> editable install with dev extras
# python -m build -> dist/greetkit-0.3.0-py3-none-any.whlRemember
- pyproject.toml declares the build backend and
[project]metadata - Use a src layout and
pip install -e .during development - Libraries declare version ranges; applications pin exact versions in lock files
Watch out for
- Pinning exact dependency versions in a library, causing conflicts for its users.