The pycmdcheck package (GitHub, PyPI, documentation) answers a question Python packaging never gave a single tool for: is my package healthy? R users have had R CMD check for two decades - one command that walks a package and tells you, plainly, where it falls short. Python has the pieces, but they are scattered across a dozen places: build to confirm the package builds, twine check for metadata, mypy and ruff and pytest for the code, and a long oral tradition for everything else (does it ship a py.typed? a LICENSE? a changelog? does requires-python still allow a dead Python?). pycmdcheck gathers a representative set of those checks behind one command.
The motivation was concrete. I wanted to triage pyOpenSci software submissions - to run a quick, honest health check on a submitted repository before a human reviewer spends time on it - and there was no off-the-shelf way to do it. The catch is that you cannot install a stranger’s package just to inspect it: it may not build, it may pull in heavy dependencies, it may do something you would rather it not do on your CI runner. So pycmdcheck is designed, first, to read a package statically, and only optionally to run the heavier tooling.
One command
Install it from PyPI and point it at a directory:
pip install pycmdcheck
pycmdcheck /path/to/packageThe default run prints a table you can read at a glance, one row per check:
┏━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ St… ┃ Check ┃ Message ┃
┡━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ ✓ │ metadata │ Package metadata is valid │
│ ✓ │ structure │ Valid src layout structure │
│ ✓ │ license │ License file present │
│ ✓ │ docs │ Documentation present │
│ ✓ │ version │ Version 1.2.0 is consistent │
│ ℹ │ citation │ No citation file found │
│ ⚠ │ python_versions │ requires-python allows EOL Python │
│ … │ … │ … (every enabled check runs) │
└─────┴─────────────────┴──────────────────────────────────────┘
Each check reports one of five statuses - OK, NOTE, WARNING, ERROR, or SKIPPED - and the process exits non-zero when something at or above your chosen threshold turns up, so pycmdcheck drops straight into a CI step. You can run a single check (-c metadata), skip one (-s typing), or ask for machine output with --json and parse the report yourself.
The checks cover the surface a reviewer actually looks at: package metadata and PEP 621 fields, the project layout, the license file, README and docstrings, version consistency between pyproject.toml and the code, the py.typed marker, a changelog, a citation file, community health files, CI configuration, and whether requires-python still admits an end-of-life Python. A second tier - tests, linting, type checking, imports, the dependency audit, the build, and doctests - runs the real tooling when you want it.
Checking packages you cannot install
The part I am most pleased with is the triage profile. It selects only the checks that are purely static - no installing the package, no spawning external tools, no network - so it is safe to run against an arbitrary, un-trusted repository:
pycmdcheck /path/to/package --profile triage --json --exit-zeroThat single line is the heart of the pyOpenSci triage bot: clone a submission, run the static profile, and post the findings as a comment. --exit-zero is there for a subtle but important reason - in a triage context the exit code should tell you whether pycmdcheck ran, not what it found. A package with a real problem is the normal, expected outcome; it should not turn the CI job red. With --exit-zero, a non-zero exit means pycmdcheck itself could not run, and everything it discovered lives in the JSON report.
Reading packages statically sounds simple until you meet the actual diversity of how Python projects ship. The version is frequently not a literal in pyproject.toml - it is dynamic, computed at build time by setuptools_scm, hatch-vcs, or uv-dynamic-versioning. Plenty of projects still carry legacy Poetry metadata under [tool.poetry] with no [project] table at all. Source lives in src/, or flat at the root, or as a single foo.py module, or as a PEP 420 namespace package with no __init__.py. A license might be in LICENSE, LICENCE, COPYING, or a dual-licensed LICENSE-MIT / LICENSE-APACHE pair. A first cut of pycmdcheck flagged a perfectly good package as “no version specified” simply because it used dynamic versioning - exactly the kind of false positive that erodes trust in a tool like this. Getting the static checks to recognize the real ecosystem, rather than an idealized one, has been most of the work, and most of the value.
A library and a plugin system
Everything the CLI does is available from Python:
from pycmdcheck import check
report = check(".", checks=["metadata", "structure", "license"])
if not report.passed:
for result in report.results:
print(f"{result.status}: {result.name} - {result.message}")And the check set is not fixed. pycmdcheck discovers checks through the pycmdcheck.checks entry-point group, so a third-party package - or your own project - can register additional checks and have them picked up automatically alongside the built-ins. The built-in roster is just the first set of plugins.
For a tour of every check and its options, see the Getting Started, Configuration, and CLI Reference pages in the documentation.
Installation
pycmdcheck is available now on PyPI:
pip install pycmdcheck
# optional extras for the tool-backed checks
pip install pycmdcheck[all]For details, see the pycmdcheck release notes below.
pycmdcheck release highlights for version 0.1.1
Features
- A single
pycmdcheckcommand (and acheck()Python API) that runs a battery of package checks and reports OK / NOTE / WARNING / ERROR / SKIPPED with Rich tables or--json. - Built-in checks spanning metadata, structure, license, documentation, version consistency,
py.typed, dependencies, build, formatting, linting, typing, imports, tests, doctests, CI, changelog, citation, community files, project URLs, and end-of-life Python versions. - Profiles (
minimal,triage,default,pyopensci,strict) that select a curated set of checks. Thetriageprofile is static only - no install, no external tools, no network - for safely checking arbitrary repositories. --exit-zero, so CI/automation that consumes--jsoncan treat a non-zero exit as “pycmdcheck failed to run” rather than “the package has findings.”- Robust handling of real-world packaging: PEP 621
dynamicfields, legacy[tool.poetry]projects, src / flat / single-module / PEP 420 namespace layouts, and dual-license filenames. - A plugin system: checks are discovered via the
pycmdcheck.checksentry-point group, so third-party packages can register their own.