'How can I make silent exceptions louder in tkinter?

If I run the following code from a terminal, I get a helpful error message in the terminal:

import Tkinter as tk

master = tk.Tk()

def callback():
    raise UserWarning("Exception!")

b = tk.Button(master, text="This will raise an exception", command=callback)
b.pack()

tk.mainloop()

However, if I run it without a terminal (say, by double-clicking an icon), the error message is suppressed.

In my real, more complicated Tkinter application, I like that the GUI is a little crash-resistant. I don't like that my users have a hard time giving me useful feedback to fix the resulting unexpected behavior.

How should I handle this? Is there a standard way to expose tracebacks or stderror or whatnot in a Tkinter application? I'm looking for something more elegant than putting try/except everywhere.

EDIT: Jochen Ritzel gave an excellent answer below that pops up a warning box, and mentioned attaching it to a class. Just to make this explicit:

import Tkinter as tk
import traceback, tkMessageBox

class App:
    def __init__(self, master):
        master.report_callback_exception = self.report_callback_exception
        self.frame = tk.Frame(master)
        self.frame.pack()
        b = tk.Button(
            self.frame, text="This will cause an exception",
            command=self.cause_exception)
        b.pack()

    def cause_exception(self):
        a = []
        a.a = 0 #A traceback makes this easy to catch and fix

    def report_callback_exception(self, *args):
        err = traceback.format_exception(*args)
        tkMessageBox.showerror('Exception', err)

root = tk.Tk()
app = App(root)
root.mainloop()

My remaining confusion: Jochen mentions the possibility of having different exception reporting functions in different frames. I don't yet see how to do that. Is this obvious?



Solution 1:[1]

There is report_callback_exception to do this:

import traceback
import tkMessageBox

# You would normally put that on the App class
def show_error(self, *args):
    err = traceback.format_exception(*args)
    tkMessageBox.showerror('Exception',err)
# but this works too
tk.Tk.report_callback_exception = show_error

If you didn't import Tkinter as tk, then do

Tkinter.Tk.report_callback_exception = show_error

Solution 2:[2]

First a followup: Just today, a patch on the CPython tracker for the tkinter.Tk.report_callback_exception docstring made it clear that Jochen's solution is intended. The patch also (and primarily) stopped tk from crashing on callback exceptions when run under pythonw on Windows.

Second: here is a bare-bones beginning of a solution to making stderr function with no console (this should really be a separate SO question).

import sys, tkinter

root = tkinter.Tk()

class Stderr(tkinter.Toplevel):
    def __init__(self):
        self.txt = tkinter.Text(root)
        self.txt.pack()
    def write(self, s):
        self.txt.insert('insert', s)

sys.stderr = Stderr()

1/0 # traceback appears in window

More is needed to keep the popup window hidden until needed and then make it visible.

Solution 3:[3]

Here's my 2 cents — cut and paste code that will display a dialog whether or not it occurred when tkinter was executing a callback function.

import sys
import tkinter as tk
from tkinter.messagebox import showerror
import traceback


def excepthook(exctype, excvalue, tb):
    """Display exception in a dialog box."""
    msg = ('An uncaught exception has occurred!\n\n'
           + '\n'.join(traceback.format_exception(exctype, excvalue, tb))
           + '\nTerminating.')
    showerror('Error!', msg)
    sys.exit()

sys.excepthook = excepthook  # Replace default system exception hook.


class MyTk(tk.Tk):
    """Tk subclass with callback exception reporting method that displays the
    exception is a dialog box.
    """
    def report_callback_exception(self, exctype, excvalue, tb):
        excepthook(exctype, excvalue, tb)


def raise_exception():
    """Callback function."""
    x = 1 / 0

def main():
    root = MyTk()
#    y = 1 / 0  # Uncomment to test exception handling in non-callback functions.
    tk.Button(root, text='Do something', command=raise_exception).pack()
    root.mainloop()


if __name__ == '__main__':
    main()

Solution 4:[4]

Just too add more value in @Jochen Ritzel's answer, You try except from beginning of your program to catch the python error too.

for example to detect index out of range error of for loop, you can use:

# Note import files before the try statement

from tkinter import *
from tkinter import messagebox 
import sys,traceback
def show_error(slef, *args):
    err = traceback.format_exception(*args)
    messagebox.showerror('Exception',err)
try:
    root=Tk()
    Tk.report_callback_exception = show_error
    a=[1,2,3]
    for i in range(10):
        print(a[i])
    exec(input()) # Just used to throw error
    a=Button(text='s',command=lambda: print(8/0)) # Just used to throw error
    a.pack()
    root.mainloop()
except BaseException as e:
    messagebox.showerror('Exception',e)

Solution 5:[5]

I found this thread while looking to solve the same problem. I had to make some modifications based on current tkinter version methodologies. I also added a customized error message handler since I figured my user wouldn't know what the Tk error message meant.

In the __init__ of your class include:

parent.report_callback_exception = self.report_callback_exception

then in your Tk class:

    def report_callback_exception(self, exc, val, tb):
        #Handles tkinter exceptions
        err_msg = {'Index 0 out of range': 'None found'}
        try:
            err = err_msg[str(val)]
        except KeyError:
            err = 'Oops, try again'
        messagebox.showerror('Error Found', err)

You could expand the err_msg dictionary to include whatever else you wanted. In my case I am building a searchable database off an Entry object, so if the user has typed in a string that doesn't exist in the database, it is giving the user a plain language error.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 martineau
Solution 2 Stevoisiak
Solution 3 martineau
Solution 4
Solution 5 Dharman