stackademic

The leading education platform for anyone with an interest in software development.

Python Packaging

Structure, build, and publish distributable packages with pyproject.toml

Overview

Packaging turns your modules into a distributable, installable artifact that others can pip install. Modern Python standardizes on pyproject.toml (PEP 517/518/621) to declare metadata, dependencies, and the build backend. The build system produces two artifacts: a source distribution (sdist) and a platform-independent wheel.

Syntax / Usage

Declare everything in pyproject.toml. A src/ layout keeps the import package isolated from project config.

# pyproject.toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "mytool"
version = "0.1.0"
description = "A small CLI tool"
requires-python = ">=3.10"
dependencies = ["httpx>=0.27"]

[project.optional-dependencies]
dev = ["pytest>=8", "mypy>=1.10"]

[project.scripts]
mytool = "mytool.cli:main"   # exposes a console entry point

The layout pairs this file with src/mytool/__init__.py and src/mytool/cli.py.

Examples

Define the console entry point referenced above; installing the package puts mytool on the PATH:

# src/mytool/cli.py
import sys

def main() -> int:
    args = sys.argv[1:]
    if not args:
        print("usage: mytool <name>")
        return 1
    print(f"Hello, {args[0]}!")
    return 0

Build and (optionally) publish from the shell:

# Build sdist + wheel into dist/, then upload
import subprocess

subprocess.run(["python", "-m", "build"], check=True)
subprocess.run(["python", "-m", "twine", "check", "dist/*"], check=True)
# subprocess.run(["python", "-m", "twine", "upload", "dist/*"], check=True)

Install locally in editable mode with pip install -e ".[dev]" so code changes take effect without reinstalling.

Common Mistakes

  • Hardcoding versions in multiple places instead of a single source of truth
  • Using a flat layout so tests accidentally import from the working directory rather than the installed package
  • Forgetting to list runtime dependencies, so installs succeed locally but fail for users
  • Publishing without bumping the version—PyPI rejects duplicate versions
  • Committing build artifacts (dist/, *.egg-info) instead of ignoring them

See Also

python-modules python-type-hints python-testing-pytest