Building a Dynamic Menu System for Python3

I’m rubbish at GUI work. When I can, I limit what I do to the command line. Having a nice menu is always key for me so I can group a few utilities in a single call and not have to just call functions directly from the command line. So I’ve been thinking a lot about how to build a reusable menu system for Python. Something that can go at least two levels deep, and then at the final level be able to execute code. Something that, with little modification, I can make work with any utility app I write.

I came up with the solution described in this article. It makes use of the fact that Python is object oriented, and that functions are first class objects. What that means is, we can pass functions around as we would data as well as execute them.

When we want to execute a function we call it with parenthesis like this

#define my function first
def my_toy_function():
    print(‘We would do stuff here’)

#executing the function, which would print ‘We would do stuff here’
my_toy_function()

but if we just want a REFERENCE to the function, to the memory location that points to the function, we can pass the function name WITHOUT PARENTHESIS like this

#passing the reference to the function
some_other_func_name = my_toy_function

What good is this? As you will see in my code, this allows me to store a reference to a function in a dictionary, so that I can call the function later. We can also create decorators which actually return functions (references to functions!) based on the execution of code.

In the above example I can now call the my_toy_function code two ways

#calling my_toy_function directly, which prints ‘We would do stuff here’
my_toy_function()

#calling the passed reference, which prints ‘We would do stuff here’
some_other_func_name()

Crazy, right? As I said above, you can also use this to create decorators, which we will not cover here.

Let’s dig in!

This example is not too pretty, I don’t do any error catching yet, but I think it’s a good start.

We’ll start looking at some supporting pieces, like our dictionaries that have tuples nested in them. The me_menu_dict is a second level menu dictionary. We’ll get to that in a minute.

The main_menu_dict is just as it says, our dictionary for the main text screen.

main_menu_dict = {
    '0': ('Me', me_menu_dict),
    '1': ('Quit', 0)
}

It has string keys to prompt the user to choose a selection, this way we don’t have to do the int conversion on the input function.

The tuple value is the name of the sub-menu (eg. ‘Me) and a pointer to the dictionary holding the sub menu (eg. me_menu_dict). I’ve named the dictionaries NAME_menu_dict, and the NAME is the same as the index 0 string in the main_menu_dict tuple

('Me', me_menu_dict),

I could have embedded the sub menu directly in the main_menu_dict tuple, but wanted it to be cleaner and easier to maintain.

With the embedded dictionary called, it actually embeds the sub-dictionary in the original for me at run time. If I did a print statement on the values of the main menu dictionary it would look like


‘0’: (‘Me’, {
        ‘0’: (‘Profile’, get_profile), 
        ‘1’: (‘Rank’, get_rank), 
        ‘2’: (‘Tokens’, get_tokens)})

The sub menu is magical. It contains a call to the functions get_profile, get_rank and get_tokens. Otherwise it acts just like the main menu – a dictionary with a string key and tuple value.


me_menu_dict = {
    '0': ('Profile', get_profile),
    '1': ('Rank', get_rank),
    '2': ('Tokens', get_tokens)
}

Here are the functions referenced in the sub menu tuple. See above there are no parens? This is so we have a REFERENCE and don’t actually CALL the function.

def get_profile():
    pass

def get_rank():
    pass

def get_tokens():
    pass

Here’s our main text menu function. I’ve pulled out some of the new lines and things I added to just get to the good code. Instead of explaining it here, I’ve added comments to the code

def text_menu():
    while True:
        print('Main Menu')
        for k,v in sorted(main_menu_dict.items()):
            print("\t{}. {}".format(k, v[0]))

        selection = input('Please enter the number of your selection: ')

        if selection == '6':
            break
        else:
            while True:

        # going in one level to get the menu attached to the main menu
        # this is actually just the key to that submenu
        
        print("\n{} Menu\n".format(main_menu_dict[selection][0]))
        
        # The dict doesn’t give us data sorted
        # Dicts are called by key so order isn’t a problem.
        # but we WANT it sorted by key in this case 
        # so we wrap the main_menu_dict in a sorted() call.
        
        for item in sorted(main_menu_dict[selection][1]):

            """this is a bit more complex
            because we embedded dicts in dicts in tuples

            here we go
            
            1. main_menu_dict[selection] 
               This returns the main menu selection key/val tuple
                   ('Me', me_menu_dict)
               
               Which remember expands to the full dictionary
                   (‘Me’, {
                           ‘0’: (‘Profile’, get_profile), 
                           ‘1’: (‘Rank’, get_rank), 
                           ‘2’: (‘Tokens’, get_tokens)
                           })

            2. main_menu_dict[selection][1] 
               returns the embedded MENU in the main menu tuple
               which in main menu is the call to me_menu_dict in the tuple
                   {‘0’: (‘Profile’, get_profile), 
                    ‘1’: (‘Rank’, get_rank), 
                    ‘2’: (‘Tokens’, get_tokens)}

            3. main_menu_dict[selection][1][item] 
               returns the value from the embedded menu - our next tuple
                   (Profile, <function get_profile at 0x7f08ee5b1840>)

            4. main_menu_dict[selection][1][item][0]
               is the first value in the tuple 
               Which is in the VALUE of the embedded menu
                   ‘Profile’

            5. If we grabbed the second index in the tuple to
               main_menu_dict[selection][1][item][1]     
               it would return the pointer to the function
                   <function get_profile at 0x7f08ee5b1840>
            
               See how that’s a pointer to a memory location 
               of the function? In this case 0x7f08ee5b1840
            """

            # here we just print out the keys to the sub menu. 
            # See number four above.
            print("\t{}. {}".format(item, 
                                main_menu_dict[selection][1][item][0]))

        # Our sub menu selection 
        # We’re out of the for loop above, which just prints our menu.
        # now we want to know where the user wants to go
        print("Please enter the number of your selection.")
        new_selection = input('Hit enter to return to main menu: ')

        if new_selection == '':
            break
        else:
            """So now we have our last selection 
               This is from the SUB menu. 
               Let's go ahead and execute the function
            
               This is a cool thing about Python, 
               in the dictionary we are storing a pointer 
               to each function, but not executing it since
               there are no parens after the function name. 
               first class objects! So when we call below, 
               we add the parens and BAM! function call.
            """
            main_menu_dict[selection][1][new_selection][1]()

That’s it, a dynamic menu system. If I want to add items to the main menu, I add them as dictionary items with
Key = number as string
Value = tuple(menu name to print, submenu to call)

Then add a submenu in a similar format
Key = number as string
Value = tuple (menu name to print, function to call)

As I said, there’s still some work to be done. For one, we need to catch some exceptions. We also need to pretty up the printing. Last, I’d like to be able to have a menu item on the main menu that calls a function, not possible with the current config.

But as it is, it’s usable and makes my life easier!