Posted on 05 Jan 2026

In the previous version of our task management CLI, we built a Python command-line interface using argparse and the Command Pattern. If you missed that article, you can read it here.
Version 0.0.2 replaces argparse with Click — the code gets shorter, the types get safer, and the Command Pattern stays intact.
You should read this article if:
execute(**kwargs) interface.While Python’s standard library argparse is perfectly capable of building command-line interfaces, the Click library offers several advantages that make developing CLI applications simpler, more maintainable, and less error-prone.
Before diving into the code, it’s worth reiterating the CLI syntax we designed in our previous article using argparse (read it here).
Even though we are now using Click, the overall structure and commands remain the same. Our simple Task Manager CLI allows us to:
The syntax follows the familiar pattern:
# Add a new task
python3 src/cli.py task add --name "Buy milk"
# List all tasks
python3 src/cli.py task list
# Delete a task
python3 src/cli.py task delete --id 1
# Commands with subcommands
python3 src/src.cli stats summary
python3 src/src.cli stats export
By keeping the syntax consistent, users of the previous version will feel right at home while we enjoy the cleaner implementation provided by Click.
The heart of our Task Manager CLI is the cli.py file. With Click, we can organize commands, subcommands, and options in a clean, readable way, while keeping the modularity of the Command Pattern.
We start by defining the main CLI group:
import click
@click.group(help="Task Manager CLI")
def cli():
pass
Each command is defined with minimal boilerplate. For example, the add command:
@cli.command(help="Add a new task")
@click.option("--name", "-n", required=True, help="Task name")
def add(name):
AddTaskCommand().execute(name=name)
And the delete command with type checking:
@cli.command(help="Delete a task")
@click.option("--id", "-i", "task_id", required=True, type=int, help="Task ID to delete")
def delete(task_id):
DeleteTaskCommand().execute(task_id=task_id)
Key points:
Click makes nested commands simple. For instance, the stats group:
@cli.group(help="Show or export task statistics")
def stats():
pass
@stats.command(help="Show a summary of tasks")
def summary():
StatsCommand().summary()
@stats.command(help="Export tasks to CSV file")
@click.option("--output", "-o", default="tasks.csv", help="Output CSV file name")
def export(output):
StatsCommand().export(output=output)
For the complete version of the CLI file, including all commands and groups, see the repository:
View the full cli.py on GitHub
In the previous version of our CLI, each command implemented an execute() method in a base class. This allowed us to invoke commands in a uniform way, but it also introduced type-checking issues when we tried to enforce argument types with mypy.
With Click, we can simplify this pattern while keeping modular, testable command classes.
We still define a base class, but we intentionally use it as a marker rather than as a functional interface, and we deliberately remove any shared execute() method.
# commands/base.py
from abc import ABC
class Command(ABC):
"""Marker base class for CLI commands."""
Each command focuses on its own functionality. Not all command classes map one-to-one to a single CLI command: some, like StatsCommand, group related operations, while Click handles dispatching the correct action.
# commands/add_task_command.py
from .base import Command
class AddTaskCommand(Command):
def execute(self, name: str):
with open("tasks.txt", "a") as f:
f.write(name + "\n")
print(f"✅ Task added: {name}")
# commands/list_task_command.py
from .base import Command
class ListTaskCommand(Command):
def execute(self):
try:
with open("tasks.txt") as f:
tasks = [line.strip() for line in f if line.strip()]
except FileNotFoundError:
tasks = []
if not tasks:
print("🗒️ No tasks found.")
return
print("📋 Tasks:")
for i, t in enumerate(tasks, 1):
print(f"{i}. {t}")
# commands/delete_task_command.py
from .base import Command
class DeleteTaskCommand(Command):
def execute(self, task_id: int):
try:
with open("tasks.txt") as f:
tasks = [line.strip() for line in f if line.strip()]
except FileNotFoundError:
print("❌ No tasks found.")
return
if task_id < 1 or task_id > len(tasks):
print(f"❌ Invalid task ID: {task_id}")
return
removed = tasks.pop(task_id - 1)
with open("tasks.txt", "w") as f:
f.write("\n".join(tasks))
print(f"🗑️ Task deleted: {removed}")
# commands/stats_command.py
import csv
import os
from .base import Command
class StatsCommand(Command):
TASKS_FILE = "tasks.txt"
def summary(self):
tasks = self._load_tasks()
print(f"📊 You have {len(tasks)} task(s).")
for i, t in enumerate(tasks, 1):
print(f"{i}. {t}")
def export(self, output: str):
tasks = self._load_tasks()
if not tasks:
print("No tasks to export.")
return
with open(output, "w", newline="") as csvfile:
writer = csv.writer(csvfile)
writer.writerow(["ID", "Task"])
for i, task in enumerate(tasks, 1):
writer.writerow([i, task])
print(f"✅ Tasks exported to {output}")
def _load_tasks(self):
if not os.path.exists(self.TASKS_FILE):
return []
with open(self.TASKS_FILE) as f:
return [line.strip() for line in f if line.strip()]
For a full view of the updated command classes and their implementation, see the repository:
Click Version Command Classes on GitHub
In this article we covered:
@click.group and @click.command.execute(**kwargs) interface, using explicit typed method signatures instead.Click is a pragmatic choice: less boilerplate, stronger typing, and a cleaner architecture — an architecture that grows with the application. If using classes feels like overengineering for your use case, nothing prevents you from implementing commands as standalone functions instead. The pattern is flexible by design.
This is the second and final article in the Python CLI with the Command Pattern series. If you missed Part 1, start there to understand the argparse-based foundation this article builds upon.
If you enjoyed this article, don’t forget to give it a clap 👏, share it with your friends 🔗, and follow me for more tips and tutorials on software development 📘. Your support helps me create more content like this — thank you! 🙌