A local-first health checker for Python imports, dependencies, startup time, and package bloat.
Project description
Project Doctor
A local-first health checker for Python imports, dependencies, startup time, and package bloat.
Project Doctor is a zero-dependency analyzer for finding project optimization work that usually hides in plain sight:
- entrypoint startup drag
- slow third-party imports
- likely unused dependencies
- possible undeclared imports
- opt-in large installed package checks
- top-level imports that look safe to move into deferred code paths
- CI thresholds for dependency and import hygiene
- uv.lock sync status and package explanations
Project Doctor never edits your code. It produces reviewable reports and CI-friendly checks so teams can make deliberate changes.
Security and privacy
Project Doctor is local-first and safe by default. Static scans parse source files with Python's ast module and do not import your project code. Optional import timing must be enabled with --import-time; it imports third-party modules in child processes, so leave it off when reviewing sensitive projects or code with import-time side effects. Installed package size checks are also opt-in with --package-sizes because they walk installed distribution metadata.
Install
Current reliable install path:
From this folder:
python3 -m pip install -e .
Then run:
project-doctor analyze /path/to/your/project
Or without installing:
PYTHONPATH=src python3 -m project_doctor analyze /path/to/your/project
After the PyPI release is live:
pip install project-doctor
uv tool install project-doctor
Quick examples
project-doctor doctor
project-doctor analyze examples/sample_project
project-doctor analyze examples/sample_project --json -o project-doctor-report.json
project-doctor analyze examples/sample_project --jobs auto --package-sizes
project-doctor analyze examples/sample_project --entrypoint "python app.py"
project-doctor analyze examples/sample_project --uv
project-doctor check examples/sample_project --max-unused 0
project-doctor check examples/sample_project --import-time --json --max-import-ms 150
project-doctor sync-check examples/sample_project/uv.lock
project-doctor explain-package pandas examples/sample_project --uv
analyze writes a shareable "wow" report by default. Use --report detailed for the longer audit report. check prints a compact status report and exits nonzero when a configured threshold is exceeded.
Example report
Project Doctor Report
Startup time: 1.42s
Potential avoidable import cost: 630ms
Top startup contributors:
- pandas: 312ms
- boto3: 205ms
- matplotlib: 113ms
Likely unused dependencies:
- openpyxl
- beautifulsoup4
- python-dotenv
Possible undeclared imports:
- requests imported but not declared
Largest installed packages:
- torch: 742MB
- pandas: 78MB
- botocore: 71MB
Suggested quick wins:
1. Move `pandas` import at `reports.py:3` inside the deferred function that uses it.
2. Remove `openpyxl` if it is no longer used by active code paths.
3. Add `requests` to `pyproject.toml`.
Python API
from project_doctor import AnalysisContext, analyze_project
context = AnalysisContext.from_environment()
report = analyze_project(
"examples/sample_project",
context=context,
jobs="auto",
run_import_timing=False,
collect_package_sizes=False,
entrypoint="python app.py",
)
print(report.unused_dependencies)
The returned AnalysisReport and nested report objects are dataclasses and can be converted to dictionaries with report.to_dict(). Reuse AnalysisContext when analyzing multiple projects in one process; it keeps installed package metadata and package size estimates cached.
What Project Doctor checks
Static import scan
Project Doctor parses Python source with ast, so it can read imports without importing your project code. Syntax errors are reported as warnings instead of aborting the full scan.
Dependency usage
Project Doctor reads dependencies from:
pyproject.toml[project.dependencies]pyproject.toml[project.optional-dependencies]pyproject.toml[dependency-groups], including{ include-group = "..." }- common Poetry dependency sections
requirements*.txt, including nested-rand--requirementincludes
It compares declared dependencies against static imports. Results marked unused should be treated as a review queue, not an automatic delete list.
Import timings
When --import-time is enabled, Project Doctor runs a subprocess like this for each likely third-party top-level import:
python -X importtime -c "import pandas"
This keeps imports out of the analyzer process, but the imported library can still run import-time side effects in the child process. Leave import timing disabled when you want a purely static scan.
Entrypoint startup timing
Entrypoint mode measures the command users actually wait on:
project-doctor analyze . --entrypoint "python app.py"
project-doctor analyze . --entrypoint "uvicorn app:app"
project-doctor analyze . --entrypoint "python -m my_cli"
Project Doctor runs the command with Python import profiling enabled and shell=False, then folds the parsed startup import costs into the report. Server-style commands may time out by design; Project Doctor still reports import data captured before the timeout.
Lazy-import candidates
Project Doctor looks for imports that are defined at module load but only used inside deferred function or method bodies.
For example:
import pandas as pd
def make_report(rows):
return pd.DataFrame(rows)
Project Doctor may suggest:
def make_report(rows):
import pandas as pd
return pd.DataFrame(rows)
That kind of change can reduce startup time for CLIs, Lambdas, Flask/FastAPI apps, and agent scripts when a heavy dependency is only needed on a less-common path.
CLI
project-doctor doctor [path] [options]
Runs the default project health report. It is an alias for `project-doctor analyze .` with the same analysis options.
project-doctor analyze [path] [options]
Options:
--json Emit JSON instead of Markdown
--report wow|detailed Human report style, default wow
--output FILE, -o FILE Write report to a file
--import-time Run subprocess import timing checks
--no-import-time Skip subprocess import timing checks, default
--import-time-limit N Max third-party modules to time, default 20
--import-time-timeout SECONDS Timeout per import, default 10
--package-sizes Collect installed package sizes
--no-package-sizes Skip installed package size checks, default
--jobs N|auto Static scan workers, default auto
--entrypoint COMMAND Measure startup for a real entrypoint command
--entrypoint-timeout SECONDS Timeout for entrypoint measurement, default 10
--uv Include uv.lock status
--max-files N Max Python files to scan, default 5000
--exclude DIR Extra directory name to exclude; repeatable
project-doctor check [path] [options]
Options:
--json Emit machine-readable check results
--max-unused N Max likely unused dependencies, default 0
--max-undeclared N Max possible undeclared imports, default 0
--max-lazy-imports N Max lazy-import candidates
--max-import-ms N Max cumulative import time for any measured module
--max-package-mb N Max installed package size; enables package size checks
--import-time Run subprocess import timing checks
--no-import-time Skip subprocess import timing checks, default
--import-time-limit N Max third-party modules to time, default 20
--import-time-timeout SECONDS Timeout per import, default 10
--package-sizes Collect installed package sizes
--no-package-sizes Skip installed package size checks, default
--jobs N|auto Static scan workers, default auto
--entrypoint COMMAND Measure startup for a real entrypoint command
--entrypoint-timeout SECONDS Timeout for entrypoint measurement, default 10
--uv Include uv.lock status
--max-files N Max Python files to scan, default 5000
--exclude DIR Extra directory name to exclude; repeatable
project-doctor sync-check [uv.lock] [options]
Options:
--json Emit machine-readable sync results
project-doctor explain-package PACKAGE [path] [options]
Options:
--uv Include uv.lock status
--json Emit machine-readable package explanation
CI
Add Project Doctor to GitHub Actions as a dependency hygiene gate:
- name: Check Python dependency health
run: project-doctor check . --max-unused 0 --max-undeclared 0 --max-package-mb 100
Intended full workflow after first package release and name/package ownership confirmation:
name: project-doctor
on: [push, pull_request]
jobs:
project-doctor:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install project-doctor
- run: project-doctor check . --max-unused 0 --max-undeclared 0
For projects that use uv:
- name: Check uv lock sync
run: project-doctor sync-check uv.lock
Badge for project docs:

