Why Python's Debugging Tools Need a Modern Reboot
Python's debugging tools have lagged behind the language's evolution, especially for async code. This editorial argues for a unified, modern debugger and highlights what a reboot could look like.
You know that feeling — you've just written a solid block of code, run it, and… nothing. No error, no output, just silence. Then comes the print() haphazardly placed in random spots, hoping to catch a glimpse of what's going wrong. We've all been there. But here's the thing: Python's debugging experience hasn't evolved much in decades, and that's a problem.
Don't get me wrong — I love Python. It's my daily driver for everything from data munging to API prototyping. But the debugging story? It's stuck in 2004 while the language keeps sprinting forward. And for a community that prides itself on developer experience, that feels like a glaring blind spot.
The print() Trap We've All Fallen Into
Let's be honest — most of us still debug with print(). Why? Because it works. Barely. But it's also a crutch that hides real issues. When you pepper your code with print('here 1'), print('here 2'), you're not debugging — you're guessing. By the time you find the bug, you've added and removed a dozen temporary lines, and you still don't understand why the state was wrong in the first place.
pdb — the built-in debugger — exists. But let's talk about its usability. It's a REPL that stops your code, gives you a (Pdb) prompt, and expects you to recall a handful of slash commands like n, s, c, l. For a beginner, it's intimidating. For a seasoned dev, it's clunky. We have better tools for everything else, but Python's official debugging story still feels like a command-line artifact from the 80s.
The Modern Landscape: Why Others Got It Right
Look at what's happened elsewhere. JavaScript has DevTools with breakpoints, watch expressions, and live UI inspection. Rust integrates with gdb/lldb but also has rust-gdb with nice wrapper scripts. Even Go, known for its simplicity, shipped delve — a full-featured debugger that feels like a modern tool.
Python? We have pdb, ipdb (which at least adds syntax highlighting), and third-party tools like pudb that try to bridge the gap with a full-screen interface. But none of them feel native. None of them are bundled with the interpreter. And most critically, none of them work out-of-the-box with the frameworks people actually use — especially web frameworks like Django or FastAPI, where async complicates everything.
The Async Elephant in the Room
Here's where things get particularly messy. Python's debugging tools were designed for synchronous, single-threaded code. But modern Python is increasingly async-first. FastAPI, aiohttp, anyio — these are mainstream. Try to debug an async function with pdb and watch what happens. Breakpoints set inside an event loop? Good luck. You'll get confusing errors or just silence. The debugger can't pause and resume the loop cleanly.
Some workarounds exist — like setting breakpoints before the event loop starts — but that's not debugging infrastructure, that's gymnastics. We shouldn't have to architect our code around a debugger's limitations. The debugger should adapt to the code, not the other way around.
What a Reboot Could Look Like
I'm not just here to complain. Let's think about what a modern Python debugger should be. Not a pip package you install and hope works, but something that ships with the ecosystem and just works.
First: time-travel debugging. Tools like RR (record/replay) have been around for C programs, and JavaScript browsers offer conditional breakpoints and live expressions. Imagine being able to rewind your Python code to see exactly when a variable became malformed. That's not fantasy — PyPy has experimented with this, and modern tracing backends make it feasible. We just need someone to ship it.
Second: visualize the stack. Let's have a debugger that shows you the call stack, local variables, and expressions in a pane — but also shows you how data flows between frames. That's what pdb lacks: context. A simple GUI overlay or a TUI that fits in your terminal could change everything. Something like debugpy from the VS Code team is a start, but it's tightly coupled to their editor. We need a standalone tool.
Third: async-aware stepping. The debugger needs to understand await points. When you hit a breakpoint inside an async function, it should pause the entire event loop gracefully, let you inspect, and resume without corrupting state. That's a hard problem, but it's solvable — and it would save thousands of developer hours annually.
The Community Needs to Push for This
Here's the kicker: Python's core team has historically deprioritized debugger improvements. It's not glamorous work — it's infrastructure that everyone uses but nobody wants to maintain. But Google, Meta, and every company with a massive Python backend has felt this pain. The tools exist on the periphery — py-spy for sampling profilers, snoop for expression tracing — but they're scattered and inconsistent.
We need a unified effort. CPython's sys.monitoring (added in 3.12) is a huge step forward — it's a low-level API for tracing that debuggers can build on. But that's a foundation, not a finished house. The next step is building a debugger that uses this properly, with a UI that doesn't give you PTSD.
A Practical Middle Ground
Until that dream arrives, here's what I actually do to debug modern Python without losing my mind:
- Use
breakpoint()built-in — at least it's a modern name, notpdb.set_trace(). - Pair it with
ipdbfor nicer output — it's a drop-in replacement that adds tab completion and colors. - For async code, use
debugpyeven outside VS Code — it has a console interface that works okay, and it handles async decently. - For state inspection, use
snoop— it prints every line executed with variable values. It's likeprint()on steroids.
But that's a patchwork. It's not a coherent experience.
The Bottom Line
Python's debugging tools aren't broken — they're just old. They were designed for a simpler era of programming, and the ecosystem has moved past them. The language itself keeps evolving with new syntax, type hints, and async support, but the debugging experience hasn't kept pace.
We deserve better. We deserve a debugger that feels like a modern tool, not a relic we tolerate. And until the core team or the community steps up to build it, we'll keep debugging the way our ancestors did — one print() at a time.
What's your debugging workflow? If you've found a setup that works well for async Python or complex projects, I'd love to hear about it. We're all muddling through this together.
Comments
Questions, corrections, and tips stay visible for everyone reading this page.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.