Course outline · 0% complete

0/29 lessons0%

Course overview →

Print vs Debugger, Honestly

lesson 9-2 · ~10 min · 27/29

Two ways to see inside a running program

Both tools in this lesson answer the same question, which is what the variables are actually doing while the code runs. They trade off differently, and picking the wrong one wastes real time, which is why professionals keep both within reach.

Print debugging is the checkpoint technique from lesson 7-3, and despite the sneering it gets, professionals use it daily. It shines when you want the history of values across a whole run, when code runs somewhere interactive tools cannot reach, such as a remote server or the logs of a continuous integration run from lesson 6-3, and it needs zero setup.

A debugger pauses a live program and lets you look around. In Python, drop breakpoint() on any line, run, and you land in pdb with commands like n for the next line, s to step into a call, p expr to print any expression, and c to continue.

It shines when you do not yet know what to print. You can inspect everything at the frozen moment, walk up the call stack, and poke at objects interactively, which is impossible with prints you have to write in advance.

The honest rule is to know both. Prints answer how a value evolved, and the debugger answers what the state is right now. Neither replaces the loop from lesson 7-1, since they are only how experiments get run.

Watching state evolve

A print inside the loop gives the history of one variable across a whole run, which is the print flavor of what a debugger shows you stepping with n.

def balance_after(payments, start):
    balance = start
    for i, p in enumerate(payments):
        balance -= p
        print(f"step {i}: paid {p}, balance now {balance}")
    return balance

final = balance_after([30, 50, 40], 100)
print("final:", final)

Output

step 0: paid 30, balance now 70
step 1: paid 50, balance now 20
step 2: paid 40, balance now -20
final: -20

One glance at the step lines reveals exactly when the balance goes negative, which the final value alone cannot tell you. That is the specific strength of prints, since three lines of history answer a question about when rather than what.

With a debugger you would put breakpoint() inside the loop and press n repeatedly, printing balance with p balance, and no code edits would be needed. The tradeoff is that you would sit through three interactive pauses to learn what three printed lines showed at once, and on a thousand-element list the debugger approach becomes unusable while the prints stay readable.

enumerate supplies the index alongside the value, which is what makes the step labels possible. Labeling each line is the same discipline as lesson 7-3's checkpoint labels, and unlabeled numbers scrolling past teach very little.

One habit worth adopting: temporary prints like this are debugging scaffolding and should come out before the change ships. Prints you intend to keep belong in a real logging call, which can be turned down in production rather than shouting into every run forever.

When the debugger wins

The debugger beats print statements for a crash deep in unfamiliar code where you do not yet know which variables matter.

When you do not know what to print, the frozen-moment inspection wins. You land at the crash, look at everything in scope, walk up the call stack to see the caller's variables, and evaluate expressions against the real objects, all without editing and rerunning.

Prints require you to guess the interesting variables before the run, so unfamiliar code means a cycle of adding prints, rerunning, discovering the wrong things were printed, and adding more. Each lap costs a full run.

The reverse cases favor prints just as clearly:

SituationBetter toolWhy
unfamiliar crash, unknown variablesdebuggerinspect everything at once
how a value evolved over 500 iterationsprintshistory in one scrollable run
failure only visible in productionprints or logsno interactive session available
failure only visible in CIprints or logsthe printed output is all you get
a value you need a permanent record ofloggingsurvives the run

The pattern is that a debugger needs a live program you can sit with, and anything remote, automated, or historical falls to prints.

An initialization bug found by watching a variable

running_max works on the first list and returns [0, 0, 0] for the all-negative one. Before the fix, best started at 0.

def running_max(nums):
    best = float("-inf")
    result = []
    for n in nums:
        if n > best:
            best = n
        result.append(best)
    return result

print(running_max([3, 1, 4, 2]))
print(running_max([-5, -2, -7]))

Output

[3, 3, 4, 4]
[-5, -2, -2]

Watching best in the negative run is what locates this. It starts at 0 and no negative number ever beats it, so best never changes and the result is three zeros. The bug is on the initialization line, not in the loop, which is the kind of conclusion a printed history makes obvious and a stare at the loop body does not.

float("-inf") is smaller than every real number, so the first element always wins the comparison regardless of sign. That is the standard way to seed a maximum search when the input range is unknown.

The alternative fix is to seed from the data, with best = nums[0] and a loop over nums[1:]. It is equally correct and it reintroduces the empty-input crash from lesson 3-1, so the -inf version needs no guard while the other one does.

This is an edge-case bug straight off the lesson 3-1 checklist, under extremes and negatives. A typical-case test with positive numbers could never catch it, which is the whole argument for walking the checklist instead of testing the input you had in mind while writing the code.

The pdb command for one line at a time

The command is n, short for next. It executes the current line and pauses on the following one, staying inside the current function.

The four commands from the lesson divide up cleanly:

CommandDoesUse when
nnext line, stepping over callsfollowing the flow in this function
sstep into the call on this linethe bug may be inside the callee
p exprprint any expressionchecking a value or a hunch
ccontinue to the next breakpointdone looking, let it run

The distinction between n and s is the one that matters most in practice. n treats a function call as a single step, which keeps you at one level of abstraction, and s descends into it, which is right only when you suspect the callee.

Pressing s by reflex is how people end up lost in library internals. Stepping into a call to sorted or a framework method drops you into code that is almost certainly not the bug, so n is the default and s is a decision.