1 The Problem
We want a real command-line tool: tasks add "Buy milk", tasks list, tasks done 2 — commands and arguments, just like git or npm. It teaches argparse, the proper way to build CLI tools that feel professional, not like a toy menu.
2 How to Think About It
Think about commands and arguments, before any code:
add, list, done. → 2. Each takes arguments (the task text, or which number). → 3. argparse reads the command line, validates it, and routes to the right action. → 4. Tasks persist in a file between commands. This is how real CLI tools are structured.
3 The Build — explained part by part
Here is the complete task manager using argparse. Each part is explained below.
import argparse
import json
import os
FILE = "tasks.json"
def load():
return json.load(open(FILE)) if os.path.exists(FILE) else []
def save(tasks):
json.dump(tasks, open(FILE, "w"))
def add(args):
tasks = load()
tasks.append({"task": args.text, "done": False})
save(tasks)
print(f"Added: {args.text}")
def list_tasks(args):
for i, t in enumerate(load(), 1):
mark = "x" if t["done"] else " "
print(f"{i}. [{mark}] {t['task']}")
def done(args):
tasks = load()
tasks[args.number - 1]["done"] = True
save(tasks)
print(f"Marked task {args.number} done.")
parser = argparse.ArgumentParser(description="A simple task manager.")
sub = parser.add_subparsers()
add_cmd = sub.add_parser("add")
add_cmd.add_argument("text")
add_cmd.set_defaults(func=add)
list_cmd = sub.add_parser("list")
list_cmd.set_defaults(func=list_tasks)
done_cmd = sub.add_parser("done")
done_cmd.add_argument("number", type=int)
done_cmd.set_defaults(func=done)
if __name__ == "__main__":
args = parser.parse_args()
args.func(args)
--help message for free.sub = parser.add_subparsers() — set up subcommands, so
tasks add and tasks list are different commands — exactly like git commit vs git push.add_cmd.add_argument("text") — the
add command needs the task text. done_cmd.add_argument("number", type=int) — done needs a number, and type=int makes argparse convert and validate it.set_defaults(func=add) — the clever bit: attach the handler function to each subcommand. After parsing,
args.func(args) calls the right one automatically.load/save — tasks persist in JSON between separate command runs.
sys.argv — reinventing what argparse does better.type=int on the number argument — then it arrives as text.4 Test & Prove Each Part
We test the task operations — add and mark-done — directly, without invoking the command-line parser.
def add_task(tasks, text):
tasks.append({"task": text, "done": False})
return tasks
def mark_done(tasks, number):
tasks[number - 1]["done"] = True
return tasks
def test_add():
"""Adding appends an undone task."""
tasks = add_task([], "Buy milk")
assert tasks[0]["task"] == "Buy milk"
assert tasks[0]["done"] is False
def test_mark_done():
"""Marking done sets the flag."""
tasks = add_task([], "Task")
mark_done(tasks, 1)
assert tasks[0]["done"] is True
def test_done_correct_task():
"""Marking task 2 done leaves task 1 alone."""
tasks = add_task(add_task([], "a"), "b")
mark_done(tasks, 2)
assert tasks[0]["done"] is False
assert tasks[1]["done"] is True
Run with pytest -v. The task logic is separate from argparse, so we test it directly. The parser’s job is only to route command-line input to these tested functions.
5 The Interface
Command
python3 tasks.py add "Buy milk"Output
1. [ ] Buy milk
2. [x] Walk dog6 Run It & Automate It
python3 tasks.py add "Buy milk"python3 tasks.py listpython3 tasks.py done 1Try
python3 tasks.py --help — argparse generated it for free.Jenkins runs the tests automatically — each line explained below.
$ python3 tasks.py add "Buy milk"
Added: Buy milk
$ python3 tasks.py list
1. [ ] Buy milk
$ python3 tasks.py done 1
Marked task 1 done.AttributeError: 'Namespace' object has no attribute 'func'.IndexError on done.// Jenkinsfile — automated tests on every change.
pipeline {
agent any
stages {
stage('Get the code') { steps { checkout scm } } // download the code
stage('Install') { steps { sh 'python3 -m venv venv && . venv/bin/activate && pip install pytest' } } // test tool
stage('Test') { steps { sh '. venv/bin/activate && pytest -v' } } // run every test
}
post {
success { echo 'All tests passed.' }
failure { echo 'A test failed — look above.' }
}
}
- Delete command. Add
tasks remove N. (Teaches: another subcommand.) - Priorities. Add a
--priorityoptional flag. (Teaches: optional arguments.) - Filter list. Show only done or only pending. (Teaches: flags that change behaviour.)