about things notes.zzstoatzz.io
notes
5.9 kB
Markdown
at main

tooling #

project layout and pyproject.toml as single source of truth; ruff for linting and formatting, ty for type checking, pre-commit to enforce both.

project layout #

myproject/
├── src/myproject/
│   ├── __init__.py
│   ├── cli.py
│   └── _internal/       # private implementation
├── tests/
├── pyproject.toml
├── justfile
└── .pre-commit-config.yaml

the src/ layout prevents accidental imports from the working directory — the package is only importable when properly installed.

pyproject.toml #

[project]
name = "myproject"
requires-python = ">=3.10"
dynamic = ["version"]
dependencies = ["httpx>=0.27", "pydantic>=2.0"]

[project.scripts]
myproject = "myproject.cli:main"
myproject-mcp = "myproject.mcp:main"

[build-system]
requires = ["hatchling", "uv-dynamic-versioning"]
build-backend = "hatchling.build"

[tool.hatch.version]
source = "uv-dynamic-versioning"

[dependency-groups]
dev = ["pytest>=8.0", "ruff>=0.8", "ty>=0.0.1a6"]
  • dynamic = ["version"] with uv-dynamic-versioning derives the version from git tags — no manual version edits, release by git tag v0.1.0 && git push --tags
  • [project.scripts] declares CLI entry points; multiple are fine (CLI + MCP server)
  • projects that don't need dynamic versioning can use the lighter uv_build backend

dependency groups vs optional dependencies #

dependency groups (PEP 735) are local-only and never appear in published metadata:

[dependency-groups]
dev = ["pytest", "ruff"]
docs = ["mkdocs", "mkdocs-material"]

install with uv sync --group dev; CI installs only what it needs.

optional dependencies are published, installable by consumers as pip install mypackage[aws]:

[project.optional-dependencies]
aws = ["prefect-aws"]

use groups for dev/test/CI, optional deps for features consumers might want.

PyPI rejects a distribution whose Requires-Dist contains a direct reference (pkg @ git+https://...), and an optional dependency is only a Requires-Dist with an extra == "..." marker, so a git dependency cannot hide in an extra. twine check passes that metadata; the upload fails. a git pin in a dependency group is fine because groups are never published. to offer a fork through an extra, publish the fork's package under its own name.

exclude-newer in [tool.uv] also holds back a dependency released inside the window, including one you just published; exempt it with exclude-newer-package.

ruff #

replaces black, isort, flake8, and dozens of plugins. one tool, fast.

uv run ruff format src/ tests/   # format
uv run ruff check --fix          # lint and auto-fix
[tool.ruff]
line-length = 88

[tool.ruff.lint]
fixable = ["ALL"]
extend-select = ["I", "B", "C4", "UP", "SIM", "RUF"]
ignore = ["COM812"]  # conflicts with formatter

[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401", "I001"]  # unused imports ok in __init__
"tests/**/*.py" = ["S101"]        # assert ok in tests

ty #

astral's new type checker. still early but fast and improving.

uv run ty check
[tool.ty.src]
include = ["src", "tests"]

[tool.ty.environment]
python-version = "3.10"

[tool.ty.rules]
# start permissive, tighten over time
unknown-argument = "ignore"
no-matching-overload = "ignore"

pre-commit #

enforce standards before commits reach the repo. install with uv run pre-commit install; never skip hooks with --no-verify — fix the issue instead.

repos:
  - repo: https://github.com/abravalheri/validate-pyproject
    rev: v0.24.1
    hooks:
      - id: validate-pyproject

  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.8.0
    hooks:
      - id: ruff-check
        args: [--fix, --exit-non-zero-on-fix]
      - id: ruff-format

  - repo: local
    hooks:
      - id: type-check
        name: type check
        entry: uv run ty check
        language: system
        types: [python]
        pass_filenames: false

  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v5.0.0
    hooks:
      - id: no-commit-to-branch
        args: [--branch, main]

pytest #

[tool.pytest.ini_options]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
testpaths = ["tests"]

asyncio_mode = "auto" means async tests just work — no @pytest.mark.asyncio needed.

justfile #

a justfile names the project's commands so every repo runs the same way:

install:
    uv sync

test:
    uv run pytest tests/ -xvs

lint:
    uv run ruff format src/ tests/
    uv run ruff check src/ tests/ --fix

check:
    uv run ty check

uv workspaces #

for multi-package repos, [tool.uv.workspace] in the root pyproject.toml shares one lockfile:

[tool.uv.workspace]
members = ["packages/*"]

[tool.uv.sources]
core = { workspace = true }

packages can depend on each other; uv sync resolves the whole workspace together.

alternatives #

  • typsht - parallel type checking across multiple checkers
  • prek - pre-commit reimplemented in rust

sources #