What are you even talking about?

Let’s say I want to add some Python code to a large repo. Maybe I want to add a function that returns the nth multiple of 2. The test suite will probably catch any mistakes I make.

def get_nth_multiple_of_2(data):
  return data * 2

Send it!

$ python -m unittest
F...............
======================================================================
FAIL: test_1 (test_t.TestMyCode)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/user/test_t.py", line 6, in test_1
    self.assertEqual(main(), [0, 2, 4, 6, 8, 10, 12, 14, 16, 18])
AssertionError: Lists differ: ['00', '11', '22', '33', '44', '55', '66', '77', '88', '99'] != [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

First differing element 0:
'00'
0

- ['00', '11', '22', '33', '44', '55', '66', '77', '88', '99']
+ [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

----------------------------------------------------------------------
Ran 16 tests in 0.002s

FAILED (failures=1)

Uh oh.

What’s going on? Let’s throw in a breakpoint().

def get_nth_multiple_of_2(data):
  breakpoint()
  return data * 2

And give it another go . . .

$ python -m unittest
> /home/user/t.py(8)get_nth_multiple_of_2()
-> return data * 2
(Pdb) data
'0'
(Pdb) type(data)
<class 'str'>
(Pdb)

Oops. Looks like something somewhere in the repo is calling this function with strings instead of integers. Should be a pretty quick fix—just need to close pdb and tweak a line or two.

(Pdb) exit
E> /home/user/t.py(8)get_nth_multiple_of_2()
-> return data * 2
(Pdb)

No, pdb, I figured out the problem. I don’t need the debugger anymore.

(Pdb) exit
E> /home/user/t.py(8)get_nth_multiple_of_2()
-> return data * 2
(Pdb) exit
E> /home/user/t.py(8)get_nth_multiple_of_2()
-> return data * 2
(Pdb)

No, no, I said, exit. Please exit.

(Pdb) exit
E> /home/user/t.py(8)get_nth_multiple_of_2()
-> return data * 2
(Pdb) exit
E> /home/user/t.py(8)get_nth_multiple_of_2()
-> return data * 2
(Pdb) exit
E> /home/user/t.py(8)get_nth_multiple_of_2()
-> return data * 2
(Pdb)

Exit? Please, pdb?

(Pdb) exit
E> /home/user/t.py(8)get_nth_multiple_of_2()
-> return data * 2
(Pdb) exit
E> /home/user/t.py(8)get_nth_multiple_of_2()
-> return data * 2
(Pdb) exit
E> /home/user/t.py(8)get_nth_multiple_of_2()
-> return data * 2
(Pdb) exit
E> /home/user/t.py(8)get_nth_multiple_of_2()
-> return data * 2
(Pdb)

What do you want from me? ctrl+d?

(Pdb) exit
E> /home/user/t.py(8)get_nth_multiple_of_2()
-> return data * 2
(Pdb)
E> /home/user/t.py(8)get_nth_multiple_of_2()
-> return data * 2
(Pdb)

Have it your way, then. ctrl+c it is.

(Pdb) exit
> /home/user/t.py(8)get_nth_multiple_of_2()
-> return data * 2
(Pdb) --KeyboardInterrupt--
(Pdb)

Oh, a funny guy?

$ ps aux | grep unittest
user      4952  0.0  0.1  50960 13732 pts/2    S+   10:39   0:00 /home/user/.pyenv/versions/3.10.5/bin/python -m unittest
$ kill 4952
(Pdb) Terminated
$

I thought so.

That was terrible! How can I never experience that ever again?

If you switch into interactive debugging and then exit() that interpreter, everything comes grinding to a halt.

$ python -m unittest
> /home/user/t.py(8)get_nth_multiple_of_2()
-> return data * 2
(Pdb) interact
*interactive*
>>> exit()

<output truncated to only show one failing test>

ERROR: test_9 (test_t.TestMyCode)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/user/test_t.py", line 33, in test_9
    main()
  File "/home/user/t.py", line 3, in main
    processed_data = [get_nth_multiple_of_2(x) for x in data]
  File "/home/user/t.py", line 3, in <listcomp>
    processed_data = [get_nth_multiple_of_2(x) for x in data]
  File "/home/user/t.py", line 8, in get_nth_multiple_of_2
    return data * 2
  File "/home/user/t.py", line 8, in get_nth_multiple_of_2
    return data * 2
  File "/home/user/.pyenv/versions/3.10.5/lib/python3.10/bdb.py", line 90, in trace_dispatch
    return self.dispatch_line(frame)
  File "/home/user/.pyenv/versions/3.10.5/lib/python3.10/bdb.py", line 114, in dispatch_line
    self.user_line(frame)
  File "/home/user/.pyenv/versions/3.10.5/lib/python3.10/pdb.py", line 262, in user_line
    self.interaction(frame, None)
  File "/home/user/.pyenv/versions/3.10.5/lib/python3.10/pdb.py", line 357, in interaction
    self._cmdloop()
  File "/home/user/.pyenv/versions/3.10.5/lib/python3.10/pdb.py", line 322, in _cmdloop
    self.cmdloop()
  File "/home/user/.pyenv/versions/3.10.5/lib/python3.10/cmd.py", line 126, in cmdloop
    line = input(self.prompt)
ValueError: I/O operation on closed file.

----------------------------------------------------------------------
Ran 16 tests in 3.126s

FAILED (errors=16)

Now that pdb isn’t in the way anymore, ctrl+c can stop the tests as well. Stopping the tests manually has come in handy for me when there are so many tests that it still takes a long time just to print all the failing tests.

Why does this happen?

Every test that hits the breakpoint will break there and open pdb. If there are a lot of tests hitting the breakpoint, it will start new pdb sessions faster than you can press ctrl+c. Luckily for me, some weird behavior in the interactive pdb debugger provides a path to salvation.

I imagine this is how it feels when a deus ex machina lifts someone out of a corner they’ve painted themselves into.