Recap 1

Had WillHad Will
29 min read

< Previous Post

After writing a few posts, I started to feel lost: I had taken a loose approach to tracking the topics I covered. Did I post about dispatch techniques in my second or third post? Maybe in the first?

I accepted my fate and got to listing them one by one on a piece of paper. As I was writing, I figured this was the beginning of a recap post.

Looking back, you've made a lot of progress. You've accumulated some valuable programming mileage. Give yourself a pat on the back cause you can be proud of yourself 🎉🥳

This post will recap all the topics we've covered so far. If you haven't read them, here are the articles this post will cover:

  1. Part 1: We wrote a program that asks for user input and create a personalized greeting.

  2. Part 2: A restaurant bill calculator

  3. Part 3: A grocery list program

  4. Part 4: A simple poker card game

  5. Part 5: A barbershop appointment booking system

Variables and data types

What's a variable? What's a constant?

Variables are names referring to values in memory. You assign a value to a variable with =. To change a variable's value, you reassign it with =.

>>> x = 10
>>> x
10

>>> x = 12
>>> x
12

Constants are variables whose values don't change. We recognize them by their all-caps names. This is a convention between programmers, and Python doesn't enforce it.

PLANETS = [
    'Mercury',
    'Venus',
    'Earth',
    'Mars',
    'Jupiter',
    'Saturn',
    'Uranus',
    'Neptune'
]

Try to give meaningful names to your variables. student_age is better than st_a, or a. Remember that you might return to your code in a few months. By then, you'll be happy you spent a few moments creating descriptive names!

Let's review the various data that can go in a variable.

Integers and floating-point numbers

Integers are whole numbers. They can be positive or negative. Python allows integers of arbitrary size. You can go as big as your computer memory will allow!

Floats, in contrast, represent numbers with decimal points. They work like the scientific notation you learned in high school.

You can't represent all possible numbers as floats: there are max and min values.

The biggest positive float is 1.7976931348623157e+308. The positive float closest to zero that has good precision is 2.2250738585072014e-308.

You can go even closer to zero but will lose some precision.

There's little chance you'll need to care about these values, which can even vary from system to system. Use sys.float_info if you need to check them. Here's what it looks like on my computer.

>>> import sys
>>> sys.float_info

sys.float_info(max=1.7976931348623157e+308, max_exp=1024,
max_10_exp=308, min=2.2250738585072014e-308, min_exp=-1021,
min_10_exp=-307, dig=15, mant_dig=53,
epsilon=2.220446049250313e-16, radix=2, rounds=1)

While integers are exact values, floating points are subject to precision errors. Keep this in mind because it can cause annoying bugs.

>>> 0.1 + 0.2
0.30000000000000004

Python converts your numbers from ints to floats as needed. Use the int and float functions if you need to be explicit. Those two functions also work to convert strings to numbers.

Booleans

Booleans are the values True and False.

You convert values to a boolean by using the bool function. It's intuitive: 0, None, "", [], {}, or () become False. Nonzero numbers, nonempty lists, and strings become True.

>>> bool(())
False

>>> bool((1))
True

>>> bool(2)
True

>>> bool(None)
False

Python also converts to a boolean when needed: 2 and True is True.

Strings

A string is a sequence of characters. We delimit strings with either single ('), double("), or triple (""") quotes.

my_string = "This is a string."
other_string = 'Don\'t forget to escape special characters!'
multiline_string = """ This is a multiline
string."""

Strings support indexing and slicing.

>>> s = "Hello."

>>> s[1]  # indexing
'e'

>>> s[1:3]  # slicing
'el'

Slicing can take up to three arguments inside brackets, separated by a colon <:>. The first is the slice's start, the second is the end, and the third indicates whether you go in order or reverse.

>>> s = "Hello."

>>> s[1:3:1]
'el'

>>> s[3:1:-1]
'll'

>>> s[:3]
'Hel'

Try some combinations until you get a good grasp of slicing strings.

Slicing is how you reverse a string, too.

>>> s[::-1]
'.olleH'

We saw some methods on strings:

  • strip: my_string.strip() removes leading and trailing whitespace. e.g., " a ".strip() returns a new string "a".

  • isdigit checks if the string contains only numerical characters. e.g., "123".isdigit() is True, while "abc1".isdigit() is False.

  • lower converts a string to lowercase characters. e.g., "Aa1Bb".lower() returns a new string "aa1bb".

  • capitalize capitalizes the first letter of a string. The other characters are in lowercase. e.g., "i love Python".capitalize() returns the new string "I love python". Note that if there was already a capital letter after the first character, it will be in lowercase.

  • title capitalizes the first letter of each word and turns the other letters into lowercase. e.g., "i lOve Python".title() returns the new string "I Love Python".

  • rsplit(separator, maxsplits) splits the string into a list. It starts from the right and uses the separator to choose where to split. The number of resulting parts is one more than maxsplit. If you split once, you'll have two parts; if you split twice, you'll get three. If you don't specify a maxsplit, there's no limit on the number of splits. When you don't specify a separator, any whitespace will be a separator. e.g., "This will return a list of all the words".rsplit() returns ['This', 'will', 'return', 'a', 'list', 'of', 'all', 'the', 'words'].

Basic operations

Arithmetic operations

Arithmetic operations are +, -, /, % (modulo), and ** (power). When operands (i.e., the numbers) are of a different type (e.g., a float and an int), Python will handle the conversions.

Logical operations

Python provides logical operators: and, not, or, xor.

The result is either True or False. Look at the truth tables below.

AND Truth Table
---------------

 A | B | A AND B
---+---+--------
 0 | 0 |   0
 0 | 1 |   0
 1 | 0 |   0
 1 | 1 |   1
OR Truth Table
--------------

 A | B | A OR B
---+---+-------
 0 | 0 |   0
 0 | 1 |   1
 1 | 0 |   1
 1 | 1 |   1
XOR Truth Table
---------------

 A | B | A XOR B
---+---+--------
 0 | 0 |   0
 0 | 1 |   1
 1 | 0 |   1
 1 | 1 |   0
NOT Truth Table
---------------

 A | NOT A
---+------
 0 |   1
 1 |   0

Comparison operations

Here are the ways to compare values in Python:

  • ==: equal to

  • !=: not equal to

  • >: greater than

  • <: smaller than

  • <=: smaller or equal

  • >=: greater or equal

The operators work on numbers. Be careful when comparing floats, as precision errors are always possible.

But you can compare other data types.

Like strings:

>>> "hello" == "hello"
True

>>> "Hello" == "hello"  # case sensitive
False

Booleans: True is 1 and False is 0.

>>> True == 1
True

>>> False == 0
True

>>> True == 'a'  # True == 1 != 'a'
False

>>> True < 2 # pretty useless but funny
True

Lists and tuples: the items' values AND ordering must be the same for equality.

>>> [1, 2, 3] == [1, 2, 3]
True

>>> (1, 2, 3) == (3, 2, 1) # not the same order
False

>>> [1, 2, 3] == (1, 2, 3) # a list and a tuple
False

Sets and dictionaries: only the elements' values matter for equality, not their ordering.

>>> {"a": 1, "b": 2} == {"a": 1, "b": 2}
True

>>> {1, 2, 3} == {3, 1, 2}
True

You can even compare the None type.

>>> None == None
True

>>> None == 0
False

Membership testing

Use the in and not in keywords to test if an item is in a sequence, be it a list, dictionary, set, or tuple.

>>> 2 in [1, 2, 3]
True

>>> 1 in (1, 2, 3)
True

>>> "a" in {"a'": 1, "b": 2}
True

>>> 1 in {1, 2, 3}
True

>>> 1.0 in (1, 2, 3)  # type conversion
True

Here's a common idiom. (let's say we have a program dealing with birds)

<some code>

if bird not in birds:  # let's say birds is a dictionary
    birds[bird] = some_value

<some other code>

Control flow

Programs need to be more than linear. They need to make decisions, which is where control flow comes in.

Conditional statements

The most basic way to control the flow of the program is with an if statement.

Here's a general template.

if <condition 1>:
    <code 1>
elif <condition 2>:
    <code 2>

... More elif branches can go here ...

else:
    <catch-all code>

Read it as "if <condition 1> is True, do <code 1>. If <condition 1> is False, but <condition 2> is True, do <code 2>. If no condition is ever met, do the <catch-all code>."

To understand elif, consider the difference between these two pieces of code. Before typing them in a Python environment, try to figure out what each does.

Here's elif in action.

x = 1
y = 2

if x == 1:
    print("x is 1")
elif y == 2:
    print("y is 2")

And here's the same code but with if instead of elif.

if x == 1:
    print("x is 1")

if y == 2:
    print("y is 2")

The conditions in if and elif statements need to evaluate to either a True or False value. This means:

  • Simple predicates with comparison operators. e.g., x == 2, y < 3, etc.

  • More complex predicates gluing simple predicates with logical operators. e.g., x == 2 and y < 3.

  • Membership testing, i.e., testing if an item is an element of a sequence. e.g., if fruits is a list of fruit name strings, you can check if the list contains the string "banana" with if "banana" in fruits.

  • "Truthy" and "Falsy" values. Remember we said that 0, "", [], {}, (), and None evaluate to False. Those are "Falsy" values. All values that eval to True are "Truthy" values. This means if you have a variable my_var, you can use it as a predicate in an if statement: if <my_var>:

for loops

Here’s the general form of a for loop.

for <element> in <collection>:
    <code involving element>

You can translate this to "for each element in the collection, execute some code". In fact, outside of Python, the programming jargon for this type of loop is a "for-each" loop rather than a for loop.

For loops iterate over sequences and ranges. This is what we call definite iteration: we know the number of repetitions in advance. If we iterate over a 3-element list, we know for sure we'll have three iterations.

Here's a basic for loop iterating over a list.

>>> fruits = ["banana", "strawberry", "cherry", "apple", "pear"]
>>> for fruit in fruits:
...     print(fruit.title())

Banana
Strawberry
Cherry
Apple
Pear

If you want to iterate over natural numbers, use the range function. The range(start, stop, step) generates all numbers between start (included) and stop (not included), with a step increment.

>>> for i in range(1, 10):
...     print(i, end=" ") 

1 2 3 4 5 6 7 8 9

You can also iterate over each character of a string.

>>> all_caps = "LET\'S TURN THIS TO LOWERCASE"

>>> for char in all_caps:
...     print(char.lower(), end="")
let's turn this to lowercase

The typical way to iterate over a dictionary is to extract key-value pairs with the items method.

>>> bob = {
...     'name': 'Bob',
...     'age': 30,
...     'job': 'Pilot',
... }

>>> for key, value in bob.items():
...     print(f"{key.title()}:\t{value}")

Name: Bob
Age: 30
Job: Pilot

Sometimes, you create 2-D structures. A common way to traverse them is with nested for loops.

>>> for i in range(4):
...     for j in range(5):
...         print(f"i:{i} j={j}", end="\t|\t")
...     print("\n")

i:0 j=0 | i:0 j=1 | i:0 j=2 | i:0 j=3 | i:0 j=4 |
i:1 j=0 | i:1 j=1 | i:1 j=2 | i:1 j=3 | i:1 j=4 |
i:2 j=0 | i:2 j=1 | i:2 j=2 | i:2 j=3 | i:2 j=4 |
i:3 j=0 | i:3 j=1 | i:3 j=2 | i:3 j=3 | i:3 j=4 |

while loops

While loops repeat a block of code as long as a condition is True. This is indefinite iteration: the while statement has no idea how many times it will loop.

Here is a basic while loop

>>> x = 0

>>> while x < 4:
...     print(x)
...     x = x + 1

0
1
2
3

Can you guess the output if you switched print(x) and x = x + 1?

One practical application of while loops is infinite loops. For example, you might want to query users for input until they type "quit". A break statement can stop the loop.

while True:
    response = input("Type "quit" to stop: ")
    if response == "quit:
        break

for vs while

Use for when…Use while when…
You know how many iterations you needYou don’t know the number of iterations
You iterate ofver a sequence (list, dictionary, string)You’re waiting for a condition to change
You’re using range()You're waiting for user input or checking a state.

Loop control with break, continue and else

To exit a loop, use a break statement.

For example, in a for loop.

