Skip to Content
Course content

148: Debugging with breakpoint()

Click on the "Edit" button in the top corner of the screen to edit your slide content.

We've all been there. You've got a bug that only triggers under a specific set of conditions, and your first instinct is to start peppering your code with print() statements. You print the input, you print the intermediate calculation, and you print "I got here" just to make sure the loop is actually running. For a while, it works. But then you realize you're spending more time adding and removing print statements than actually fixing the bug.

The Print-Statement Rabbit Hole

Let's look at a realistic scenario. Imagine you're writing a function that calculates a final price for an e-commerce cart, applying different discount tiers based on user membership and product categories. It looks something like this:

def calculate_total(cart, membership_level):
    total = 0
    for item in cart:
        price = item['price']
        if item['category'] == 'electronics' and membership_level == 'gold':
            price *= 0.9
        elif item['category'] == 'clothing':
            price *= 0.8
        total += price
    return total

# This works for most, but fails for a specific "platinum" edge case 
# where the logic is missing or behaving weirdly.
my_cart = [{'price': 100, 'category': 'electronics'}, {'price': 50, 'category': 'clothing'}]
print(calculate_total(my_cart, 'gold'))

When this fails, the "naive" approach is to add print(f"Debug: item is {item}, price is {price}") inside the loop. The problem is that printing is static. If you realize you actually needed to know the membership_level at the moment the electronics discount was applied, you have to stop the program, add another print statement, and run the whole thing from the beginning. If your data comes from a slow API or a heavy database query, this cycle becomes agonizingly slow. You're essentially guessing what you need to know before you actually know it.

Pausing Time with breakpoint()

This is where breakpoint() comes in. Instead of asking Python to tell you what happened after the code has already run, breakpoint() tells Python to stop exactly where you are and hand you the steering wheel. It's like hitting a pause button on a movie so you can walk onto the set and inspect the props.

If we drop breakpoint() into that loop, the experience changes completely:

for item in cart:
    breakpoint()  # The program freezes here
    price = item['price']
    # ... rest of the logic

When Python hits that line, it drops you into the PDB (Python Debugger) interactive shell. You aren't just looking at a log; you are inside the running process. You can type price to see its current value, or item['category'] to verify the data. More importantly, you can test hypotheses in real-time. You can try typing price * 0.9 to see if that's the value you expected, or even change the value of a variable on the fly to see how the rest of the function reacts. I've spent hours of my life saving days of work just by changing a variable mid-execution to see if a specific fix would work before actually writing the code.

When to Stop Printing and Start Probing

Now, I'm not saying print() is evil. For a quick check of a single variable in a three-line script, it's perfectly fine. But there's a mental shift that happens when you move to breakpoint(). You stop treating your code as a "black box" that you feed inputs into and hope for the right output, and you start treating it as a living environment.

The trade-off is mainly about the "flow." Printing is non-intrusive; the program runs to completion. breakpoint() is intrusive; it halts everything. If you're debugging a multi-threaded application or something that relies on a strict timeout (like a network socket), a breakpoint can actually cause the very timeout you're trying to debug. But for the vast majority of logic errors, the ability to probe the state of your application interactively is a superpower. Once you get used to the PDB commands—like n for next line, s to step into a function, and c to continue execution—you'll find that you rarely go back to the "print and pray" method.




📋 Practical Task

Fixing the Inventory Miscount

You have been handed a piece of code that manages a warehouse inventory. The goal is to update the stock levels based on a list of shipments, but there is a bug: some items are being subtracted when they should be added, and some are disappearing entirely.

Your Task: Use breakpoint() to diagnose the issue. Instead of adding print statements, place a breakpoint inside the process_shipments loop. Once the debugger opens, inspect the action and quantity variables to find why the inventory dictionary is ending up with incorrect values. Fix the logic error in the code and ensure the final inventory count is correct.

def process_shipments(inventory, shipments):
    for shipment in shipments:
        item = shipment['item']
        quantity = shipment['quantity']
        action = shipment['action']
        
        # BUG: The logic here is slightly flawed.
        # It should add for 'receive' and subtract for 'ship'.
        if action == 'receive':
            inventory[item] -= quantity 
        elif action == 'ship':
            inventory[item] += quantity
            
    return inventory

current_inventory = {'widgets': 10, 'gadgets': 5}
shipment_logs = [
    {'item': 'widgets', 'quantity': 5, 'action': 'receive'},
    {'item': 'gadgets', 'quantity': 2, 'action': 'ship'},
    {'item': 'widgets', 'quantity': 3, 'action': 'ship'},
]

# The expected result should be {'widgets': 12, 'gadgets': 3}
result = process_shipments(current_inventory, shipment_logs)
print(f"Final Inventory: {result}")
Rating
0 0

There are no comments for now.

to be the first to leave a comment.