6

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:\Python27\lib\lib-tk\Tkinter.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?

4
  • Tkinter runs on its own thread, and everything that comes after root.mainloop is executed only after you close the window. Note also that cause_an_error is executed only when you click the button, but first root.mainloop is executed. What's happening probably is that the exception is not being caught, because it's being thrown in a different "environment"...I hope someone comes up with a more detailed and technical answer. Commented Jul 19, 2016 at 17:00
  • Interesting. Now that you mention it, the 'raise' command is unable to retrieve the error as well, so it definitely seems to be thrown in a different environment. Is there a way that I could modify Tkinter's handling of errors to include logging after I import it? Commented Jul 19, 2016 at 17:08
  • By you could catch the exception in the function directly... Commented Jul 19, 2016 at 17:33
  • Yes, that's definitely true. I would hope there is a more elegant solution than encasing every single function in try/except statements though - especially in a much more complex program that has lots of functions. Commented Jul 19, 2016 at 17:48

2 Answers 2

10

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:

Sign up to request clarification or add additional context in comments.

3 Comments

Perfect, thanks! :) Is there any way to retrieve the line number or the function that produced the exception?
@SamKrygsheld: look at the traceback argument.
Ah yes, thank you. For anyone who might see this in the future, I had assumed that the traceback being passed in was a string, but it's actually a traceback object. Using traceback.tb_lineno will allow you to access the line number.
1

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.