2. Data structures

Data structures

Combining lists and dicts

Often, we need more structure than just a list or just a dict. In this case, For example, if you wanted to create an address book with your friends’ contact information, you might structure it as a list of dicts, like this:

friends = [
    {
        "name": "Anna Applesauce",
        "email": "braeburn@hotmail.com", 
        "favorite_colors": ["peach", "azure"]
    },
    {
        "name": "Enrique Anteojos",
        "email": "lentes@yahoo.com", 
        "favorite_colors": ["chartreuse", "sea foam"]
    }, 
]

Given the data above, friends[0] will return the dict of information about Anna Applesauce, and friends[0]["email"] will return her email address. You could look up her second favorite color with friends[0]["favorite_colors"][1].

💻 friend_functions.py defines a bunch of functions which expect to receive precisely this data structure, and which return useful information about your friends and family. Implement these functions. You can check your work using test_friends.py.

⚡✨
It is good practice to commit your changes every time you get a function working. Once all the tests are passing, push your work to the server.

Communication between systems

Lists and dictionaries are essential structures for thinking about how computer systems ingest and export data. Let’s look at inputs and outputs of computer systems at a few different scales.

Functions

You are already familiar with functions which accept positional arguments. For example, the function below takes two arguments. These arguments are called positional because the function recognizes them by their position. The first argument is called num_sides; the second is called side_length.

from turtle import forward, right

def polygon(num_sides, side_length):
    for each_side in range(num_sides):
        forward(side_length)
        right(360 / num_sides)

However, you can also pass arguments by name. In this case, the order doesn’t matter; named arguments have a key-value structure just like a dictionary.

polygon(side_length=100, num_sides=7)

The most common use of named arguments is to give functions default values which the user can override if desired. This creates more powerful and flexible functions without adding complexity for the most basic use cases. Consider fancy_polygon:

from turtle import forward, right
from superturtle.stroke import dashes, dots

def fancy_polygon(num_sides, side_length=100, stroke="solid"):
    if stroke == "solid":
        polygon(num_sides, side_length)
    elif stroke == "dashes":
        with dashes():
            polygon(num_sides, side_length)
    elif stroke == "dots":
        with dots():
            polygon(num_sides, side_length)
    else:
        raise ValueError("Invalid stroke: " + stroke)

If you call fancy_polygon(7), you will get a seven-sided polygon with side length of 100 and solid stroke. Either of these optional arguments can be provided when you need them. For example, you could draw a small dashed square with fancy_polygon(4, side_length=20, stroke="dashes").

Programs

Programs can also accept positional arguments (like lists) and named arguments (like dictionaries). For example, tree shows part of your computer’s file structure. Tree takes one positional argument, the location for the root of the tree. For example, tree ~/Desktop will show you all the files and directories on your desktop.

tree can also take a wide variety of optional named arguments. For example, -L level limits the depth of the tree, -C adds color, -J causes the output to be in JSON format (see the next section), and -o filename saves the output to a file. Try the following:

$ tree ~/Desktop
$ tree ~/Desktop -L 2
$ tree ~/Desktop -L 2 -J
$ tree ~/Desktop -L 2 -C

Consult the manual (man tree) for the full list of named options.

Distributed Services

Most computer systems today don’t just run on one computer; they rely on services distributed across many computers all over the world. The most common means of transferring data between services is JSON (JavaScript Object Notation), which is basically just a combination of lists and dictionaries.

Have you ever wondered how mwc knows how to set up the repos you need for each lab and project? mwc fetches the file https://code.computationalliteracies.net/index.json from this site, which contains a description of which labs and projects are available and how to get them set up.

We will play more with distributed services in the next section.

Weather CLI

In this lab’s final exercise, we are going to write a program which fetches current weather data from an external service and presents it in the Terminal as a command-line interface. I think we can all agree that life would be better if we could accomplish all our computing tasks without leaving Terminal.

Conceptual overview

In the United States, the National Weather Service (NWS) provides weather forecasting services across US states and territories. NWS divides the country into a grid of 2.5km squares. Each NWS regional office is responsible for forecasting the weather for its grid squares.

In order to get a weather forecast, we need to start with a location. Our program will allow the user to enter a location, or if the user doesn’t provide a location, we will estimate their current location using their IP address. (This is uncomfortably effective.) If the user did give a location, we need to geocode this location, or convert the description into latitude/longitude coordinates. We will rely on the Open Street Map project for geocoding.

Once we have coordinates for the location, we need to find out which grid square we are in and which regional NWS office is responsible for our forecast. Then we can request the correct weather forecast. Here’s a visualization showing the functions we’ll use to accomplish these steps:

Weather functions diagram

The data returned in the weather forecast is a list of dictionaries, each representing a forecast for a certain time period. Here’s an example:

[
  {
    'name': 'Tonight', 
    'temperature': 27, 
    'wind_speed': '5 to 9 mph', 
    'wind_direction': 'SW', 
    'description': 'Partly Cloudy'
  }, 
  {
    'name': 'Thursday', 
    'temperature': 38, 
    'wind_speed': 
    '3 to 8 mph', 
    'wind_direction': 'SW', 
    'description': 'Mostly Cloudy'
  }, 
  ...
]

That’s a lot of data. After you get your forecast, your job is to decide which you care about and how to present it. This might be a good moment to mention that you can put emoji into Python strings and you can print them to the Terminal ☀️.

Exploring the codebase

All the code you need for the weather program is in the weather directory.

  • weather.py contains the function print_weather. This function is the only code you need to edit for this lab.
  • weather_apis.py contains the functions geocode_location, estimate_location, get_weather_office, and get_forecast. You will need to read this file to learn how these functions work, but you don’t need to make any changes.
  • weather_cli.py contains the function weather_cli, which collects arguments from the command line and calls print_weather. You don’t need to interact with this file.

💻 Make sure you are in this lab’s Poetry shell. Then run weather. You should see: Not finished... because that’s what print_weather prints right now.

Try changing what print_weather prints. Run weather again and you’ll see that the output has changed.

Now run weather --help.

usage: weather [-h] [-l LOCATION] [-m] [-v]

Prints out a weather report

optional arguments:
  -h, --help            show this help message and exit
  -l LOCATION, --location LOCATION
                        Location for weather forecast
  -m, --metric          Use metric units
  -v, --verbose         Verbose output

The help message explains how you use weather. For example, to get the weather in Chicago in Celcius, you could run weather --location Chicago --metric, or equivalently, weather -ml Chicago. The program currently accepts these arguments, but they don’t change its behavior. That’s next.

Implementation

A fully-working weather program will:

  • Print out a weather report when given a location.
  • Print out a weather report for an estimated location when no location is provided.
  • Show metric units when the --metric flag is used.
  • Show a more detailed weather report when the --verbose flag is used.
  • Gracefully handle errors, particularly:
    • When the given location can’t be found
    • When a location is found but doesn’t have a weather station (perhaps because it’s not in the US)

It’s a good idea to work on this lab step by step. For example, an initial goal might be to have weather just print out the latitude and longitude of the requested location.

If you are confused about how the functions in weather.weather_apis work, load them up in a Python shell and mess around with them:

$ python -i weather/weather_apis.py
>>> paris = geocode_location("Paris, Texas")
>>> paris
{'lat': 33.6617962, 'lng': -95.555513}
>>> paris["lat"]
33.6617962

Wrapping up

Congratulations–you’ve written your first useful program! If you want to make weather available in your terminal all the time (not just when you’re in this lab’s Poetry shell), run source globalize_weather.sh. This is optional.