Interactive debugging: dropping into a REPL at a breakpoint
A REPL—short for “read eval print loop”—is a program that accepts an input like print(f"2 + 2 = {2+2}"), evaluates it, prints the result, and loops back for more. The value of an interactive prompt isn't limited to exploring a language in isolation. In Python and Ruby (and, with caveats, in C via GDB), you can start a REPL at a breakpoint inside a running program, giving you both access to whatever variables are currently in scope and the ability to call your program's functions on the fly.
$ ipython3
Python 3.9.5 (default, May 24 2021, 12:50:35)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.24.1 -- An enhanced Interactive Python. Type '?' for help.
In [1]: print(f"2 + 2 = {2+2}")
2 + 2 = 4
In [2]:
Solving problems in place with IPython
Python developers can insert a breakpoint with import ipdb; ipdb.set_trace() in their code. ipdb requires installation, but it offers a much more comfortable experience than the standard library's pdb. While Python 3's built-in breakpoint() function also halts execution, it drops you into pdb, which isn't as pleasant.
import requests
def make_request():
result = requests.get("https://google.com")
import ipdb; ipdb.set_trace()
make_request()
When the interpreter reaches that line, execution stops and you're presented with a prompt where you can inspect variables like result or run any other code you'd like to test.
python3 test.py
--Return--
None
> /home/bork/work/homepage/test.py(5)make_request()
4 result = requests.get("https://google.com")
----> 5 import ipdb; ipdb.set_trace()
6
ipdb> result.headers
{'Date': 'Thu, 16 Sep 2021 13:11:19 GMT', 'Expires': '-1', 'Cache-Control': 'private, max-age=0', 'Content-Type': 'text/html; charset=ISO-8859-1', 'P3P': 'CP="This is not a P3P policy! See g.co/p3phelp for more info."', 'Content-Encoding': 'gzip', 'Server': 'gws', 'X-XSS-Protection': '0', 'X-Frame-Options': 'SAMEORIGIN', 'Set-Cookie': '1P_JAR=2021-09-16-13; expires=Sat, 16-Oct-2021 13:11:19 GMT; path=/; domain=.google.com; Secure, NID=223=FXhKNT7mgxX7Fjhh6Z6uej9z13xYKdm9ZuAU540WDoIwYMj9AZzWTgjsVX-KJF6GErxfMijl-uudmjrJH1wgH3c1JjudPcmDMJovNuuAiJqukh1dAao_vUiqL8ge8pSIXRx89vAyYy3BDRrpJHbEF33Hbgt2ce4_yCZPtDyokMk; expires=Fri, 18-Mar-2022 13:11:19 GMT; path=/; domain=.google.com; HttpOnly', 'Alt-Svc': 'h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"', 'Transfer-Encoding': 'chunked'}
binding.pry: the same trick in Ruby
Ruby offers an equivalent workflow using the Pry library. Inserting binding.pry at the desired line of a file like test.rb creates an interactive session where you can explore the state right at that point in execution.
require 'net/http'
require 'pry'
def make_request()
result = Net::HTTP.get_response('example.com', '/')
binding.pry
end
make_request()
Running the program yields a prompt that puts you right at the spot where the breakpoint was set.
$ ruby test.rb
From: /home/bork/work/homepage/test.rb:6 Object#make_request:
4: def make_request()
5: result = Net::HTTP.get_response('example.com', '/')
=> 6: binding.pry
7: end
[1] pry(main)> result.code
=> "200"
This pattern can also be used inside web application request handlers. In Ruby's Sinatra framework, for example, you can start a REPL in the middle of an HTTP request to investigate the request state before a response is returned. Flask and Django probably support similar approaches, but this has primarily been tested in Sinatra.
GDB: a rough REPL for C
GDB isn't a literal REPL for C—you can't just type in arbitrary C expressions and have them evaluated in the compiled program. It is, however, a surprisingly close approximation. You can call functions and inspect structs using something like p var->field->subfield, assuming the program was compiled with debugging symbols.
That functionality only exists because GDB's developers did a significant amount of “Very Weird Things” under the hood to enable function calls, a feat explored in detail in an earlier post about how GDB calls functions. This REPL-like mode is all that's needed for practical debugging—simply set a few breakpoints and poke around at those locations, skipping fancier features like watchpoints.
Where this debugging style shines
The approach works well in languages that offer breakpoint-activated REPLs:
- Python with
pdb,ipdb, orbreakpoint() - Ruby with
binding.pry - Possibly PHP, though unconfirmed
- C, "sort of," through GDB's idiosyncratic mechanisms
- JavaScript via
debugger;withnode inspector the browser console, though there are limitations like the inability to useawaitin Node's REPL - Java, where IntelliJ can evaluate arbitrary expressions at a breakpoint, which isn't quite a REPL but is still useful
Most compiled languages don't offer this experience. The technique sits somewhere between print-statement debugging and using a full-fledged debugger. One advantage over a traditional debugger is the low barrier to entry: instead of recalling debugger-specific commands each time—especially while switching between languages—a developer can rely on a known breakpoint pattern and simply start running code to figure out what's wrong.



