Summary
Once you ship your software, most of your end users will be better served by replacing the stack trace with some actionable error message in case of a crash.
This means you need to think of the user experience you want for when things go completely wrong.
You can do this by setting sys.excepthook
to a custom callable, and use sys.__excepthook__
to print the stack trace only under some conditions:
import sys
SHOW_STACK_TRACE = False
def on_crash(exctype, value, traceback):
if SHOW_STACK_TRACE:
sys.__excepthook__(exctype, value, traceback)
else:
print('Actionable error message')
sys.excepthook = on_crash
You can put other things in this hook, like logging or alerting, and use environment variables or the Python dev mode to switch the result at will. But make sure your function is very robust.
When the stack trace should be opt-in
In the previous article, I insisted that seeing the stack trace was a good thing, so how come I want you to hide it now?
If your software is not something like a library or a framework, you have a majority of end users that care about solving their problem with your tool, but not about the tool itself.
In fact, even if your program targets techies and some of them will happily report bugs or attempt to debug crashes, for the vast majority of cases, people don't want to see the guts of your code base when something goes wrong.
Worse, the less technical of your users will be confused, of even deterred if you add to the insult of crashing the injury of a wall of cryptic text. They have no idea what to do with that and run away.
For all those reasons, outside of the development phase, you should hide the stack trace by default, and only display it if explicitly requested.
If not the stack trace, then what?
The stack trace is there to help the devs, so its replacement should be there to help the end user.
If there is a stack trace displayed, it means you have an unexpected error that you didn't handle, and it led to the program to crashing.
So you need to ask yourself, what do I want the users to be able to do when my program crashes?
Do I want them to report the bug?
Do I want them to try again?
Do I want them to contact me?
Or something else?
Once you have decided the experience you want to provide, you replace the stack trace with a user feedback that helps them achieve what you want for them.
E.G:
When something goes wrong, you decided you want your users to check a specific page of your documentation. Instead of the stack trace, you shut down the program, and you display the message:
Something unexpected went wrong, and must stop. Go to https///docs/unexpected.html for more help
sys.excepthook() to catch any exception
If an exception rises and is never handled by a try
/except
, then it will be handled by a special function in the standard library: sys.excepthook
.
By default, sys.excepthook
is set to use the function in sys.__excepthook__
, which prints the stack trace to the terminal, but you can replace this function by your own:
import sys
SHOW_STACK_TRACE = False
def on_crash(exctype, value, traceback):
# "exctype" is the class of the exception raised
# "value" is the instance
# "traceback" is the object containing the what python needs to print the
# stack trace
if SHOW_STACK_TRACE:
# sys.__excepthook__ is the default excepthook that prints the stack trace
# so we use it directly if we want to see it
sys.__excepthook__(exctype, value, traceback)
else:
# Instead of the stack trace, we print an error message to stderr
print('Something went wrong, you can contact us at <email>', file=sys.stderr)
print('Or go to <website> for more help', file=sys.stderr)
# Now we replace the default excepthook by our own
sys.excepthook = on_crash
You are not the only one doing this
There are libraries out there that are also replacing sys.excepthook
. The most famous one is probably sentry, which is a service that collects crash reports and display them in a nice interface, but there are others like rich or pretty-traceback. Even some shells do it.
This means you have to be careful when you replace sys.excepthook
, because you may very well be overriding someone else's function.
If sys.__excepthook__ != sys.excepthook
, then the function has already been replaced.
When this happens, you have to choose between 3 behaviors:
Not adding your hook.
Adding your hook, and override any previous one.
Save the other code hook, and use it in your own hook:
import sys
old_hook = sys.excepthook
def on_crash(exctype, value, traceback):
# now you can call old_hook(exctype, value, traceback) somewhere here
...
sys.excepthook = on_crash
There is no single answer to this, and this depends very much of your situation.
Tips and tricks
sys.excepthook
is a good place to put a Hail Mary to dump something in a log file, send some email or SMS alerts, or phone home with telemetry (although I'm on the opt-in camp for this one).
But remember that sys.excepthook
is the last line of defense: if your code crashes in the excepthook, you are in trouble. So be very, very careful with what you put in there. Use try
/except
generously, make sure you deal with things like encoding and timeouts.
In the example, we use a SHOW_STACK_TRACE
constant to decide to show the stack trace or not, but this is likely something you want to put as a CLI option or an environment variable (if you are not comfy with env vars, we have an article for that).
import sys
import os
SHOW_STACK_TRACE = os.environ.get('SHOW_STACK_TRACE', "").strip().lower() not in ("", "0", "false")
def on_crash(exctype, value, traceback):
if SHOW_STACK_TRACE:
sys.__excepthook__(exctype, value, traceback)
...
Alternatively, you can use the underrated Python dev mode to cue the stack trace display:
import sys
# This is set to True if you run python with the -X dev option
# or if PYTHONDEVMODE is set to 1
SHOW_STACK_TRACE = sys.flags.dev_mode
def on_crash(exctype, value, traceback):
if SHOW_STACK_TRACE:
sys.__excepthook__(exctype, value, traceback)
...
Or, most probably, a combination of all of them, with a priority order.
Thank you for the interesting article! I think I can use this well.
A short question though: In this sentences: "If sys.excepthook != sys.excepthook`, then the function has already been replaced."
Should it not compare to the sys.__excepthook__ function?
Best!
Just use friendly/friendly-traceback and let it take care of it! ;-) https://github.com/friendly-traceback/friendly