Introduction

To learn more about backend programming, I explored the project list on roadmap.sh. They provide a variety of backend projects, ranging from beginner to advanced levels.

The first project involved writing a CLI-based task tracker. I built this in Python and discovered some cool features of argparse along the way.

Usage

The options for this tool are very simple. You can add, update, delete, mark, and list tasks. After installing the tool, the CLI commands are similar to those of other CLI-based programs. Instead of using flags, I used subcommands to make writing commands easier:

1
todo add "Buy coffee"

Previously, I always used flags, which are more suitable for most projects. But in this case, using subcommands makes the tool feel more like writing sentences.

What also comes in handy is that with subcommands, setting functions as defaults is possible, so writing if-else statements to retrieve arguments isn’t necessary anymore.

argparse and sub-commands

Before discovering subcommands, I always used the standard parser, as shown in the following example:

1
2
3
4
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--add", "-a", type=str, help="Add a todo to the todo list")
args = parser.parse_args()

This works fine if the program needs to parse multiple flags at once. But in my case, I only wanted one command processed by the program. Originally, this wasn’t an issue for me, as I could just add every command to an exclusive group like this:

1
2
3
4
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group()

group.add_argument("--add", "-a", type=str, help="Add a todo to the todo list")

This works perfectly well if some flags are simple, but it can become problematic if they are more complex and can’t be fully represented by this syntax. Specifically, I wanted to specify the types of arguments with two values. For example, update needs an ID of type int and a new description of type str. With add_argument(), it’s only possible to set the type for all values.

So, I needed an alternative. That’s where subcommands come in handy. They are more verbose, but they allow for a lot of customization. I probably only scratched the surface with my code:

1
2
3
4
parser_update = subparsers.add_parser("update", help="Update a todo in the todo list")
parser_update.add_argument('todo_id', type=int)
parser_update.add_argument("description", type=str)
parser_update.set_defaults(func=todo_list.update_todo)

The neat thing is that I can also set a function for each subparser! With that, I only needed to code the functions and directly add them to each subparser.

The next section briefly covers the data structure for saving the tasks.

Data structure

Here is a brief overview of the data structure. I save all data in a single JSON file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  "id_count": 2,
  "tasks": [
    {
      "id": 1,
      "description": "Buy coffee",
      "status": "todo",
      "createdAt": "2024-08 19-14:43",
      "updatedAt": "2024-08 19-14:43"
    }
  ]
}

The key id_count keeps track of the already assigned IDs. After each new task, the count increments by one. The tasks are saved in the tasks list.

Each task contains id, description, status, createdAt, and updatedAt keys. The status can be todo, in-progress, or done.

Conclusion

To sum up, the first project on roadmap.sh seems very easy on the surface, but I still learned new things about coding. What I didn’t mention is that the whole project is managed with Poetry, and I also wrote some unit tests and a GitHub pipeline for CI/CD. That could be a topic for the future.

References