>>> for num in range(1, 6):
...     if num == 3:
...         break
...     print(num)

1
2

And here's a while loop that does the same thing.

>>> x = 0

>>> while x < 5:
...     x += 1
...     if x == 3:
...         break
...     print(x)

1
2

If you don't want to exit the loop but rather skip some iterations, use the continue statement.

The following code prints all numbers not divisible by three and smaller than 11. It uses a continue statement to skip multiples of 3 (and 3 itself).

>>> for i in range(1, 11):
...     if i % 3 == 0:
...         continue
...     print(i)

1
2
4
5
7
8
10

Try to write the corresponding while loop, then check your solution against mine.

>>> x = 0

>>> while x < 10:
...     x += 1
...     if x % 3 == 0:
...         continue
...     print(x)

1
2
4
5
7
8
10

If you decide to execute some code after the loop, add an else clause.

>>> for num in range(1, 4):
...     print(num)
... else:
...     print("Loop finished!")

1
2
3
Loop finished!

Let's do the same with a while loop.

>>> x = 0

>>> while x < 3:
...     print(x)
...     x += 1
... else:
...     print("Loop completed!")

If the program execution exits the loop with a break statement, the else block won't execute.

>>> for i in range(1, 10):
...     if i % 3 == 0:
...         break
...     print(i)
... else:
...     print("won't print")

1
2

Functions

Functions are pieces of code that you can call from the rest of your program. Use them to break down problems into manageable parts and to avoid repetitive code.

Defining and calling functions

Define functions with def, then the function's name, then parentheses with the arguments. Don't forget to end the line with a colon (:).

You then add the function's body, indented right from the definition. A function's body is the instructions that do the actual work.

def function_name(arguments):
    body

You call a function by its name, with its arguments (if any) in parentheses.

Function arguments

There are four kinds of arguments in Python

  • positional arguments

  • arbitrary arguments (*args)

  • keyword arguments

  • keyword arbitrary arguments (**kwargs)

Positional arguments are your plain old regular arguments. You must pass them in the order in which they appear in the function definition.

>>> def my_function(a, b):
...     print(f"a is {a}")
...     print(f"b is {b}")

>>> my_function(2, 3)

a is 2
b is 3

You must provide as many arguments in your function call as there are parameters in your function.

Arbitrary arguments let you pass a variable number of arguments to a function.

>>> def sum_all(*numbers):
...     return sum(numbers)

>>> print(sum_all(1, 2, 3))
6

In sum_all's body, numbers is a tuple.

Keyword arguments let you pass arguments using their names.

Imagine we were in the business of launching rockets off Cape Canaveral. We could create a function to launch rockets, taking some default parameters.

def rocket_launch(latitude=28.396837, longitude=-80.605659, payload=0, stages=3):
    <launch sequence>

We can launch without explicit arguments if we're OK with the default params. rocket_launch()

We can also change the parameters to send a 2-stage rocket. rocket_launch(stages=2)

Sometimes, you don't know in advance the exact arguments you'll need AND also want keywords. That's where keyword arbitrary arguments (**kwargs) come in. Here's an example.

def show_info(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

show_info(name="Alice", age=30, job="Engineer")

In the function's body, kwargs is a dictionary: the keywords are the keys, and the arguments are the values.

Return values and None

When a function finishes executing, it returns control of the program to its caller. From the caller’s point of view, the evaluated function is its return value. Let's look at an example:

c = 0

def callee(a, b):
    print("the caller only sees the return value")
    c = a + b  # caller doesn't care about this line
    return a * b  # caller only cares about the return value

def caller(a, b):
    print(callee(a, b))  # callee(a, b) is the same as a * b

In our somewhat contrived example, caller calls callee. Despite callee having a print statement and an assignment, caller doesn't 'see' them. It can only use the return statement. This doesn't mean the body doesn't do interesting work (like callee's print statement).

The default return statement is None.

Python functions can pack several values together and return them as a tuple. return 3, 4 returns the tuple (3, 4).

Anonymous functions and lambdas

Sometimes, you want a throwaway function: its name is unimportant, and you'll use it only once. An example we saw was <sort>. It takes several arguments, among which is a "sorting function", often written as a lambda.

The syntax for defining anonymous functions is

lambda arguments: expression

For example:

>>> (lambda x, y: x + y)(1, 3)  # note the parentheses
4

Here is another example using arbitrary arguments:

>>> (lambda *numbers: sum(numbers))(1, 2, 3, 4)

10

Here's how you use lambdas with the sort method.

>>> words = ['we', 'will', 'sort', 'by', 'word', 'length']
>>> words.sort(key=lambda word: len(word))

>>> words
['we', 'by', 'will', 'sort', 'word', 'length']

Now let's sort by word length modulo three.

>>> words.sort(key=lambda word: len(word) % 3)

>>> words
['length', 'will', 'sort', 'word', 'we', 'by']

I/O: input & output

Programs communicate with the world through input and output. Up to now, we've been using simple textual I/O.

input()

The input function waits for user input during program execution. It returns the user input as a string, which you can store as a variable. Provide a nice prompt as an argument when calling input.

name = input("Enter your name: ")
age = input(f"How old are you, {name}? ")

input doesn't do much to your input: it stores it as is in a string. If a string is not the best type for your data (e.g., age should be an int, not a string), you must convert it further.

Cleaning input

One of the first things you can do to user input is to strip leading and trailing whitespace with strip.

username = input("Enter your name: ").strip()

String methods I find useful to change the case of your input's characters are:

  • title: words start with a capital letter, and the other letters are lowercase. "This Is In Title Case."

  • capitalize: the first character in the string is in uppercase, and the rest is in lowercase. "Only the first letter becomes a capital letter.

  • lower: all the characters are in lowercase. "this is all in lowercase".

  • upper: all the characters are in uppercase. "LOOKS LIKE I\'M YELLING!"

Validating & converting input

I showed you a simple (and error-prone) way of validating and converting input.

  1. You get input.

  2. Check it to prevent errors.

  3. Then, use the input in your code once you know it's valid.

Validating before using inputs is the LBYL approach (Look Before You Leap).

This is error-prone because you have to consider everything that could go wrong and defend against it. In a later post, I'll show you an alternative way of handling errors.

The first step is to check if the user has entered an input, not an empty string.

user = input("Enter your name: ").strip()

while not user:
    print("You didn't enter anything.")
    user = input("Enter your name: ").strip()

After ensuring the user has entered something, you'll want to make sure it's the right type. Only then can you convert the input to the desired type.

age = input("Enter your age: ").strip()

if age.isdecimal():
    age = int(age)
else:
    print("Invalid input. Please enter a number: ")

We could combine the two examples above.

user_age = get_age()

while not user_age:
    while not age.isdecimal():
        user_age = get_age()

user_age = int(user_age)

We've used isdecimal to check if we could convert a string to a number. There are other string testing methods.

  • str.isalnum: returns True if the string is not empty and holds only alphanumeric chars.

  • str.isalpha: returns True if the string is not empty and holds only alphabetic chars.

  • str.isascii: returns True if the string is empty (!!) or holds only ASCII characters. Read more on ASCII characters here.

  • str.isdecimal: returns True if the string is not empty and holds only numeric chars in base 10 (i.e., regular numbers).

  • str.isdigit and str.isnumeric: like str.isdecimal but accept a broader set of characters, like ². <"²".isdigit()> and <"²".isnumeric()> both return True, but <"²".isdecimal()> is False.

  • str.islower, str.istitle, str.isupper: return True if the string is not empty and also tests for case.

print & pprint

We've covered input; now, let's move on to output. Our programs have used simple textual output, for the sake of simplicity.

In its simplest form, the print function takes a variable number of objects and prints them.

>>> print("a")
a

>>> print(1, 2, "3")
1 2 3

You control print's behavior with keyword arguments. Among those, sep and end control how the output looks like. sep is a string separating your inputs: print(1, 2, 3, sep=", ") is 1, 2, 3. end is what comes at the end of your output.

>>> print(1, 2, end=" -- this is the end of the output --")

1 2 -- this is the end of the output -->>>

The default value for sep is a single space; for end, it's a newline. You can learn more about print here.

Printing out your function outputs or data structures is often helpful as we program. This ensures the code does what it's supposed to. When the data structures get too big or too complex, you want something more practical than print.

