The Moment I Knew I Had a Problem

A few months ago, I inherited a Django project at work. Nothing unusual there — it’s part of the job when you manage an ICT division. But when I opened the pyproject.toml to check the dependencies, I nearly choked on my coffee.

Forty-seven dependencies. And I was pretty sure at least a third of them had nothing to do with the actual codebase.

That’s the thing about Python projects. Dependencies pile up over time. Someone tries a library for a feature that never ships. Another developer adds a package “just in case” and forgets to remove it when the approach changes. Before you know it, your pyproject.toml looks like a packed JRPG inventory — half the items are potions you’ll never use, but removing any of them feels like tempting fate.

I needed a way to audit my dependencies. Not manually grep through every import statement — there had to be a tool. That’s when I found pyproject-udeps.

A computer monitor displaying Python source code on a dark editor background
Image: Fæ via Wikimedia Commons (CC0)

What Is pyproject-udeps?

pyproject-udeps is a CLI tool built by Luke Hsiao that scans your Python project for unused dependencies declared in pyproject.toml. It’s written in Rust — which means it’s fast — and it was inspired by the equally brilliant cargo-udeps from the Rust ecosystem.

Here’s what it supports out of the box:

  • Poetry projects — reading from [tool.poetry.dependencies] and group sections
  • uv projects — works with $UV_PROJECT_ENVIRONMENT
  • Plain PEP 621 projects — those using the standard [project.dependencies] table
  • PEP 735 dependency groups — the newer standard for organizing dev dependencies
  • Mixed projects — it reads from every place dependencies can be declared, so hybrid setups work fine

Originally released as poetry-udeps, it was renamed to pyproject-udeps to reflect that it works with any modern Python project, not just Poetry-based ones.

How It Works Under the Hood

This is the part I really appreciate. Instead of relying on fragile regex patterns or running your code just to see what breaks, pyproject-udeps uses Ruff’s error-resilient parser to build an AST (Abstract Syntax Tree) of every Python file in your project.

It then collects every import it finds — plain imports, from imports at any nesting level, importlib.import_module() calls with literal arguments, and even __import__() calls. It ignores strings, docstrings, and comments, so you don’t get false matches on someone writing “import os” inside a documentation string.

Those collected imports are matched against your declared dependencies using a built-in name map and a set of naming heuristics. Anything declared but never imported gets flagged.

The clever bit: Python package names don’t always match their import names. For example, you install python-dotenv but import dotenv. The tool’s name map handles these common mismatches. And if your specific package isn’t in the map, you can add it to the src/name_map.rs file and contribute upstream (or just use the ignore file — more on that later).

Getting Started

You’ll need Rust installed on your machine. If you don’t have it yet, install it from rustup.rs — it’s one command and takes a few minutes.

Installation

The most straightforward way:

cargo install pyproject-udeps --locked

Alternatively, if you have cargo-binstall installed, you can download a prebuilt binary directly — faster than compiling from source:

cargo binstall pyproject-udeps

That’s it. The pyproject-udeps command is now globally available.

Running Your First Scan

Navigate to your project root (where pyproject.toml lives) and just run:

pyproject-udeps

On my project, the output was both humbling and satisfying:

httpx
python-decouple
whitenoise
django-debug-toolbar

Four dependencies I was carrying around for absolutely no reason. httpx had been added for an API client that was later replaced with requests. django-debug-toolbar was a dev dependency from an earlier sprint that never got removed. whitenoise and python-decouple were left over from a previous deployment strategy.

I removed all four. The project still worked. My pip freeze output got a little shorter. My deployment images got a tiny bit smaller. It wasn’t a life-changing victory, but it was satisfying — like finally cleaning out that drawer full of random cables.

Dealing With False Positives

The tool’s README is upfront about this: Python dependencies don’t always map 1:1 with import names. You will get false positives, and that’s okay. The goal is to surface candidates for manual review, not to automatically delete packages from your pyproject.toml.

Common cases where a dependency exists but isn’t directly imported in your code:

  • Transitive dependencies — packages like asyncpg that SQLAlchemy uses internally
  • Plugins — Django apps that auto-register themselves when installed
  • CLI tools — packages you use only from the command line, not imported in code
  • Type stubstypes-requests and similar packages used only by type checkers

