Skip to content

Decisions and repetition

  • Lesson 6
  • 45 min
  • Module 2: Values and control flow
Lesson 6 of 18 · Python for Practical AI EngineeringDecisions and repetition

What you will be able to do

Write the first real part of ticket-cleaner: a script that keeps only the tickets with one status and counts them by category. It must also give a correct answer when there are no tickets, because real data is sometimes empty.

NotebookDo this lesson in a notebookColab · Kaggle · GitHub

The notebook has every example, task and check of this lesson, with the instructions. Its first cell makes a ticket-cleaner folder for count_tickets.py, so it works without your local project. It needs no install. Read the explanations here, and run the code there.

Colab and Kaggle open your own copy. They need a free Google or Kaggle account. Stuck? See the solution notebook.

Before you startWhat to know and to have readyAbout 45 minutes

What you need to know

What you need on your computer (only for the local route; the notebook and the boxes on this page need none of it)

Tested with Python 3.14 (terminal) and Pyodide 314.0 (browser). The notebook was tested in Jupyter.

An if statement runs some lines only when a condition is True. Here, each ticket’s priority number becomes a label:

if priority == 1:
label = "urgent"
elif priority == 2:
label = "normal"
else:
label = "low"

Python checks the conditions from the top. It runs the block under the first condition that is True, and skips all the others. elif means “else, if”. The else block runs only when no condition was True. You can have many elif blocks, and elif and else are both optional.

Each line that starts a block (if, elif, else, for) ends with :. The lines in the block are indented by four spaces. Python uses the indent to know where a block ends, so the indent is part of the code, not only its layout.

A condition does not have to be a comparison. Python treats some values as false: 0, "", None, and an empty list, dictionary or set. Every other value counts as true. So if not selected: means “if selected is empty”.

In the previous lesson, a loop printed each item. Usually a loop builds a result. The pattern has three parts: make an empty result before the loop, add to it in the loop, and use it after the loop.

This loop keeps only the open tickets:

selected = [] # 1. an empty result
for ticket in tickets:
if ticket["status"] == "open":
selected.append(ticket) # 2. add to it
print(len(selected)) # 3. use it

The task: for each category, count the tickets. The result is a dictionary: each key is a category, and each value is a count.

category_counts = {}
for ticket in tickets:
category = ticket["category"]
category_counts[category] = category_counts.get(category, 0) + 1

The last line does the work. category_counts.get(category, 0) gives the count so far, or 0 the first time that a category appears. Then + 1 adds this ticket, and the result goes back into the dictionary.

A loop trace shows the state after each step of the loop. Here it is for the six tickets:

Step ticket["id"] category category_counts after the step
1 T-1001 billing {'billing': 1}
2 T-1002 login {'billing': 1, 'login': 1}
3 T-1003 login {'billing': 1, 'login': 2}
4 T-1004 billing {'billing': 2, 'login': 2}
5 T-1005 billing {'billing': 3, 'login': 2}
6 T-1006 shipping {'billing': 3, 'login': 2, 'shipping': 1}

In step 1 and step 6, the category is new, so .get() gives 0. In the other steps, it gives the count from an earlier step.

Run Python in your browser

Trace the loop yourself

The print in the loop makes the same trace as the table. Change the loop to count by "status" instead of "category". Predict the result first.

Sometimes you do not need every item. Two statements change how a loop runs:

  • break ends the loop at once. Use it when you found what you looked for.
  • continue ends this step only, and goes to the next item. Use it to skip items.

This loop finds the first open ticket with priority 1, and stops there:

Run Python in your browser

Stop at the first match

How many checking lines do you expect? Run it. Then remove the break line, and run it again. What changes, and is the answer still correct?

first_urgent starts as None. After the loop, None means “the loop did not find one”. This is a common use of None. Check is None after the loop. Do not assume that the loop found a ticket.

The filter loop above is so common that Python has a shorter way to write it, a list comprehension:

open_ids = [ticket["id"] for ticket in tickets if ticket["status"] == "open"]
# ['T-1001', 'T-1003', 'T-1005']

Read it in three parts: what to put in the new list (ticket["id"]), for each item in which collection, and if which condition. The if part is optional. The result is the same as with a loop and append.

Use a comprehension when it fits on one line and reads clearly. When the loop has several steps, or uses break, write the full loop.

