Saving exceptions in Tkinter using Python

Question:

I have developed several Python programs for others that use Tkinter to receive input from the user. In order to keep things simple and user-friendly, the command line or python console are never brought up (ie. .pyw files are used), so I’m looking into using the logging library to write error text to a file when an exception occurs. However, I’m having difficulty getting it to actually catch the exceptions. For example:

We write a function that will cause an error:

def cause_an_error():
    a = 3/0

Now we try to log the error normally:

import logging
logging.basicConfig(filename='errors.log', level=logging.ERROR)

try:
    cause_an_error()
except:
    logging.exception('simple-exception')

As expected, the program errors, and logging writes the error to errors.log. Nothing appears in the console. However, there is a different result when we implement a Tkinter interface, like so:

import logging
import Tkinter
logging.basicConfig(filename='errors.log', level=logging.ERROR)

try:
    root = Tkinter.Tk()
    Tkinter.Button(root, text='Test', command=cause_an_error).pack()
    root.mainloop()
except:
    logging.exception('simple-exception')

In this case, pressing the button in the Tkinter window causes the error. However, this time, nothing is written to the file, and the following error appears in the console:

Exception in Tkinter callback
Traceback (most recent call last):
  File "C:Python27liblib-tkTkinter.py", line 1536, in __call__
    return self.func(*args)
  File "C:/Users/samk/Documents/GitHub/sandbox/sandbox2.pyw", line 8, in cause_an_error
    a = 3/0
ZeroDivisionError: integer division or modulo by zero

Is there a different way to catch and log this error?

Asked By: Sam Krygsheld

||

Answers:

It’s not very well documented, but tkinter calls a method for exceptions that happen as the result of a callback. You can write your own method to do whatever you want by setting the attribute report_callback_exception on the root window.

For example:

import tkinter as tk

def handle_exception(exception, value, traceback):
    print("Caught exception:", exception)

def raise_error():
    raise Exception("Sad trombone!")

root = tk.Tk()
# setup custom exception handling
root.report_callback_exception=handle_exception

# create button that causes exception
b = tk.Button(root, text="Generate Exception", command=raise_error)
b.pack(padx=20, pady=20)

root.mainloop()

For reference:

Answered By: Bryan Oakley

Since this question was about logging and being explicit where the error is occurring, I will expand on Bryan (totally correct) answer.

First, you have to import some additional useful modules.

import logging
import functools  # for the logging decorator
import traceback  # for printing the traceback to the log

Then, you have to configure the logger and the logging function:

logging.basicConfig(filename='/full/path/to_your/app.log',
                    filemode='w',
                    level=logging.INFO,
                    format='%(levelname)s: %(message)s')

def log(func):
    # logging decorator
    @functools.wraps(func)
    def wrapper_log(*args, **kwargs):
        msg = ' '.join(map(str, args))
        level = kwargs.get('level', logging.INFO)
        if level == logging.INFO:
            logging.info(msg)
        elif level == logging.ERROR:
            logging.exception(msg)
            traceback.print_exc()

    return wrapper_log


@log
def print_to_log(s):
    pass

Note that the function doing the logging is actually log and you can use it for both printing errors and regular info into your log file. How you use it: with the function print_to_log decorated with log. And instead of pass you can put some regular print(), so that you save the message to the log and print it to the console. You can replace pass with whatever commands you prefer.

Note nb. 2 we use the traceback in the log function to track where exactly your code generated an error.

To handle the exception, you do as in the already accepted answer. The addition is that you pass the message (i.e. the traceback) to the print_to_log function:

def handle_exception(exc_type, exc_value, exc_traceback):
    # prints traceback in the log
    message = ''.join(traceback.format_exception(exc_type,
                                                 exc_value,
                                                 exc_traceback))
    print_to_log(message)

Having configured all this, now you you tell your Tkinter app that it has to use the handle_exception function:

class App(tk.Tk):
    # [the rest of your app code]

if __name__ == '__main__':
    app = App()
    app.report_callback_exception = handle_exception
    app.mainloop()

Or alternatively, if you work with multiple classes, you can even instruct the class itself to use the andle_exception function:

class App(tk.Tk):

    def __init__(self, *args, **kwargs):
        tk.Tk.__init__(self, *args, **kwargs)
        tk.Tk.report_callback_exception = handle_exception
        # [the rest of your app code]

if __name__ == '__main__':
    app = App()
    app.mainloop()

In this way, your app code looks lean, you don’t need any try/except method, and you can log at info level whichever event in your app.

Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.