Dynamic, Nested Menus in a Python CLI
Tags: python, cli
Published: 2024-01-29 03:02:51

**<u>The Problem</u>**

Recently, I was working on a Python Command Line Interface that allowed a user to explore different art movements, artists, and paintings stored in a database. Movements to artists was a one-to-many relationship, as was artists to paintings.

I wanted a the main menu of the CLI to have three top level menu items that each led to submenus: movements, artists, and paintings.

![Main menu screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nq2kohb5f9fnk7kmkds8.png)

Within each of these menus, I wanted an option to explore a particular row of the corresponding table.

![Artist menu screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9koflkn5k7pfo41gnq0e.png)

Choosing option 3 would list all available options and open a menu based on the user's choice.

![Explore artist screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bt7zw4rto7nwangsigci.png)

I needed a way to make these 'exploration' menus. In addition to accessing these dynamic menus from the top level menus, a user would also be able to access them in the following ways:
- Within the 'explore movement' menu, there would be an option to explore an artist in that movement.
- Within the 'explore artist' menu, there would be an option to explore a painting by that artist. 
- Creating a new movement, artist, or painting would automatically enter the exploration menu for the newly created table row.

I started with an outline of how I needed the menu navigation to work.

Main Menu
    1. Movements Menu -> Explore Movement -> Explore Artist -> Explore Painting
    2. Artists Menu -> Explore Artist -> Explore Painting
    3. Paintings Menu -> Explore Painting

I wanted the user to be able to move down these lines and back up to the main menu.

I was unsure where to begin, seeing as the only CLI menu with which I was familiar had the following structure:

```
def main():
    while True:
        menu()
        choice = input("> ")
        if choice == "0":
            exit_program()
        elif choice == "1":
            function_one()
        elif choice == "2":
            function_two()
        else:
            print("Invalid choice")
```
with `menu()` calling a function that prints out options for the user. I knew nothing about nested menus or about making menus dynamic.

**<u>The Process</u>**

Looking at the basic menu function, I realized that I already had a hint as to how to make these menus nested: the while loop. The while loop in the basic menu serves the purpose of keeping the user in the menu until they choose to exit the program. A choice can be made, the function that corresponds to that choice will be executed, and then the options will be reprinted and the user will again be prompted to make a choice.

So what if the function that corresponded to the user's choice contained its own while loop?

**The Navigation**

In order to move from one menu to the next, calling the function for the secondary menu would suffice. The while loop inside of the function for the secondary menu would keep the user there until they chose to exit.

I initially thought that the backwards navigation would be more complicated. However, I quickly realized that the only thing needed to return to the previous menu was terminating the current while loop.

```
def artists():
    m = "artists"
    while m == "artists":

        # print user options
        artists_menu()

        choice = input("Enter your choice: ")
        if choice == "0":
            m = ""
        
        # other functions

        else:
            print("Invalid choice")
```

**Making It Dynamic**

Beyond what I already had, I knew that each of the 'exploration' menus would need to take in an argument in order to dynamically display information based on one of the following: the option that was chosen upon entering the menu or a newly created row of data. 

To handle the first situation, I wrote helper functions that would print the available options and return row of the table that corresponded to the user's choice.

To handle the second situation, I simply had any create functions return the newly created object.

I could then save these return values to variables and pass them variable into the explore menu functions.

```
elif choice == "2":
    artist = create_artist()
    if artist:
        explore_artist(artist)
elif choice == "3":
    artist = choose_artist()
    if artist:
        explore_artist(artist)
```

I could then use the data passed in to these functions to display specific information and options to the user based on their choice.

![Screenshot of explore artist menu](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cch2ohb5ryui7tuj1b4y.png)
```
def explore_artist(artist):
    m = "explore artist"
    while m == "explore artist":

        # print user options
        artist_options_menu(artist)

        choice = input("Enter your choice: ")

        if choice == "0":
            m = ""
        elif choice == "1":
            list_paintings_by_artist(artist)
        elif choice == "2":
            painting = choose_painting_by_artist(artist)
            if painting:
                explore_painting(painting) 
        elif choice == "3":
            painting = create_painting(artist.id)
            if painting:
                explore_painting(painting)
        elif choice == "4":
            update_artist(artist)
        elif choice == "5":
            result = delete_artist(artist)
            if result == "deleted":
                m = ""
        else:
            print("Invalid choice")
```

I could now allow some of my functions, like `create_painting`, take in an optional parameter: in this case, an artist's ID. When creating a new painting from the main paintings menu, nothing would be passed in and the user would be asked to enter a painting name, choose an artist, enter a year, and choose a medium. However, when creating a new painting from the explore artist menu, the artist's ID would already be provided, eliminating the step of choosing a artist.

**<u>Conclusion</u>**

This solution will probably seem obvious, even simplistic, to some of you. However, as someone just beginning to learn about Python and CLIs, the path to solving this problem was not at all clear at first. I hope that my solution will be able to help someone in their software engineering journey!