AlgoInsight

Free lesson · Python Basic to Advance

Print Debugging

Hard8 min readpython

Print Debugging

What Print Debugging Is

Print debugging - adding print() calls to see what code is doing - is the most basic debugging technique. A line printed at the right place shows what value a variable held, whether a branch ran, or how often a function was called. No tools to install, no syntax to learn, instant visibility.

def calculate_discount(price, customer_type):
    print(f"calculating discount: price={price}, type={customer_type}")
    if customer_type == "premium":
        discount = 0.2
    elif customer_type == "regular":
        discount = 0.1
    else:
        discount = 0.0
    print(f"discount applied: {discount}")
    result = price * (1 - discount)
    print(f"final price: {result}")
    return result
This code shows print debugging. Each print statement reveals state at a point in the function. Useful for understanding how data flows and which branches execute. The information appears in the terminal as the function runs - immediate feedback without any tooling overhead.

Print debugging works because it requires nothing. Open a file, add prints, run, see output. That's it.

When Print Debugging Earns Its Place

Print debugging is the right tool when:

For scripts, small programs, and the early stages of investigating any bug, prints are often faster than firing up a debugger.

# Quick sanity check - print is the right tool
def process(data):
    print(f"got {len(data)} items")    # quick confirmation
    return [item.upper() for item in data]
This code uses print as a quick check. The single print confirms the function received the expected input size. Useful as a fast way to verify assumptions during development. The print can be removed once the assumption is confirmed.

When Print Debugging Is the Wrong Tool

Print debugging stops working when:

For these cases, the pdb debugger (Topic 24.2) or the logging module (Topic 24.3) is usually a better tool.

# Bad fit for print - production code
def serve_request(request):
    # Can't ship code with prints to production
    # print(f"handling request: {request}")    # NO
    # Use logging instead
    logger.info("handling request", extra={"request_id": request.id})
This code shows where print fails. Production code can't ship with print statements - they pollute logs, can't be filtered, and provide no structured context. Logging - Topic 24.3 - provides leveled, configurable output that works the same in development and production.

The repr vs str Distinction

When printing a value, repr is often more useful than str:

text = "hello\nworld"

print(text)
# hello
# world

print(repr(text))
# 'hello\nworld'

# In f-strings, !r calls repr
print(f"got: {text!r}")
# got: 'hello\nworld'
This code contrasts str and repr. Str shows the value as it should appear to users. Repr shows the value as Python source - quotes around strings, escape sequences visible. For debugging, repr is usually clearer - it distinguishes the empty string from None, shows whitespace, and makes types visible at a glance.

For debugging, f"{value!r}" is often more useful than f"{value}".

Using print for Structured Output

Adding labels and structure makes prints scannable:

def process(items):
    print(f"[process] start, len={len(items)}")
    for i, item in enumerate(items):
        print(f"[process] item {i}: {item!r}")
        if item.is_invalid():
            print(f"[process] WARN: invalid item at {i}")
            continue
        print(f"[process] processed {i} ok")
    print("[process] done")
This code uses structured prints. The bracket prefix identifies which function the output came from - useful when many functions print at once. The label after the bracket categorizes the message - start, item, warn, done. Useful for tracing flow through a function with many steps. As prints accumulate, structure helps reading them later.

For more than a handful of prints, structure is the difference between useful output and noise.

Print to stderr for Diagnostics

By default, print writes to stdout. Diagnostic output is often better on stderr - separates it from the program's regular output:

import sys

def parse(text):
    if not text:
        print("warning: empty input", file=sys.stderr)
        return None
    return text.upper()

# stdout and stderr can be redirected separately
# python script.py 2>errors.log
This code uses stderr. The file argument routes the print to standard error instead of standard output. Useful for scripts that produce data on stdout - diagnostics on stderr don't pollute the data stream. Shells can redirect them separately, capturing data while showing diagnostics or vice versa.

For diagnostic prints in scripts, file=sys.stderr is a good default.

Cleaning Up After Yourself

Print statements added for debugging should be removed before committing - or replaced with proper logging. A common discipline:

# Bad - orphan prints left in production code
def calculate(x):
    print(x)    # what is this for?
    return x * 2

# Better - explicit debug tagging
def calculate(x):
    # XXX DEBUG - remove before commit
    print(f"calculate input: {x!r}")
    return x * 2

# Best - proper logging when diagnostic value is permanent
import logging
logger = logging.getLogger(__name__)

def calculate(x):
    logger.debug("calculate input: %r", x)
    return x * 2
This code shows the progression. Orphan prints with no context are confusing. Tagged prints can be found and cleaned up. Proper logging persists the diagnostic with controllable verbosity - debug level can be enabled in development and disabled in production without code changes.

Print with pprint for Complex Data

For nested data structures, pprint produces readable output:

from pprint import pprint

data = {
    "users": [
        {"id": 1, "name": "alice", "roles": ["admin", "user"]},
        {"id": 2, "name": "bob", "roles": ["user"]},
    ],
    "meta": {"page": 1, "total": 2},
}

# Default print - single line, hard to read
print(data)

# pprint - formatted, indented
pprint(data)
# {'meta': {'page': 1, 'total': 2},
#  'users': [{'id': 1, 'name': 'alice', 'roles': ['admin', 'user']},
#            {'id': 2, 'name': 'bob', 'roles': ['user']}]}
This code uses pprint. The pretty printer formats nested structures with indentation and line breaks. Useful when print would produce a wall of text. Pprint accepts a width parameter to control line length and a depth parameter to truncate very deep structures.

For deeply nested or large data, pprint is usually more useful than plain print.

Print and Flushing

By default, print buffers output. In some cases - especially during long-running programs - buffering delays the output:

import time

# Default - output may not appear immediately
for i in range(5):
    print(f"step {i}")
    time.sleep(1)

# Flush=True - output appears immediately
for i in range(5):
    print(f"step {i}", flush=True)
    time.sleep(1)
This code shows flushing. Without flush, prints may be buffered and appear in batches. With flush, each line appears immediately. Useful for long-running programs where the output should track progress in real time. For most cases, the default buffering is fine.

For progress-bar-style output or anything time-sensitive, flush=True ensures immediate visibility.

The Limits of Print

Print is a hammer. Some bugs need different tools:

# Print can show what - but not interactively
def find_bug():
    x = compute()
    print(f"x = {x}")    # see the value, but can't probe further
    return process(x)

# breakpoint() lets you inspect x and everything else
def find_bug():
    x = compute()
    breakpoint()    # interactive - inspect x, call methods, change values
    return process(x)
This code contrasts print and breakpoint. Print shows one value. Breakpoint stops execution and gives an interactive prompt - inspect any variable, run arbitrary expressions, step through subsequent lines. Topic 24.2 covers the pdb debugger that breakpoint launches.

Performance

Print overhead is small but real:

For very hot code, even prints can change timing. For most debugging, the overhead is negligible.

Interview Relevance

Interview: Print debugging is fundamental:

  • When prints are the right tool (small, reproducible, interactive)
  • When they aren't (production, large codebases, timing-sensitive)
  • repr vs str for showing values
  • file=sys.stderr for diagnostics
  • pprint for nested data
  • The need to clean up debug prints before committing
  • That logging exists for permanent diagnostics

A common interview moment: a candidate dismisses print debugging as primitive. The right framing is that prints are a valid tool for many real situations - small, fast, reliable, and zero-cost to set up.

Another moment: a candidate uses print everywhere instead of logging. The right framing is that prints are scaffolding; logging is infrastructure.

Module 30 covers Python interview preparation.

Common Pitfalls

Warning: Leaving debug prints in committed code. Pollutes production logs, confuses readers.

Using print where repr would clarify. Empty strings, whitespace, None all look the same with plain print.

Buffering hiding output during long runs. Use flush=True for progress-style output.

Print in tight loops. Generates thousands of lines that drown the signal.

Print for concurrent code. Output from multiple threads interleaves unpredictably.

No structure or labels. Many prints with no context become noise.

Print to stdout polluting script output. Diagnostics should go to stderr.

Try It

Exercise: Practice with print debugging:

  1. Add prints to a function to trace its execution. Verify the output shows what you expected.
  2. Try print(value) and print(repr(value)) for a string with a newline. Note the difference.
  3. Use pprint on a nested dict. Compare with plain print.
  4. Route a diagnostic message to stderr with file=sys.stderr. Redirect stdout to a file and confirm only data is captured.
  5. Add flush=True to prints in a sleeping loop. Note the difference in timing.
  6. Take code with many prints and convert them to logging.debug calls. Note the difference in control.

Back to Python Basic to Advance