BYU logo Computer Science

Menus and state

Sometimes when our programs interact with a person, we want to show them a menu of options. For example, if we were writing a weather app, we might show a menu at the bottom:

iphone weather app

Here, the menu items are:

  • Weather
  • Cities
  • Radar
  • Settings

This application also needs to keep track of some state, which is stored in the settings. This might include things like your home city, whether you want to use light mode or dark mode, etc.

We will be writing program that operates entirely on the terminal, but will use the same concepts.

A lighting-control application

We will write a small application that controls the lights in a home. A person using the app will use a menu like this:

(0) Show system status
(1) Turn on/off light
(2) Quit

If they want to show the system status, it will look like this:

Front Porch: OFF
Entry: OFF
Kitchen: OFF
Bedroom: OFF

If they want to turn on or off a light, the program will display a new menu:

(0) Front Porch
(1) Entry
(2) Kitchen
(3) Bedroom

and if they choose an option, such as 1, then the system will change that light (from on to off, or from off to on): Entry is now ON.

State for the application

The state for the application is stored in a file called lights.json. An example is:

{
  "Front Porch": false,
  "Entry": true,
  "Master Bedroom": false,
  "Master Bathroom": false,
  "Bedroom 1": false,
  "Bedroom 2": false,
  "Living Room": false,
  "Hallway": false,
  "Bathroom": false,
  "Kitchen": true
}

For each light, we store true or false, representing whether it is on or off.

Flow chart

Any time you have a significant program to write, you should create (a) a flow chart or (b) an outline of an algorithm in English.

work with a friend

Here is a flow chart showing the algorithm for this program.

flow chart showing loading light state, showing menu, getting a choice from the user and acting on it

  • Load light state / save light state: We will need functions that can read a JSON file into a dictionary and save a dictionary to a JSON file.

  • Get valid input: We will need a function that, given a set of valid inputs, prompts a user for an input and continues until that input is valid. The function should return the valid input.

  • Show status: We will need to read the status of each light from the dictionary.

  • Turn on/off light: We will need to update the dictionary.

Getting started

A good way to get started is to write the main function for the program:

import sys
import json


def main(filename):
    lights = load_state(filename)
    while True:
        option = main_menu()
        if option == 0:
            show_system_status(lights)
        elif option == 1:
            turn_on_or_off(lights)
            save_state(lights, filename)
        else:
            return



if __name__ == "__main__":
    main(sys.argv[1])

Notice that we are pretending some functions exist that we haven’t written yet:

  • load_state()
  • main_menu()
  • show_system_status()
  • turn_on_or_off()
  • save_state()

Let’s pretend all of these functions exist:


def load_state(filename):
    return {}


def main_menu():
    return 2


def show_system_status(lights):
    pass


def turn_on_or_off(lights):
    pass


def save_state(lights, filename):
    pass

We have load_state() return an empty dictionary since it needs to return a dictionary eventually. We have main_menu() return 2, the option for quitting, so we can exit our while loop. The rest return pass, which just means do nothing.

Now we can run the program with python lights.py lights.json and … it does nothing! :-)

We run load_state(), which just returns an empty dictionary, we run main_menu(), which returns 2. We run save_state(), which does nothing, and then we return (which breaks out of the while loop).

Even though this program does nothing useful, we are organized. And that is the start of something great.

Loading state

Let’s write our first function, which loads state:

def load_state(filename):
    with open(filename) as file:
        return json.load(file)

We saw this previously. That is all it takes to load a JSON file into a dictionary.

We can also modify our main() function so that we print the dictionary after we load it:

lights = load_state(filename)
print(lights)

Now when we run the program with python lights.py lights.json, we get:

{"Front Porch": False, "Entry": True, "Master Bedroom": False, "Master Bathroom": False, "Bedroom 1": False, "Bedroom 2": False, "Living Room": False, "Hallway": False, "Bathroom": False, "Kitchen": True}

Great, this shows that piece is working properly!

Delete the print(lights) line, we don’t need that any more.

Now we can work on showing the main menu and getting valid input from the user. Notice in the flow chart that we need to both ‘show the main menu’ and ‘get valid input’. This is a good sign that we can use two functions here. Especially since we have similar functions for turning on/off a light in a different part of the flow chart. If we write these functions carefully, we can re-use them!

def main_menu():
    menu = [
        'Show system status',
        'Turn on/off light',
        'Quit'
    ]
    show_menu(menu)
    return get_valid_input(range(len(menu)))

A menu is just a list of strings. We then have two more functions — one to show the menu and one to get valid input from the user.

To show the menu, we are going to use enumerate:

def show_menu(menu):
    print()
    for index, option in enumerate(menu):
        print(f'({index}) {option}')

Remember, enumerate() lets us iterate through a list by giving us tuples of (index, item).

To get valid input from the user, we need to loop until one of their inputs is in the supplied list of valid inputs:

def get_valid_input(valid_inputs):
    while True:
        response = input('Option: ')
        if not response.isdigit():
            print(f'Invalid option: {response}')
            continue
        response = int(response)
        if response not in valid_inputs:
            print(f'Invalid option: {response}')
            continue

        return response

Notice that we:

  • use a while True loop
  • continue if the input is not a digit
  • continue if the input is not in our valid inputs
  • return (which breaks out of the while loop) if we get a valid response

Now we can run the program with python lights1.py lights.json:


(0) Show system status
(1) Turn on/off light
(2) Quit
Option: 0

(0) Show system status
(1) Turn on/off light
(2) Quit
Option: 1

(0) Show system status
(1) Turn on/off light
(2) Quit
Option: 3
Invalid option: 3
Option: 2

None of the options do anything useful, except option 2 will break out of the while loop so we can quit the program. But we have a working menu system!

System status

Now we can write a function to show the system status:

def get_status(bool_status):
    if bool_status:
        return "ON"
    else:
        return "OFF"

def show_system_status(lights):
    print()
    for name, status in lights.items():
        on_off = get_status(status)
        print(f'{name}: {on_off}')

We first write a function get_status() that translates true/false to ON/OFF.

Then show_system_status() uses for .. in to iterate through the dictionary items. Remember, items() gives us a list of (key, value) tuples to iterate through.

You can re-run the program and see that it shows the system status now.

Turn on/off light

Next we will write the function that turns on/off a light. Notice, in the flow chart we have ‘show lights menu’ and ‘get valid input’. These are very similar to the flow for the main menu. We should be able to re-use these functions we wrote earlier.

def turn_on_or_off(lights):
    menu = list(lights.keys())
    show_menu(menu)
    option = get_valid_input(range(len(menu)))
    key = menu[option]
    lights[key] = not lights[key]
    on_off = get_status(lights[key])
    print(f'{option} is now {on_off}')
  • We can re-use show_menu() by providing a list of the keys in the lights dictionary for its menu
  • We can re-use get_valid_input()
  • We can get the key for the lights dictionary (which is the name of the room), by indexing into the menu.
  • We can toggle the lights with not lights[option]
  • We can re-use get_status() to translate true/false to ON/OFF

Now when we run this, most of the system is working!


(0) Show system status
(1) Turn on/off light
(2) Quit
Option: 0

Front Porch: OFF
Entry: OFF
Master Bedroom: OFF
Master Bathroom: OFF
Bedroom 1: OFF
Bedroom 2: OFF
Living Room: OFF
Hallway: OFF
Bathroom: OFF
Kitchen: OFF

(0) Show system status
(1) Turn on/off light
(2) Quit
Option: 1

(0) Front Porch
(1) Entry
(2) Master Bedroom
(3) Master Bathroom
(4) Bedroom 1
(5) Bedroom 2
(6) Living Room
(7) Hallway
(8) Bathroom
(9) Kitchen
Option: 2
2 is now ON

(0) Show system status
(1) Turn on/off light
(2) Quit
Option: 0

Front Porch: OFF
Entry: OFF
Master Bedroom: ON
Master Bathroom: OFF
Bedroom 1: OFF
Bedroom 2: OFF
Living Room: OFF
Hallway: OFF
Bathroom: OFF
Kitchen: OFF

(0) Show system status
(1) Turn on/off light
(2) Quit
Option: 2

Saving state

The last step is to write save_state() so that we can save the state of the system back to the lights.json file. We saw this previously.

def save_state(lights, lights_file):
    with open(lights_file, 'w') as file:
        json.dump(lights, file)

You should run the program, changing some lights, quit, and be sure the new state is what you see when you start up again.

Important Lessons

  1. Plan your code out before you write it.

  2. Write your code one or two functions at a time. You can create “dummy” functions (with pass or a return statement) for those you haven’t written yet.

  3. Each function should have one, clear, specific purpose. This is known as the single responsibility principle.

  4. Where possible, don’t repeat yourself (DRY). Sometimes this means re-writing your code when you notice you are writing a function that is nearly the same as one you wrote before.

  5. Validate input.

  6. Each option a user takes either reads the state or changes the state.

  7. Save state as you go. When it changes you can write it to a file immediately.