Now you write the script in your project. It keeps the tickets with one status, and counts them by category.

  1. In VS Code, open the ticket-cleaner folder. Make a new file, count_tickets.py.

  2. Copy the six tickets from the box above into the file, from tickets = [ to ].

  3. Under the list, add the status that you want, and the filter loop:

    count_tickets.py
    wanted_status = "open"
    # Keep only the tickets with the wanted status.
    selected = []
    for ticket in tickets:
    if ticket["status"] == wanted_status:
    selected.append(ticket)
  4. Add the count loop. It uses selected, not tickets:

    count_tickets.py (continued)
    # Count the selected tickets in each category.
    category_counts = {}
    for ticket in selected:
    category = ticket["category"]
    category_counts[category] = category_counts.get(category, 0) + 1
  5. Add the report. sorted() puts the categories in alphabetical order, so the output is the same each time:

    count_tickets.py (continued)
    print(f"Tickets with status '{wanted_status}': {len(selected)}")
    for category, count in sorted(category_counts.items()):
    print(f" {category}: {count}")
  6. Save the file. In the terminal, activate the environment, and run the script:

    Terminal window
    python count_tickets.py
    Output
    Tickets with status 'open': 3
    billing: 2
    login: 1

Change wanted_status to "resolved". No ticket has that status. Before you run it, predict the output. Then run it.

The script prints Tickets with status 'resolved': 0 and nothing else. That is correct, but a reader cannot tell if the script worked or stopped early. Add one decision so the output says so clearly:

Expected output
Tickets with status 'resolved': 0
No tickets to count.

Then add one more line to the report: the average priority of the selected tickets, rounded to 1 decimal place. For "open", it is 2.0. For "resolved", your script must not stop with an error.

Show one answer
count_tickets.py (the report)
print(f"Tickets with status '{wanted_status}': {len(selected)}")
if not selected:
print("No tickets to count.")
else:
for category, count in sorted(category_counts.items()):
print(f" {category}: {count}")
total_priority = 0
for ticket in selected:
total_priority = total_priority + ticket["priority"]
average_priority = total_priority / len(selected)
print(f"Average priority: {round(average_priority, 1)}")

The average is inside the else block, so the division runs only when selected has at least one ticket. See the common mistake below for what happens without this decision.

Common mistake

ZeroDivisionError on empty input

Symptom: the script works for "open", but stops for "resolved":

ZeroDivisionError: division by zero

Cause: the average is total / len(selected). When no ticket matches, len(selected) is 0, and division by zero is not possible. The code was tested only with data that had matches. Empty input is a normal case, not a rare one. A new day with no tickets gives an empty list, and so does a filter with a typing mistake.

Fix:

  1. Decide what the correct answer is for empty input. Here: a clear message, and no average.
  2. Check for the empty case before the division: if not selected:.
  3. Test the script with input that matches nothing, every time that you change it.

Check your understanding

Predict the output: for n in [3, 0, 5, 1]: if n == 0: continue; if n == 5: break; print(n). (Each part is on its own line, with the usual indents.)

Show the answer

Only 3. The first step prints 3. The second step (0) uses continue, so it skips the print. The third step (5) uses break, so the loop ends, and 1 is never checked.

Check your understanding

Rewrite as a list comprehension: ids = []; for t in tickets: if t["priority"] == 1: ids.append(t["id"])

Show the answer

ids = [t["id"] for t in tickets if t["priority"] == 1]. With the six tickets, the result is ['T-1003', 'T-1006'].

Check your understanding

Module check: set wanted_status to "closed". Predict the full output of count_tickets.py, then run it. Then set it to a status that does not exist, and run it again.

Show the answer

For "closed", the output is Tickets with status 'closed': 2, then login: 1, shipping: 1, and Average priority: 2.0 (tickets T-1002 and T-1006 have priorities 3 and 1). For a status that does not exist, the output is the count 0 and No tickets to count., with no error. If your prediction was different, make a loop trace for the part that surprised you.

Recap

  • if, elif and else run the block of the first true condition. An indent of four spaces marks each block.
  • A loop that builds a result has three parts: an empty result before, add in the loop, use after.
  • dict.get(key, 0) + 1 counts items by key.
  • break ends a loop. continue skips to the next item. Start a “found” variable as None and check it after the loop.
  • A list comprehension is a short filter: [what for item in items if condition].
  • Always test with empty input. Check for it before you divide by a count.

You can now filter and count data in a script. In the next module, you split count_tickets.py into functions, each with one job, so you can test each part.

Next: Function contracts