A Python-native task runner with content-hash caching and DAG execution.
Project description
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
Create a tasks.py:
from ntask import task, cached, depends, shell
@task
def install():
shell("pip install -e .")
@task
@cached(inputs=["src/**/*.py", "tests/**/*.py"])
def test(pattern: str = "", verbose: bool = False):
"""Run the test suite."""
flags = "-v" if verbose else ""
k = f"-k {pattern}" if pattern else ""
shell(f"pytest {flags} {k}")
@task
@cached(inputs=["src/**/*.py"])
def lint():
shell("ruff check src/")
@task
def check():
"""All quality checks."""
depends(lint, test)
Run:
ntask --list # show every registered task
ntask test --pattern=auth --verbose
ntask check # lint + test in order; both cache
ntask check -j # all CPU cores; bare -j picks the count
ntask watch test # rerun on every src/ or tests/ change
ntask --why test # explain why the last cache lookup missed
ntask --graph check # ASCII DAG (mermaid / dot also available)
The second time you run ntask check, both lint and test are content-hash cache hits and finish in milliseconds. Change one file in src/ and only the affected tasks rerun, transitively.
Team cache
Share cache hits across machines via S3 (or GCS, HTTP, NFS):
# pyproject.toml
[tool.ntask.remote_cache]
type = "s3"
bucket = "my-team-cache"
pip install ntask[s3]
ntask check # first person populates, the rest hit instantly
ntask check --offline # skip the remote for fast dev loops
S3-compatibles (MinIO, R2, B2) take an endpoint_url. GCS and HTTP backends work the same way; the HTTP backend is plain GET/PUT/HEAD over stdlib urllib, so any object store with PUT enabled is fair game.
Live DAG display
Run ntask check from an interactive terminal and a Textual TUI shows a live tree of the DAG with per-task state icons and durations. Pipe the output, set tui = false in [tool.ntask], or pass --no-tui to fall back to the line-based renderer.
Other things you'll reach for
- Exclusive tasks.
@task(parallel=False)makes a task a DAG-wide barrier: it waits for everything in flight to drain, then runs alone. Use it for releases, migrations, anything that mutates shared state. - Monorepos.
@group("api")over a class namespaces every task method asapi.<name>. Cross-group dependencies via@task(deps=[Other.task, ...])or string fqns. - Capture output.
shell("git rev-parse HEAD", capture=True)returns aShellResultwith.stdout,.stderr,.returncode,.duration,.ok. - Force a rerun.
ntask --force <task>bypasses the cache for that one task.ntask --no-cacheignores the cache entirely.ntask cleanwipes entry manifests;ntask clean --allwipes the whole.ntask/directory.
Examples
Six runnable, self-contained examples under examples/:
| # | 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 directory 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 |
Cache miss messages name the specific file or env change that caused the invalidation. ntask --why <task> prints the full breakdown.
Docs
- Technical handbook - the comprehensive reference, 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
- Roadmap
Requirements: Python 3.11 or newer. BSD-3-Clause.
License
BSD-3-Clause. Copyright © 2026 Sean Nieuwoudt.
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 ntask-1.0.0.tar.gz.
File metadata
- Download URL: ntask-1.0.0.tar.gz
- Upload date:
- Size: 57.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ca7ab37f332e994a5b81257207f7851f45c502c3725b3f3664284dcea491b306
|
|
| MD5 |
06e2f468bb909f03e07bd60956f3abe6
|
|
| BLAKE2b-256 |
bad8583450c470f7822745392f13670ded90e2159386d105dbf36c4aef103b45
|
File details
Details for the file ntask-1.0.0-py3-none-any.whl.
File metadata
- Download URL: ntask-1.0.0-py3-none-any.whl
- Upload date:
- Size: 49.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dcc6e08e11a2a9f4828c2e05f5ae00dcef6dd99ec814aa81dc7626eedcf219cf
|
|
| MD5 |
b983d5a65535e0380e589f96333b8280
|
|
| BLAKE2b-256 |
292951c99b56f2a89367bd2f06da3ea8102194db503f4e9b23472fe3ad255f95
|