Tooling & Packaging
The modern Python toolchain -- uv, ruff, virtual environments, pyproject.toml packaging and distribution, and the import system
Tooling & Packaging
Python's packaging ecosystem was historically painful; modern tools have transformed it. This page covers the toolchain (uv, ruff, mypy), building and distributing installable packages with pyproject.toml, and the import-system mechanics that underlie all of it.
Virtual Environments and Tooling
The Python packaging ecosystem has historically been painful. Modern tools have improved things significantly:
| Tool | Purpose | When to Use |
|---|---|---|
uv | Fast package installer and resolver | New projects -- it is dramatically faster than pip |
poetry | Dependency management with lockfile | Teams that need reproducible builds |
pip + venv | Built-in, always available | Simple projects, CI environments |
ruff | Linter and formatter (replaces flake8, black, isort) | Every project -- it is fast and comprehensive |
mypy / pyright | Static type checking | Any project with type hints |
Packaging and Distribution
A senior Python engineer is expected to ship installable, reproducible packages -- not a folder of loose scripts. Modern packaging centres on a single pyproject.toml:
[project]
name = "myservice"
version = "1.2.0"
requires-python = ">=3.11"
dependencies = ["httpx>=0.27", "pydantic>=2.0"]
[project.optional-dependencies]
dev = ["pytest", "ruff", "mypy"]
[project.scripts]
myservice = "myservice.cli:main" # creates a console command on install
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"Conventions that prevent real bugs:
- Use a
src/layout (src/myservice/). It forces you to install the package to import it, so your tests run against the installed package -- catching missing-data-file and packaging errors that a flat layout hides. pip install -e .(editable install) for development: code changes take effect without reinstalling.- Pin for apps, range for libraries. Applications commit a lockfile (
uv.lock,poetry.lock, orpip-compile'drequirements.txt) for byte-for-byte reproducible deploys; libraries specify loose ranges so they compose with others. entry_points/[project.scripts]is how you expose CLI commands and plugin hooks -- far better than telling users to runpython -m.- Build with
python -m build(produces a wheel + sdist); publish withtwine uploadoruv publish. The wheel is the fast, pre-built format; the sdist is the fallback that builds from source.
The Import System and Module Mechanics
import is not magic -- it runs code. A module's top level executes exactly once, the first time it is imported; the result is cached in sys.modules and every later import returns that same object. This is why module-level state behaves like a singleton.
# config.py -- runs once; every importer shares this object
settings = load_settings() # executed a single time, at first import
# Re-importing does not re-run it:
import config # cached hit, settings is the same instanceCircular imports are the classic failure. a imports b, b imports a; whichever loads first hits a half-initialised module and a name that does not exist yet:
# a.py
from b import helper # ImportError if b imports a at its top level
# Fixes, in order of preference:
# 1. Restructure -- the cycle usually signals a missing third module.
# 2. Import inside the function that needs it (deferred, breaks the cycle).
# 3. `import b` then use `b.helper` (module object resolves lazily at call time).Two distinctions experts hold:
import modulevsfrom module import name. The first binds the module object (so reassignments in the module are seen); the second binds the current value of a name -- if the module later rebinds it, you keep the stale one.- Namespace packages (PEP 420) let a package span multiple directories with no
__init__.py. Useful for plugins, but an accidental missing__init__.pycan also turn a normal package into one and silently change import behaviour.
python -m mypackage runs a package's __main__.py with the package importable -- the correct way to launch a package, versus python script.py, which puts the script's directory on the path and breaks relative imports. if __name__ == "__main__": is what distinguishes "run directly" from "imported".