Finding and fixing bugs is one of the most important skills any Python developer can master. Studies estimate that developers spend between 30% and 50% of their time debugging code — and doing it efficiently can literally double your productivity. In this comprehensive guide, you'll learn everything from pdb (Python Debugger) basics to advanced debugging techniques used by senior engineers.
If you still rely on print() for everything, get ready to discover a world of tools that will transform how you code. We'll cover the interactive pdb debugger, the breakpoint() function introduced in Python 3.7, visual debuggers in VS Code and PyCharm, and professional techniques like post-mortem and remote debugging.
What Is Code Debugging?
Debugging is the systematic process of identifying, isolating, and fixing defects in source code. Unlike testing — which verifies whether the code works — debugging investigates why it doesn't. It's detective work: you gather clues, formulate hypotheses, and test each one until you find the root cause.
Python offers a mature debugging ecosystem, from the built-in pdb to full-featured IDEs with graphical interfaces. The key is knowing which tool to use in each situation and, more importantly, how to think during the debugging process.
Why Stop Using print()?
print() is the most basic tool, and everyone starts there. But it has severe limitations:
- You must modify code, add prints, rerun, remove prints — a slow, repetitive cycle
- You can't inspect variables in real time during execution
- You can't pause execution, step through code, or change variables on the fly
- It can alter program behavior (side effects in production)
- In large loops, prints can generate millions of lines of useless output
pdb solves all of these: inspect variables without modifying code, control execution step by step, set conditional breakpoints, and even run arbitrary Python code while paused.
Introduction to pdb — Python Debugger
The pdb (Python Debugger) is Python's built-in interactive debugger. It's part of the standard library since Python 1.5 — no installation required. With it, you can pause execution at any point, inspect variable values, step through code line by line, and understand exactly what's happening.
Starting pdb
There are three main ways to start a pdb debugging session:
# 1. Explicit import and call
import pdb; pdb.set_trace()
2. Using breakpoint() (Python 3.7+)
breakpoint()
3. Running a script with pdb
python -m pdb my_script.py
The breakpoint() function is the modern, recommended approach. By default, it calls pdb.set_trace(), but you can redirect it to other debuggers by setting the PYTHONBREAKPOINT environment variable. This means you can deploy code with breakpoints and selectively activate them in different environments.
Essential pdb Commands
When execution pauses at a breakpoint, you enter pdb's interactive shell. Here are the commands you'll use most:
| Command | Shortcut | What it does |
|---|---|---|
list | l | Shows code around the current line |
next | n | Executes next line (without stepping into functions) |
step | s | Executes next line (stepping into called functions) |
continue | c | Resumes execution until next breakpoint |
print | p | Prints the value of an expression |
pp | — | Pretty-prints expression values |
args | a | Shows all arguments of the current function |
where | w | Shows the call stack (stack trace) |
up | u | Moves up one level in the call stack |
down | d | Moves down one level in the call stack |
break | b | Sets a breakpoint at a line or function |
clear | cl | Removes a breakpoint |
quit | q | Exits the debugger and terminates the program |
Practical pdb Example
Let's see pdb in action with a real example:
def calculate_average(grades):
total = sum(grades)
average = total / len(grades)
return average
def process_students(students):
results = []
for student in students:
breakpoint() # Pause here to inspect each student
average = calculate_average(student['grades'])
student['average'] = average
results.append(student)
return results
students = [
{'name': 'Alice', 'grades': [8.5, 9.0, 7.5]},
{'name': 'Carlos', 'grades': [6.0, 7.0]},
{'name': 'Beatriz', 'grades': []}, # Bug! Division by zero
]
print(process_students(students))
When you run it, pdb pauses on the first iteration. Type p student to inspect the data, n to advance, c to continue to the next breakpoint. When you reach the third student with an empty list, the bug becomes obvious before it even happens.
breakpoint() — The Modern Way to Debug
Introduced in PEP 553, the breakpoint() function is the recommended way to insert breakpoints since Python 3.7. Its main advantage is flexibility: you can control which debugger it targets without changing source code.
Configure the default debugger with the PYTHONBREAKPOINT environment variable:
# Use default pdb (already the default)
set PYTHONBREAKPOINT=pdb.set_trace
Use ipdb (prettier, with syntax highlighting)
set PYTHONBREAKPOINT=ipdb.set_trace
Disable all breakpoints (useful in production)
set PYTHONBREAKPOINT=0
This is a game-changer for development teams: you can commit code with breakpoint() and each developer decides locally whether to activate or ignore these points. In production, just set PYTHONBREAKPOINT=0 and all breakpoints are silently ignored.
Post-Mortem Debugging with pdb
One of the most powerful techniques is post-mortem debugging. When an unhandled exception occurs, pdb can automatically open an interactive session right at the error site, letting you inspect every variable at the moment of failure.
import pdb
try:
result = 10 / 0
except ZeroDivisionError:
pdb.post_mortem() # Opens debugger at the exception point
You can also run any script in post-mortem mode with:
python -m pdb -c continue my_script.py
When an exception occurs, pdb automatically enters post-mortem mode. Use commands like where to see the full stack, p variable to inspect values, and up/down to navigate between stack frames.
Third-Party Debuggers You Need to Know
pdb is powerful, but alternatives offer extra features:
ipdb — Enhanced pdb Interface
ipdb is pdb powered by the IPython engine. It offers syntax highlighting, autocomplete, command history, and seamless integration with the IPython/Jupyter ecosystem. Install with pip install ipdb and use it just like pdb.
IPython — %debug Magic
If you use Jupyter Notebook or IPython, the %debug magic command activates automatic post-mortem debugging after any exception. Just run %debug after an error and you drop directly into the frame where the exception occurred.
PyCharm Debugger
PyCharm has one of the most complete visual debuggers on the market. Set breakpoints by clicking in the gutter, inspect variables in a graphical interface, evaluate arbitrary expressions, and even modify variables during execution. The "Evaluate Expression" mode (Alt+F8) lets you run any Python code in the current context.
VS Code Python Debugger
VS Code offers an excellent debugging experience with Microsoft's Python extension. Features like inline values (variable values appear next to your code), interactive debug console, conditional breakpoints, and logpoints (breakpoints that only log without pausing) make it extremely productive.
Strategic Logging — When the Debugger Isn't Enough
In production, you can't use an interactive debugger. That's where Python's logging module comes in. A well-configured logging system is essential for debugging issues in production environments.
import logging
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
filename='app.log'
)
logger = logging.getLogger(name)
logger.debug('Processing order for user %s', user_id)
logger.info('Order %s processed successfully', order_id)
logger.warning('Response time above threshold: %.2fs', elapsed)
logger.error('Payment processing failed: %s', str(error))
Well-structured logging lets you investigate issues without needing to reproduce them locally. Combine logging with observability tools like Sentry, Datadog, or New Relic for full visibility into your application's behavior.
Advanced Debugging Techniques
Conditional Breakpoints
In pdb, you can set breakpoints that only trigger when a condition is true:
# In the pdb shell:
b 42, x > 100
# Only stops at line 42 when x is greater than 100
With breakpoint() + conditional:
breakpoint() # Then use 'condition' in the pdb shell
Remote Debugging
Tools like remote-pdb or pdb-over-http let you debug code running on remote servers or Docker containers. This is especially useful for microservices and cloud applications.
IceCream — Smart Print Debugging
The icecream library turns print debugging into something much more productive. Instead of print(f"x = {x}"), use ic(x) and it automatically shows the variable name, value, file, and line — with zero extra effort.
from icecream import ic
def calculate(x, y):
result = x * y + 10
ic(x, y, result) # ic| x: 5, y: 3, result: 25
return result
Traceback Analysis
Understanding tracebacks is an essential skill. The traceback module lets you format and extract detailed exception information. In web applications, use middleware that captures full tracebacks and sends them to monitoring systems.
Common Errors and How to Debug Them
AttributeError: 'NoneType' object has no attribute 'X'
This is one of the most frequent Python errors. It's almost always caused by a function returning None when you expected something else. Use pdb to inspect the function's return value before accessing attributes.
breakpoint() # Inspect the return value before access
user = fetch_user(id)
print(user.name) # If user is None, error here
IndexError and KeyError
Index errors in lists or key errors in dictionaries usually indicate incorrect assumptions about your data. A breakpoint right before the access reveals the problem quickly.
Unexpected NameError
Often caused by variables defined inside a conditional block that didn't execute. A pdb.set_trace() at the suspicious location shows which variables are actually defined.
5-Step Debugging Strategy
After years of debugging code, here's a workflow that works for most cases:
- Reproduce the bug — Before anything else, have a reliable way to reproduce the problem. Without this, you're debugging in the dark.
- Isolate the cause — Use strategic breakpoints to find exactly where the behavior deviates from expectations. Start in the middle of the flow and narrow down.
- Understand why — Don't just fix the symptom. Inspect variables and understand which assumption in your code was wrong.
- Fix and test — Make the minimal fix needed and verify the bug is resolved without introducing new issues.
- Document — Write a test for the bug you found. This prevents regressions and helps other developers.
Remember: debugging isn't about finding the error — it's about understanding what the code is actually doing. When you understand the real behavior versus the expected one, the solution becomes obvious.
Want to go deeper with Python? Check out our guide on Python Clean Code and Best Practices to write code that's easier to debug from the start. And for automated tests that prevent bugs before they appear, see our tutorial on Automated Testing with Pytest.
Conclusion
Mastering Python debugging is an investment that pays for itself many times over throughout your career. pdb and breakpoint() are essential tools every Python developer should know. Combined with visual debuggers, strategic logging, and techniques like post-mortem analysis, you'll have a complete arsenal to find and fix bugs quickly and confidently.
The key is practice: start replacing your prints with breakpoints, learn one new pdb command per day, and in a week you'll be debugging like a pro. The time invested in learning to debug well is time you'll save on every bug you encounter for the rest of your career.