CLI tool
pydoit/doit avatar
pydoit/doit

doit: a make replacement that keeps its task list in Python

CLI task management & automation tool

2,096 stars200 forksPythonMIT

At a glance

What is it?
doit is a Python task runner that tracks file dependencies and skips work already done. It suits people automating data pipelines and documentation in Python, and it costs little to install, but its own quick example still leans on a POSIX shell.
Who is it for?
doit earns a place when the automation you need is already written in Python and you want file level incrementality without maintaining timestamp rules. Leave it alone when the task list belongs in a plain shell script, when people outside Python have to own the definitions, or when a Windows host must run your actions unchanged, since the README example relies on tr.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 32 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 21, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A make replacement written as Python functions

doit is a command line task management and automation tool built to do what make does, in pure Python. Instead of a Makefile full of timestamp rules, a project keeps a dodo.py whose functions each return a plain dict, and doit reads those dicts, works out what changed and runs only the tasks that are stale. The pitch on the README is two lines: Define tasks in Python. Run only what changed. No DSL, no YAML.

That design puts it in reach of people who already automate in Python: data pipelines, documentation builds, repeated test and quality assurance runs. pyproject.toml classifies the project under Build Tools, Testing and Quality Assurance, and names Developers, System Administrators and Science/Research as intended audiences. The repository topics run from build-automation and task-runner to workflow-management, and the home page is pydoit.org.

An install pulls nothing from PyPI. pyproject.toml carries an empty dependencies = [] list, so adopting it inside a build image costs the wheel and nothing else.

file_dep and targets are the caching contract

Everything incremental in doit hangs off two keys. In the example, task_shout returns actions with a tr command, file_dep pointing at hello.txt, targets pointing at shout.txt, and 'clean': True. doit tracks those declared dependencies, caches results, and skips a task that is already up-to-date.

The console output makes the two states easy to read. A first run prints a dot in front of each task, a second run prints two dashes instead, and the alignment gap between the marker and the task name is part of the printed line, not a formatting accident in this write-up.

One gap is worth naming. The README does not document which comparison doit makes between a dependency and its target, whether that is timestamps, content or something else, so anyone who needs to reason about precise staleness has to go to pydoit.org. A task that returns only actions, with nothing in file_dep and nothing in targets, gives the runner nothing to compare.

clean: True is what makes the cycle repeatable

Both example tasks set 'clean': True, which is the switch behind `doit clean`. That command deletes the generated files, and the next `doit` runs both tasks again and prints the two dot lines. Without that key, a generated artifact stays on disk after a clean, so the flag is what turns a one-shot demo into a repeatable loop.

The repository dogfoods this. A dodo.py sits at the top level of the project itself, so the maintainers build doit with doit, and the tests/ and doc/ directories next to it are the targets that build has to keep in step.

The README does not say what `doit clean` does to a task that omits 'clean': True. Treat that as unspecified rather than assuming it sweeps everything.

The quick example still depends on tr and a shell

Read the two claims in the opening lines against the two tasks and a gap opens. The README calls doit pure Python and says there is no DSL and no YAML, only Python functions. What the tasks actually contain are shell command strings: `echo "Hello from doit" > hello.txt` and `tr a-z A-Z < hello.txt > shout.txt`. The redirection is the shell's, and tr is a separate program, not a Python call.

So the purity claim holds for task definition, not for task execution. A Windows host without tr loads dodo.py without complaint and then fails on the second action, and the project classifiers list Operating System :: OS Independent alongside POSIX without settling that question.

A second detail a skim misses: the actions are given only as strings. The README never shows an action as a Python callable, so anyone planning to call a library function directly from a task has no documented example to copy.

DAG ordering, subtasks and plugin hooks

Beyond file tracking, the feature list names a directed acyclic graph so tasks run in dependency order, subtask generation where a single function yields several tasks, computed dependencies through `calc_dep` for graphs that are not known until runtime, parallel execution over multiprocessing or threading, and a plugin architecture for extra commands, reporters, backends and task loaders.

Each of those is a claim about capability, not a demonstration. No sample in the README shows `calc_dep`, a yield based subtask or a loaded plugin, and none shows how the two execution modes behave differently for a task that touches shared state. pydoit.org is the only source given for those contracts.

Parallelism is the one worth pressing on. Threads suit tasks that spend their time inside Python libraries, while multiprocessing suits tasks that shell out, and the README does not explain the failure reporting either mode produces when a child dies.

Installing doit and running the first dodo.py

Install is one line, and pyproject.toml sets requires-python to 3.10 or newer, with classifiers listed up through 3.13. Put a dodo.py in the directory you want to work in:

