Skip to main content

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 as api.<name>. Cross-group dependencies via @task(deps=[Other.task, ...]) or string fqns.
  • Capture output. shell("git rev-parse HEAD", capture=True) returns a ShellResult with .stdout, .stderr, .returncode, .duration, .ok.
  • Force a rerun. ntask --force <task> bypasses the cache for that one task. ntask --no-cache ignores the cache entirely. ntask clean wipes entry manifests; ntask clean --all wipes 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

Requirements: Python 3.11 or newer. BSD-3-Clause.

License

BSD-3-Clause. Copyright © 2026 Sean Nieuwoudt.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ntask-1.0.0.tar.gz (57.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ntask-1.0.0-py3-none-any.whl (49.4 kB view details)

Uploaded Python 3

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

Hashes for ntask-1.0.0.tar.gz
Algorithm Hash digest
SHA256 ca7ab37f332e994a5b81257207f7851f45c502c3725b3f3664284dcea491b306
MD5 06e2f468bb909f03e07bd60956f3abe6
BLAKE2b-256 bad8583450c470f7822745392f13670ded90e2159386d105dbf36c4aef103b45

See more details on using hashes here.

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

Hashes for ntask-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 dcc6e08e11a2a9f4828c2e05f5ae00dcef6dd99ec814aa81dc7626eedcf219cf
MD5 b983d5a65535e0380e589f96333b8280
BLAKE2b-256 292951c99b56f2a89367bd2f06da3ea8102194db503f4e9b23472fe3ad255f95

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page