Skip to main content
  1. Blogs/

Python Code Quality CI Pipeline with uv and Ruff

·928 words·5 mins·
Subhajit Bhar
Author
Subhajit Bhar
I build production-grade document extraction pipelines for businesses that process invoices, lab reports, contracts, and other document types at scale.
Table of Contents

You can create a Python Code Quality CI pipeline using uv, Ruff, and ty within 5 minutes.

TL;DR

  • Replace pip + requirements.txt with uv for fast, reproducible installs.
  • Replace Flake8 + Black + isort with ruff — one tool, 10–100× faster.
  • Add ty for type checking (Astral’s faster mypy replacement).
  • Total CI time: ~30s. GitHub Actions config fits in 20 lines.

Most of us begin a Python project with high hopes. We set up a clean virtual environment, organize a requirements file, and plan to add a linter—then forget.

But as we add more dependencies, the requirements file can get messy. The same thing happens with our tests and documentation. They start out organized but quickly become hard to manage. So, we end up with a codebase that is difficult to maintain and understand.

Before long, we start wondering why the code is breaking, tests are failing, or the documentation is out of date. So, we need to add a CI pipeline to help us catch these errors early and make sure everything works as it should.

In this article, we’ll set up a fast CI pipeline for code quality using uv, Ruff, and ty from Astral. We’ll use a single lockfile (uv.lock), one linter/formatter (ruff), and one type checker (ty).

  • lockfile consistency ensures the lockfile is in sync with pyproject.toml so installs are reproducible. We will use uv lock --locked for this.
  • lint check spots potential bugs and code smells before they cause problems and helps write better code. We will use ruff for this.
  • formatter check keeps your code style consistent across the entire project and across different editors making it easier to review code. We will use ruff for this.
  • type checker makes sure your functions use the right data types and avoid type errors early. We will use ty for this.

Dependency check
#

When we initialize a new project with uv init, it creates an empty uv.lock alongside pyproject.toml. This lockfile ensures everyone installs identical dependency graphs. If we modify pyproject.toml directly, the lock can drift out of sync. We can verify the lock is up to date with uv lock --locked (fails if regeneration would be required).

So in our CI pipeline, we add a step to check that the lockfile is in sync with pyproject.toml. If it isn’t, CI fails. Create .github/workflows/code-quality.yml and add:

name: Python Code Quality
on: [push, pull_request]
jobs:
  lockfile-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: install uv
        uses: astral-sh/setup-uv@v6
        with:
          version: 0.6.12
      - run: uv lock --locked

Since we’ll run lint and format checks in parallel, extract uv setup to a composite action. Create .github/actions/setup/action.yml with:

name: Install UV
description: Install UV package manager
runs:
  using: composite
  steps:
    - name: install uv
      uses: astral-sh/setup-uv@v6
      with:
        version: 0.6.12

You can then use this action in other workflows via the uses keyword:

name: Python Code Quality
on: [push, pull_request]
jobs:
  lockfile-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/setup
      - run: uv lock --locked

Lint check
#

The second step is linting. We’ll use ruff to enforce standards and catch likely bugs. Add a lint-check job to .github/workflows/code-quality.yml:

lint-check:
    runs-on: ubuntu-latest
    needs: [lockfile-check]
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/setup
      - run: uvx ruff check .

Formatter check
#

Use ruff format to enforce consistent formatting. Import sorting is handled by Ruff’s isort rules during linting (not by the formatter). To validate formatting in CI, run ruff format --check. Add a format-check job to .github/workflows/code-quality.yml:

format-check:
    runs-on: ubuntu-latest
    needs: [lockfile-check]
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/setup
      - run: uvx ruff format --check .

Type check
#

Use ty check to run static type checks. ty is a fast, modern type checker. Add a type-check job to .github/workflows/code-quality.yml:

type-check:
    runs-on: ubuntu-latest
    needs: [lockfile-check]
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/setup
      - run: uvx ty check .

!!! warning “Note” ty is in preview state. Use this with caution. Another good alternative for static type checking is pyright.

Full CI pipeline
#

name: Python Code Quality
on: [push, pull_request]
jobs:
  lockfile-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/setup
      - run: uv lock --locked

  lint-check:
    runs-on: ubuntu-latest
    needs: [lockfile-check]
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/setup
      - run: uvx ruff check .

  format-check:
    runs-on: ubuntu-latest
    needs: [lockfile-check]
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/setup
      - run: uvx ruff format --check .

  type-check:
    runs-on: ubuntu-latest
    needs: [lockfile-check]
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/setup
      - run: uvx ty check .

