Python Debugging: Why Print Statements Still Beat Your Fancy IDE
Aaron Rose
Software Engineer & Technology Writer
Your IDE promised you the world. Breakpoints, variable inspection, call stacks that look like NASA mission control. But here's the truth: when your code breaks at 2 AM and the client is breathing down your neck, you're going to reach for print()
.
And you know what? You should.
The Debugger Lie
Modern IDEs sell us a beautiful story. Set a breakpoint, step through line by line, hover over variables to see their values. It's supposed to be surgical, precise, professional.
But real debugging isn't a controlled lab experiment. It's messy, urgent, and often happening in environments where your fancy debugger can't even connect.
# What the tutorials show you
def calculate_total(items):
total = 0 # <- Set breakpoint here, inspect variables
for item in items:
total += item.price
return total
# What actually happens in production
def calculate_total(items):
print(f"DEBUG: Starting with {len(items)} items")
total = 0
for i, item in enumerate(items):
print(f"DEBUG: Item {i}: {item.price} (running total: {total})")
total += item.price
if total < 0: # This shouldn't happen...
print(f"ALERT: Negative total detected! Item was: {item}")
print(f"DEBUG: Final total: {total}")
return total
Guess which one actually helps you find the bug?
When Debuggers Fail You
Remote Servers Don't Care About Your IDE
Your Django app works fine locally. In production? It's throwing 500s. Your debugger can't connect to the remote server, but print()
statements show up in the logs every time.
# This works everywhere, every time
def process_payment(amount, user_id):
print(f"Processing payment: ${amount} for user {user_id}")
if not user_id:
print("ERROR: No user_id provided!")
raise ValueError("User ID required")
# More debugging breadcrumbs...
Docker Containers Are Print Statement Territory
Ever tried debugging inside a Docker container? Good luck attaching your IDE's debugger to that. But print statements? They show up in docker logs
like faithful soldiers.
The Async/Threading Nightmare
Debugging async code or multithreaded applications with traditional debuggers is like trying to catch smoke with your hands. Print statements with timestamps, though? They tell the story exactly as it unfolded.
import asyncio
from datetime import datetime
async def fetch_data(url):
timestamp = datetime.now().strftime("%H:%M:%S.%f")
print(f"[{timestamp}] Starting fetch for {url}")
# Your async code here
print(f"[{timestamp}] Completed fetch for {url}")
The Print Statement Advantages
1. They're Always There
Your debugger might crash. Your IDE might freeze. VS Code might decide today's the day it doesn't feel like connecting. But print()
never lets you down.
2. They Show the Flow
Debuggers show you one moment in time. Print statements show you the entire story - the sequence of events that led to the problem.
def mysterious_bug(data):
print("=== Starting mysterious_bug ===")
for item in data:
print(f"Processing item: {item}")
result = transform(item)
print(f"Transform result: {result}")
if validate(result):
print(f"Validation passed for {result}")
else:
print(f"VALIDATION FAILED for {result}")
# Now you know exactly where it breaks
3. They're Permanent (Until You Remove Them)
Set a breakpoint, restart your app, and oops - breakpoint's gone. Print statements stick around through restarts, deployments, and that time you accidentally closed your IDE.
4. They Work in Any Environment
Production server? Print works. Lambda function? Print works. Raspberry Pi running in your garage? Print definitely works.
The Right Way to Print Debug
Don't just throw print("here")
everywhere like confetti. Be strategic:
Use Descriptive Messages
# Bad
print(user)
# Good
print(f"User object before validation: {user}")
Add Context
# Bad
print(total)
# Good
print(f"Cart total after applying discount {discount_code}: ${total}")
Use Timestamps for Race Conditions
import time
def debug_timing_issue():
print(f"[{time.time()}] Function started")
# your code
print(f"[{time.time()}] Function finished")
The Hybrid Approach
Here's the secret: the best developers use both. Use your debugger for stepping through complex logic. Use print statements for everything else - especially the stuff that happens over time, across requests, or in environments where debuggers fear to tread.
def smart_debugging_example(complex_data):
# Debugger territory: complex transformations
processed = transform_complex_data(complex_data)
# Print statement territory: what actually happened
print(f"Processed {len(complex_data)} items into {len(processed)} results")
return processed
The Bottom Line
Your IDE's debugger is a beautiful, sophisticated tool. But when the chips are down and you need to figure out why your code is misbehaving in the wild, print()
is your most reliable friend.
It's not glamorous. It's not what the coding bootcamps teach. But it works, everywhere, every time.
Sometimes the old ways are the best ways.
Now excuse me while I go remove 47 print statements from my production code...
Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of The Rose Theory series on math and physics.
Comments
Post a Comment