For these, you have two options:

Option 1: The --virtualenv Flag

If a dependency is used by something in your virtualenv (like SQLAlchemy needing asyncpg), pass the --virtualenv flag:

pyproject-udeps --virtualenv

This searches through the site-packages in your virtualenv as well, which catches many of these cases.

Option 2: The Ignore File

For persistent false positives, create a .pyprojectudepsignore file in your project root:

# Packages we know are needed but not directly imported
asyncpg
django-cors-headers
whitenoise

Empty lines and lines starting with # are ignored. The tool supports the legacy filename .poetryudepsignore as a fallback if you’re migrating from the original poetry-udeps.

Checking Dev Dependencies

This is where things get interesting. Dev dependencies are often the most neglected part of a project’s dependency tree. How many times have you seen black, pylint, pytest, and a handful of other tools sitting in [tool.poetry.group.dev.dependencies] without anyone checking if they’re still actively used?

Run pyproject-udeps with the --dev flag:

pyproject-udeps --dev

I tried this on my project and found coverage still listed even though we’d switched to pytest-cov months ago. An easy cleanup.

CI/CD Integration: Automate the Audit

The real power of this tool shines when you add it to your CI/CD pipeline. Pre-built binaries install quickly with taiki-e/install-action:

jobs:
  unused-deps:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v5
        with:
          persist-credentials: false
      - uses: taiki-e/install-action@v2
        with:
          tool: pyproject-udeps
      - name: Check for unused dependencies
        run: pyproject-udeps

The step fails with exit code 1 when unused dependencies are found, printing them one per line. This is exactly what you want in a CI pipeline — it forces developers to either remove the dependency or add it to the ignore file with a documented reason.

I’ve started adding this to every new Python project at work — it’s a small but meaningful part of keeping the whole software supply chain clean, something I explored in depth in how to lock down your supply chain when AI writes your code. It only adds about a second to the CI run — the Rust binary is that fast — and it catches bloat before it becomes a permanent resident of your pyproject.toml.

How It Compares to Other Options

pyproject-udeps isn’t the only game in town. Here’s how it stacks up against the alternatives:

Tool Language What It Checks Speed (170k lines)
pyproject-udeps Rust Unused deps only ~110 ms
deptry Python/Rust Unused, missing, transitive ~165 ms
fawltydeps Python Unused + missing ~3.5 seconds
pip-extra-reqs Python Unused deps Failed on large project

The benchmark numbers are from the project’s own testing on a 170,000-line codebase, measuring only the “unused dependencies” feature. pyproject-udeps and deptry (which has a Rust core in recent versions) are both fast enough that you won’t notice them. For smaller projects — which is most of what I work on — both finish in under a second.

The main differentiator is focus. As I wrote in my piece on how better models don’t always mean better tools, picking the right tool for the job matters more than raw power. pyproject-udeps does one thing: find unused dependencies. It doesn’t check for missing dependencies, transitive dependency issues, or version mismatches. If you want all of those in one tool, deptry is the more comprehensive option.

Personally, I use both. pyproject-udeps for its speed and simplicity, deptry for a deeper audit when I’m doing a proper dependency review.

Should You Use It?

If you maintain any Python project with more than a handful of dependencies, yes. It takes about 30 seconds to install, a second to run, and has zero configuration needed for the basic case. The cost-to-benefit ratio is absurdly good.

I’ve made it a habit to run pyproject-udeps once a month on my active projects — the same way I periodically audit the dependencies of self-hosted tools like my private AI meeting assistant. It’s part of my “spring cleaning” routine — a few minutes to check if the dependency tree is still lean. And every single time, I find at least one package that shouldn’t be there.

Three things I’d recommend you do right now:

  1. Install it: cargo install pyproject-udeps --locked
  2. Run it in your main project: pyproject-udeps
  3. Review the output and clean up anything that shouldn’t be there

That’s it. A cleaner pyproject.toml, smaller deployments, and one less source of technical debt to worry about.

Filed under Tech & Gadgets
Last Update: July 11, 2026 by Felix AlterEgo
0 0 votes
Article Rating
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Newest
Oldest Most Voted