Let’s learn some geography as we pursue Carmen Sandiego around the world. We might not catch her, but we’ll increase our worldly knowledge! And we’ll practice with a couple of our recently learned Python skills with functions and data structures.
We’re going to learn more than simple programming here. We’ll focus on building bigger and better software, and gain three powerful skills: how to make programs span multiple files, how to make our projects self-contained, and how to integrate software libraries written by others.
Comments Match statements Multi-file Programs Virtual Environments Third-party Libraries
Begin by creating the folder ~/cmsi1010/lab06, and in it, a new file called carmen.py, with the following contents:
current_country_name = "colombia" while True: print("Guess where Carmen is, or say 'hint' or 'exit'.") guess = input("Where are you going to look? ").strip().lower() if guess == current_country_name: print("She was here, but you missed her by one hour!") elif guess == "hint": print("Sorry, no hints yet.") elif guess == "exit": print("Thank you for playing!") break else: print("Oh no, she’s not here!")
Hopefully that program should be not be too hard to follow after doing the two previous labs. Test it out to make sure it’s working.
Let’s make it slightly more interesting, adding a few more countries:
import random country_names = ["colombia", "barbados", "vanuatu", "eswatini", "bhutan", "iceland"] current_country_name = random.choice(country_names) while True: print("Guess where Carmen is, or say 'hint' or 'exit'.") guess = input("Where are you going to look? ").strip().lower() if guess == current_country_name: print("She was here, but you missed her by one hour!") current_country_name = random.choice(country_names) elif guess == "hint": print("Sorry, no hints yet.") elif guess == "exit": print("Thank you for playing!") break else: print("Oh no, she’s not here!")
Now, to provide hints, we’ll create a little “database” of country facts. We’ll also learn some nice programming practices like not letting our files get too big, so we’ll put the country facts in a separate file! Create a new file in the lab06 folder called geography.py, starting things off with:
countries = { "colombia": { "capital": "Bogotá", "coordinates": (4.7110, -74.0721), "region": "South America", "landmarks": ["Monserrate", "El Peñón de Guatapé", "Las Lajas Sanctuary"], }, "barbados": { "capital": "Bridgetown", "coordinates": (13.0971, -59.6132), "region": "Caribbean", "landmarks": ["Crane Beach", "Harrison's Cave", "St. Nicholas Abbey"] }, "vanuatu": { "capital": "Port Vila", "coordinates": (-17.7430, 168.3173), "region": "Oceania", "landmarks": ["Mount Yasur", "Champagne Beach", "Chief Roi Mata's Domain"] }, "eswatini": { "capital": "Mbabane", "coordinates": (-26.3264, 31.1442), "region": "Southern Africa", "landmarks": ["Sibebe Rock", "Mlilwane Wildlife Sanctuary", "Mantenga Nature Reserve"] }, "bhutan": { "capital": "Thimphu", "coordinates": (27.4716, 89.6386), "region": "South Asia", "landmarks": ["Paro Taktsang", "Punakha Dzong", "Rinpung Dzong"] }, "iceland": { "capital": "Reykjavik", "coordinates": (64.1470, -21.9408), "region": "Northern Europe", "landmarks": ["Blue Lagoon", "Hallgrímskirkja", "Gullfoss Waterfall"] } }
Congratulate yourself on creating a dictionary containing dictionaries, each with a list and a tuple. (A tuple is like a list, except you cannot update any of its components: we say a tuples are immutable. Later in your career you will learn why immutability is awesome.) A good deal of what programmers do is organize data.
Putting that information in a separate file is a good idea: it keeps things clean! Of course, the main program in carmen.py will need to “see” the code in geography.py. Here’s how we will do it. In carmen.py, right after import random add the line:
from geography import countries
Is this the best way to organize this data?If you already know Python, you may be wondering why the information above was not represented with a “class.” Answer: We’re just learning dictionaries before classes, that’s all. We have to start somewhere. Later, in a subsequent lab, we’ll see how classes can improve things a bit. Or a lot.
Now that we have this database, we can update our game engine to give hints. How should hints look? We have the capital city, three landmarks, and a geographical region. So there are several possible hints we can give. For Colombia, these are possible hints:
We want to randomly select one of the five hints. Sometimes, when you have an idea, it’s good to write a function for it. You can now safely add the following function to carmen.py. (Since it is never called, there is no harm in adding it.)
def random_hint(country): match random.choice(["capital", "region", "landmark"]): case "capital": hint = "whose capital is " + country["capital"] case "region": hint = "in " + country["region"] case "landmark": hint = "where you can find " + random.choice(country["landmarks"]) return "Carmen is in a country " + hint
The country parameter will be one of the objects in the big countries dictionary we defined in countries.py.
Previously, we selected a random country name from a list of country names. Now that we have a “database” of countries, our random selection has to come from the dictionary’s keys. But random.choice() only works on lists, so getting a random key from a dictionary can get messy. When things get messy, it’s often advisable to stick all the messy code in a nicely-named function. And sometimes, to explain the funky goings-on, it is useful to write a comment. A comment is like a note in the code. It never gets executed. Comments can be useful for explaining things, and helping you to remember things when you look at your code in the future and forget what you’ve done. But you do need to remember to keep them relevant and accurate. Ready? Add this function to carmen.py:
def random_country_name(): # Select a random country name from the key in the countries dictionary we imported. # We have to convert the keys to a list, because random.choice() only works on lists, # and the keys of a dictionary are (surprisingly!) not a list in Python. return random.choice(list(countries.keys()))
Now we need to rewrite the game playing logic, just a little, to take advantage of our new functions. You are welcome to try to figure it out on your own, but feel free to follow the in-class code along where we will be growing carmen.py to this:
import random from geography import countries def random_country_name(): # Select a random country name from the key in the countries dictionary we imported. # We have to convert the keys to a list, because random.choice() only works on lists, # and the keys of a dictionary are (surprisingly!) not a list in Python. return random.choice(list(countries.keys())) def random_hint(country): match random.choice(["capital", "region", "landmark"]): case "capital": hint = "whose capital is " + country["capital"] case "region": hint = "in " + country["region"] case "landmark": hint = "where you can find " + random.choice(country["landmarks"]) return "Carmen is in a country " + hint current_country_name = random_country_name() while True: country = countries[current_country_name] print("Guess where Carmen is, or say 'hint' or 'exit'.") guess = input("Where are you going to look? ").strip().lower() if guess == current_country_name: print("She was here, but you missed her by one hour!") current_country_name = random_country_name() elif guess == "hint": print(random_hint(country)) elif guess == "exit": print("Thank you for playing!") break else: print("Oh no, she’s not here!")
Test it out! You should be able to get hints, and the game should continue until you type exit.
We haven’t used those latitudes and longitudes yet. We can integrate them into our hints, enabling us to say how far away Carmen is. To do that, we have to be able to compute the distance between two points on the Earth’s surface. While you could write that function yourself, it’s often easier to use a library that already has this functionality. One such library is haversine.
We need to fetch this library over the Internet and install it on our local machine, somewhere in our ~/cmsi1010 folder. The accepted way to do this in Python-land is to first create a virtual environment for this folder.
In your cmsi1010 folder, run, if python3 is the way you run Python programs so far, this command:
python3 -m venv env
or, if you normally use python:
python -m venv env
A virtual environment, or venv, lets you isolate this folder’s Python code from all the other Python environments on your machine. You can now install whatever libraries you want into this environment, and they won’t affect any other projects you happen to be working on.
Run the ls command to see that you have a new folder env inside of your cmsi1010 folder. You don’t ever want to commit this folder, so make sure the line env is added to your .gitignore file. Add it if it is not already there.
Now, enter the virtual environment:
source env/bin/activatesource env/Scripts/activateYour terminal prompt should now be decorated with (env). That’s how you know you are “in” the virtual environment. If you ever need to leave the virtual environment, enter the command deactivate. Whether you leave on purpose, or whether you accidentally get kicked out of the environment, you can always return with the proper command above.
Now, while in the virtual environment:
pip install haversine
You should see activity on your terminal that indicates the pip program is fetching the haversine library from somewhere on the Internet and downloading it to your machine, where it will be tucked away into the new env folder and therefore usable by programs you write.
Make sure you stay in the virtual environmentWhen programming in Python, you should always have a virtual environment for each of your projects, and always work “in” this environment. If you ever make a new terminal window, make sure to enter the environment with the proper
sourcecommand above. Ditto if you ever end up deactivating the venv for any reason.You can always check that you are in the environment by looking for the
(env)prefix on your terminal prompt.
Let’s browse the haversine library documentation in class. Now let’s integrate it into our code. First we need to import from this library. The syntax is the same as importing from one of our own files. Rewrite the top three lines of carmen.py like so:
import random from haversine import haversine from geography import countries
Granted, it might look weird at first that the library is called haversine and the function we are importing is also called haversine. But it’s totally fine.
Now, let’s make the distance from Los Angeles (where I am as I am writing this lab) one of hints. Here’s the new random_hint function:
def random_hint(country):
match random.choice(["capital", "region", "landmark", "distance"]):
case "capital":
hint = "whose capital is " + country["capital"]
case "region":
hint = "in " + country["region"]
case "landmark":
hint = "where you can find " + random.choice(country["landmarks"])
case "distance":
los_angeles = (34.0522, -118.2437)
from_LA = haversine(los_angeles, country["coordinates"], unit="km")
hint = "approximately " + str(from_LA) + " km from Los Angeles"
return "Carmen is in a country " + hint
If you run this long enough to get some distance hints, you’ll notice that the distances, while correct, are far too precise. This is something we’ll clean up in the challenges below.
Teaser for the next lab: You might also notice the random_hint function has a lot of string additions (concatenations). This is not a good look. In the next lab, we’ll learn the right way to combine fragments of text together. You’ll like it!
Now it’s your turn. Here are some ideas for you to extend the activities above:
countries dictionary in geography.py.We’re making still more progress with Python. Continue with your reading:
We’ve covered:
Here are some questions useful for your spaced repetition learning. Many of the answers are not found on this page. Some will have popped up in lecture. Others will require you to do your own research.
while True: loop? break statement.circle_area. How would you arrange to use that function in another file? from geometry import circle_area.favorites? random.choice(favorites).capitals with contents such as {"CA": "Sacramento", "HI": "Honolulu", ...}, how would you select a random state? random.choice(list(capitals.keys())).python -m venv env in your project folder.source env/bin/activate; on Windows Git Bash, use source env/Scripts/activate.pip install followed by the mane of the package.