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, or breakpoint()
  • Ruby with binding.pry
  • Possibly PHP, though unconfirmed
  • C, "sort of," through GDB's idiosyncratic mechanisms
  • JavaScript via debugger; with node inspect or the browser console, though there are limitations like the inability to use await in 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.