July 28, 2026 · All About CS

Debugging in Python: Stop Guessing and Start Fixing Bugs Fast

Learn how to read Python tracebacks, pause execution with the built-in breakpoint() debugger, and replace messy print statements with professional logging. Includes a real-world project debugging a broken pricing pipeline.

pythondebuggingpython-debuggingloggingbreakpointtracebackerror-handlingpdbpython-pdbdebug-python

Debugging in Python: Stop Guessing and Start Fixing Bugs Fast

No matter how experienced you are, you will write broken code. The difference between a beginner and a pro isn't that the pro writes bug-free code — it's that the pro knows exactly how to track down the problem and fix it fast.

This tutorial covers three essential debugging skills: reading Python tracebacks, pausing execution with the built-in breakpoint() function, and replacing messy print() statements with professional logging. We'll wrap up with a real-world project: debugging a broken Configure-Price-Quote (CPQ) pipeline for a digital showroom.

Reading Tracebacks

The most common error screen you'll encounter in Python is the Traceback — a map of exactly what went wrong and where. Let's look at an example.

Python
def calculate_discount(price, discount):
    final_price = price - (price * discount / 100)
    return final_price

# Accidentally passing the discount as a string instead of a number
print(calculate_discount(100, "20"))
# -> Traceback (most recent call last):
# ->   File "example.py", line 6, in <module>
# ->     print(calculate_discount(100, "20"))
# ->   File "example.py", line 2, in calculate_discount
# ->     final_price = price - (price * discount / 100)
# -> TypeError: unsupported operand type(s) for /: 'str' and 'int'

The key skill is reading a traceback from the bottom up:

  1. The last line gives the exact error: TypeError: unsupported operand type(s) for /: 'str' and 'int'.
  2. The line above it points to the exact location: final_price = price - (price * discount / 100).

Here, discount was passed as the string "20" instead of a number, so Python couldn't perform division on it.

A quick way to confirm a suspected type issue is to drop a print(type(discount)) right before the failing line. It's fast, but it gets messy in larger files — which is where more structured tools come in.

The Built-in Python Debugger (breakpoint())

Instead of just printing data, you can pause execution entirely and step through your code line-by-line. Modern Python includes a built-in breakpoint() function for exactly this — no extra installations needed.

Python
def get_average(numbers):
    total = 0
    for num in numbers:
        breakpoint()
        total += num
    return total / len(numbers)

get_average([10, 20, 30])

Running this script doesn't finish immediately — it pauses at the breakpoint() line and drops you into a special (Pdb) prompt, frozen at the first loop iteration.

From this prompt, you can:

CommandEffect
p numPrint the current value of num (10)
p totalPrint the current value of total (0)
nExecute the next line and pause again
cContinue running until the next breakpoint or end of script
qQuit the debugger

This lets you step through logic exactly as the computer reads it, watching variables change in real time.

breakpoint() is perfect for local debugging, but it should never ship to production — pausing a live web server would freeze the app for every user hitting that code path.

Professional Logging (A Smarter Print)

For applications running live — where pausing execution isn't an option — Python's built-in logging module is the professional upgrade to print(). It lets you categorize messages by severity.

Python
import logging

logging.basicConfig(level=logging.DEBUG, format='%(levelname)s - %(message)s')

def connect_to_database():
    logging.debug("Attempting to connect to db...")
    logging.info("Successfully connected!")
    logging.warning("Database space is getting low.")

connect_to_database()
# -> DEBUG - Attempting to connect to db...
# -> INFO - Successfully connected!
# -> WARNING - Database space is getting low.

The logging module supports multiple severity levels:

  • debug — fine-grained, under-the-hood details useful only while fixing bugs.
  • info — general milestones confirming things are working as expected.
  • warning — non-crashing issues that need attention soon.

In a real application, you can configure logging to write these messages to a file instead of the console. If your app crashes overnight, you can read the log file the next morning to see exactly what happened.

Real-World Project: Debugging a CPQ Pricing Pipeline

Let's put tracebacks, breakpoint(), and logging together on a realistic scenario. Imagine a Configure-Price-Quote (CPQ) system for a custom furniture showroom, "Timber and Style Furniture," that calculates final quotes for wooden tables — but it's outputting wildly incorrect numbers.

Python
import logging

logging.basicConfig(level=logging.DEBUG, format='%(levelname)s - %(message)s')

def generate_furniture_quote(base_price, material_cost, tax_rate):
    logging.info("Starting CPQ quote generation...")
    logging.debug(f"Inputs - Base: {base_price}, Material: {material_cost}, Tax: {tax_rate}")

    # BUG: should be addition, not multiplication
    subtotal = base_price * material_cost
    logging.debug(f"Calculated subtotal: {subtotal}")

    tax_amount = subtotal * tax_rate
    logging.debug(f"Calculated tax: {tax_amount}")

    final_quote = subtotal + tax_amount
    logging.info(f"Final quote generated: ${final_quote}")

    return final_quote

generate_furniture_quote(500, 150, 0.10)
# -> INFO - Starting CPQ quote generation...
# -> DEBUG - Inputs - Base: 500, Material: 150, Tax: 0.1
# -> DEBUG - Calculated subtotal: 75000
# -> DEBUG - Calculated tax: 7500.0
# -> INFO - Final quote generated: $82500.0

The logs immediately expose the problem: with a $500 base price and $150 in materials, the subtotal should be $650 — not 75000. Scanning the logging.debug output pinpoints the exact line at fault:

Python
subtotal = base_price * material_cost  # should be +

The pipeline was multiplying the base price and material cost instead of adding them. Because each intermediate value was logged, the logical bug was found instantly — no guesswork required.

Quick Reference

Tool / ConceptSyntaxPurpose
Traceback(automatic on crash)Read bottom-up: error type first, then the exact failing line
Built-in debuggerbreakpoint()Pauses execution and opens an interactive (Pdb) prompt
Print variable (Pdb)p variable_nameInspects a variable's current value while paused
Step forward (Pdb)nExecutes the next line of code
Continue (Pdb)cResumes execution until the next breakpoint
Configure logginglogging.basicConfig(level=..., format=...)Sets the minimum severity level and output format
Debug loglogging.debug(msg)Fine-grained detail, visible only when actively debugging
Info loglogging.info(msg)Confirms a milestone completed successfully
Warning loglogging.warning(msg)Flags a non-critical issue needing attention

What's Next?

You now have three complementary tools for hunting down bugs: reading tracebacks to locate errors, using breakpoint() to freeze and inspect code interactively, and using logging to surface issues in code that can't be paused — like a live production pipeline.

From here, consider exploring:

  • Higher log levels like logging.error() and logging.critical() for handling failures gracefully.
  • Writing logs to rotating files with logging.handlers.RotatingFileHandler for long-running applications.
  • Using try/except blocks alongside logging to catch and report errors without crashing the whole program.