A good example would take a fair amount of space, but let's try this bogus example:
This is ipython:
In [2]: os.path.join(None)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-2-ba05fdaae739> in <module>
----> 1 os.path.join(None)
/opt/anaconda3/lib/python3.8/posixpath.py in join(a, *p)
74 will be discarded. An empty last part will result in a path that
75 ends with a separator."""
---> 76 a = os.fspath(a)
77 sep = _get_sep(a)
78 path = a
TypeError: expected str, bytes or os.PathLike object, not NoneType
This is SBCL:
* (merge-pathnames nil)
debugger invoked on a TYPE-ERROR in thread
#<THREAD "main thread" RUNNING {1000560083}>:
The value
NIL
is not of type
(OR (VECTOR CHARACTER) (VECTOR NIL) BASE-STRING PATHNAME SYNONYM-STREAM
FILE-STREAM)
when binding PATHNAME
Type HELP for debugger help, or (SB-EXT:EXIT) to exit from SBCL.
restarts (invokable by number or by possibly-abbreviated name):
0: [ABORT] Exit debugger, returning to top level.
(MERGE-PATHNAMES NIL 70256781343884 MERGE-PATHNAMES) [external]
0]
I find the first much quicker to read and parse (better layout, no SHOUTING, color coded, context info) and you can immediately see what file and location you'd need to "fix". What is an example were you prefer the lisp stacktrace to something you'd get in interactive development with ipython or in production with newrelic or anything else that captures python stacktraces?
The difference is that SBCL caught the error directly on entry of MERGE-PATHNAMES. SBCL did on call to MERGE-PATHNAME a runtime type check. It knows the expected types for the arguments.
SBCL told you that the call to MERGE-PATHNAME is already wrong. A backtrace then will only show higher up code from the environment and the call to MERGE-PATHNAME.
Your Python code went into the routine...
Often the Python backtrace will be easier to understand, since it is source/line oriented, since Python code usually does not have extensive code transformations (-> Lisp macros) and using an optimizing compiler like SBCL may make the code less debuggable (for example when using tail call optimization).
Ugh o/c sorry, my bad, I should just have done it in slime to start with (esp since I've compared it ipython); was just too lazy:
The value
NIL
is not of type
(OR (VECTOR CHARACTER) (VECTOR NIL) BASE-STRING PATHNAME
SYNONYM-STREAM FILE-STREAM)
when binding PATHNAME
[Condition of type TYPE-ERROR]
Restarts:
0: [RETRY] Retry SLY mREPL evaluation request.
1: [*ABORT] Return to SLY's top level.
2: [ABORT] abort thread (#<THREAD "sly-channel-1-mrepl-remote-1" RUNNING {1004894CD3}>)
Backtrace:
0: (MERGE-PATHNAMES NIL 69988797066848 MERGE-PATHNAMES) [external]
1: (SB-INT:SIMPLE-EVAL-IN-LEXENV (MERGE-PATHNAMES NIL) #<NULL-LEXENV>)
2: (EVAL (MERGE-PATHNAMES NIL))
3: ((LAMBDA NIL :IN SLYNK-MREPL::MREPL-EVAL-1))
--more--
This is nicer than "raw" sbcl but I still have trouble seeing how anyone could prefer looking at common lisp backtraces (with the caveat that I only have used open source lisp implementations; I have no idea what allegro or lispworks are like).
However, as I wrote common lisp is much nicer in some other respects (as you undoubtedly know). For a few other toy examples let's say I do:
(/ 1 (random 2))
This will cause DIVISION-BY-ZERO 50% of the time. But if that happens one of the possible restarts (also seen above) is just try the same thing again. I can try as many times as necessary to get (/ 1 1). Of course this is a silly example, but realistic cases are not hard to come up: you forgot to copy a file to the right place or the disk is full and you need to make some space before retrying. Or you have a transient network failure etc. Similarly
(mapcar #'sine '(1 2 3))
The function sine does not exists, but one of the possible restarts allows me to supply something else instead:
The function COMMON-LISP-USER::SINE is undefined.
[Condition of type UNDEFINED-FUNCTION]
Restarts:
0: [CONTINUE] Retry using SINE.
1: [USE-VALUE] Use specified function
[...]
If I press 1 and then provide #'sin I'll get (0.84147096 0.9092974 0.14112). But the more fun thing to do is to just implement the missing function there and then. Whilst the debugger window stays active, I can just write my "sine" function in the editor or repl and then retry, e.g. writing (defun sine (x) (sin x)) will give the same result.
This is pretty cool, because it means you can start writing some topdown code start running it an incrementally fill in the missing functions you are calling bad haven't yet defined without ever losing your state.
One other nicety I'd add that you don't get from the Python stacktrace is the ability to inspect each frame to see the local bindings, and even restart evaluation from a previous frame. I agree that the Python stacktrace looks nicer on a surface level, but I'd argue that in practice SBCL's debugger is more helpful.
> One other nicety I'd add that you don't get from the Python stacktrace is the ability to inspect each frame to see the local bindings
No, you do get that. Even in the plain python interpreter you can do import pdb; pdb.pm() after an error to do post mortem debugging and walk up and down the stackframes and inspect or manipulate local variables. In ipython that happens automatically (if you run with --pdb) or after you type `debug` after an exception. And tooling for production stacktraces normally also captures local variables.
There are a bunch of additional niceties that Common Lisp has, such as turtles-most-of-the-way-down: you might eventually hit a foreign function call you cannot further inspect, but for most Common Lisp implementations almost everything is implemented in lisp. In python you a significant proportion are C extensions which are opaque to the built in debugger, although you can make gdb work. Also remote debugging is much more natural in common lisp.
Generally the stuff that is better about the debugging experience in python is in a way more superficial and the stuff that's nicer in common lisp is much more fundamental, and yet, my experience is different than yours: the superficial stuff that python does well and common lisp does badly or simply less well matters more for overall productivity for most things I tend to do. This is although the debugger related stuff you can't do as well with common lisp amounts more or less to minor friction whereas the stuff you can't do with python is really hard to work around if you need it.
I think this is an object lesson on focussing on the (right) low hanging fruit.
This is ipython:
This is SBCL: I find the first much quicker to read and parse (better layout, no SHOUTING, color coded, context info) and you can immediately see what file and location you'd need to "fix". What is an example were you prefer the lisp stacktrace to something you'd get in interactive development with ipython or in production with newrelic or anything else that captures python stacktraces?