python
def task_hello():
    """create a greeting file"""
    return {
        'actions': ['echo "Hello from doit" > hello.txt'],
        'targets': ['hello.txt'],
        'clean': True,
    }

The function name gives the task name and the docstring is carried into the listing. Add a second task that consumes the first output:

python
def task_shout():
    """convert greeting to uppercase"""
    return {
        'actions': ['tr a-z A-Z < hello.txt > shout.txt'],
        'file_dep': ['hello.txt'],
        'targets': ['shout.txt'],
        'clean': True,
    }

Run doit with no arguments in that directory and it discovers dodo.py and works through the graph:

bash
$ pip install doit
$ doit
.  hello
.  shout
$ doit            # nothing to do - already up-to-date
-- hello
-- shout
$ doit clean      # remove generated files
$ doit            # runs again
.  hello
.  shout

Two dot lines mean both tasks executed. Two dash lines mean doit considered both current. A third `doit clean` before the final run deletes hello.txt and shout.txt and the run after it executes again. A full list of commands is not in the README, so check the reference on pydoit.org before assuming a flag exists.

A dev version string sits beside a stable classifier

pyproject.toml declares version = "0.38.0.dev0" while its classifiers include Development Status :: 5 - Production/Stable. The version string describes the working tree of the default branch, master, and the PyPI badge on the README points at the published wheel rather than that branch, so the two are describing different artifacts.

The build uses setuptools>=68.0 with build-backend setuptools.build_meta. Contributor work is split into dependency groups: the dev group pins pyflakes>=3.0, pycodestyle>=2.10, rut>=0.3 and coverage>=7.0, plus tomli only when python_version < '3.11', and the docs group pins furo>=2025.12,<2027 with sphinx-sitemap, sphinx-reredirects and sphinx-design. Both groups are installed with the commands left as comments in the file:

bash
uv sync --group dev --group docs
pip install --dependency-groups dev,docs -e .

Shell completion ships at the top level as bash_completion_doit and zsh_completion_doit. The last push was on 2026-09-21, the licence is MIT with a copyright line reading 2008-2026, and the repository publishes no GitHub releases, so upgrade notes come from the CHANGES file and from what the version string in pyproject.toml tells you.

Where doit is the wrong tool

The real difference from make is where the rules live. make reads timestamp prerequisites from a Makefile and hands each rule to the shell; doit asks a Python function for a dict and runs it in dependency order, which buys you any library inside a task and costs you a Python interpreter to read the task list. Where a team keeps build steps in shell scripts because nobody wants Python at all, that trade runs the wrong way.

The caching is also an honest liability. An up-to-date task is skipped rather than executed, so the correctness of an incremental run rests entirely on file_dep and targets naming every real input and output, and nothing in the README checks that for you. A task reading a generated file it never declared stays stale.

Beyond that, the README documents no scheduler for recurring runs, no remote or distributed execution, no rollback after a partial failure, and no way to supply secrets to an action. Treat each as absent until pydoit.org or the CHANGES file says otherwise.

Editorial conclusion

doit earns a place when the automation you need is already written in Python and you want file level incrementality without maintaining timestamp rules. Leave it alone when the task list belongs in a plain shell script, when people outside Python have to own the definitions, or when a Windows host must run your actions unchanged, since the README example relies on tr. Before adopting, install it on Python 3.10 or newer, confirm dodo.py is picked up from your repository layout, and read the staleness rules on pydoit.org, because the README does not say how a dependency is compared with its target.

Frequently asked questions

How do I install doit and run the example?

Run `pip install doit`, which needs Python 3.10 or newer, then create a dodo.py in the directory you want to automate and run `doit` with no arguments. The runner discovers dodo.py and executes the tasks it defines.

Does doit need any third-party Python packages at runtime?

No. pyproject.toml lists an empty dependencies = [] set, so the install brings no runtime packages. The example actions still call out to the shell for things like `tr a-z A-Z`.

How do I make a finished task run again?

Run `doit clean`, which removes the targets of tasks marked 'clean': True, then run `doit` again and the tasks execute a second time. The README does not describe what happens to a task that omits that key.

Which Python versions does doit support?

pyproject.toml sets requires-python to 3.10 or newer and lists classifiers for 3.10, 3.11, 3.12 and 3.13. The version string on the default branch is 0.38.0.dev0.

Can doit run tasks in parallel?

Yes, independent tasks can run concurrently through multiprocessing or threading, and tasks run in dependency order as a directed acyclic graph. The README does not show how failures inside a child process are reported.

Official sources

  1. Issues
  2. License: MIT
  3. Project website
  4. pydoit/doit on GitHub
  5. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/pydoit-doit.svg)](https://hysenlabs.com/projects/pydoit-doit)

Was this useful?