invoke review
Invoke 3.0.3 turns decorated Python functions into project commands and gives those functions a Context for running local subprocesses. A tasks.py module can define flags, aliases, prerequisites, namespaces, help text, configuration, prompt responders, and PTY behavior. It suits shell-heavy automation that has outgrown a short Make target but still belongs beside the source. Release 3.0.3 specifically removed a @task return annotation introduced one day earlier because that typing change broke uses where decorated tasks were passed to collection methods. Our install added only 1 MB and no dependencies.
Invoke 3.0.3 installed in 0.3 seconds as 1 MB with 0 dependencies in our sandbox, so package cost is negligible. Install it when project commands need real Python control flow; skip it for incremental builds, environment matrices, or a couple of fixed subprocess calls.
We installed it
| Install | ✓ · 0.3s | 1 package on disk · 1 MB |
| Import | ✓ | import invoke in 0.55s · pure Python · py.typed · requires Python >=3.9 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does invoke install cleanly?
Yes. In a fresh container with an empty cache, pip install invoke finished in 0.3s, leaving 1 package and 1 MB on disk. pip-audit reported no known vulnerabilities.
What does invoke need to run?
Python >=3.9, and nothing compiled: it is pure Python. In our run import invoke succeeded in 0.55s, and the package ships py.typed for type checkers.
invoke or doit: which should you use?
doit: Use it when Python 3 tasks need dependencies plus target files and up-to-date checks. Invoke 3.0.3 installed in 0.3 seconds as 1 MB with 0 dependencies in our sandbox, so package cost is negligible.
When should you not use invoke?
Build work depends on file freshness; Invoke orders tasks but does not compare input and output timestamps like make or doit
Use it if
- Python 3.9+ is already required and build or release commands need branching that is awkward in a Makefile
- Contributors need inv --list plus generated per-task flags and help without maintaining a separate command parser
- Subprocess calls need captured output, live streaming, environment overrides, prompt responses, or explicit failure handling
- Local tasks may later share Invoke's command model with Fabric for remote execution
- Build work depends on file freshness; Invoke orders tasks but does not compare input and output timestamps like make or doit
- You need isolated Python-version matrices; tox and nox create environments and sessions while Invoke does not
- All commands must live in pyproject.toml; taskipy or poethepoet avoids the extra tasks.py discovery convention
- Windows cmd.exe is a first-class target; PTY mode is unavailable and cd or prefix contexts assemble shell command strings
- Fast upstream triage is a requirement; GitHub currently counts 460 open issues and pull requests in a largely maintainer-led project
Setup reality
We installed Invoke 3.0.3 in 0.3 seconds under Python 3.12. It left 1 package and 1 MB on disk, with 0 direct dependencies and 0 known vulnerabilities from pip-audit. import invoke completed in 0.55 seconds. The distribution is pure Python, requires Python 3.9+, and includes py.typed. PyPI leaves its license field empty, even though the GitHub repository identifies BSD-2-Clause. The How we test run used a clean unprivileged container.
Both invoke and inv commands are installed. Invoke searches upward from the current directory for tasks.py or a tasks package, so starting in a nested checkout can select a different collection than expected. Each @task function reserves argument 1 for Context. Underscores become dashes in CLI options, and inferred one-letter flags may vanish when two names share an initial. Add explicit help and aliases to commands used by a team.
Context.cd() changes the shell command prefix, not Python's current directory. A pathlib read inside that block still resolves from the original process location. Context.run() raises UnexpectedExit for a nonzero exit unless warn=True. PTY mode combines stderr with stdout and fails on Windows. Prompt watchers can send responses repeatedly, so narrow the regular expression and keep credentials out of tasks.py and checked-in invoke.yaml files.
Invoke 3 merges defaults, machine and user files, project config, environment variables, collection config, and call overrides. INVOKE_ values only map to known keys and still undergo type conversion. Pre and post tasks establish order within 1 invocation; they do not cache outputs for tomorrow's run. If a project has only 2 or 3 fixed subprocess calls, plain subprocess.run is easier to trace and removes task discovery from the execution path.
Patterns
Expose a Python function through inv define-task
from invoke import task
@task
def test(c, coverage=False):
command = 'pytest --cov' if coverage else 'pytest'
c.run(command)
# inv test --coverageArgument 1 is always the Context and does not become a command-line option in Invoke 3.
Document generated flags add-option-help
@task(help={'env': 'Deployment environment', 'dry-run': 'Print only'})
def deploy(c, env='staging', dry_run=False):
print(env, dry_run)Use dry-run in the help mapping because Python's dry_run parameter is exposed as --dry-run.
Inspect a failed subprocess handle-command-failure
result = c.run('ruff check .', warn=True, hide=True)
if result.failed:
print(result.exited, result.stdout, result.stderr)Without warn=True, a nonzero status raises UnexpectedExit. hide=True suppresses display but leaves output on Result.
Run cleanup before a build declare-prerequisite
from invoke import call, task
@task
def clean(c, docs=False):
c.run('rm -rf build')
@task(pre=[call(clean, docs=True)])
def build(c):
c.run('python -m build')A pre-task gives order for 1 run. Invoke does not check whether build outputs are already newer than their sources.
Prefix shell work with a directory change run-in-directory
@task
def frontend(c):
with c.cd('web'):
c.run('npm ci')
c.run('npm run build')c.cd() affects commands sent to the shell. Python open() and pathlib still use the process working directory.
Publish namespaced task groups group-namespaces
from invoke import Collection
from tasks import docs, database
ns = Collection()
ns.add_collection(Collection.from_module(docs), name='docs')
ns.add_collection(Collection.from_module(database), name='db')Only tasks added to ns appear in inv --list once an explicit root collection exists.
Pass a scoped environment value set-subprocess-environment
c.run(
'alembic upgrade head',
env={'DATABASE_URL': c.database.url},
echo=True,
)env merges with the inherited environment. replace_env=True removes everything else, including PATH unless you add it.
Answer one known prompt respond-to-prompt
from invoke import Responder
confirm = Responder(pattern=r'Continue\? ', response='yes\n')
c.run('tool deploy', watchers=[confirm])A watcher may respond more than 1 time when its pattern repeats. Keep its match narrow and do not put secrets in source.
Accept multiple values for one option collect-repeatable-flag
@task(iterable=['tag'])
def publish(c, tag=()):
for value in tag:
print(value)
# inv publish --tag latest --tag v3Iterable flags arrive as a sequence. Validate each value before using it in a shell command.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| doit | PyPI | Use it when Python 3 tasks need dependencies plus target files and up-to-date checks |
| taskipy | PyPI | Use it for a small set of commands declared directly in pyproject.toml |
| poethepoet | PyPI | Use it when pyproject tasks need sequences, environment values, and script references |
More cli & tooling guides
commander · chalk · typescript · esbuild · yargs · click · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