uv
Project Doctor understands the common pyproject.toml + uv.lock workflow:
project-doctor analyze --uv
project-doctor sync-check uv.lock
project-doctor explain-package pandas --uv
sync-check verifies that direct dependencies declared in pyproject.toml are represented in uv.lock. explain-package shows whether a package is declared, which import names map to it, whether it is installed locally, and whether uv has locked it.
Performance
Project Doctor keeps the default path fast:
- installed package metadata is indexed once per analysis context
- package size checks are skipped unless requested or needed by
--max-package-mb - static scans stream into aggregate results instead of retaining every file scan object
--jobs autouses serial scanning for small projects and bounded parallel scanning for larger projects
Run the benchmark helper against a synthetic project:
PYTHONPATH=src python3 scripts/benchmark.py --files 1000 --runs 5 --jobs auto
Or benchmark a real project:
PYTHONPATH=src python3 scripts/benchmark.py /path/to/project --runs 5 --jobs auto
Development
python3 -m venv .venv
.venv/bin/python -m pip install -e ".[dev,security]"
.venv/bin/python -m pytest
.venv/bin/python -m ruff check .
.venv/bin/python -m mypy src/project_doctor tests
.venv/bin/python -m bandit -r src examples scripts -q
.venv/bin/python -m pip_audit
.venv/bin/python scripts/benchmark.py --files 200 --runs 2
.venv/bin/python -m build
Current limitations
- Static analysis misses dynamic imports and plugin systems.
- Dependency names do not always match import names.
- Optional dependencies may be marked unused if their optional code path is not statically imported.
- Opt-in import timing imports third-party packages in a subprocess, which can still trigger child-process side effects.
- Entrypoint mode runs your command in a subprocess; use it for commands that are safe to execute locally.
- Package size checks are opt-in and only work for dependencies installed in the current environment.
Roadmap
The next serious versions should add:
- Deeper entrypoint startup benchmarks with clearer default-path waste attribution.
project-doctor fix --lazy-importswith AST-safe rewrites and backups.- Lockfile awareness for Poetry, PDM, and pip-tools.
- Docker/image-size analysis.
- Richer package-name/import-name mapping.
- Profiler integration for hot-loop acceleration suggestions.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file project_doctor-0.6.0.tar.gz.
File metadata
- Download URL: project_doctor-0.6.0.tar.gz
- Upload date:
- Size: 37.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
22f14f81a336e028e8136394d536911dbe61c5646be353404b7d1aa3028e65cc
|
|
| MD5 |
87b1736022856dff1cabc460c01d0cf3
|
|
| BLAKE2b-256 |
4854fdb2aa48cd011ac8b55a9239c692ddd813074c77248e5810ac952da88726
|
Provenance
The following attestation bundles were made for project_doctor-0.6.0.tar.gz:
Publisher:
publish.yml on billjamesevans/project-doctor
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
project_doctor-0.6.0.tar.gz -
Subject digest:
22f14f81a336e028e8136394d536911dbe61c5646be353404b7d1aa3028e65cc - Sigstore transparency entry: 1630518644
- Sigstore integration time:
-
Permalink:
billjamesevans/project-doctor@c3ae5ac131bb2682afe8186e6302b88bd9845c02 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/billjamesevans
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c3ae5ac131bb2682afe8186e6302b88bd9845c02 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file project_doctor-0.6.0-py3-none-any.whl.
File metadata
- Download URL: project_doctor-0.6.0-py3-none-any.whl
- Upload date:
- Size: 31.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
841ae348509651da95050b6f9838609bc1475058977456005d2a12408495397d
|
|
| MD5 |
3b1957842b704ebc305afa5acc132427
|
|
| BLAKE2b-256 |
c551638a390602ae852bc2fa794eaa2ba0bd6a08693c086f53f65d2e09589b97
|
Provenance
The following attestation bundles were made for project_doctor-0.6.0-py3-none-any.whl:
Publisher:
publish.yml on billjamesevans/project-doctor
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
project_doctor-0.6.0-py3-none-any.whl -
Subject digest:
841ae348509651da95050b6f9838609bc1475058977456005d2a12408495397d - Sigstore transparency entry: 1630518743
- Sigstore integration time:
-
Permalink:
billjamesevans/project-doctor@c3ae5ac131bb2682afe8186e6302b88bd9845c02 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/billjamesevans
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c3ae5ac131bb2682afe8186e6302b88bd9845c02 -
Trigger Event:
workflow_dispatch
-
Statement type: