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.

How to use this article

  1. Read the statement of a level and do not look at the solution yet.
  2. Write your version, run it and compare your output with the expected one.
  3. If you get stuck, look only at the 🔵 hint and try again.
  4. 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] and grades[-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) and grades[::-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 for we 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.
  • minimum and maximum start at grades[0], the first element. Had we started minimum at 0, the minimum would always be 0, even if nobody got it (the story from the practice).
  • The two if statements for the extremes go one after the other, not with elif: 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), passed packs 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 :.2f shows 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 = [] and failed = [] 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 not name: + only joins sequences of the same type, and passed + name (list plus string) would give a TypeError.
  • A single traversal decides which of the two lists each student goes to: it is a selective traversal with two branches (if and else), because here every element is processed, just in a different way.
  • split returns two lists with return passed, failed (a tuple) and the program unpacks them into two variables.
  • first_fail is the usual search pattern: the return pos goes inside the if and the return -1 outside the loop. With enumerate we get the position and the student at the same time.
  • STUDENTS[pos] is only safe because in this program there are fails. If pos were -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:

  1. In the first call, the list is [7.5, 4.0, 9.0]. Inside has_fail, grade is 7.5 on the first lap.
  2. The if grade < 5 is false, so it jumps to the else. And there is the problem: the return False runs on that very lap. The function frame disappears and returns False without having seen the 4.0.
  3. Check it by counting the laps: grade only ever becomes 7.5. Positions 1 and 2 are never visited.
  4. In the second call, the list starts with 4.0. The first lap already meets the if, runs return True and the output is True.
  5. Both lists contain a fail, but the result depends only on which element comes first. That is what gives away a misplaced return in a loop.

Cheat sheet: Lists and tuples

The essentials in 30 seconds

  • Three sequences: str (quotes), tuple (parentheses) and list (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) - 1 or -1.
  • Slice: s[start:end:step]. The start is included, the end is not. s[::-1] reverses.
  • **for item in s reads; for pos in range(len(s))** lets you modify and know the position.
  • Search: return inside the if; the “not there” outside the loop.
  • Extremes: the initial candidate is the first element, not an invented value.

Syntax

I want…CodeResult
The last elementgrades[-1]10.0
The first threegrades[:3][6.5, 8.0, 5.5]
The last twogrades[-2:][4.5, 10.0]
Remove the first and the lastgrades[1:-1]The ones in the middle
One element out of every twogrades[::2]Positions 0, 2, 4…
The sequence backwardsgrades[::-1]A reversed copy
Join / repeata + b / [0] * 5New sequence
Go from one type to anotherlist(t) / tuple(l)New sequence
Pack / unpackt = a, b / a, b = tTuple / variables
Know whether a value is therevalor in gradesTrue or False
Position and element togetherfor pos, item in enumerate(s):(pos, item) pairs
Position of a valuegrades.index(5.5)Error if it is not there
Count, add, extremeslen, sum, max, minShortcuts 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

SymptomLikely cause
IndexError: list index out of rangeIndex equal to or greater than len(s), or an empty list with s[0]
TypeError: 'tuple' object does not support item assignmentYou 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 unpackFewer 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 fewerThe end is not included: leave the end empty to reach the end
The search always decides with the first elementreturn False inside the loop (in the else)
The minimum or maximum is a value that is not in the dataInvented initial candidate: use s[0]
The list does not change after a forYou 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

  1. Do my slices include what I want? Remember that the end is not included.
  2. In my search, is the “not found” return outside the loop?
  3. Have I started each candidate (maximum, minimum) with the first element, and have I thought about what happens with an empty list?
  4. 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) articles

Did this article help you? Here is how you can support SergioLearns →

Sigue aprendiendo

Leave a Reply

Your email address will not be published. Required fields are marked *