Use pretty-printing, part of the print module, to format your strings with proper indentation, line breaks, and spacing.

from pprint import pprint

data = {
    "name": "Alice",
    "age": 30,
    "hobbies": ["reading", "coding", "hiking"],
    "address": {
        "city": "Paris",
        "country": "France"
    }
}

pprint(data)

Lists

Lists are collections. They hold items in a specific order. This means you can access elements by their position or index, like in strings.

Lists can hold items of any type. A list inside a list is a nested list.

Creating lists

The simplest way to make a list is with square brackets. my_list = [] creates an empty string and assigns it to the variable my_list. You can also define non-empty lists: fruit_list = ['apples', 'bananas', 'strawberries']. Note the commas separating each element.

Another way to create a list is with the list function. You hand it a sequence to turn it into a list.

>>> list('abc')
['a', 'b', 'c']

>>> list(('a', 'b', 'c'))
['a', 'b', 'c']

The last way to create a list is with a list comprehension, which we'll see at the end of this section.

Accessing list elements

Since lists are ordered sequences, we can access their elements by their index.

>>> cars = ["Ford", "Ferrari", "Hyundai"]

>>> cars[0]
"Ford"

>>> cars[1]
'Ferrari"

>>> cars[-1]
"Hyundai"

>>> cars[-2]
"Ferrari"

Notice how we can index from beginning to end (0, 1, 2, 3, etc) and from end to beginning (-1, -2, -3, etc).

You'll get an error if you try to use an item that is out of range.

Modifying lists

Since lists are mutable sequences, you can change, add, and remove elements.

You add an element using the append method or inserting it at a specific index.

>>> fruits = ["apple", "banana", "cherry"]

# Append to end
>>> fruits.append("orange")  
['apple', 'mango', 'cherry', 'orange']

# Insert at specific index
>>> fruits.insert(1, "grape")  
['apple', 'grape', 'mango', 'cherry', 'orange']

To change an item, access it by index.

fruits[1] = "strawberry"

To remove an element, either use the remove method, the del keyword, or the pop method.

Use list.remove when you want to remove an item by specifying its value.

fruits.remove("mango")

The del keyword lets you remove an item by index.

del fruits[2]

The pop method removes and returns the last element of a list.

last_fruit = fruits.pop()

Looping over lists

The most common way to loop over a list is with a for loop. It's common practice to name the items after the list, like in the example below.

for fruit in fruits:
    print(fruit)

This makes for clear and readable code.

If you want to use both the list's elements and their index, use the enumerate function.

>>> fruits = ["apple", "banana", "cherry"]

>>> for index, fruit in enumerate(fruits):
...     print(index, fruit)

0 apple
1 banana
2 cherry

List slicing

Like strings, lists are subject to slicing.

>>> my_list =  ['this', 'is', 'a', ['nested', 'list']]

>>> my_list[1:2]
['is']

>>> my_list[::-1]
[['nested', 'list'], 'a', 'is', 'this']

List comprehensions

List comprehensions let you create complex lists with a one-liner.

The basic syntax is

new_list = [expression for item in iterable if condition]

Let's unpack this definition. To understand a list comprehension, you have to start with the iterable (can be a list, a range of numbers, etc). This tells you where the data comes from.

Next, for item in iterable means you go through each element from the data source.

Then, we can either

  • filter those elements with if condition

  • do something from the elements with expression

Let's look through some examples. They'll build upon each other.

We generate the list of the first 10 (non-zero) numbers.

nums = [x for x in range(1, 11)]

Here, x is the expression, x in range(1, 11) tells you the data source. Now we can use a more interesting expression, like squares.

squares = [x**2 for x in range(1, 11)]

Last, we can filter the numbers from our source so that we only consider odd numbers.

squares_odd = [x**2 for x in range(1, 11) if x % 2 == 1]

Let's see how much code we would have written without list comprehensions.

squares_odd = []
for i in range(1, 11):
    if i % 2 == 1:
        squares_odd.append(i**2)

We used a range to generate integers for our list, but we could have used a list.

names = ["joe", "sam", "tom", "bob"]

cap_names = [name.title() for name in names]

Dictionaries

Dictionaries are collections where keys pair with values. The keys are not ordered; rather, their order is an implementation detail, and you shouldn't count on it.

Creating dictionaries

You create a dictionary in very much the same way you would create a list:

  1. By creating an empty dictionary {}, like you would do with an empty list []

  2. By creating the dictionary with some key-value pairs separated by commas.

  3. With a function dict, just like you would create a list with the function list.

  4. With a dictionary comprehension

# With an empty dict
empty_dict = {}

# With key-value pairs
capitals = {
    "France": "Paris",
    "USA": "Washington, D.C.",
    "Italy": "Rome",
}

As you can see above, we separate key-value pairs with commas and include a colon between each key and its value.

# With dict()
scandinavian_capitals = dict(
    Sweden="Stockholm",
    Norway="Oslo",
    Finland="Helsinki",
    Denmark="Copenhagen"
)

dict takes a variable number of keyword arguments. Each keyword become a key, while the arguments will become the values. Note that keywords cannot be strings. In the example above, Sweden, Norway, Finland, and Denmark don't have quotes around them.

# With a comprehension
>>> squares_of_evens = {n: n**2 for n in range(1, 11) if n%2==0}
>>> squares_of_evens
{2: 4, 4: 16, 6: 36, 8: 64, 10: 100}

Values can be of any data type, and you can make nested dictionaries.

>>> built_ins_dict = {
...     'input': {
...         'name': 'input',
...         'type': 'function',
...         'number of args': 1
...     },
...     'float': {
...         'name': 'float',
...         'type': 'function',
...         'number of args': 1
...     },
...     True: {
...         'name': 'True', 
...         'type': 'boolean constant',
...     },
... }

Accessing dictionary values by key

There are two ways to get a value when you know its key:

  • by encasing the key in square brackets

  • by using the <get> method.

Given the capitals dictionary we defined as an example a few paragraphs back:

>>> capitals["USA"]  # Square brackets method
'Washington, D.C.'

>>> capitals["China"]  # There's no China key in the dict
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'China'

The square brackets method produces an error whenever we use a key it doesn't recognize. The get method is more lenient and won't crash your program.

>>> capitals.get("China")

>>> capitals.get("France")
'Paris'

Modifying dictionaries

There's one way to both add and change a value.

capitals["China"] = "Berlin"  # add a (wrong) value

capitals["China"] = "Beijing"  # change the value

If you want to delete a dictionary entry, you have two options: you can use the del keyword or pop a value.

# del keyword
>>> del capitals["China"]

# pop
>>> cn_capital = capitals.pop("China")
>>> cn_capital
'Beijing'

Iterating over keys and values

Since dictionary entries are key-value pairs, we can loop over:

  • keys

  • values

  • key-value pairs

The most basic way to loop is by key.

for key in dict:
    do_something(key)

For example:

>>> for country in capitals:

>>>     print(country)
France
USA
Italy

Use the values method to loop through values.

for value in dict.values():
    do_something(value)

For example:

>>> for capital in capitals.values():
>>>     print(capital)

Paris
Washington, D.C.
Rome

The most useful looping idiom is to loop through key-value pairs.

for key, value in dict.items():
    do_something(key, value)

For example:

>>> for country, capital in capitals.items():
...     print(f"country: {country}\t\tcapital: {capital}")

country: France capital: Paris
country: USA capital: Washington, D.C.
country: Italy capital: Rome

Dictionary comprehensions

Dictionary comprehensions work the same as list comprehension (and all comprehensions, really).

new_dict = {key_expression: value_expression for item in iterable if condition}

In this definition, key_expression defines the keys, and value_expression defines the values. Note that if iterable is a dictionary, you can replace for item in iterable with for key, value in iterable.items().

Here's how you would create a dictionary of the squares of even integers ranging from 1 to 10.

squares_even = {n: n**2 for n in range(1, 11) if n%2==0}

Here's our capitals dict, but now everything's in lowercase:

caps = {country.lower(): capital.lower() for country, capital in capitals.items()}

Sets

Properties of sets

Sets are collections containing unique elements (i.e., no duplicates). They are suitable for membership testing and set operations like union, intersection, etc.

