PYTHON • DEBUGGING AND TESTING

Print Debugging — Use print debugging to inspect variables and control flow

Learn the simplest yet most powerful technique for finding and fixing bugs in your Python code.

Historical Context & Motivation

Every programmer, from beginners to seasoned professionals, encounters bugs — unexpected behaviors in code that produce wrong results or crash the program entirely. The practice of print debugging (also called "printf debugging" after the C language's printf function) is one of the oldest and most widely used techniques for tracking down these errors. The idea is beautifully simple: insert temporary output statements into your code so you can see what your program is actually doing as it runs.

The term "bug" itself dates back to the earliest days of computing. When something went wrong, engineers would inspect their machines to find the source of the problem. Over time, programmers realized that having the computer report its own internal state — printing variable values and noting which lines of code were executing — was far more efficient than guessing. This approach has survived decades of technological change because it requires no special tools and works in virtually every programming language.

1947
The First Literal Bug
Grace Hopper's team finds a moth stuck in the Harvard Mark II computer, coining the term "debugging." Engineers began adding output statements to trace program execution.
1972
C Language and printf
Dennis Ritchie creates the C language, which includes the printf function. "Printf debugging" becomes a universal developer practice that persists to this day.
1991
Python Is Born
Guido van Rossum releases Python, which features a simple print statement that makes outputting variable values extremely easy, lowering the barrier for debugging.
2008
Python 3 and print()
Python 3 converts print from a statement to a function, adding powerful features like sep, end, and file parameters.
2019
f-strings and = Specifier
Python 3.8 introduces the = specifier inside f-strings, letting you write print(f"{x=}") to automatically show both the variable name and its value.

Despite the availability of sophisticated debugging tools with breakpoints and step-through execution, print debugging remains the go-to method for many programmers. The central question this lesson addresses is: how do you strategically place print statements to efficiently diagnose what's going wrong in your code?

Core Principles of Print Debugging

Print debugging is more than randomly scattering print() calls throughout your code. Effective print debugging follows a set of guiding principles that help you locate bugs quickly and systematically. Understanding these principles transforms print debugging from a clumsy guessing game into a precise diagnostic tool.

1

Inspect Variables

Print the current value of variables at key points. This reveals whether a variable holds the value you expect or if something has gone wrong during assignment or calculation.
2

Trace Control Flow

Place print statements at the entrance of branches and loops to confirm which path your program actually takes. Often, a bug is simply the program entering the wrong if-branch or skipping a loop entirely.
3

Label Your Output

Always include a descriptive label with each print statement. Printing just a value with no context makes it nearly impossible to interpret the output when you have many print calls.
4

Check Types

Use type() inside your print statements to verify that variables hold the expected data type. A string "5" and an integer 5 look the same when printed but behave very differently.
5

Remove When Done

Debug prints are temporary. Once you've found and fixed the bug, remove or comment out your print statements so they don't clutter the program's real output.
✦ KEY TAKEAWAY
Think of print debugging like leaving a trail of breadcrumbs through a maze. Each print() statement is a breadcrumb that tells you "the program was here, and this is what it was carrying." Without labels, your breadcrumbs all look the same and you can't tell which path led where. With clear labels and strategic placement, you can retrace your program's exact journey and find the moment things went wrong.

Visualizing Print Debugging in Action

The following diagram shows a simple Python function that calculates the average of a list of numbers, but it contains a bug. Strategic print() statements have been inserted to reveal the program's internal state at each step. On the left you see the code with debug prints, and on the right you see the console output that those prints produce.

The yellow dots on the left mark each debug print() statement. The right panel shows the console output they produce. Notice how the labeled output makes it immediately clear what each value represents and where in the code it was captured.

In the diagram above, each debug print uses an f-string with the = specifier (like f"{total=}"), which automatically prints both the variable name and its value. Each print also includes a descriptive prefix — "Start," "Loop," or "Result" — so you can immediately tell which print statement produced each line of output. This labeling strategy is crucial when your program has dozens of print calls.

How Print Debugging Works

Print debugging works by leveraging Python's sequential execution model. Python runs your code one line at a time, from top to bottom. When it encounters a print() call, it evaluates whatever expression is inside the parentheses and sends the result to the console. By placing prints at strategic locations, you create a real-time log of your program's journey through the code.

Technique 1: Inspecting Variable Values

The most common use of print debugging is checking what a variable holds at a particular moment. Python provides several ways to format this output. The simplest approach is to pass the variable directly: print(x). A better approach uses f-strings for labeled output: print(f"x = {x}"). The most efficient method, available in Python 3.8 and later, uses the = specifier: print(f"{x=}"), which outputs x=42 automatically.

Technique 2: Checking Data Types

Many bugs come from unexpected data types. A variable might hold the string "5" instead of the integer 5. When you print these, they look identical. The fix is to also print the type: print(f"{x=}, type={type(x)}"). This reveals the crucial difference between x='5', type=<class 'str'> and x=5, type=<class 'int'>.

Technique 3: Tracing Control Flow

Sometimes the bug isn't a wrong value — it's that your program takes the wrong path. By placing a print at the beginning of each branch in an if/elif/else block, you can confirm which branch executes. For example, writing print("Entering the if branch") and print("Entering the else branch") immediately tells you which condition was True. Similarly, printing inside a loop header — like print(f"Loop iteration {i}") — reveals whether the loop runs the expected number of times.

Technique 4: Using repr() for Hidden Characters

When debugging strings, invisible characters like spaces, tabs, or newlines can cause subtle bugs. The repr() function shows the "raw" representation of a value, including escape characters and quote marks. Using print(repr(name)) might reveal that name contains 'Alice\n' — with a hidden newline character at the end that was causing a comparison to fail.

💡 Pro Tip: The DEBUG Flag
If you want to easily turn all your debug prints on and off, create a variable at the top of your file: DEBUG = True. Then wrap each debug print in a condition: if DEBUG: print(f"{x=}"). When you're done debugging, just change DEBUG to False and all debug output disappears without deleting any lines.

Strategic Placement of Print Statements

Knowing where to put your print statements is just as important as knowing how to write them. A common mistake is to add prints everywhere, which floods the console with so much output that you can't find the useful information. Instead, use a systematic strategy called binary search debugging: start with a print roughly in the middle of your code, determine whether the bug occurs before or after that point, and then narrow your search by half each time.

The left panel shows six key locations where print statements should be placed. The right panel demonstrates these placements in a grading function. Color-coded comments (①–⑥) match the location guide. Notice how each print is labeled to identify its purpose.
Common print debugging strategies and when to apply them
StrategyWhen to UseExample Print Statement
Input validationFunction gives wrong output but you're unsure if the inputs are correctprint(f"args: {a=}, {b=}")
Loop trackingLoop runs too many or too few times, or accumulates the wrong totalprint(f"i={i}, sum={s}")
Branch detectionAn if/elif/else block seems to choose the wrong branchprint("Entered elif")
Type checkingArithmetic fails or comparisons behave unexpectedlyprint(type(x), repr(x))
Binary searchBug location is completely unknown in a long programprint("CHECKPOINT A")

Worked Example: Finding a Bug with Print Statements

Let's walk through a realistic debugging scenario. A student writes a function to count how many words in a sentence have more than four letters, but the function returns the wrong answer. Here's the buggy code:

def count_long_words(sentence): words = sentence.split() count = 0 for word in words: if len(word) > 4: count + 1 return count When called with count_long_words("The quick brown fox jumps"), the function returns 0 instead of the expected 3 ("quick", "brown", and "jumps" each have 5 letters).

Debugging with Strategic Prints
1
Step 1 — Verify the InputAdd a print at the function entry to confirm the input looks right: print(f"Input: {sentence=}"). Also check the split result: print(f"Words: {words}"). Output shows Words: ['The', 'quick', 'brown', 'fox', 'jumps']. The input and split are correct — the bug is elsewhere.
Input is correct ✓
2
Step 2 — Trace the LoopAdd a print inside the loop to see each word and its length: print(f" Checking: {word=}, len={len(word)}"). Output shows all five words with correct lengths: The(3), quick(5), brown(5), fox(3), jumps(5). The loop iterates correctly.
Loop iteration is correct ✓
3
Step 3 — Check the Condition BranchAdd a print inside the if-block: print(f" Found long word: {word}, count is now {count}"). This print runs three times (for quick, brown, jumps), confirming the condition works. But wait — the output says count is now 0 every time! The count never increases.
count stays at 0 — bug found! ✗
4
Step 4 — Identify the BugLook at the line inside the if-block: count + 1. This expression computes count + 1 but never stores the result! It should be count = count + 1 (or equivalently count += 1). The print debugging pinpointed exactly where and why the count wasn't updating.
Fix: change count + 1 to count += 1
5
Step 5 — Verify the Fix and Clean UpAfter making the fix, run the code again. The debug prints now show count incrementing to 1, 2, and 3. The function returns 3 as expected. Now remove all the debug print statements so they don't appear in the final output.
Function returns 3 ✓ — Bug fixed!
🔍 DEBUGGING LESSON
Notice how each print statement narrowed the search area. First we ruled out the input, then the loop, then the condition — until only the count update line remained. This systematic narrowing is far more efficient than staring at the code and hoping to spot the bug visually.

Strengths and Limitations of Print Debugging

Print debugging is an essential tool, but like any tool, it has strengths and weaknesses. Understanding these helps you decide when print debugging is the right choice and when you might benefit from a more powerful technique.

Comparing the advantages and disadvantages of print debugging
StrengthsLimitations
Requires no special tools — works in any editor or environmentCan clutter the output if too many prints are added
Easy to learn — you already know how to use print()Must modify source code and remember to remove prints later
Works in any Python environment (scripts, notebooks, servers)Cannot pause execution or step through code line by line
Produces a permanent log you can scroll through and analyzeCan change program timing, potentially masking timing-related bugs
Effective for loops that run many iterations — shows all valuesNot ideal for complex object graphs or deeply nested data structures
✦ KEY TAKEAWAY
Print debugging is like using a flashlight to search a dark room — it's always available, easy to use, and works great for small spaces. But if you need to search an entire building, you'd eventually want to turn on the overhead lights (a full debugger). For most high school programming tasks, the flashlight is more than sufficient, and learning to use it well builds strong debugging instincts that carry over to every other technique.

Connection to Advanced Debugging Tools

Print debugging is the foundation upon which more sophisticated debugging techniques are built. As your programs grow in complexity, you'll encounter situations where print debugging alone becomes cumbersome. It's valuable to know what other tools exist so you can grow into them when the time is right.

Print debugging compared to more advanced debugging approaches
FeaturePrint DebuggingInteractive Debugger (pdb / IDE)Logging Module
Setup requiredNone — just add print()Import pdb or configure IDEImport logging and configure
Pause executionNo — program runs to completionYes — breakpoints stop the codeNo — records to a file
Inspect any variableOnly those you explicitly printYes — explore any variable at a breakpointOnly those you explicitly log
Difficulty levelBeginner-friendlyIntermediate — requires learning commandsIntermediate — requires configuration
Leaves code modifiedYes — must remove prints manuallyNo — breakpoints are set in the debuggerYes, but log lines can stay permanently
Best forQuick checks, simple programs, learningComplex bugs, state explorationProduction code, long-running applications

Python's built-in pdb module (Python DeBugger) lets you set breakpoints — places where the program pauses so you can inspect any variable interactively. You can start it by adding breakpoint() anywhere in your code. Many IDEs like VS Code and PyCharm offer visual debuggers that let you click in the margin to set breakpoints and hover over variables to see their values. The logging module provides a professional-grade alternative to print debugging that can write messages to files, include timestamps, and be switched on or off with severity levels. As you advance in programming, you'll naturally add these tools to your debugging toolkit — but print debugging will always remain useful for quick investigations.

Practice Problems

PROBLEM 1 — CONCEPTUAL
A student writes print(x) to debug, and the console shows 5. However, when they try x + 3, Python raises a TypeError. What's most likely going on, and what print statement could they use to confirm their suspicion?
PROBLEM 2 — BASIC
The following function is supposed to return the largest number in a list, but it always returns the first element. Add print statements to locate the bug, then fix it. def find_max(numbers): biggest = numbers[0] for n in numbers: if n > biggest: biggest = n return biggest
PROBLEM 3 — INTERMEDIATE
A student's program reads test scores from user input and calculates the class average, but the average is always wrong. Here's their code: scores = input("Enter scores separated by spaces: ").split() total = 0 for s in scores: total += s average = total / len(scores) print(f"Average: {average}") Using print debugging techniques, identify the bug and explain how you would find it.
PROBLEM 4 — APPLIED
You are building a password validator that checks three rules: (1) at least 8 characters, (2) contains at least one digit, (3) contains at least one uppercase letter. Your function returns True for passwords that should fail. Write the function with strategic print statements that would help you trace which rules are being checked and whether each passes or fails. Show what the output might look like for the password "hello".
PROBLEM 5 — CRITICAL THINKING
A classmate argues that print debugging is a waste of time and that you should always use a proper debugger with breakpoints. Another classmate says print debugging is the only method you'll ever need. Evaluate both claims. In your answer, describe a scenario where print debugging is clearly better and another where a debugger would be more appropriate. What general guideline would you recommend?

Lesson Summary

Print debugging is the practice of inserting temporary print() statements into your code to inspect variable values and trace control flow. The five core principles are: inspect variables with labels, trace which branches and loops execute, always label your output for clarity, check data types with type() and repr(), and remove debug prints when you're done.

Place print statements strategically at function entries, before conditions, inside branches, inside loops, and before return statements. Use the binary search strategy to narrow down the location of a bug efficiently. Python's f-string = specifier (e.g., f"{x=}") is the fastest way to print labeled variable values. As you grow as a programmer, print debugging serves as a foundation for learning more advanced tools like interactive debuggers and the logging module.

Varsity Tutors • Python • Print Debugging — Use print debugging to inspect variables and control flow