Mine is the python debugger. I was a long holdout thinking that print statements were sufficient. That was untill I started having errors crop up in functions that took minutes to run. The thing that I most notably wish I would have known about is post_mortem.

Example

[ins] In [4]: def repeater(msg, repeats=1):
         ...:     "repeats messages {repeats} number of times"
         ...:     print(f'{msg}\n' * repeats)

[ins] In [5]: repeater('hi', 3)
hi
hi
hi

[ins] In [6]: repeater('hi', 'a')
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-6-0ec595774c81> in <module>
----> 1 repeater('hi', 'a')

<ipython-input-4-530890de75cd> in repeater(msg, repeats)
      1 def repeater(msg, repeats=1):
      2     "repeats messages {repeats} number of times"
----> 3     print(f'{msg}\n' * repeats)
      4

Debug with iPython/Jupyter

%debug

Vanilla Debug

import pdb
import sys

pdb.post_mortem(sys.last_traceback)

More

For more information about the debugger checkout the real python article. https://realpython.com/python-debugging-pdb/

Also keep a bookmark of the table of pdb commands from the article https://realpython.com/python-debugging-pdb/#essential-pdb-commands

Debug Session

debug session