modern-python
Modern Python Skill
Overview
This skill implements Trail of Bits' modern Python coding standards for the agent-studio framework. The core philosophy is: use Rust-based tools for faster feedback loops, especially when working with AI agents. Every tool in this stack (uv, ruff, ty) is written in Rust and provides sub-second execution times, enabling tight iteration cycles.
Source repository: https://github.com/trailofbits/skills
Template: https://github.com/trailofbits/cookiecutter-python
License: CC-BY-SA-4.0
When to Use
- When creating new Python projects from scratch
- When migrating Python projects from legacy tooling
- When setting up CI/CD pipelines for Python projects
- When standardizing Python tooling across a team
- When writing standalone Python scripts that need proper structure
- When an AI agent needs fast feedback from Python tooling
Iron Laws
- ALWAYS configure all Python tooling in
pyproject.toml-- no separate config files (setup.cfg,.flake8,mypy.ini,black.toml) are permitted. - ALWAYS use
uv add/uv removefor dependency management -- never use barepip installin projects managed by uv. - NEVER commit
venv/,.venv/, or pip-generatedrequirements.txt-- commituv.lockfor reproducible builds. - ALWAYS use
uv runto execute tools and scripts -- this ensures the correct virtual environment and dependency resolution. - NEVER use legacy linting/formatting tools (flake8, black, isort, mypy) when ruff and ty are available -- consolidate to the Rust-based stack for speed and consistency.
Anti-Patterns
| Anti-Pattern | Why It Fails | Correct Approach |
|---|---|---|
Using pip install directly in a uv-managed project |
Bypasses lockfile and dependency resolution; creates reproducibility drift | Use uv add <pkg> to add dependencies and uv sync to install |
Maintaining .flake8, mypy.ini, or black.toml config files |
Fragments configuration across multiple files; hard to maintain and audit | Consolidate all tool config into pyproject.toml under [tool.ruff] and [tool.ty] |
Running python script.py instead of uv run python script.py |
Uses system Python instead of project venv; dependency mismatches | Always use uv run to execute within the managed environment |
Globally installing CLI tools with pip install --user |
Pollutes global environment; version conflicts across projects | Use uv tool run <tool> or uvx <tool> for one-off tool execution |
Ignoring ruff security rules (S select) |
Misses bandit-equivalent security checks like hardcoded passwords and SQL injection | Enable select = ["S"] in [tool.ruff.lint] for security linting |
The Modern Python Stack
| Tool | Replaces | Purpose | Speed |
|---|---|---|---|
| uv | pip, Poetry, pipenv, pip-tools | Package & project management | 10-100x faster |
| ruff | flake8, isort, black, pyflakes, pycodestyle, pydocstyle | Linting + formatting | 10-100x faster |
| ty | mypy, pyright, pytype | Type checking | 5-10x faster |
| pytest | unittest | Testing | -- |
| hypothesis | (manual property tests) | Property-based testing | -- |
Project Setup
New Project
# Create new project with uv
uv init my-project
cd my-project
# Add dependency groups
uv add --group dev ruff ty
uv add --group test pytest pytest-cov hypothesis
uv add --group docs sphinx myst-parser
# Install all dependencies
uv sync --all-groups
pyproject.toml Configuration
[project]
name = "my-project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []
[dependency-groups]
dev = ["ruff", "ty"]
test = ["pytest", "pytest-cov", "hypothesis"]
docs = ["sphinx", "myst-parser"]
# === Ruff Configuration ===
[tool.ruff]
target-version = "py312"
line-length = 100
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"N", # pep8-naming
"UP", # pyupgrade
"B", # flake8-bugbear
"A", # flake8-builtins
"C4", # flake8-comprehensions
"SIM", # flake8-simplify
"S", # flake8-bandit (security)
"TCH", # flake8-type-checking
"RUF", # ruff-specific rules
]
[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["S101"] # Allow assert in tests
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
# === Pytest Configuration ===
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = [
"--strict-markers",
"--strict-config",
"-ra",
]
[tool.coverage.run]
source = ["src"]
branch = true
[tool.coverage.report]
fail_under = 80
show_missing = true
exclude_lines = [
"if TYPE_CHECKING:",
"if __name__ == .__main__.:",
]
Daily Workflow Commands
Package Management (uv)
# Add a dependency
uv add requests
# Add a dev dependency
uv add --group dev ipdb
# Remove a dependency
uv remove requests
# Update all dependencies
uv lock --upgrade
# Update a specific dependency
uv lock --upgrade-package requests
# Run a script in the project environment
uv run python script.py
# Run a tool (without installing globally)
uv run --with httpie http GET https://api.example.com
Linting and Formatting (ruff)
# Check for lint errors
uv run ruff check .
# Auto-fix lint errors
uv run ruff check --fix .
# Format code
uv run ruff format .
# Check formatting (dry run)
uv run ruff format --check .
# Check specific rules
uv run ruff check --select S . # Security rules only
Type Checking (ty)
# Run type checker
uv run ty check
# Check specific file
uv run ty check src/main.py
Testing (pytest)
# Run all tests
uv run pytest
# Run with coverage
uv run pytest --cov
# Run specific test file
uv run pytest tests/test_auth.py
# Run with verbose output
uv run pytest -v
# Run and stop at first failure
uv run pytest -x
Migration Guide
From pip/requirements.txt
# Install uv
curl -LsSf https://astral.sh/uv/install.sh | sh
# Initialize project from existing requirements
uv init
uv add $(cat requirements.txt | grep -v '^#' | grep -v '^$')
# Remove old files
rm requirements.txt requirements-dev.txt
From Poetry
# uv can read pyproject.toml with Poetry sections
uv init
# Move Poetry dependencies to [project.dependencies]
# Move [tool.poetry.group.dev.dependencies] to [dependency-groups]
# Remove [tool.poetry] section
uv sync
From flake8/black/isort to ruff
# Remove old tools
uv remove flake8 black isort pyflakes pycodestyle
# Add ruff
uv add --group dev ruff
# Convert .flake8 config to ruff (manual)
# ruff supports most flake8 rules with same codes
# Remove old config files
rm .flake8 .isort.cfg pyproject.toml.bak
From mypy to ty
# Remove mypy
uv remove mypy
# Add ty
uv add --group dev ty
# ty uses the same type annotation syntax as mypy
# Most code requires no changes
CI/CD Configuration
GitHub Actions
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v4
with:
enable-cache: true
- name: Install dependencies
run: uv sync --all-groups
- name: Lint
run: uv run ruff check .
- name: Format check
run: uv run ruff format --check .
- name: Type check
run: uv run ty check
- name: Test
run: uv run pytest --cov --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
file: coverage.xml
Dependabot Configuration
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: 'uv'
directory: '/'
schedule:
interval: 'weekly'
groups:
all:
patterns:
- '*'
Pre-commit Hooks
# .pre-commit-config.yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.0
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
Code Patterns
Type Annotations
from __future__ import annotations
from collections.abc import Sequence
from typing import TypeAlias
# Use modern syntax (Python 3.12+)
type Vector = list[float] # Type alias (PEP 695)
def process_items(items: Sequence[str], *, limit: int = 10) -> list[str]:
"""Process items with a limit."""
return [item.strip() for item in items[:limit]]
# Use | instead of Union
def maybe_int(value: str) -> int | None:
try:
return int(value)
except ValueError:
return None
Project Structure
my-project/
pyproject.toml # Single config file for all tools
uv.lock # Locked dependencies (commit this)
src/
my_project/
__init__.py
main.py
models.py
utils.py
tests/
__init__.py
test_main.py
test_models.py
conftest.py # Shared fixtures
.github/
workflows/
ci.yml
dependabot.yml
.pre-commit-config.yaml
Common Pitfalls
- Using pip directly: Always use
uv add/uv remove/uv run. Neverpip install. - Separate config files: All configuration goes in
pyproject.toml. Delete.flake8,mypy.ini,black.toml. - Global installs: Use
uv runoruv tool runinstead of globally installing CLI tools. - Missing lock file: Always commit
uv.lockfor reproducible builds. - Old Python syntax: Use
ruff --select UPto auto-upgrade to modern syntax (match statements,|unions, etc.). - Ignoring security rules: Enable
S(bandit) rules in ruff to catch security issues.
Integration with Agent-Studio
Recommended Workflow
- Use
modern-pythonto set up or migrate Python projects - Use
python-backend-expertfor framework-specific patterns (Django, FastAPI) - Use
tddskill for test-driven development workflow - Use
comprehensive-unit-testing-with-pytestfor test strategy
Complementary Skills
| Skill | Relationship |
|---|---|
python-backend-expert |
Framework-specific patterns (Django, FastAPI, Flask) |
comprehensive-unit-testing-with-pytest |
Testing strategies and patterns |
comprehensive-type-annotations |
Type annotation best practices |
prioritize-python-3-10-features |
Modern Python language features |
tdd |
Test-driven development methodology |
property-based-testing |
Hypothesis-based testing patterns |
Memory Protocol
Before starting: Check if the project already has Python tooling configured. Identify which legacy tools need migration.
During setup: Write configuration incrementally, verifying each tool works before moving to the next. Run ruff check, ruff format --check, and uv run pytest at each step.
After completion: Record the toolchain versions and any migration issues to .claude/context/memory/learnings.md for future reference.