Every developer I know starts the same way: sprinkle a few print() calls around the suspicious code, run it, squint at the output, and hope for the best. It works, until it doesn't. The moment you have a bug that only shows up on the fifth loop iteration, that stack of prints turns into guesswork.
pdb is the tool for that moment. It is Python's built-in, terminal-based debugger, shipped as part of the standard library. No installation, no IDE, nothing to configure. If you can run a Python script, you can use pdb.
The core idea is simple: pause your program at a chosen line, look around, and step forward one line at a time. Here is the workflow.
Step 1: get in. The modern way is the built-in breakpoint(), added in Python 3.7. Drop it wherever you want execution to pause:
def calculate_total(prices):
total = 0
breakpoint() # execution stops here
for price in prices:
total += price
return total
Run the script normally and you land at the (Pdb) prompt with your cursor inside the function, all local variables in scope. The older form, import pdb; pdb.set_trace(), still works fine. If you'd rather not touch your source at all, launch the whole script under the debugger with python -m pdb script.py.
Step 2: move through the code. Three commands cover most of your needs. n (next) runs the current line and stops on the next one, skipping over any function bodies. s (step) does the same but dives into functions it calls. c (continue) runs freely until the next breakpoint or the end of the program. Pick n until you reach the suspect region, then switch to s to trace every call.
Step 3: look at values. p prints any expression in the current context: p prices shows the list, p total shows the running sum, p len(prices) evaluates on the spot. pp does the same but pretty-prints, which is a lifesaver for nested dicts. Type l to list the nearby source and ll to see the whole current function, so you never lose your bearings.
Step 4: set real breakpoints. Put a breakpoint ahead of time instead of pausing mid-function. Inside the debugger, b 12 breaks at line 12 of the current file. The real power is that breakpoints can be conditional: b 12, price > 100 only stops when the condition is true. Use it to catch that fifth loop iteration without babysitting the first four. w (where) shows the call stack, and u / d move between frames when you need to inspect a caller.
Step 5: inspect a crash. When an exception already happened, pdb.pm() drops you into post-mortem mode at the frame that raised it, so you can inspect the state that caused the failure.
One habit worth keeping: search for breakpoint() before you commit. Forgotten ones pause production code at the worst moment. Run a grep before you push, and future you stays on schedule.
pdb won't make the bug vanish. It gives you a window into what your code is actually doing at the moment it goes wrong, which is most of the fight.