Quick walkthrough
#

  • lockfile-check: Verifies uv.lock is in sync with pyproject.toml using uv lock --locked. Fails early if the lock needs regeneration.
  • lint-check: Runs uvx ruff check . to catch bugs and style issues. Import sorting is enforced by Ruff’s isort rules here.
  • format-check: Runs uvx ruff format --check . to ensure consistent formatting without modifying files.
  • type-check: Runs uvx ty check . for fast static type analysis. ty is preview; pyright is a stable alternative.
  • Parallelism: needs: [lockfile-check] means lint, format, and type checks run in parallel after the lockfile passes.

Conclusion
#

With just a few lines of YAML, you now have reproducible installs with uv lock --locked, fast linting and formatting via Ruff, and type checks with ty running on every push and pull request. This keeps drift in check, makes reviews calmer, and stops easy-to-miss issues from slipping into main.

If you want to take it further, add tests with pytest, coverage thresholds, caching for uv and lint artifacts, and a build matrix across Python versions. Keep it fast and opinionated—the best CI is the one that runs on every change without friction.

Related

Ruff: Modern Python Linter & Formatter Walkthrough

·1016 words·5 mins
TL;DR Ruff replaces Flake8, Black, isort, and pydocstyle — one tool, 10–100× faster (written in Rust). Install: uv add --dev ruff or pip install ruff. Run: ruff check . (lint) and ruff format . (format). Add pre-commit hooks + GitHub Actions to enforce on every commit and PR. Pair with the Python CI Pipeline guide for the full uv + Ruff + ty setup. Writing clean, readable code is essential for collaboration and maintainability. Linters and formatters help us keep our codebase consistent and easy to understand.

Git & GitHub: The Definitive Version Control Guide

·1559 words·8 mins
Version control is the foundation of reliable software delivery. This guide teaches Git from first principles, then layers in practical GitHub workflows used by high-performing teams. You’ll learn the mental models, the everyday commands, and the advanced tools to collaborate confidently without fear of breaking anything.

Git 101 – Commands and Workflows Cheat Sheet

·448 words·3 mins
A quick, task-oriented Git reference. Pair this with the in-depth guide for concepts and best practices. TL;DR Working tree → git add → Staging area → git commit → Local repo → git push → Remote. Most common daily commands: status, add, commit, push, pull, checkout, merge. Undo staged changes: git restore --staged <file>. Undo last commit (keep changes): git reset HEAD~1. For concepts and workflows, read the full Git guide. Minimal Mental Model # graph LR WD[Working Dir] -- add --> ST[Staging] ST -- commit --> REPO[Local Repo] REPO -- push --> ORI[Origin] ORI -- fetch/pull --> REPO Setup # git --version git config --global user.name "Your Name" git config --global user.email "you@example.com" git config --global init.defaultBranch main Create or Clone # git init git clone <url> Status and Diffs # git status git diff # unstaged git diff --staged # staged vs HEAD Stage and Commit # git add <path> git add -p # interactive hunks git commit -m "feat: message" git commit --amend # edit last commit Branching # git branch git switch -c feature/x git switch main git branch -d feature/x Sync with Remote # git remote -v git fetch git pull # merge git pull --rebase # rebase git push -u origin my-branch Merge vs Rebase # git switch my-branch && git merge main git switch my-branch && git rebase main Resolve Conflicts # git status # edit files, remove markers git add <file> git commit # after merge git rebase --continue # during rebase Stash Work # git stash push -m "wip" git stash list git stash pop Undo Safely # git restore --staged <file> # unstage git restore <file> # discard local edits git revert <sha> # new commit to undo git reset --soft HEAD~1 # keep changes, drop last commit git reflog # find lost commits Tags and Releases # git tag -a v1.0.0 -m "msg" git push --tags Ignore and Clean # echo "node_modules/" >> .gitignore git clean -fdx # dangerous: removes untracked files Authentication (Quick) # # HTTPS + PAT git clone https://github.com/owner/repo.git # SSH ssh-keygen -t ed25519 -C "you@example.com" ssh-add ~/.ssh/id_ed25519 git clone git@github.com:owner/repo.git Conventional Commits (Optional) # feat(auth): add oauth login fix(api): handle null pointer in user service chore(ci): update node to 20 Common One-Liners # # See last commit summary git log -1 --stat # Interactive rebase last 5 commits git rebase -i HEAD~5 # Squash branch onto main git switch my-branch && git rebase -i main Quick PR Flow (GitHub) # git switch -c feat/x # edit, add, commit git push -u origin feat/x # open PR on GitHub See also: the full guide “The Definitive Guide to Version Control with Git and GitHub”.