Problem: When exceptions are raised in slots, invoked by signals, they do not seem to propagate as usual through Pythons call stack. In the example code below invoking:
on_raise_without_signal(): Will handle the exception as expected.
on_raise_with_signal(): Will print the exception and then unexpectedly print the success message from the else block.
Question: What is the reason behind the exception being handled surprisingly when raised in a slot? Is it some implementation detail/limitation of the PySide Qt wrapping of signals/slots? Is there something to read about in the docs?
PS: I initially came across that topic when I got surprising results upon using try/except/else/finally when implementing a QAbstractTableModels virtual methods insertRows() and removeRows().
# -*- coding: utf-8 -*-
"""Testing exception handling in PySide slots."""
from __future__ import unicode_literals, print_function, division
import logging
import sys
from PySide import QtCore
from PySide import QtGui
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
class ExceptionTestWidget(QtGui.QWidget):
raise_exception = QtCore.Signal()
def __init__(self, *args, **kwargs):
super(ExceptionTestWidget, self).__init__(*args, **kwargs)
self.raise_exception.connect(self.slot_raise_exception)
layout = QtGui.QVBoxLayout()
self.setLayout(layout)
# button to invoke handler that handles raised exception as expected
btn_raise_without_signal = QtGui.QPushButton("Raise without signal")
btn_raise_without_signal.clicked.connect(self.on_raise_without_signal)
layout.addWidget(btn_raise_without_signal)
# button to invoke handler that handles raised exception via signal unexpectedly
btn_raise_with_signal = QtGui.QPushButton("Raise with signal")
btn_raise_with_signal.clicked.connect(self.on_raise_with_signal)
layout.addWidget(btn_raise_with_signal)
def slot_raise_exception(self):
raise ValueError("ValueError on purpose")
def on_raise_without_signal(self):
"""Call function that raises exception directly."""
try:
self.slot_raise_exception()
except ValueError as exception_instance:
logger.error("{}".format(exception_instance))
else:
logger.info("on_raise_without_signal() executed successfully")
def on_raise_with_signal(self):
"""Call slot that raises exception via signal."""
try:
self.raise_exception.emit()
except ValueError as exception_instance:
logger.error("{}".format(exception_instance))
else:
logger.info("on_raise_with_signal() executed successfully")
if (__name__ == "__main__"):
application = QtGui.QApplication(sys.argv)
widget = ExceptionTestWidget()
widget.show()
sys.exit(application.exec_())
As you've already noted in your question, the real issue here is the treatment of unhandled exceptions raised in python code executed from C++. So this is not only about signals: it also affects reimplemented virtual methods as well.
In PySide, PyQt4, and all PyQt5 versions up to 5.5, the default behaviour is to automatically catch the error on the C++ side and dump a traceback to stderr. Normally, a python script would also automatically terminate after this. But that is not what happens here. Instead, the PySide/PyQt script just carries on regardless, and many people quite rightly regard this as a bug (or at least a misfeature). In PyQt-5.5, this behaviour has now been changed so that qFatal() is also called on the C++ side, and the program will abort like a normal python script would. (I don't know what the current situation is with PySide2, though).
So - what should be done about all this? The best solution for all versions of PySide and PyQt is to install an exception hook - because it will always take precedence over the default behaviour (whatever that may be). Any unhandled exception raised by a signal, virtual method or other python code will firstly invoke sys.excepthook, allowing you to fully customise the behaviour in whatever way you like.
In your example script, this could simply mean adding something like this:
def excepthook(cls, exception, traceback):
print('calling excepthook...')
logger.error("{}".format(exception))
sys.excepthook = excepthook
and now the exception raised by on_raise_with_signal can be handled in the same way as all other unhandled exceptions.
Of course, this does imply that best practice for most PySide/PyQt applications is to use largely centralised exception handling. This often includes showing some kind of crash-dialog where the user can report unexpected errors.
According to the Qt5 docs you need to handle exceptions within the slot being invoked.
Throwing an exception from a slot invoked by Qt's signal-slot connection mechanism is considered undefined behaviour, unless it is handled within the slot
State state;
StateListener stateListener;
// OK; the exception is handled before it leaves the slot.
QObject::connect(&state, SIGNAL(stateChanged()), &stateListener, SLOT(throwHandledException()));
// Undefined behaviour; upon invocation of the slot, the exception will be propagated to the
// point of emission, unwinding the stack of the Qt code (which is not guaranteed to be exception safe).
QObject::connect(&state, SIGNAL(stateChanged()), &stateListener, SLOT(throwUnhandledException()));
If the slot was invoked directly, like a regular function call,
exceptions may be used. This is because the connection mechanism is
bypassed when invoking slots directly
In the first case you call slot_raise_exception() directly, so this is fine.
In the second case you are invoking it via the raise_exception signal, so the exception will only propagate up to the point where slot_raise_exception() is called. You need to place the try/except/else inside slot_raise_exception() for the exception to be handled correctly.
Thanks for answering guys. I found ekhumoros answer particularly useful to understand where the exceptions are handled and because of the idea to utilize sys.excepthook.
I mocked up a quick solution via context manager to temporarily extend the current sys.excepthook to record any exception in the realm of "C++ calling Python" (as it seems to happen when slots are invoked by signals or virtual methods) and possibly re-raise upon exiting the context to achieve expected control flow in try/except/else/finally blocks.
The context manager allows on_raise_with_signal to maintain the same control flow as on_raise_without_signal with the surrounding try/except/else/finally block.
# -*- coding: utf-8 -*-
"""Testing exception handling in PySide slots."""
from __future__ import unicode_literals, print_function, division
import logging
import sys
from functools import wraps
from PySide import QtCore
from PySide import QtGui
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
class ExceptionHook(object):
def extend_exception_hook(self, exception_hook):
"""Decorate sys.excepthook to store a record on the context manager
instance that might be used upon leaving the context.
"""
#wraps(exception_hook)
def wrapped_exception_hook(exc_type, exc_val, exc_tb):
self.exc_val = exc_val
return exception_hook(exc_type, exc_val, exc_tb)
return wrapped_exception_hook
def __enter__(self):
"""Temporary extend current exception hook."""
self.current_exception_hook = sys.excepthook
sys.excepthook = self.extend_exception_hook(sys.excepthook)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Reset current exception hook and re-raise in Python call stack after
we have left the realm of `C++ calling Python`.
"""
sys.excepthook = self.current_exception_hook
try:
exception_type = type(self.exc_val)
except AttributeError:
pass
else:
msg = "{}".format(self.exc_val)
raise exception_type(msg)
class ExceptionTestWidget(QtGui.QWidget):
raise_exception = QtCore.Signal()
def __init__(self, *args, **kwargs):
super(ExceptionTestWidget, self).__init__(*args, **kwargs)
self.raise_exception.connect(self.slot_raise_exception)
layout = QtGui.QVBoxLayout()
self.setLayout(layout)
# button to invoke handler that handles raised exception as expected
btn_raise_without_signal = QtGui.QPushButton("Raise without signal")
btn_raise_without_signal.clicked.connect(self.on_raise_without_signal)
layout.addWidget(btn_raise_without_signal)
# button to invoke handler that handles raised exception via signal unexpectedly
btn_raise_with_signal = QtGui.QPushButton("Raise with signal")
btn_raise_with_signal.clicked.connect(self.on_raise_with_signal)
layout.addWidget(btn_raise_with_signal)
def slot_raise_exception(self):
raise ValueError("ValueError on purpose")
def on_raise_without_signal(self):
"""Call function that raises exception directly."""
try:
self.slot_raise_exception()
except ValueError as exception_instance:
logger.error("{}".format(exception_instance))
else:
logger.info("on_raise_without_signal() executed successfully")
def on_raise_with_signal(self):
"""Call slot that raises exception via signal."""
try:
with ExceptionHook() as exception_hook:
self.raise_exception.emit()
except ValueError as exception_instance:
logger.error("{}".format(exception_instance))
else:
logger.info("on_raise_with_signal() executed successfully")
if (__name__ == "__main__"):
application = QtGui.QApplication(sys.argv)
widget = ExceptionTestWidget()
widget.show()
sys.exit(application.exec_())
This way of handling expcetions is not a surprise taking into consideration that the Signal/Slot architecture proposes a loosely coupled intercation between the signals and the slots. This means that the signal should not be expecting anything to happen inside the slots.
Although timmwagener's solution was pretty clever, it should be used with caution. Probably the problem is not with how Exceptions are handled between Qt Connections, but that the signal/slot architecture is not ideal for your application. Also, that solution would not work if a slot from a different thread is connected, or a Qt.QueuedConnection is used.
A good way of tackling the problem of errors raised in slots is to determine that at the connection and not the emitting. Then the erros can be handled in a loosely coupled way.
class ExceptionTestWidget(QtGui.QWidget):
error = QtCore.Signal(object)
def abort_execution():
pass
def error_handler(self, err):
self.error.emit(error)
self.abort_execution()
(...)
def connect_with_async_error_handler(sig, slot, error_handler, *args,
conn_type=None, **kwargs):
#functools.wraps(slot)
def slot_with_error_handler(*args):
try:
slot(*args)
except Exception as err:
error_handler(err)
if conn_type is not None:
sig.connect(slot_with_error_handler, conn_type)
else:
sig.connect(slot_with_error_handler)
This way, we would be complying to the requirements in the Qt5 docs, stating that you need to handle exceptions within the slot being invoked.
Throwing an exception from a slot invoked by Qt's signal-slot
connection mechanism is considered undefined behaviour, unless it is
handled within the slot
PS: This is just a sugestion based on a very small overview of your use case. There is no right/wrong way of solving this, I just wanted to bring out a different point of view : )
Related
I am trying to set a maximum run time for my celery jobs.
I am currently recovering from exceptions with a context manager. I ended up with code very similar to this snippet:
from celery.exceptions import SoftTimeLimitExceeded
class Manager:
def __enter__(self):
return self
def __exit__(self, error_type, error, tb):
if error_type == SoftTimeLimitExceeded:
logger.info('job killed.')
# swallow the exception
return True
#task
def do_foo():
with Manager():
run_task1()
run_task2()
run_task3()
What I expected:
If do_foo times out in run_task1, the logger logs, the SoftTimeLimitExceeded exception is swallowed, the body of the manager is skipped, the job ends without running run_task2 and run_task3.
What I observe:
do_foo times out in run_task1, SoftTimeLimitExceeded is raised, the logger logs, the SoftTimeLimitExceeded exception is swallowed but run_task2 and run_task3 are running nevertheless.
I am looking for an answer to following two questions:
Why is run_task2 still executed when SoftTimeLimitExceeded is raised in run_task1 in this setting?
Is there an easy way to transform my code so that it can performs as expected?
Cleaning up the code
This code is pretty good; there's not much cleaning up to do.
You shouldn't return self from __enter__ if the context manager isn't designed to be used with the as keyword.
is should be used when checking classes, since they are singletons...
but you should prefer issubclass to properly emulate exception handling.
Implementing these changes gives:
from celery.exceptions import SoftTimeLimitExceeded
class Manager:
def __enter__(self):
pass
def __exit__(self, error_type, error, tb):
if issubclass(error_type, SoftTimeLimitExceeded):
logger.info('job killed.')
# swallow the exception
return True
#task
def do_foo():
with Manager():
run_task1()
run_task2()
run_task3()
Debugging
I created a mock environment for debugging:
class SoftTimeLimitExceeded(Exception):
pass
class Logger:
info = print
logger = Logger()
del Logger
def task(f):
return f
def run_task1():
print("running task 1")
raise SoftTimeLimitExceeded
def run_task2():
print("running task 2")
def run_task_3():
print("running task 3")
Executing this and then your program gives:
>>> do_foo()
running task 1
job killed.
This is the expected behaviour.
Hypotheses
I can think of two possibilities:
Something in the chain, probably run_task1, is asynchronous.
celery is doing something weird.
I'll run with the second hypothesis because I can't test the former.
I've been bitten by the obscure behaviour of a combination between context managers, exceptions and coroutines before, so I know what sorts of problems it causes. This seems like one of them, but I'll have to look at celery's code before I can go any further.
Edit: I can't make head nor tail of celery's code, and searching hasn't turned up the code that raises SoftTimeLimitExceeded to allow me to trace it backwards. I'll pass it on to somebody more experienced with celery to see if they can work out how it works.
I'd like to have an exception that always sends an email when raised. As for now, I was planning to put that code into __init():
import sendmail
import configparser
def MailException(BaseException):
"""
sends mail when initiated
"""
def __init__(self, message, config=None):
# Call the base class constructor with the parameters it needs
BaseException.__init__(message)
if config is None:
config = configparser.RawConfigParser()
config.read('mail.ini')
config = config['email']
text = 'exception stack trace + message'
sendmail.sendMail(text=text, config=config['email'], title='Alert')
I'd explicitly like to have the mail-sending here, instead of in every except-block I create. Hence I wonder how to get the stack trace, the code has to be compatible with Python 2.7.
The only thing I could find was traceback, but that apparently only works within the except: realm - or is there a way to implement it within the Exception class? Any other ideas?
Firstly, note that (per the docs) you shouldn't subclass BaseException.
Secondly, rather than implement that logic in the exception itself you could define a new sys.excepthook (see e.g. Python Global Exception Handling), then you get access to the full traceback:
import sys
class MailException(Exception):
pass
def mailing_except_hook(exctype, value, traceback):
if exctype == MailException:
... # do your thing here
else:
sys.__excepthook__(exctype, value, traceback)
sys.excepthook = mailing_except_hook
I am using the unittest framework to automate integration tests of multi-threaded python code, external hardware and embedded C. Despite my blatant abuse of a unittesting framework for integration testing, it works really well. Except for one problem: I need the test to fail if an exception is raised from any of the spawned threads. Is this possible with the unittest framework?
A simple but non-workable solution would be to either a) refactor the code to avoid multi-threading or b) test each thread separately. I cannot do that because the code interacts asynchronously with the external hardware. I have also considered implementing some kind of message passing to forward the exceptions to the main unittest thread. This would require significant testing-related changes to the code being tested, and I want to avoid that.
Time for an example. Can I modify the test script below to fail on the exception raised in my_thread without modifying the x.ExceptionRaiser class?
import unittest
import x
class Test(unittest.TestCase):
def test_x(self):
my_thread = x.ExceptionRaiser()
# Test case should fail when thread is started and raises
# an exception.
my_thread.start()
my_thread.join()
if __name__ == '__main__':
unittest.main()
At first, sys.excepthook looked like a solution. It is a global hook which is called every time an uncaught exception is thrown.
Unfortunately, this does not work. Why? well threading wraps your run function in code which prints the lovely tracebacks you see on screen (noticed how it always tells you Exception in thread {Name of your thread here}? this is how it's done).
Starting with Python 3.8, there is a function which you can override to make this work: threading.excepthook
... threading.excepthook() can be overridden to control how uncaught exceptions raised by Thread.run() are handled
So what do we do? Replace this function with our logic, and voilĂ :
For python >= 3.8
import traceback
import threading
import os
class GlobalExceptionWatcher(object):
def _store_excepthook(self, args):
'''
Uses as an exception handlers which stores any uncaught exceptions.
'''
self.__org_hook(args)
formated_exc = traceback.format_exception(args.exc_type, args.exc_value, args.exc_traceback)
self._exceptions.append('\n'.join(formated_exc))
return formated_exc
def __enter__(self):
'''
Register us to the hook.
'''
self._exceptions = []
self.__org_hook = threading.excepthook
threading.excepthook = self._store_excepthook
def __exit__(self, type, value, traceback):
'''
Remove us from the hook, assure no exception were thrown.
'''
threading.excepthook = self.__org_hook
if len(self._exceptions) != 0:
tracebacks = os.linesep.join(self._exceptions)
raise Exception(f'Exceptions in other threads: {tracebacks}')
For older versions of Python, this is a bit more complicated.
Long story short, it appears that the threading nodule has an undocumented import which does something along the lines of:
threading._format_exc = traceback.format_exc
Not very surprisingly, this function is only called when an exception is thrown from a thread's run function.
So for python <= 3.7
import threading
import os
class GlobalExceptionWatcher(object):
def _store_excepthook(self):
'''
Uses as an exception handlers which stores any uncaught exceptions.
'''
formated_exc = self.__org_hook()
self._exceptions.append(formated_exc)
return formated_exc
def __enter__(self):
'''
Register us to the hook.
'''
self._exceptions = []
self.__org_hook = threading._format_exc
threading._format_exc = self._store_excepthook
def __exit__(self, type, value, traceback):
'''
Remove us from the hook, assure no exception were thrown.
'''
threading._format_exc = self.__org_hook
if len(self._exceptions) != 0:
tracebacks = os.linesep.join(self._exceptions)
raise Exception('Exceptions in other threads: %s' % tracebacks)
Usage:
my_thread = x.ExceptionRaiser()
# will fail when thread is started and raises an exception.
with GlobalExceptionWatcher():
my_thread.start()
my_thread.join()
You still need to join yourself, but upon exit, the with-statement's context manager will check for any exception thrown in other threads, and will raise an exception appropriately.
THE CODE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED
This is an undocumented, sort-of-horrible hack. I tested it on linux and windows, and it seems to work. Use it at your own risk.
I've come across this problem myself, and the only solution I've been able to come up with is subclassing Thread to include an attribute for whether or not it terminates without an uncaught exception:
from threading import Thread
class ErrThread(Thread):
"""
A subclass of Thread that will log store exceptions if the thread does
not exit normally
"""
def run(self):
try:
Thread.run(self)
except Exception as self.err:
pass
else:
self.err = None
class TaskQueue(object):
"""
A utility class to run ErrThread objects in parallel and raises and exception
in the event that *any* of them fail.
"""
def __init__(self, *tasks):
self.threads = []
for t in tasks:
try:
self.threads.append(ErrThread(**t)) ## passing in a dict of target and args
except TypeError:
self.threads.append(ErrThread(target=t))
def run(self):
for t in self.threads:
t.start()
for t in self.threads:
t.join()
if t.err:
raise Exception('Thread %s failed with error: %s' % (t.name, t.err))
I've been using the accepted answer above for a while now, but since Python 3.8 the solution doesn't work anymore because the threading module doesn't have this _format_exc import anymore.
On the other hand the threading module now has a nice way to register custom except hooks in Python 3.8 so here is a simple solution to run unit tests which assert that some exceptions are raised inside threads:
def test_in_thread():
import threading
exceptions_caught_in_threads = {}
def custom_excepthook(args):
thread_name = args.thread.name
exceptions_caught_in_threads[thread_name] = {
'thread': args.thread,
'exception': {
'type': args.exc_type,
'value': args.exc_value,
'traceback': args.exc_traceback
}
}
# Registering our custom excepthook to catch the exception in the threads
threading.excepthook = custom_excepthook
# dummy function that raises an exception
def my_function():
raise Exception('My Exception')
# running the funciton in a thread
thread_1 = threading.Thread(name='thread_1', target=my_function, args=())
thread_1.start()
thread_1.join()
assert 'thread_1' in exceptions_caught_in_threads # there was an exception in thread 1
assert exceptions_caught_in_threads['thread_1']['exception']['type'] == Exception
assert str(exceptions_caught_in_threads['thread_1']['exception']['value']) == 'My Exception'
I am using Python 2.5 and trying to use a self-defined excepthook in my program. In the main thread it works perfectly fine. But in a thread started with the threading module the usual excepthook is called.
Here is an example showing the problem. Uncommenting the comment shows the desired behaviour.
import threading, sys
def myexcepthook(type, value, tb):
print 'myexcepthook'
class A(threading.Thread, object):
def __init__(self):
threading.Thread.__init__(self, verbose=True)
# raise Exception('in main')
self.start()
def run(self):
print 'A'
raise Exception('in thread')
if __name__ == "__main__":
sys.excepthook = myexcepthook
A()
So, how can I use my own excepthook in a thread?
It looks like this bug is still present in (at least) 3.4, and one of the workarounds in the discussion Nadia Alramli linked seems to work in Python 3.4 too.
For convenience and documentation sake, I'll post the code for (in my opinion) the best workaround here. I updated the coding style and comments slightly to make it more PEP8 and Pythonic.
import sys
import threading
def setup_thread_excepthook():
"""
Workaround for `sys.excepthook` thread bug from:
http://bugs.python.org/issue1230540
Call once from the main thread before creating any threads.
"""
init_original = threading.Thread.__init__
def init(self, *args, **kwargs):
init_original(self, *args, **kwargs)
run_original = self.run
def run_with_except_hook(*args2, **kwargs2):
try:
run_original(*args2, **kwargs2)
except Exception:
sys.excepthook(*sys.exc_info())
self.run = run_with_except_hook
threading.Thread.__init__ = init
I just stumbled over this problem and as it turns out, it was the right time to do so.
New in version 3.8: threading.excepthook
Handle uncaught exception raised by Thread.run().
The args argument has the following attributes:
exc_type: Exception type.
exc_value: Exception value, can be None.
exc_traceback: Exception traceback, can be None.
thread: Thread which raised the exception, can be None.
I don't know why, but be aware, that unlike sys.excepthook, threading.excepthook receives the arguments as a namedtuple instead of multiple arguments.
It looks like there is a related bug reported here with workarounds. The suggested hacks basically wrap run in a try/catch and then call sys.excepthook(*sys.exc_info())
I have a wxPython application that relies on an external config file. I want provide friendly message dialogs that show up if there are any config errors. I've tried to make this work by wrapping my app.MainLoop() call in a try/except statement.
The code below works for the init code in my MainWindow frame class, but doesn't catch any exceptions that occur within the MainLoop. How can I catch these exceptions as well?
if __name__ == '__main__':
app = MyApp(0)
try:
MainWindow(None, -1, 'My Cool App')
app.MainLoop()
except ConfigParser.Error, error_message:
messagebox = wx.MessageDialog(None, error_message, 'Configuration Error', wx.OK | wx.ICON_ERROR)
messagebox.ShowModal()
I've read some mention of an OnExceptionInMainLoop method that can be overridden in the wx.App class, but the source I read must be out of date (2004) since wx.App no longer seems to have a method by that name.
EDIT:
I need to be able to catch unhandled exceptions during my mainloop so that I can further handle them and display them in error dialogs, not pass silently, and not terminate the app.
The sys.excepthook solution is too low level and doesn't play nice with the wxPython mainloop thread. While the link to the other answer does the same try/except wrapping around the mainloop which doesn't work due, once again, to wxPython spawning a different thread for the app/ui.
Don't know if this will work for a wxPython application, but in the sys module you can overwrite the excepthook attribute, which is a function called with 3 arguments, (type, value, traceback), when an uncaugth exception is caught. You can install your own function in there that handles only the exceptions you want, and call the original function for all the others.
Consult: http://docs.python.org/library/sys.html#sys.excepthook
I coded something like this for Chandler, where any unhandled exceptions pop up a window with the stack and other info, and users can put in additional comments (what did they do when it happened etc.) and submit it for Chandler developers. A bit like the Mozilla Talkback (nowadays they use Breakpad I believe) feature if you will.
To do this in wxPython, you need to provide redirect parameter to wx.App. This will pop up wx.PyOnDemandOutputWindow (you will probably want to override it to provide a nicer looking implementation).
The relevant source files in Chandler are here:
Chandler.py starts the application and sets the redirect attribute, as well as tries to catch and display error dialogs in case normal application startup fails
Application.py customizes the application object, including setting up our customized wx.PyOnDemandOutputWindow
feedback.py has the implementation for the customized wx.PyOnDemandOutputWindow; it will additionally need feedback.xrc and feedback_xrc.py
Perhaps this question might be of some use, it tries to capture all exceptions.
Posting the solution that worked for me with a very similar problem.
import wx
import sys
import traceback
class Frame(wx.Frame):
def __init__(self):
wx.Frame.__init__(self, None)
panel = wx.Panel(self)
m_close = wx.Button(panel, -1, "Error")
m_close.Bind(wx.EVT_BUTTON, self.OnErr)
def OnErr(self, event):
1/0
def handleGUIException(exc_type, exc_value, exc_traceback):
err_msg = ''.join(traceback.format_exception(exc_type, exc_value, exc_traceback))
err_msg += '\n Your App will now terminate'
# Here collecting traceback and some log files to be sent for debugging.
# But also possible to handle the error and continue working.
dlg = wx.MessageDialog(None, err_msg, 'Termination dialog', wx.OK | wx.ICON_ERROR)
dlg.ShowModal()
dlg.Destroy()
sys.exit()
sys.excepthook = handleGUIException
if __name__ == '__main__':
app = wx.App(redirect=False)
top = Frame()
top.Show()
app.MainLoop()
Using sys.excepthook is very fine to me.
I find the following article of great help: wxPython: Catching Exceptions from Anywhere.