thecodex.expert · The Codex Family of Knowledge
Tier 3 · Upper-Intermediate · Python Project

CLI Task Manager

A proper command-line task manager with commands and arguments, like real CLI tools. Learn to build a polished terminal program.

🧠 Teaches how to think spoonfed, every age Last verified:

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.

Where this shows up: every developer tool — git, docker, npm, pip. Building proper CLIs with subcommands and arguments is a core skill for automation, dev tools, and scripts others will use.

2 How to Think About It

Think about commands and arguments, before any code:

The plan — in plain English
1. The tool has subcommands: 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.

add

list

done

Command line input

argparse parses it

Which subcommand?

Add task

Show tasks

Mark task done

Save to file

3 The Build — explained part by part

Here is the complete task manager using argparse. Each part is explained below.

Pythontasks.py
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)
What each part does — in plain words
argparse.ArgumentParser(...) — Python’s built-in CLI builder. It handles parsing, validation, and even generates a --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.
Common mistakes — and how to avoid them
✗ Building your own argument parsing with sys.argv — reinventing what argparse does better.
✓ Use argparse; it handles parsing, validation, and help.
✗ Forgetting type=int on the number argument — then it arrives as text.
✓ Declare argument types so argparse converts and validates them.
✗ Mixing task logic into the handler functions — harder to test.
✓ Keep add/mark-done as pure functions the handlers call.

4 Test & Prove Each Part

We test the task operations — add and mark-done — directly, without invoking the command-line parser.

Adding a task appends it, not done
Marking done sets the flag
Listing reflects the current state
Pythontest_tasks.py
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

addtasks add "text"create a task
Command
python3 tasks.py add "Buy milk"
listtasks listshow all tasks
Output
1. [ ] Buy milk
2. [x] Walk dog
donetasks done Nmark task N complete

6 Run It & Automate It

Run it locally
python3 tasks.py add "Buy milk"
python3 tasks.py list
python3 tasks.py done 1
Try python3 tasks.py --help — argparse generated it for free.

Jenkins runs the tests automatically — each line explained below.

What you should see when it works
Terminala real run
$ 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.
If it breaks — how to fix it
🚨 AttributeError: 'Namespace' object has no attribute 'func'.
You ran no subcommand. That happens when called with nothing — add a default or print help.
🚨 IndexError on done.
The task number is out of range. Validate it exists before marking.
GroovyJenkinsfile
// 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.' }
    }
}
🎯 Try this next — make it yours
  1. Delete command. Add tasks remove N. (Teaches: another subcommand.)
  2. Priorities. Add a --priority optional flag. (Teaches: optional arguments.)
  3. Filter list. Show only done or only pending. (Teaches: flags that change behaviour.)
What you learned
You learned to build a real CLI tool with argparse: subcommands, typed arguments, automatic help, and routing to handlers. This is how every professional command-line tool is structured. Related: argparse, json.