A Python Toolchain Built for the Agentic Engineering Era
High performance linting, formatting, package management, pre-commit hooks and other boring tooling that will make you and your coding assistants more productive
I have to admit: I have never been a huge fan of pre-commit hooks. I always felt they were slow and broke up my development pace. That opinion and my broader laziness about automated quality gates in general have been changed by two shifts happening at the same time:
Rust ate the toolchain. Thanks to the wide adoption of Rust, we now have developer tooling that is orders of magnitude faster than what came before. Checks that used to cost you a coffee break now cost milliseconds.
Agentic Engineering (or Vibe Coding, if you will) raised the stakes. When an AI agent is writing a meaningful share of your code, automated quality controls stop being nice-to-haves, especially in dynamically typed languages like Python. Add to this equation many people in a team contributing to the same product and the requirement to have these quality checks done automatically and at scale is even more imperative.
Fast tools mean you can afford to run checks everywhere - in your editor, on every commit, in CI, and inside the agent’s own feedback loop. And agents generating code at superhuman speed mean you need checks everywhere. This post is a tour of the stack I’ve landed on.
Rust adoption is giving birth to incredible tooling
You probably already know what these tools have in common: uv, ruff, ty, complexipy. The common thread: they’re all written in Rust, and they all deliver lightspeed performance for the unglamorous housekeeping side of development.
For my Python projects, the following tools have become part of an essential quartet.
uv
A drop-in replacement for pip, pip-tools, pipx, poetry, pyenv, twine, virtualenv, and more. uv manages Python projects and their dependencies, installs and manages Python versions, and runs and installs tools published as Python packages. It’s 10–100x faster than pip, which by itself is already a good reason to adopt it. Beyond raw speed, it makes environments reproducible and disposable - the same lockfile drives your laptop, your CI runner, and your agent’s sandbox.
Installation
curl -LsSf https://astral.sh/uv/install.sh | shGetting started
Once uv is installed, you can use it to import your requirements.txt file (if you had been using solely pip), or your pyproject.toml (if you use poetry, for instance):
pip + requirements.txt
# Initialize a uv project
uv init
# Import existing requirements
uv add -r requirements.txt
# If you have requirements-dev.txt for dev dependencies
uv add --dev -r requirements.txt
# One caveat: uv add -r keeps specifiers as written,
# so if your file is a pip freeze dump of == pins,
# you'll end up with everything hard-pinned in pyproject.toml.pyproject.toml
# Migrate project definitions
uvx migrate-to-uv
# Update lockfile
uv lock
# Install/update/reinstall dependencies
uv syncruff
Code linter and formatter with drop-in parity with Flake8, isort, and Black, plus great extras like built-in caching and monorepo support.
If you're not familiar: a linter reads your code without running it and flags likely bugs, dead code, and style inconsistencies - think of it as a spell-checker for code. A formatter then rewrites the code into one consistent style so nobody argues about whitespace in code review.
Installation
# Install Ruff globally.
uv tool install ruff@latest
# Or add Ruff to your project.
uv add --dev ruff
# With pip.
pip install ruff
# With pipx.
pipx install ruffUsage
# Lint the code at <path>
uv run ruff check <path>
# Lint and auto-fix violations where possible
uv run ruff check --fix <path>
# Format the code
uv run ruff format <path>ty
Type checker and language server, 10–100x faster than mypy and Pyright. It’s still young compared to those two, but it’s already usable for most projects.
A type checker verifies that your code uses values consistently - that you're not passing a string where a function expects a number, or calling a method that doesn't exist. Python won't catch these mistakes until the code actually runs (often in production); a type checker catches them before that.
Installation
# Project scoped
uv add --dev ty
# Global installation
uv tool install ty@latestUsage
# Runs type-check tests over <path>
uv run ty check <path>complexipy
Very fast cognitive complexity analysis for Python. Complexipy is inspired by G. Ann Campbell’s research at SonarSource, and it basically calculates how hard a function or method is to understand. This is done by calculating a score which is based on the number of conditions, nested conditions, loops, etc.
Folks who work in highly regulated industries will have probably come across SonarQube scans at some point - while it’s not 100% clear to me if complexipy has full coverage compared to SonarQube, it covers most of its static checks.
If you are not familiar with the concept of cognitive complexity, it’s totally fine -the example below will help illustrate it
Cognitive Complexity - The Don’t:
def calculate_discount(order, customer):
discount = 0
if order.total > 0: # +1
if customer.is_active: # +2 (nested)
if customer.tier == "gold": # +3 (nested deeper)
if order.total > 100: # +4
discount = 0.20
else: # +1
discount = 0.10
elif customer.tier == "silver": # +1
if order.total > 100: # +4
discount = 0.10
for item in order.items: # +3 (nested)
if item.is_clearance: # +4
discount = 0
break
return discountComplexipy results
What’s wrong with the code above? Every branch is trivial, yet the function scores well above a sane threshold, because to understand any single line, you have to keep the whole tower of conditions in your head.
The Do - same logic, guard clauses and early returns:
def calculate_discount(order, customer):
if order.total <= 0 or not customer.is_active: # +1
return 0
if any(item.is_clearance for item in order.items): # +1
return 0
is_large_order = order.total > 100
if customer.tier == "gold": # +1
return 0.20 if is_large_order else 0.10 # +1
if customer.tier == "silver" and is_large_order: # +1
return 0.10
return 0Complexipy results
What’s right with this code? Identical behavior, a fraction of the score. Note what did the work — none of it is clever:
Guard clauses. Handle the disqualifying cases first and
returnearly, so the happy path reads flat, top to bottom.Naming a condition (
is_large_order). Extracting a boolean into a variable costs nothing at runtime and removes a mental parse.any()instead of afor+if+break. Python’s comprehensions and builtins collapse an entire nested structure into one line the reader already knows the shape of.
Another way of reducing cognitive complexity for this example would be to follow the corollary: when a nested block does something nameable, extract it into a function. Cognitive complexity resets to zero inside the new function, which is the metric telling you “this was a separate idea all along.”
I added these examples mainly for illustration purposes- but in an agentic coding workflow, you don’t really need to worry about checking and fixing all of these issues manually: raising complexipy failures to your coding assistant goes a long way in having it clean up your code for you.
Installation
# Project scoped
uv add --dev complexipy
# Global installation
uv tool install complexipyWiring it all together with prek
uv, ty and complexipy can be quite powerful, but you don’t want to keep running each of these manually, and most of the time you will not want to run them for your entire codebase. This is where prek comes in.
prek is a git pre-commit hook manager - a faster, dependency-free, drop-in alternative to the classic pre-commit. It supports monorepos, integrates with uv, and improves toolchain installation for Python, Node.js, Bun, Go, Rust, and Ruby, with toolchains shared between hooks. Supports .pre-commit.yaml hook declaration, or you can shift to prek’s native YAML or TOML formats.
Installation
# Using uv (recommended)
uv tool install prek
# Using uvx (install and run in one command)
uvx prek
# Adding prek to the project dev-dependencies
uv add --dev prek
# Using pip
pip install prek
# Using pipx
pipx install prekHow I use it
Bare Minimum
As the bare minimum, I want to have linting, formatting and type checking, which makes my pre-commit definition file quite simple:
[[repos]]
repo = "local"
hooks = [
{
id = "ruff-format",
name = "ruff format",
entry = "uv run ruff format",
language = "system",
types = ["python"]
},
{
id = "ruff-check",
name = "ruff check",
entry = "uv run ruff check --fix",
language = "system",
types = ["python"]
},
{
id = "ty-check",
name = "ty check",
entry = "uv run ty check --error-on-warning",
language = "system",
types = ["python"]
}
]Adding Cognitive Complexity checks with complexipy
Depending on the size of the codebase and the number of people working on it, I also like to hook up complexipy to make sure that the codebase stays minimally clean and readable:
[[repos]]
repo = "local"
hooks = [
{
id = "ruff-format",
name = "ruff format",
entry = "uv run ruff format",
language = "system",
types = ["python"]
},
{
id = "ruff-check",
name = "ruff check",
entry = "uv run ruff check --fix",
language = "system",
types = ["python"]
},
{
id = "ty-check",
name = "ty check",
entry = "uv run ty check --error-on-warning",
language = "system",
types = ["python"]
},
{
id = "complexipy",
name = "cognitive complexity with complexipy",
entry = "uv run complexipy --max-complexity-allowed 15 --quiet",
language = "system",
types = ["python"]
}
]Extra: skill installation
If you run into any issues or just want to offload the configuration work to your coding assistant, prek also has an official SKILLS.md definition, which you can install with the following GitHub CLI command:
gh skill install j178/prek prekThe outcome: three layers of defense
Running these checks locally while developing (or vibe coding) is great, but the bonus here is that these tools are now fast enough to run at every layer of your workflow, giving you consistent results from your local machine all the way to CI:
At commit time. prek runs the same checks as a gate before anything enters history. Because the underlying tools are super fast, the hook overhead is measured in milliseconds - the original reason I hated pre-commit hooks is simply gone.
In CI. The same ruff/ty/complexipy invocations run as the authoritative check on every push and PR. uv makes this fast too: dependency installation, historically the slowest part of a Python CI job, drops from minutes to seconds.
In the agent loop. This is the new layer. When Claude Code or Codex runs your linter after an edit or trips a hook on commit, it reads the error output, fixes the issue, and retries - no human review cycle, no CI round-trip. A deterministic, millisecond-fast check that catches a type error locally is dramatically cheaper than discovering it three agent-turns later, after the mistake has propagated through half a dozen files.
But the largest payout is in increased code cleanliness, readability and technical debt mitigation: you now have one more guarantee that the code you ship with coding assistants will be much cleaner and readable, without having to add any prompts or instructions.
Wrapping up
I used to skip pre-commit hooks because they were slow, keep CI checks minimal because they were annoying, and trust my own fingers for the rest. All three assumptions have expired: the tooling is now near-instant, the checks are cheap enough to run everywhere, and my fingers aren’t always the ones typing. If you built your quality-control habits in the pip-and-Flake8 era, it’s definitely worth revisiting them.
I'm considering a follow-up on the Next.js / TypeScript side of this stack (oxlint, oxfmt) and on wiring these checks into agent workflows more deeply. Reply and tell me which one you'd rather read first - I read all comments and emails.




