Skip to content

Free Practice Lab · no sign-up needed

Python debugging gym

Good debugging starts with a prediction: what should this program do, and what does it actually do? These six exercises focus on a single mistake at a time. Read the code, choose a correction and compare the explanation with your reasoning. You can retry freely.

Beginner → intermediateAbout 9 minutes to read · practise at your own pace
Read the full lesson ↓

The interactive controls are loading. The explanation and worked examples below are available to read now.

Understand. Try. Explain.

What you will learn

Build a debugging routine you can repeat: state the expected result, reduce the input, trace the execution, explain the cause and test the correction.

  • Recognise boundary, return-position and shared-reference mistakes.
  • Explain a correction using actual values rather than guessing.
  • Choose ordinary, empty and boundary tests that catch the original bug.

How to use this lesson: read one section, predict what the example will do, then compare your reasoning with the explanation. Try the self-check questions before opening their answers.

1. Use a five-step debugging routine

First, write the requirement in plain language. “This function returns the sum of every price, and an empty list returns zero” gives you a target you can check. “The code should work” does not. Record the actual result too: a wrong number, an exception or a missing return value tells you where to investigate.

Second, reduce the example until it still fails but is easy to follow. A two-item list often exposes a misplaced return more clearly than a hundred-item list. Keep the reduced example as a regression test after fixing the cause.

Third, trace values in execution order. For each loop iteration, record the index, current item and accumulated result. For a function, mark the exact statement that exits it. For a mutable object, ask whether two names point to the same object.

Fourth, describe the cause before changing the code. “range stops before its upper bound, so the final index is never visited” predicts a particular correction. Change one cause at a time so you can connect the new result to that change.

Fifth, test the original failing case and several meaningful neighbours. Include a normal input, an empty input when the requirement allows it, and values just inside and outside a boundary. Passing these tests increases confidence; it does not prove every possible input is correct.

2. Know the Python rules behind the bugs

range(3) produces 0, 1 and 2; it never produces 3. For a list of length three, these are exactly its valid indices. range(len(values) - 1) therefore omits the last item. When you need values rather than positions, iterating directly over the list removes that particular index calculation.

Indentation determines which block a statement belongs to. A return inside a for loop exits the whole function on the first reached iteration. A return after the loop can use the result from every iteration. If the loop is empty and there is no later return, the function implicitly returns None.

Text and numbers have different operations. "18" + "1" joins text; int("18") + 1 adds integers. Converting actual user input also needs a plan for invalid text such as "eighteen". Do not hide a conversion error by silently inventing a number.

Lists hold references to objects. Repeating an outer list can repeat references to the same inner row. Equality asks whether values compare equal; is asks whether two expressions refer to the same object. Use identity checks when investigating shared mutable state.

The gym checks your selected correction. It does not run arbitrary Python. To execute the complete examples, copy them into your local Python 3 environment or your preferred Python playground.

3. Turn a fix into a small test plan

Choose tests because they challenge a requirement. For inclusive scores from 0 to 100, use -1, 0, 50, 100 and 101. The endpoints distinguish >= from >; the outside values distinguish a correct and condition from an overly permissive or condition.

For a filter that removes every 2, test no matches, all matches and adjacent repeated matches. Adjacent matches are especially useful because removing a list item during forward iteration shifts the next item into a position the iterator may skip.

Keep fixes focused. Rewriting an entire function at once makes it harder to tell whether the original cause was understood. After the small correction is verified, improve names or structure in a separate step and rerun the same examples.

Follow the reasoning

A worked example

A total should include every value. The original loop uses range(len(values) - 1), so the last index is missing. Trace [2, 4, 6] before deciding how to fix it.

A worked example · step-by-step trace
IterationValue addedRunning total
Start—0
i = 022
i = 146
Loop stopsIndex 2 is never visited6 instead of 12
Python 3 example
def total_values(values): total = 0 for value in values: total += value return total print(total_values([2, 4, 6])) print(total_values([9])) print(total_values([]))
Expected output
12 9 0

Why this matters: Direct iteration includes every item without calculating an index. The empty list leaves total unchanged at zero. If you needed each position as well as its value, enumerate(values) would provide both without manually constructing a range.

Example 2: one row changes another

The expression [[0] * 2] * 2 looks like a two-row grid, but both outer entries refer to the same inner list. Changing one cell through either reference changes the shared row.

Python 3 example
shared = [[0] * 2] * 2 shared[0][0] = 1 print(shared) print(shared[0] is shared[1]) independent = [[0] * 2 for _ in range(2)] independent[0][0] = 1 print(independent) print(independent[0] is independent[1])
Expected output
[[1, 0], [1, 0]] True [[1, 0], [0, 0]] False

Why this matters: The comprehension creates a new inner list on each iteration. Copying only the outer list would keep the shared row references. Start by checking object identity; adding more assignments to repair the second row treats the symptom while leaving the underlying aliasing in place.

