Solving the Problem Sets of CS50's Introduction to Programming with Python — One at a Time: Final Project and Beyond

Eda Eren · June 8, 2022

The final week has arrived, and today we do not have any problem sets. Before anything, give yourself a pat on the back for coming this far! It has been quite a delightful journey. We have started with the basic blocks of programming, variables and functions, dealt with flow control using conditionals, iterated with loops, handled exceptions, scratched the surface of the world of Python libraries, written tests for our programs to make sure that they work as we intend them to do, worked with files, learned to love regular expressions, and last week, peeked into the realm of object-oriented programming. Today in et cetera we will be looking at some other tools in our toolkit. Phew! If you have started this journey from absolute zero, you have indeed come far! Even if you already have some knowledge before starting the course, congratulations to you as well! It is not easy to dedicate oneself through all these weeks. You can always find the posts on previous problem sets in the archive as well.

The theme of this series, as well as the course, has been one important point: when in doubt, read the documentation. Even though the official Python documentation might not seem as friendly at first, you have been using it for many weeks and must be familiar with it already. There are lots more to discover, of course, and it is our number-one friend. Some of these things to discover are already shown in the lecture, so, let’s remember them with very simple examples. (And, get ready for a bunch of Harry Potter references.)

Sets

A set is, at the very basic level, a data structure that has no duplicates. So, let’s say you want to look at the distinct broomsticks that Harry Potter used for Quidditch. Easy to do it with a set:

broomsticks = [
    'Nimbus 2000', 
    'Nimbus 2000', 
    'Firebolt',
    'Firebolt', 
    'Firebolt',
    'Firebolt',
]

print(set(broomsticks)) # {'Firebolt', 'Nimbus 2000'}

Globals

Global variables are usually frowned upon; especially, using the global keyword is something you must avoid unless you are absolutely sure what you are doing. You can think of a global variables as simply variables outside a function. They cannot just be changed right away inside a function, but are read only in that sense. To change the value of a global variable inside a function, you use the global keyword. Let’s say we are completing the title of our favorite book in the Harry Potter series:

half_title = 'Chamber of Secrets'

def change_half_title():
    half_title = 'Goblet of Fire'

change_half_title()

print(f'Harry Potter and the {half_title}')
# -> Harry Potter and the Chamber of Secrets

Of course, it did not change as we expected. However, with the global keyword, it works:

half_title = 'Chamber of Secrets'

def change_half_title():
    global half_title
    half_title = 'Goblet of Fire'

change_half_title()

print(f'Harry Potter and the {half_title}')
# -> Harry Potter and the Goblet of Fire

Again, it is not very nice to look at, so avoid this kind of implementation as much as you can.

Constants

If you have seen the lecture, you already know that Python do not have constant types. A “constant” variable, though, is indicated with capital letters:

SCHOOL_NAME = 'Hogwarts School of Witchcraft and Wizardry'


def invite_student():
    return f'We are pleased to inform you that you have been accepted at {SCHOOL_NAME}.'


print(invite_student())
# -> We are pleased to inform you that you have been accepted at Hogwarts School of Witchcraft and Wizardry.

Type Hints

Python is a dynamically-typed language, however, we can still use type hints to make sure we avoid TypeErrors.

For example, as you can find the similar example in the documentation for typing, we can indicate the expected types for arguments and return values of a function:

def greeting(name: str) -> str:
    return f'Hello, {name}!'

Also, as mentioned in the lecture, mypy is a popular library that you can use for type hinting.

Docstrings

Docstrings can occur in a module, a function, or a class. The simplest one-line docstring looks like this:

def add(n, n1):
    """Add two numbers."""
    return n + n1

The conventions on how to use docstrings can be found here in this PEP.

argparse

argparse is a module that comes built-in with Python, literally a “parser for command-line options, arguments and sub-commands”.

There is a great tutorial on the official documentation already, so, we are not going to dive deep into it here. The simplest thing you can do might look like this. Say, we have a file called spell.py, and we want to pass in the argument -s to our program to indicate the type of spell we want to create. We want the proper incantation printed on our terminal. Let’s see:

# 📁 spell.py

import argparse


incantations = {
    'patronus': 'Expecto Patronum!',
    'summon': 'Accio!',
    'unlock': 'Alohomora!',
    'explode': 'Bombarda!',
    'levitate': 'Wingardium Leviosa!',
    'stun': 'Stupefy!'
}

parser = argparse.ArgumentParser()
parser.add_argument('-s')
args = parser.parse_args()

print(incantations[args.s])

We can see it with the right command:

$ python spell.py -s unlock
Alohomora!

*args, **kwargs

We have mentioned the unpacking operators briefly in a previous post on problem set 4. The example looked like this:

values = [0, 5, 2]
print(*values) # 0 5 2

# Prints 0, 2, 4 respectively
for i in range(*values):
    print(i)


houses = {
    'Gryffindor': 'courage',
    'Ravenclaw': 'intelligence',
    'Hufflepuff': 'loyalty',
    'Slytherin': 'ambition'
}

people = {
    'Harry Potter': 'Gryffindor',
    'Hermione Granger': 'Gryffindor',
    'Luna Lovegood': 'Ravenclaw'
}

