Lists and tuples in Python — exercises with cuts, a class summary and passes against fails (and the six-element tuple that had only one)
I wanted to store my subjects in a tuple and, for now, I only had one: Python. I wrote subjects = ("Python"), asked how many elements it had and the program answered 6. Six. One subject, six elements. There was no error or warning; my “tuple” simply was not a tuple. It was a string with parentheses around it, and len was counting the letters.
Sequences have a handful of traps like that: silent, with no exceptions, and with an output that almost makes sense. Today we practise with three exercises of increasing difficulty, three snippets to debug and a cheat sheet to take away.
These exercises use the lists and tuples theory and review the practice. You can do them without those, but it will be harder.
Table of Contents
How to use this article
- Read the statement of a level and do not look at the solution yet.
- Write your version, run it and compare your output with the expected one.
- If you get stuck, look only at the 🔵 hint and try again.
- When you finish, compare with the solution and notice why it is written that way, not just that it gives the same output.
No exercise uses input(). The data is fixed in the code so that your output is identical to the expected one.
Level 1 · Basic: cuts of a list of grades
Statement. With the list grades = [6.5, 8.0, 5.5, 9.5, 7.0, 4.5, 10.0], write a program that shows, using only indexes and slices (no loops): the first grade, the last one, the first three, the last two, all but the first and the last, every other one starting from the first, and the list reversed.
Expected output:
First: 6.5 Last: 10.0 First three: [6.5, 8.0, 5.5] Last two: [4.5, 10.0] Without the first and the last: [8.0, 5.5, 9.5, 7.0, 4.5] Every other one: [6.5, 5.5, 7.0, 10.0] Reversed: [10.0, 4.5, 7.0, 9.5, 5.5, 8.0, 6.5]
The last grade is reached with index -1. For “the last two” leave the end of the slice empty ([-2:]) and to remove the first and the last use [1:-1]. The reversed list is [::-1] and “every other one” is [::2].
Level 2 · Intermediate: the summary of a class
Statement. Write a function summary(grades) that returns a tuple with the minimum grade, the maximum, the average and how many grades are passing (5 or more). Do not use min, max or sum: compute everything with a single traversal of the list. Then unpack the tuple into four variables and show the summary with the same data as in Level 1.
Expected output:
Minimum: 4.5 Maximum: 10.0 Average: 7.29 Passed: 6 of 7
A single traversal means a single for. Inside it you will update four variables: two candidates (minimum and maximum, which start at the first grade), an accumulator for the total and a counter for the passing grades. At the end divide the total by len(grades) and return the four things separated by commas.
Level 3 · Challenge: passes against fails
Statement. We have the students as a list of (name, grade) tuples:
STUDENTS = [("Nora", 7.0), ("Pablo", 3.0), ("Elena", 9.5), ("Hugo", 4.5), ("Sara", 6.5), ("Tomás", 8.0)]
CUTOFF = 5.0
Write split(students, cutoff), which returns two lists of names: those who pass (grade equal to or higher than the cutoff) and those who fail, in the order they appear. Also write first_fail(students, cutoff), which returns the position of the first failing student (or -1 if there is none). Then show the two groups, the first failing student with their tuple and the last three students in the list. To build the lists you cannot use methods: only traversal, concatenation (+) and the neutral element [].
Expected output:
Passed: ['Nora', 'Elena', 'Sara', 'Tomás']
Failed: ['Pablo', 'Hugo']
First failing student at position 1 -> ('Pablo', 3.0)
Last three: [('Hugo', 4.5), ('Sara', 6.5), ('Tomás', 8.0)]
Start with the two empty lists, go through for name, grade in students and, depending on the grade, do passed = passed + [name] or the same with the fails (notice the square brackets: two lists are concatenated). For the position, enumerate and a return inside the loop. The last three are a slice with a negative index.
What is wrong here?
Three snippets with one bug each. Before reading the explanation, do two things: predict whether it gives an error and, if it does not, what it prints.
Snippet 1
has_fail.py
def has_fail(grades):
for grade in grades:
if grade < 5:
return True
else:
return False
print(has_fail([7.5, 4.0, 9.0]))
*Think about it before going on: the list has a 4.0 in the second position. What does python3 has_fail.py print?*
Explanation. There is no error at all, but the answer is wrong:
False
The function decides with the first element. The 7.5 is not a fail, it goes into the else, and the return False ends the function on the first lap; the 4.0 is never even looked at.
Putting the return False of a search in the else inside the loop. A search can only say “no” once it has looked at all the elements.
Fix. Remove the else and leave the return False outside the loop, at the level of the for:
def has_fail(grades):
for grade in grades:
if grade < 5:
return True
return False
print(has_fail([7.5, 4.0, 9.0]))
True
In a search, the positive return goes inside the if and the negative one outside the loop.
Snippet 2
subjects.py
subjects = ("Python")
print(len(subjects))
for subject in subjects:
print(subject, end=" ")
print()
Does it give an error? What does it print? I want a tuple with a single subject.
Explanation. It does not give an error either, which is why it is treacherous:
6 P y t h o n
("Python") is not a tuple: it is the string "Python" inside parentheses, which Python treats like any other expression. That is why len gives 6 and the loop goes through the letters one by one. What builds a tuple is the comma.
Forgetting the comma in a one-element tuple. With two or more elements the commas show up on their own; with one you have to put it on purpose.
Fix. Add the comma after the only element:
subjects = ("Python",)
print(len(subjects))
for subject in subjects:
print(subject, end=" ")
print()
1 Python
If a value “does not behave like a tuple”, ask Python with type(...). If it says <class 'str'>, the comma is missing.
Snippet 3
last_ones.py
grades = [6.5, 8.0, 5.5, 9.5, 7.0, 4.5, 10.0]
print("The last two:", grades[-2:-1])
Does it give an error? What does it print? I want to see the last two grades.
Explanation. It runs with no errors, but one grade is missing:
The last two: [4.5]
In a slice the end is not included. [-2:-1] starts at the second-to-last element and stops just before the last one, so it only lets the 4.5 through. The 10.0 stays out.
Using -1 as the end to reach the last element. Since the end is not included, that last one stays out.
Fix. To reach the end, leave the end empty:
grades = [6.5, 8.0, 5.5, 9.5, 7.0, 4.5, 10.0]
print("The last two:", grades[-2:])
The last two: [4.5, 10.0]
If in doubt, count: grades[-2:] must have 2 elements. If you get 1, the end is missing.
Solutions
Look at this section only when you have tried the exercise. If your code is different but gives the same output and meets the requirements, it is also a valid solution.
Level 1 solution
cuts.py
grades = [6.5, 8.0, 5.5, 9.5, 7.0, 4.5, 10.0]
print("First:", grades[0])
print("Last:", grades[-1])
print("First three:", grades[:3])
print("Last two:", grades[-2:])
print("Without the first and the last:", grades[1:-1])
print("Every other one:", grades[::2])
print("Reversed:", grades[::-1])
Why it is written this way:
- No loop is needed: each line is a direct access.
grades[0]andgrades[-1]are the first and the last element; both are a value, while slices return a list, which is why some come out without brackets and the others with them. grades[:3]starts at the beginning and stops before index 3: positions 0, 1 and 2.grades[-2:]leaves the end empty to reach the end. That is the trick for “the last n”.grades[1:-1]removes one element from each end: it starts at 1 and ends before the last one.grades[::2]goes by twos from the beginning (positions 0, 2, 4 and 6) andgrades[::-1]goes through with a negative step: the list turned around, without modifying the original.
Level 2 solution
summary.py
PASSING = 5.0
def summary(grades):
"""Returns (minimum, maximum, average, passed) in a single pass."""
minimum = grades[0]
maximum = grades[0]
total = 0
passed = 0
for grade in grades:
if grade < minimum:
minimum = grade
if grade > maximum:
maximum = grade
total += grade
if grade >= PASSING:
passed += 1
return minimum, maximum, total / len(grades), passed
grades = [6.5, 8.0, 5.5, 9.5, 7.0, 4.5, 10.0]
minimum, maximum, average, passed = summary(grades)
print(f"Minimum: {minimum}")
print(f"Maximum: {maximum}")
print(f"Average: {average:.2f}")
print(f"Passed: {passed} of {len(grades)}")
Why it is written this way:
- With a single
forwe compute four things. Each one is a different pattern (two extremes, an accumulator and a selective counter) and they do not get in each other’s way because they use different variables. minimumandmaximumstart atgrades[0], the first element. Had we startedminimumat0, the minimum would always be 0, even if nobody got it (the story from the practice).- The two
ifstatements for the extremes go one after the other, not withelif: a grade can be the new minimum and the new maximum at the same time (for instance, the very first one). return minimum, maximum, total / len(grades), passedpacks the four values into a tuple without writing parentheses, and the next line unpacks it into four variables.- The numbers add up: 6.5 + 8.0 + 5.5 + 9.5 + 7.0 + 4.5 + 10.0 = 51.0, and 51.0 / 7 ≈ 7.2857, which
:.2fshows as 7.29. All of them pass except the 4.5.
Level 3 solution
split.py
STUDENTS = [
("Nora", 7.0),
("Pablo", 3.0),
("Elena", 9.5),
("Hugo", 4.5),
("Sara", 6.5),
("Tomás", 8.0),
]
CUTOFF = 5.0
def split(students, cutoff):
"""Returns two lists of names: passed and failed."""
passed = []
failed = []
for name, grade in students:
if grade >= cutoff:
passed = passed + [name]
else:
failed = failed + [name]
return passed, failed
def first_fail(students, cutoff):
"""Position of the first failing student, or -1 if there is none."""
for pos, student in enumerate(students):
if student[1] < cutoff:
return pos
return -1
passed, failed = split(STUDENTS, CUTOFF)
print("Passed:", passed)
print("Failed:", failed)
pos = first_fail(STUDENTS, CUTOFF)
print("First failing student at position", pos, "->", STUDENTS[pos])
print("Last three:", STUDENTS[-3:])
Why it is written this way:
passed = []andfailed = []are the neutral element of concatenation. Each lap of the loop creates a new, longer list, which is why it is assigned again to the same variable.- We concatenate
[name]and notname:+only joins sequences of the same type, andpassed + name(list plus string) would give aTypeError. - A single traversal decides which of the two lists each student goes to: it is a selective traversal with two branches (
ifandelse), because here every element is processed, just in a different way. splitreturns two lists withreturn passed, failed(a tuple) and the program unpacks them into two variables.first_failis the usual search pattern: thereturn posgoes inside theifand thereturn -1outside the loop. Withenumeratewe get the position and the student at the same time.STUDENTS[pos]is only safe because in this program there are fails. Ifposwere-1, the index would give the last student instead of an error; in a real program you would have to check it first.STUDENTS[-3:]are the last three: a slice with a negative index and an empty end.
Watch it step by step in Python Tutor
The bug in snippet 1 is best seen live. Python Tutor runs the code line by line and draws the function frames and their variables. Choose Python 3 and paste this program, which calls the function with the same list in two different orders:
def has_fail(grades):
for grade in grades:
if grade < 5:
return True
else:
return False
print(has_fail([7.5, 4.0, 9.0]))
print(has_fail([4.0, 7.5, 9.0]))
Expected output:
False True
What to watch, step by step:
- In the first call, the list is
[7.5, 4.0, 9.0]. Insidehas_fail,gradeis7.5on the first lap. - The
if grade < 5is false, so it jumps to theelse. And there is the problem: thereturn Falseruns on that very lap. The function frame disappears and returnsFalsewithout having seen the4.0. - Check it by counting the laps:
gradeonly ever becomes7.5. Positions 1 and 2 are never visited. - In the second call, the list starts with
4.0. The first lap already meets theif, runsreturn Trueand the output isTrue. - Both lists contain a fail, but the result depends only on which element comes first. That is what gives away a misplaced
returnin a loop.
Cheat sheet: Lists and tuples
The essentials in 30 seconds
- Three sequences:
str(quotes),tuple(parentheses) andlist(square brackets). The list can be modified; the other two cannot. - One-element tuple:
(10,). Without the comma it is not a tuple. - Indexes: from 0; the last one is
len(s) - 1or-1. - Slice:
s[start:end:step]. The start is included, the end is not.s[::-1]reverses. - **
for item in sreads;for pos in range(len(s))** lets you modify and know the position. - Search:
returninside theif; the “not there” outside the loop. - Extremes: the initial candidate is the first element, not an invented value.
Syntax
| I want… | Code | Result |
|---|---|---|
| The last element | grades[-1] | 10.0 |
| The first three | grades[:3] | [6.5, 8.0, 5.5] |
| The last two | grades[-2:] | [4.5, 10.0] |
| Remove the first and the last | grades[1:-1] | The ones in the middle |
| One element out of every two | grades[::2] | Positions 0, 2, 4… |
| The sequence backwards | grades[::-1] | A reversed copy |
| Join / repeat | a + b / [0] * 5 | New sequence |
| Go from one type to another | list(t) / tuple(l) | New sequence |
| Pack / unpack | t = a, b / a, b = t | Tuple / variables |
| Know whether a value is there | valor in grades | True or False |
| Position and element together | for pos, item in enumerate(s): | (pos, item) pairs |
| Position of a value | grades.index(5.5) | Error if it is not there |
| Count, add, extremes | len, sum, max, min | Shortcuts for the patterns |
Templates for the exercises
Traversal with accumulator and counter (Level 2):
total = 0
passed = 0
for grade in grades:
total += grade # accumulator
if grade >= 5:
passed += 1 # selective counter
Maximum / minimum (Level 2):
candidate = grades[0] # the first element, not 0
for grade in grades:
if grade > candidate:
candidate = grade
Splitting into two lists (Level 3):
buenos = []
malos = []
for name, grade in students:
if grade >= 5:
buenos = buenos + [name]
else:
malos = malos + [name]
Position search (Level 3):
for pos, item in enumerate(sequence):
if item == wanted:
return pos # inside the if
return -1 # outside the loop
Mistakes that repeat the most
| Symptom | Likely cause |
|---|---|
IndexError: list index out of range | Index equal to or greater than len(s), or an empty list with s[0] |
TypeError: 'tuple' object does not support item assignment | You try to change an element of a tuple or a string |
TypeError: can only concatenate list (not "tuple") to list | + between different types: convert one of them |
ValueError: too many values to unpack | Fewer variables on the left than elements on the right |
len() gives more than expected in a “tuple” | The comma is missing: ("a") is a string |
| A slice returns one element fewer | The end is not included: leave the end empty to reach the end |
| The search always decides with the first element | return False inside the loop (in the else) |
| The minimum or maximum is a value that is not in the data | Invented initial candidate: use s[0] |
The list does not change after a for | You modify the for variable: go through the indexes |
ValueError: x is not in list | .index() of a missing value: check first with in |
Before you hand it in, ask yourself
- Do my slices include what I want? Remember that the end is not included.
- In my search, is the “not found”
returnoutside the loop? - Have I started each candidate (maximum, minimum) with the first element, and have I thought about what happens with an empty list?
- If I have to change the list, do I go through the indexes instead of the elements?
If you want to review the topic from the basics, go back to the lists and tuples theory.
Keep goingAll Python (en) articlesDid this article help you? Here is how you can support SergioLearns →