Example 3: a return statement ends the function

Suppose add_prices([3, 5]) returns 3. If return total is inside the loop, execution stops after the first addition. Move it after the loop while keeping it inside the function.

Python 3 example
def add_prices(prices): total = 0 for price in prices: total += price return total for prices in ([3, 5], [3], []): print(add_prices(prices))
Expected output
8 3 0

Why this matters: The two-item case exposes the original early exit. The single-item case alone would have passed even with the bug, so it is a weak test by itself. The empty case checks that a result is returned when the loop body is never entered. For real financial calculations, choose an appropriate decimal representation; these small integers isolate the control-flow lesson.

Common mistakes and how to avoid them

Choosing the fix that only matches the sample

Hard-coding 12 would make the first example look correct but fail on a different list. A correction should follow the stated requirement for every supported input.

Using print output as the only evidence

Prints can reveal a trace, but a function may print the right number and still return None. Check the returned value when that is part of the contract.

Catching every exception and returning zero

A broad except can conceal invalid input and unrelated programming errors. Catch the specific expected error and decide how the caller should recover.

Assuming a copied outer list has independent nested values

A shallow copy creates another container while retaining references to its contents. Nested mutable objects may still be shared.

Explain it in your own words

10 self-check questions

Pause before revealing each answer. These questions are unscored, so you can revisit them as often as you need.

01Which values does range(4) produce?

0, 1, 2 and 3. The stop value 4 is excluded.

02Why does range(len(values) + 1) fail when used to index a list?

It includes index len(values), but the last valid index is len(values) - 1. Accessing that extra position raises IndexError.

03What does a function return if execution reaches its end without return?

None. Printing a result inside the function does not automatically return it.

04Why is a single-item list a weak test for an early return inside a summing loop?

Returning after the first iteration happens to process every item when there is only one item. A two-item list exposes the missing later iterations.

05What is the difference between "18" + "1" and int("18") + 1?

The first concatenates text to give "181". The second performs numeric addition to give 19.

06Which condition includes both endpoints 0 and 100?

0 <= score <= 100. It assumes score is already a suitable numeric value.

07Why can removing a list element during forward iteration skip an item?

The remaining items shift left, while the iterator advances to the next position. An adjacent matching item can move into the position just processed.

08Can two different lists be equal?

Yes. [0, 0] and another separately constructed [0, 0] compare equal even though they are different objects. Equality and identity answer different questions.

09What test would catch the shared-grid-row bug?

Change one cell, assert that a different row is unchanged, and check that the two rows are not the same object. Initial equality alone will not reveal the issue.

10When is the bug investigation finished?

When you can explain the cause, demonstrate the correction against the original failing case, and check relevant neighbouring cases. Document unsupported inputs rather than assuming they work.

Try it yourself

Use these exercises after the examples. Make a prediction or write your own answer before checking the suggested approach.

Practice 1

Write a boundary table

For valid scores from 0 through 100, predict the results for -1, 0, 50, 100 and 101. Explain why or is wrong in score >= 0 or score <= 100.

Show a hint

Evaluate both sides for -1 and for 101. Only one side needs to be True when using or.

Check the explanation

Expected results: False, True, True, True, False. With or, -1 passes the upper-bound check and 101 passes the lower-bound check. Use 0 <= score <= 100.

Practice 2

Remove all matches without skipping

Filter [1, 2, 2, 3] so every 2 is removed. Then test [], [2, 2] and [1, 3].

Show a hint

Build the kept values in a new list instead of removing from the list you are traversing.

Check the explanation

Use [value for value in values if value != 2]. The four results are [1, 3], [], [] and [1, 3]. The original list is not mutated by the comprehension itself.

Practice 3

Explain a fix in three lines

Choose any gym exercise and write: expected behaviour, underlying cause and one regression test. Ask whether the test would fail before the fix.

Show a hint

Prefer a concrete input and output to “test that it works.”

Check the explanation

For the summing loop: expected behaviour is to include all items; the cause is an exclusive range stop that omits the final index; the regression test is total_values([2, 4, 6]) == 12. Add [] == 0 as a separate empty-input case.

Before you begin

Clear answers. A clear next step.

Can I try it without signing up?

Yes. The Practice Lab visualizer, debugging exercises and interview story builder are open to everyone. An account is needed to save course progress, play scored games and use a private job workspace.

What is free, and what uses AI?

Guides, Practice Lab exercises and question-bank practice do not require AI generation. Personalised job analysis and Coach or Mock answer feedback use AI and have availability and plan limits.

Does completing a practice plan make me interview-ready?

A plan helps you practise consistently. It is not a certification, skills assessment or guarantee of an interview or offer. Use it to identify useful next steps and get feedback on your explanations.

Where are my Practice Lab answers stored?

The story builder keeps text only in the current tab until you copy or download it. Starter-plan checkmarks are stored on this browser when storage is available. They are separate from signed-in course progress.