ntask
A Python-native task runner with content-hash caching and DAG execution.
pip install ntask
Why
Your Makefile runs everything every time. Your Justfile has no dependency graph. Your tasks.py for Invoke is five years old, has no types, and you still have to write ctx.run. This is what a task runner looks like when you start over with caching, types, and a DAG.
Quickstart
A complete release pipeline in under 30 lines. Drop this into tasks.py at your project root:
from ntask import task, cached, shell
@task
@cached(inputs=["src/**/*.py"])
def lint():
shell("ruff check src/")
@task
@cached(inputs=["src/**/*.py"])
def typecheck():
shell("mypy src/")
@task
@cached(inputs=["src/**/*.py", "tests/**/*.py"])
def test(pattern: str = ""):
shell(f"pytest -q {'-k ' + pattern if pattern else ''}")
@task
@cached(inputs=["src/**/*.py", "pyproject.toml"], outputs=["dist/"])
def build():
shell("python -m build")
@task(deps=[lint, typecheck, test, build])
def release(version: str):
"""Run checks, build, tag, and publish to PyPI."""
shell(f"git tag v{version} && git push --tags && twine upload dist/*")
The first time you cut a release, every step runs. -j runs independent tasks in parallel (global flags go before the task name):
$ ntask -j release --version=1.0.0
running lint
running typecheck
running test
running build
+ lint (1.4s)
+ build (2.3s)
+ typecheck (3.2s)
+ test (4.9s)
running release
+ release (0.9s)
The second time, ntask hashes the inputs, sees nothing changed, and skips every cached task. It restores dist/ from its content-addressed store, so build never runs:
$ ntask -j release --version=1.0.1
o lint cached (3a8f9c2d)
o typecheck cached (b7c5f1e9)
o test cached (9f2e8b14)
o build cached (4dab8273) <- dist/ restored, build did not run
running release
+ release (0.9s)
Edit one file under tests/ and only test reruns. lint, typecheck and build don't read that file, so they stay cached. release isn't cached, so it always runs.
Cache keys also chain: a cached task's key includes the keys of the tasks it depends on, so a change upstream invalidates everything below it, even when the downstream task's own files haven't changed.
A few more commands:
ntask --list # list every task with its docstring
ntask test --pattern=auth # type hints become CLI flags automatically
ntask --why test # explain the last cache hit or miss, item by item
ntask --graph release # ASCII DAG (mermaid / dot also available)
ntask watch test # rerun on every src/ or tests/ change
Mermaid graph for docs
--graph-format mermaid prints a Mermaid block you can paste into a
README or design doc. This is ntask's own DAG, straight from
ntask --graph --graph-format mermaid:
graph TD
install
lint
typecheck
test
check
build
release
lint --> check
typecheck --> check
test --> check
check --> release
build --> release
--graph-format dot produces Graphviz output for the same data.
Team cache
Share cache hits across machines through S3, GCS, HTTP or a shared directory (NFS, a Docker volume):
# pyproject.toml
[tool.ntask.remote_cache]
type = "s3"
bucket = "my-team-cache"
pip install ntask[s3]
ntask check # the first run fills the cache; everyone else gets hits
ntask --offline check # skip the remote for fast dev loops
For S3-compatible stores (MinIO, R2, B2), set endpoint_url. GCS and HTTP backends work the same way. The HTTP backend is plain GET/PUT/HEAD over stdlib urllib, so any server that accepts PUT will do.
When NOT to use @cached
@cached suits deterministic work driven by files: lint, typecheck,
test, build. It's the wrong fit when the point of the task is its effect
on the world. Skip it (or be deliberate about inputs= and env=) when the task:
- has external side effects: DB writes, HTTP requests, sending email, queue pushes. A cache hit means none of that happened on this run.
- changes shared state that other tasks read: caches, file ownership, the
contents of
.env, feature flags. - depends on the clock or on randomness, so the same inputs can produce different output tomorrow.
- exists to check the world: smoke tests, integration probes, "is prod still healthy" checks. The whole point is to run again.
Use plain @task for these. Add parallel=False if the task must run alone
(releases, migrations), and @group(...) to namespace related tasks.
Live DAG display
Run ntask check in an interactive terminal and a Textual TUI shows the DAG as a live tree, with each task's state and duration. Pipe the output, set tui = false in [tool.ntask], or pass --no-tui to get plain line-by-line output instead.
More features
- Exclusive tasks.
@task(parallel=False)waits for every running task to finish, then runs alone. Use it for releases, migrations and anything else that changes shared state. - Monorepos. Put
@group("api")on a class and its task methods becomeapi.<name>. Depend across groups with@task(deps=[Other.task, ...])or a string name such as"web.test". - Capture output.
shell("git rev-parse HEAD", capture=True)returns aShellResultwith.stdout,.stderr,.returncode,.duration,.ok. - Force a rerun.
ntask --force test checkrunscheckbut skips the cache fortest.ntask --no-cacheignores the cache entirely.ntask cleandeletes the cache entries;ntask clean --alldeletes the whole.ntask/directory.
Examples
Six runnable, self-contained examples under examples/:
| Example | Demonstrates |
|---|---|
| 01-hello | Smallest possible cached task |
| 02-python-lib | install / lint / typecheck / test / build |
| 03-parallel | -j N fan-out and parallel=False barrier |
| 04-watch | ntask watch rerun-on-change loop |
| 05-remote-cache | local-fs remote backend shared between clones |
| 06-monorepo | @group(...) namespacing and cross-group deps |
cd into any of them and run ntask --list.
Features
| feature | ntask | make | just | invoke | doit | poe |
|---|---|---|---|---|---|---|
| Typed Python tasks | yes | no | no | partial | no | partial |
| Content-hash input caching | yes | partial | no | no | partial | no |
| Transitive cache-key propagation | yes | no | no | no | partial | no |
| Remote cache (S3/GCS/HTTP) | yes | no | no | no | no | no |
| DAG dependency resolution | yes | yes | no | partial | yes | no |
| Type-hint to CLI args | yes | no | partial | partial | no | yes |
Parallel DAG execution (-j) |
yes | yes | no | no | yes | no |
| Live DAG TUI | yes | no | no | no | no | no |
| Windows first-class | yes | partial | yes | yes | yes | yes |
When a task misses the cache, ntask names the file or env var that changed. ntask --why <task> prints the full breakdown.
Docs
- Technical handbook: every flag and config key
- Tutorial: replace your Makefile in 10 minutes
- Caching: the full contract
- Migrating from Make / just / Invoke / Poe
- Short API reference
Requirements: Python 3.11.4 or newer.
License
BSD-3-Clause. Copyright © 2026 Sean Nieuwoudt.
Metadata
Release files for ntask 1.1.7
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| ntask-1.1.7.tar.gz | 86.0 kB | Without provenance | Details for ntask-1.1.7.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| ntask-1.1.7-py3-none-any.whl | Python 3 | none | any | Details for ntask-1.1.7-py3-none-any.whl |
Total release size: 155.1 kB
Release files / ntask-1.1.7.tar.gz
| Download URL | ntask-1.1.7.tar.gz |
|---|---|
| Size | 86.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9da6ec046bf48a8b6733ee69b2b1a59f2b8ca326e468e6240e8979b8f5dbada3
|
|
BLAKE2b-256 checksum How to use checksums |
18a2a2297456ce812e301af2c89f29d8b4d4a9136ca3018a2e5ec84f228ad1ca
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Release files / ntask-1.1.7-py3-none-any.whl
| Download URL | ntask-1.1.7-py3-none-any.whl |
|---|---|
| Size | 69.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5d5acac059c9035e01ae27271edd42c01f207a7896c1dd9cdc210bb98b618ea9
|
|
BLAKE2b-256 checksum How to use checksums |
1fee7f7eb2d29f05d2e61bdf821d277559243edaf71aa23d146576bc55b62db2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Security for ntask 1.1.7
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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.