Decisions and repetition
- Lesson 6
- 45 min
- Module 2: Values and control flow
Lesson 6 of 18 · Python for Practical AI EngineeringDecisions and repetition
1. Computer and environment basics
2. Values and control flow
3. Functions and program structure
4. Files, data and external services
5. Debugging and reliability
6. From notebook to maintainable project
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 notebook
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 ready
What you need to know
- How to read values from a list of dictionaries, from Collections: Worked example.
- The basic
forloop, from Collections: Go through every item, and.get()with a default, from Dictionaries: values by name.
What you need on your computer (only for the local route; the notebook and the boxes on this page need none of it)
- The project’s environment
.venv, active in your terminal (the prompt starts with(.venv)). You made it in Isolate dependencies: Guided practice. If it is missing or broken, delete it and make it again from the lock file, as in Make the environment again from nothing. - VS Code open on the
ticket-cleanerfolder, from Install and verify Python: Step 3. This lesson makescount_tickets.pythere.
Tested with Python 3.14 (terminal) and Pyodide 314.0 (browser). The notebook was tested in Jupyter.
Decisions: if, elif and else
Section titled “Decisions: if, elif and else”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”.
Repetition: a loop with a result
Section titled “Repetition: a loop with a result”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 resultfor ticket in tickets: if ticket["status"] == "open": selected.append(ticket) # 2. add to itprint(len(selected)) # 3. use itWorked example: count tickets by category
Section titled “Worked example: count tickets by category”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) + 1The 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.
Tab adds four spaces. Press Esc, then Tab, to leave the box. Ctrl+Enter runs the code.
Stop early, or skip one step
Section titled “Stop early, or skip one step”Sometimes you do not need every item. Two statements change how a loop runs:
breakends the loop at once. Use it when you found what you looked for.continueends 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?
Tab adds four spaces. Press Esc, then Tab, to leave the box. Ctrl+Enter runs the code.
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.
List comprehensions: a short form
Section titled “List comprehensions: a short form”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.
Guided practice: build count_tickets.py
Section titled “Guided practice: build count_tickets.py”Now you write the script in your project. It keeps the tickets with one status, and counts them by category.
-
In VS Code, open the
ticket-cleanerfolder. Make a new file,count_tickets.py. -
Copy the six tickets from the box above into the file, from
tickets = [to]. -
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) -
Add the count loop. It uses
selected, nottickets: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 -
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}") -
Save the file. In the terminal, activate the environment, and run the script:
Terminal window python count_tickets.pyOutput Tickets with status 'open': 3billing: 2login: 1
Your turn: no matching tickets
Section titled “Your turn: no matching tickets”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:
Tickets with status 'resolved': 0No 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
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 zeroCause: 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:
- Decide what the correct answer is for empty input. Here: a clear message, and no average.
- Check for the empty case before the division:
if not selected:. - 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.
Does your answer have the main points of ours?
Sign in and enroll in the course to save your answers. Every check counts toward the certificate.
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'].
Does your answer have the main points of ours?
Sign in and enroll in the course to save your answers. Every check counts toward the certificate.
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.
Does your answer have the main points of ours?
Sign in and enroll in the course to save your answers. Every check counts toward the certificate.
Recap
if,elifandelserun 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) + 1counts items by key.breakends a loop.continueskips to the next item. Start a “found” variable asNoneand 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