Intermediate: Advanced Iteration

Welcome to the Week 3 Intermediate Python Notebook. This notebook is designed for students who already have some experience with Python and are ready to build on the basics.

Your task today is to read through the material carefully and complete the exercises provided at the end. These exercises are an important part of the learning process and will help you check your understanding.

Important: Before starting this notebook, make sure you are confident with everything in the Beginner notebook. This notebook builds directly on the concepts of iterators, iterables, for loops and while loops introduced in the Beginner notebook so you should attempt at least \(4\) of the Beginner exercises before moving onto this.

This notebook builds on basic for and while loops by exploring advanced iteration techniques in Python. It covers sequence unpacking in for loops, the continue, break and pass statements and the lesser-known else clause for for loops.

Be sure to work through the examples and attempt all the exercises. They are designed to reinforce your learning and build your confidence.

Table of Contents

dicts as iterables.

The Beginner notebook for this weeks class introduces the notions of iterators and iterables, and shows how you can iterate over lists and strings. In this notebook, we will start by looking at a slightly more complex example; dictionaries.

Dictionaries (dicts) are also iterable in Python, but what exactly does that mean? When you iterate over a dictionary directly, you get its keys:

Note: If you are unfamiliar with the dict datatype, take a look at the intermediate collections notebook for week 1 of this course.

By default, iterating over a dict gives you its keys. However, you can also access values or key-value pairs using methods like .keys(), .values(), and .items(). Let’s have a look at these, using the above dictionary:

Let’s take a closer look at this last example. In this case, the iterable student_grades.items() seems to be giving us two values, name and grade, rather than one. What is happening here?

Let’s make student_grades.items() into an iterator using the iter keyword and print off it’s items one-by-one using the next keyword (further detail on iter and next can be found in the beginner notebook).

We can now see what is happening, the items() iterable object is giving us a sequence of tuples and when we write for name, grade in student_grades.items(), Python is treating the first element of each tuple as name and teh second element as grade. This is an example of sequence unpacking and is a bit like how you can write the following for tuples.

We’ll see more examples of tuple unpacking in for loops in the next section when we look at the zip and enumerate functions.

enumerate and zip

In the last section, we saw how unpacking items from a dictionary was an example of sequence unpacking in a for loop. In this section, we’ll explore two more convenient tools that use sequence unpacking: the enumerate and zip functions. To understand why these are useful, let’s start with a motivating example.

Here we have three lists of equal length and we want to loop over them all at the same time.

While this works, it’s a bit cumbersome. We’ve we spent several lines just defining variables inside the for loop. This is where the zip function comes in handy. zip takes multiple iterables (like lists) and pairs their elements together, allowing you to iterate over them in a single loop:

This code achieves the same result but is much cleaner. Here, zip(list_a, list_b, list_c) creates an iterable that yields tuples like ('Happy', 'dog', 'runs'), ('Sad', 'cat', 'sleeps'), and so on. We’re then using sequence unpacking inside the for loop to assign each part of the tuple to a, b, and c. This works for any number of lists (as long as they’re iterable), and it automatically stops at the end of the shortest one if they’re not all the same length.

Now, let’s look at another common situation where sequence unpacking is very useful: when you need both the value from a list and its index (position) during iteration. For example, suppose you have a list of exam scores and want to flag any that are below a passing mark, reporting the student’s position in the list (say, the student_number) in the process. One approach might be to write:

Again this is a little cumbersome; we have an extra line of code defining score inside the loop. It would be nice to use sequence unpacking to remove this. This is where we can use the enumerate function, like so:

In this case, enumerate(scores) yields tuples like (0, 85), (1, 92), etc and we unpack them directly into student_number and score in the for loop.

Summary: - zip lets you loop over several lists at once by pairing their elements into tuples - enumerate gives you both the index and the value while you loop, so you don’t have to count manually

continue, break and pass Statements

In the last few sections, we explored how you can use sequence unpacking to improve your loops readability. In this section, we’re going to investigate a different set of Python tools, which can also give you finer control and improve readability when it comes to writing loops; these are the continue, break, and pass statements. We’ll now look at each of these in turn.

Let’s start with continue. The continue statement allows you to skip the rest of the current loop’s code block and immediately jump to the next iteration. For instance, in the below code, when i==2 we skip over the print statement and move to the next iteration of the loop.

The above code is equivalent to running:

Whether or not to use the continue statement often comes down to personal preference. In some cases, however, it can make code more readable by reducing indentation - for example, notice how print(i) is less indented in the version of the above code that uses continue.

The break statement is similar to continue - both give you control over the flow of a loop. But while continue lets you skip ahead to the next iteration, break lets you end the whole thing right then and there.

For example, the following loop will “break” when i==2 and terminate completely:

This prints only 0 and 1, because as soon as i hits 2, the break kicks in and the loop stops dead. There are no more iterations, and no more printing.

This kind of early exit can be very useful when you know you might not need to go through the entire sequence. For instance, imagine you have a list of numbers and you’re just looking for the first duplicate - you don’t care about any others after that. In such a case, you might want to use a break as shown below:

Finally, there’s the pass statement. This statement doesn’t change the flow of your loop at all. It’s just there to keep Python happy when you need a statement syntactically but don’t want to do anything yet. Think of it like a sticky note saying “I’ll fill this in later.” For example, the below code will throw an error if we try to run it.

However, if we put a pass inside the loop, then the code will ignore the incomplete body, like so:

This is a handy statement to know when you are working on large complex coding projects with lots going on!

for and else

You might be surprised to learn that Python’s for loops can have an else clause! It’s a somewhat unusual feature, but can be very useful in specific situations.

The else block associated with a for loop is executed if and only if the loop completes normally. In other words, the else is executed if the loop completes it’s iteration without encountering a break statement. If a break statement is executed inside the loop, the else block is skipped.

Here’s an example to illustrate:

In this case, the loop completes without finding the number 6, so the else block is executed, printing "Did not find 6 in the list.".

Now, let’s change the list:

This time, the loop encounters the number 6 and executes the break statement. As a result, the else block is not executed.

You’ll most often see for...else in search scenarios where you need to take action if an item isn’t found. The else block serves as your “search failed” handler, making the code’s logic more explicit and self-contained without requiring additional variables to whether the item was found or not.

Exercises

Question 1: You are given two lists. One contains students’ names, and the other contains their exam scores in the same order:

Write a for loop using the zip() function that prints each student’s name along with their score in the following format:

Alice scored 85
Bob scored 92
Charlie scored 78

Question 2: Below is a list of strings. Using a for loop and the enumerate function create a list of indices of the strings containing the letter "t". For instance if the list was ["bat", "dog", "rat"] your code should return [0, 2].

Question 3: The following code was intended to print every odd number from the list numbers. However, in the example below it only prints the number 1. Explain why the code does not work as intended, and then fix the error by changing only one line of code.

Question 4: You are given a dictionary where the keys are unique, but the values may repeat. Write a for loop that creates a new dictionary by swapping the keys and values from the original dictionary. This means that:

  • The new keys should be the old values.
  • The new values should be lists containing all the old keys that had that value.

For instance, if the original_dict was given by:

original_dict = {
    'a': 1,
    'b': 2,
    'c': 1,
    'd': 3,
    'e': 2
}

Then your code should output:

inverted_dict = {
    1: ['a', 'c'],
    2: ['b', 'e'],
    3: ['d']
}

In your solution, you must use sequence unpacking in the for loop (i.e., unpack the key and value directly in the loop header).

Question 5: The following program is supposed to check each day in the list days and print whether it is a weekday or at the weekend. However, something has gone wrong! In the below code the day Monday is skipped entirely. Explain what has gone wrong here and fix the error.