Create sets

Sets, like dictionaries, have curly braces. If you type my_set = {} to create an empty set, you'll get a dictionary instead. That's where the set constructor set() comes in.

empty_set = set()

If you want to create a set with some elements, you can use the set function or curly braces. Both examples below are valid set definitions.

>>> fruits = set(['apples', 'pears', 'kiwis'])
{'kiwis', 'apples', 'pears'}

>>> animals = {'bears', 'cats', 'kiwis'}
{'kiwis', 'cats', 'bears'}

Note that set takes one collection as an argument. You can think of the set function as a converter between collections and sets:

# set with a dict argument
>>> set({'a': 1, 'b': 2, 'c': 3})
{'c', 'a', 'b'}

# set with a list argument
>>> set(['a', 'b', 'c'])
{'c', 'a', 'b'}

# set with a set argument
>>> set({'a', 'b', 'c'})
{'c', 'a', 'b'}

# set with a tuple argument
>>> set(('a', 'b', 'c'))
{'c', 'a', 'b'}

It also works with a range argument:

>>> set(range(0, 3))
{0, 1, 2}

Set operations

There are two kinds of set operations:

  • Constructive Operations: union, intersection, difference, and symmetric_difference. Those create a new set.

  • Relational Operations: issubset, issuperset, and isdisjoint. Those don't create a new set but return a boolean.

The union of sets A and B combine their elements. You can either write A | B or A.union(B).

The intersection of seta A and B isolates their common elements. You can either write A & B or A.intersection(B).

The difference between sets A and B is the elements in A but not in B. You can either write A - B or A.difference(B).

The symmetric difference between sets A and B is the elements in either set but not both. You can either write A ^ B or A.symmetric_difference(B).

We can test if a set A's elements are all part of set B with A.issubset(B). A is a subset of B.

The inverse tests if A is a superset of B with A.issuperset(B). If the result is True, B's elements are all in A.

We can also test if sets A and B share no elements with A.isdisjoint(B).

Check membership and looping

The in keyword lets you check if an element is part of a set.

if "apple" in fruits:
    do_something

This is also useful for looping.

for fruit in fruits:
    do_something(fruit)

Add and remove elements

To add an element to a set, use the add method.

fruits.add("cantaloupe")

To remove an element, you have two options: the remove method and the discard method. If the element was not in the set, removing it will give an error, while discarding it won't.

>>> fruits.remove('tomato')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'tomato'

>>> fruits.discard('tomato')
>>>

Tuples

A tuple is an ordered sequence of immutable elements. You can access an element by its index, but you can't change the tuple after it is created.

Tuple creation

You create an empty tuple with parentheses.

my_empty_tuple = ()

Create a tuple with elements with it in the same way. You can mix data types.

my_tuple = (1, "one", [1, 2, 3], ("a", "b"))

Note that you must add a trailing comma for single-element tuples.

one_item_tuple = ('a',)

Turn a list into a tuple with the tuple function: tuple([1, 2, 3]).

Tuple element access

Access elements by indexing.

>>> fruits = ("apple", "pineapple", "cherry")

>>> fruits[0]
"apple"

>>> fruits[-1]
"cherry"

Pack and unpack tuples

Packing is when you store several values in a tuple. It is the same as creating a regular tuple with parentheses.

Unpacking is when you extract values from a tuple. You write an assignment where the left-hand side is the variables where you unpack the tuple.

# Packing
person = ("Bob", 30, "Astronaut")

# Unpacking
name, age, job = person

You can use * for arbitrary values when unpacking.

>>> numbers = (1, 2, 3, 4, 5)

>>> first, *middle, last = numbers

>>> print(first)
1

>>> print(middle)
[2, 3, 4]

>>> print(last)
5

Conclusion

That was a lot of material.

Congrats!

We didn't review some topics from previous posts, like sorting or dispatching. I included only the info necessary for future lessons.

I encourage you to revisit our previous projects. In light of this review, reread the code, play with it, and change it a bit.

We'll go back to learning with projects in the next blog post.

See you next time!

< Previous Post

0
Subscribe to my newsletter

Read articles from Had Will directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Had Will
Had Will