print({**houses, **people}) # {'Gryffindor': 'courage', 'Ravenclaw': 'intelligence', 'Hufflepuff': 'loyalty', 'Slytherin': 'ambition', 'Harry Potter': 'Gryffindor', 'Hermione Granger': 'Gryffindor', 'Luna Lovegood': 'Ravenclaw'}

They are super handy for many kinds of problems you encounter, so, another great tool in our toolkits.

map

With the map function, we can map a function to each item of an iterable. Creating a list of the squares of each number in a “numbers” list might look like this:

numbers = [3, 5, 7, 11, 13]

squared = list(map(lambda n: n**2, numbers))

print(squared) # [9, 25, 49, 121, 169]

Notice that we also convert the return value of map to a list, as the map function returns a Map object.

List comprehensions

If you have been following the series, you already know about the list comprehensions way back in Problem Set 2. It is a Pythonic way to append to a list, so instead of doing something like this:

word = 'CS50'

digits_in_word = []

for char in word:
    if char.isdigit():
        digits_in_word.append(char)

print(digits_in_word) # ['5', '0']

Just write a one-liner that achieves the same result:

word = 'CS50'

digits_in_word = [char for char in word if char.isdigit()]

print(digits_in_word) # ['5', '0']

filter

We can also filter an iterable, returning only the values we are interested in.

The same example above in list comprehensions can also be solved like this:

word = 'CS50'

digits_in_word = list(filter(str.isdigit, word))

print(digits_in_word) # ['5', '0']

Also, just like in map, notice we also convert the return value to a list. We also do not call the str.isdigit inside filter, we only pass a reference to that function.

Dictionary comprehensions

Similar to list comprehensions, dictionary comprehensions are also another —sometimes elegant, sometimes not— way to create dictionaries. To implement a very simple one, let’s initialize all the Hogwarts house points to 0 for the start of the term:

houses = ['Gryffindor', 'Hufflepuff', 'Ravenclaw', 'Slytherin']

house_points = {house: 0 for house in houses}

print(house_points) # {'Gryffindor': 0, 'Hufflepuff': 0, 'Ravenclaw': 0, 'Slytherin': 0}

It works as intended, and initializes all the house points 0.

enumerate

Here is a Pythonic way to iterate over an iterable. Similar to the lecture example, let’s say that this time we want to print the names of the houses, also indicated with the first value of ‘1’, instead of ‘0’. We do not have to write something like this:

houses = ['Gryffindor', 'Hufflepuff', 'Ravenclaw', 'Slytherin']

for i in range(len(houses)):
    print(i + 1, houses[i])

# ->
# 1 Gryffindor
# 2 Hufflepuff
# 3 Ravenclaw
# 4 Slytherin

There is a more elegant way to do it:

houses = ['Gryffindor', 'Hufflepuff', 'Ravenclaw', 'Slytherin']

for index, house in enumerate(houses, start=1):
    print(index, house)

# ->
# 1 Gryffindor
# 2 Hufflepuff
# 3 Ravenclaw
# 4 Slytherin

Notice that the enumerate function also takes a start argument to start from the number that is passed.

Generators

Finally, also mentioned in the lecture, a generator function is a “function that returns a generator iterator”. With a generator function that yields as opposed returns a value, we can save memory with lazy evaluation. The Python documentation also has a tutorial on generators, and similar to the example in the lecture, the very simplest implementation might look like this:

def main():
    for _ in gen(1000000):
        print(_)

def gen(n):
    for _ in range(n):
        yield _

if __name__ == '__main__':
    main()

These are all the topics we have explored this week. From now on, we are left with our very own Final Project to implement. For this, you are free to create anything that excites you, any kind of problem that you want to solve — of course, following the given specifications for the project. And, after that, you might think that is all, and that we are finished, but, are we?

Conclusion

Now, looking back, we have gathered many useful tools in our toolkit to do whatever we want to do. But, should we do whatever we want to do just because we can? You probably have ideas for the answer to that question. It is easy to get excited about all kinds of things you can create once you know how to do them. But, once you start to create things, always remember that using and trusting technology as a solution to all problems is not always the case. Now that you have the power and knowledge to do so, remember that it is absolutely vital to create software that respects users’ freedom, that is open and trustworthy. Remember that privacy is a human right, even if there might have already been much talk about it — yet, usually without honesty. Do not underestimate your current level of knowledge, you have tremendous power in your hands with the tools you can use. And, yes, one more thing to remember — code is speech. These all might sound like out of context, why should you even bother to think about them? After all, assuming you only have taken this introductory course, and are still at the beginning of your programming journey, and have a long way to go. But, hopefully, you undoubtedly agree that we need good things in life — good software that respects human dignity and helps the progress of humanity is one of them. If you think these are some grand ideologies for a “beginner” like you —which, honestly, I also consider myself a “beginner” in many things at this point—, remember that each piece of knowledge will eventually add up to another, so, even if you are not going to pursue a programming path in your life at all; that is fine, because at least this will be how you look at things, have a stronger sense of self-agency, and a more educated opinion in the decisions that affects us all.

With that in mind, that is the end of the series! If you have read so far, thank you. And, as always, happy coding. 💜