tooling#

ruff for linting and formatting. ty for type checking. pre-commit to enforce both.

ruff#

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

uv run ruff format src/ tests/   # format
uv run ruff check src/ tests/    # lint
uv run ruff check --fix          # lint and auto-fix

pyproject.toml config#

[tool.ruff]
line-length = 88

[tool.ruff.lint]
fixable = ["ALL"]
extend-select = [
    "I",    # isort
    "B",    # flake8-bugbear
    "C4",   # flake8-comprehensions
    "UP",   # pyupgrade
    "SIM",  # flake8-simplify
    "RUF",  # ruff-specific
]
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

pyproject.toml config#

[tool.ty.src]
include = ["src", "tests"]
exclude = ["**/node_modules", "**/__pycache__", ".venv"]

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

.pre-commit-config.yaml#

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]

install with:

uv run pre-commit install

never use --no-verify to skip hooks. fix the issue instead.

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.

alternatives#

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

sources: