Skip to main content

testudos

CI PyPI version Python versions License: MIT

A testing harness for Python packages using uv's isolated environments.

Overview

Testudos runs your test suite across every Python version that is both still supported upstream and compatible with your package. It works out which versions those are by intersecting two sources:

  1. Currently supported Python versions — from endoflife.date
  2. Your package's compatibility — the requires-python field in your pyproject.toml

Cycles that endoflife.date lists but that have no stable release yet (a Python still in alpha, beta or rc) are left out unless you ask for them with --include-prerelease.

Quick start

# Install
uv tool install testudos

# See which versions would be tested, and why
testudos versions

# Run the suite on all of them
testudos run

# Run them concurrently
testudos run --parallel

# Show the exact commands without running anything
testudos run --dry-run

Installation

uv tool install testudos    # recommended
pip install testudos

To use testudos coverage combine and testudos coverage report, which shell out to coverage.py on your machine, install the extra:

uv tool install 'testudos[coverage]'
pip install 'testudos[coverage]'

Collecting coverage during testudos run does not need this — each isolated environment gets its own copy of coverage.

Requirements

  • Python 3.10 or newer
  • uv

How it works

For each Python version, testudos runs your tests in an ephemeral environment created by uv run --isolated:

uv run --isolated --python=3.12 --with pytest --directory /path/to/project pytest
  • --isolated builds a throwaway environment, so nothing leaks between versions
  • --python=X.Y selects the interpreter, downloading it if needed
  • --with pytest puts the test runner inside that environment, where it can see your project's dependencies

--dry-run prints exactly these commands, built by the same code that runs them, so what you see is what would execute.

Test dependencies

uv run --isolated installs your project and its runtime dependencies. If your suite needs anything more — pytest plugins, fixture packages — name it, either per-invocation or in config:

testudos run --with pytest-asyncio --with pytest-mock
[tool.testudos]
test-dependencies = ["pytest-asyncio", "pytest-cov>=7"]

Usage

Choosing versions

testudos run                                # auto-detect
testudos run --python 3.11 --python 3.12    # explicit (-p)
testudos run --python 3.14.0rc1             # pre-releases are allowed explicitly
testudos run --include-prerelease           # include unreleased cycles when auto-detecting
testudos run --offline                      # skip the API, use cached or bundled data
testudos run ./path/to/project              # a project other than the cwd

testudos versions shows the same resolution as a table, marking each cycle stable or pre-release and naming any pre-release it left out.

Offline mode is also enabled by setting TESTUDOS_OFFLINE.

Execution

testudos run --parallel                     # concurrent across versions (-P)
testudos run --parallel --jobs 4            # cap concurrency (-j)
testudos run --no-fail-fast                 # keep going after a failure (-F)
testudos run --timeout 300                  # seconds per version (-t)
testudos run --no-timeout                   # override a configured timeout
testudos run --verbose                      # more detail (-v)
testudos run --quiet                        # suppress failed-test output (-q)
testudos run --dry-run                      # print commands, run nothing (-n)

Fail-fast is on by default in sequential mode. Versions it skips are reported as SKIPPED rather than dropped, so the summary always accounts for every version you asked for. It does not apply in parallel mode, where every version is already running by the time the first failure is known.

Multiple packages

testudos run --package ./pkg1 --package ./pkg2   # explicit packages (-k)
testudos run ./monorepo --discover               # every dir with a pyproject.toml (-d)
testudos run ./pkg1 --package ./pkg2 --package-jobs 2   # cap package concurrency (-J)
testudos run --package ./pkg2 --no-parallel-packages    # one package at a time

Package-level and version-level concurrency are independent: --parallel / --jobs control versions within a package, --parallel-packages / --package-jobs control packages. Each package keeps its own configuration, results and coverage data.

Coverage

testudos run --coverage                                  # collect (-c)
testudos run --coverage --coverage-report html
testudos run --coverage --coverage-report html --coverage-report xml
testudos run --coverage --coverage-fail-under 80
testudos run --coverage --coverage-source mypackage      # default: src

Formats: term, term-missing, html, xml, json, lcov. Data lands in .testudos/coverage/. The --coverage-fail-under threshold is checked against exact totals, so it gives the same verdict whichever report formats you asked for.

Coverage data can also be managed directly:

testudos coverage combine     # merge per-version data
testudos coverage report      # generate a report (--format, --fail-under)
testudos coverage clean       # remove .testudos/coverage

Machine-readable output

testudos run --format json
testudos run --format json --output results.json    # (-o)
testudos run --format json --dry-run

JSON output covers single-package runs, and reports per-version results plus a summary of passed, failed and skipped counts. It is not available with --coverage or in multi-package mode.

Private indexes

testudos run --default-index https://pypi.internal.example.com/simple
testudos run --index https://extra.example.com/simple
testudos run --find-links /path/to/wheels
testudos run --no-index --find-links /path/to/wheels
testudos run --index-strategy first-index

Colour

testudos --color run      # force colour
testudos --no-color run   # disable it

These are options on testudos itself, so they go before the subcommand. NO_COLOR and FORCE_COLOR are respected too.

Configuration

Everything above can be set in [tool.testudos]:

[project]
requires-python = ">=3.11"

[tool.testudos]
# Version selection
python-versions = ["3.11", "3.12", "3.13"]   # override auto-detection
include-prerelease = false                    # include unreleased cycles
offline = false                               # never call endoflife.date

# What to run
test-command = "pytest"
test-args = ["-v", "--tb=short"]
test-dependencies = ["pytest-asyncio"]        # extra packages per isolated env
timeout = 300                                 # seconds per version

# How to run it
parallel = false
max-jobs = 4

# Coverage
coverage = false
coverage-combine = true
coverage-report = ["term"]
coverage-fail-under = 80
coverage-source = ["src"]

# Package indexes
default-index = "https://pypi.internal.example.com/simple"
index = ["https://extra.example.com/simple"]
find-links = ["/path/to/wheels"]
no-index = false
index-strategy = "first-index"

CLI options win over configuration. Values are validated on load: a wrong type or an out-of-range value is an error, and an unrecognised key is a warning.

test-command is a single executable name — pytest, nose2, ./run-tests. To run a module, use test-command = "python" with test-args = ["-m", "unittest", "discover"].

Documentation

  • Architecture — how the pieces fit together, and the decisions behind them

Development

git clone https://github.com/martinristovski/testudos.git
cd testudos
uv sync --extra dev

just ci            # lint, type-check, test, dogfood, version check
just test          # pytest with coverage
just fix           # auto-fix lint and formatting
just dogfood       # run testudos on itself

The test suite is hermetic: the endoflife.date API is stubbed and the version cache is redirected per test, so no test touches the network or your real ~/.cache/testudos.

License

MIT

Metadata

Release files for testudos 0.21.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for testudos 0.21.0
File Size Uploaded
testudos-0.21.0.tar.gz 125.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for testudos 0.21.0
File Interpreter ABI Platform
testudos-0.21.0-py3-none-any.whl Python 3 none any Details

Total release size: 175.9 kB

Release files / testudos-0.21.0.tar.gz

Download URL testudos-0.21.0.tar.gz
Size 125.9 kB
Tags Source
SHA-256 checksum
How to use checksums
0634c372710ed6ff76cc4e18c60d4b4a980184a03d8434dfff5e4981d4bba5b5
BLAKE2b-256 checksum
How to use checksums
c6f96dab97a137a1b5201d1739ac57262ead6f947f4cb0efa8cbdb3eec12de68
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 22, 2026.

Transparency log

Release files / testudos-0.21.0-py3-none-any.whl

Download URL testudos-0.21.0-py3-none-any.whl
Size 50.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
dc6a6b725627900e73e5a5958717b3913e1a85bf562fc03c8eec9652aebcc0db
BLAKE2b-256 checksum
How to use checksums
edf8767c476e5fafa5c961cefdf2a1ec3ebd1c08f32e789196107464b9ac061c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 22, 2026.

Transparency log

Release history Release notifications | RSS feed

0.33.9

2 release files

0.33.8

2 release files

0.33.7

2 release files

0.33.6

2 release files

0.33.5

2 release files

0.33.4

2 release files

0.33.3

2 release files

0.33.2

2 release files

0.33.1

2 release files

0.33.0

2 release files

0.32.3

2 release files

0.32.2

2 release files

0.32.1

2 release files

0.32.0

2 release files

0.31.1

2 release files

0.31.0

2 release files

0.30.1

2 release files

0.30.0

2 release files

0.29.0

2 release files

0.28.2

2 release files

0.28.1

2 release files

0.28.0

2 release files

0.27.9

2 release files

0.27.8

2 release files

0.27.7

2 release files

0.27.6

2 release files

0.27.5

2 release files

0.27.4

2 release files

0.27.3

2 release files

0.27.2

2 release files

0.27.1

2 release files

0.27.0

2 release files

0.26.9

2 release files

0.26.8

2 release files

0.26.7

2 release files

0.26.6

2 release files

0.26.5

2 release files

0.26.4

2 release files

0.26.3

2 release files

0.26.2

2 release files

0.26.1

2 release files

0.26.0

2 release files

0.25.4

2 release files

0.25.2

2 release files

0.25.0

2 release files

This release

0.21.0 This release

2 release files

0.20.0

2 release files

0.14.0

2 release files

0.13.0

2 release files

0.12.0

2 release files

0.11.1

2 release files

0.11.0

2 release files

0.10.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page