I want a context manager to catch an exception, print the stack trace, and then allow execution to continue.
I want to know if I can do this with the contextlib contextmanager decorator. If not, how can I do it?
Documentation suggests the following:
At the point where the generator yields, the block nested in the with statement is executed. The generator is then resumed after the block is exited. If an unhandled exception occurs in the block, it is reraised inside the generator at the point where the yield occurred. Thus, you can use a try…except…finally statement to trap the error (if any), or ensure that some cleanup takes place. If an exception is trapped merely in order to log it or to perform some action (rather than to suppress it entirely), the generator must reraise that exception.
So I try the obvious approach that the documentation leads me to:
import contextlib
import logging
#contextlib.contextmanager
def log_error():
try:
yield
except Exception as e:
logging.exception('hit exception')
finally:
print 'done with contextmanager'
def something_inside_django_app():
with log_error():
raise Exception('alan!')
something_inside_django_app()
print 'next block of code'
This produces the output
ERROR:root:hit exception
Traceback (most recent call last):
File "exception_test.py", line 8, in log_error
yield
File "exception_test.py", line 17, in something_inside_django_app
raise Exception('alan!')
Exception: alan!
done with contextmanager
next block of code
This loses critical information about where the exception was raised from. Consider what you get when you adjust the context manager to not supress the exception:
Traceback (most recent call last):
File "exception_test.py", line 20, in <module>
something_inside_django_app()
File "exception_test.py", line 17, in something_inside_django_app
raise Exception('alan!')
Exception: alan!
Yes, it was able to tell me that the exception was raised from line 17, thank you very much, but the prior call at line 20 is lost information. How can I have the context manager give me the actual full call stack and not its truncated version of it? To recap, I want to fulfill two requirements:
have a python context manager suppress an exception raised in the code it wraps
print the stack trace that would have been generated by that code, had I not been using the context manager
If this cannot be done with the decorator, then I'll use the other style of context manager instead. If this cannot be done with context managers, period, I would like to know what a good pythonic alternative is.
I have updated my solution for this problem here:
https://gist.github.com/AlanCoding/288ee96b60e24c1f2cca47326e2c0af1
There was more context that the question missed. In order to obtain the full stack at the point of exception, we need both the traceback returned to the context manager, and the current context. Then we can glue together the top of the stack with the bottom of the stack.
To illustrate the use case better, consider this:
def err_method1():
print [1, 2][4]
def err_method2():
err_method1()
def outside_method1():
with log_error():
err_method2()
def outside_method2():
outside_method1()
outside_method2()
To really accomplish what this question is looking for, we want to see both outer methods, and both inner methods in the call stack.
Here is a solution that does appear to work for this:
class log_error(object):
def __enter__(self):
return
def __exit__(self, exc_type, exc_value, exc_traceback):
if exc_value:
# We want the _full_ traceback with the context, so first we
# get context for the current stack, and delete the last 2
# layers of context, saying that we're in the __exit__ method...
top_stack = StringIO.StringIO()
tb.print_stack(file=top_stack)
top_lines = top_stack.getvalue().strip('\n').split('\n')[:-4]
top_stack.close()
# Now, we glue that stack to the stack from the local error
# that happened within the context manager
full_stack = StringIO.StringIO()
full_stack.write('Traceback (most recent call last):\n')
full_stack.write('\n'.join(top_lines))
full_stack.write('\n')
tb.print_tb(exc_traceback, file=full_stack)
full_stack.write('{}: {}'.format(exc_type.__name__, str(exc_value)))
sinfo = full_stack.getvalue()
full_stack.close()
# Log the combined stack
logging.error('Log message\n{}'.format(sinfo))
return True
The traceback looks like:
ERROR:root:Log message
Traceback (most recent call last):
File "exception_test.py", line 71, in <module>
outside_method2()
File "exception_test.py", line 69, in outside_method2
outside_method1()
File "exception_test.py", line 65, in outside_method1
err_method2()
File "exception_test.py", line 60, in err_method2
err_method1()
File "exception_test.py", line 56, in err_method1
print [1, 2][4]
IndexError: list index out of range
This is the same information that you would expect from doing logging.exception in a try-except over the same code that you wrap in the context manager.
Related
For example, I am using multiprocessing pool to process files:
with Pool(5) as pool:
results = pool.starmap(self.process_file, zip(files, repeat(channel)))
When an exception occurs inside the function process_file, the exception message indicates that it occurs at the pool.starmap line, but not the actual place inside process_file function.
I am using PyCharm to develop and debug. Is there a way to change this behavior? The current error message doesn't give the correct position of the error occurred.
Multiprocessing transfers the errors between processes using the pickle module, but pickle doesn't know how to preserve the tracebacks of exceptions by default.
I found tblib to be a very convenient way to address this shortcoming. Based on this example I suggest you try adding this code to the main module of your code:
from tblib import pickling_support
# all your setup code
pickling_support.install()
if __name__ == "__main__":
# your pool work
The exception has the original exception info but PyCharm is not ferreting it out.
Assuming there are no PyCharm configuration options to enhance its ability to ferret out all the exception information, and not just the outer exception as your are seeing, you need to programmatically extract it out.
For good in-program error handling, you probably want to do that anyway. Especially with sub-processes, I very often will catch Exception and log it and re-raise if not considered handled (depends), or if I catch a specific exception I'm expecting and consider it handled, I won't re-raise it.
Note, it's not only PyCharm showing the outer exception... I see the same thing with other tools.
Below will show you the original problem (see "Line 7" below) and re-raise. Again, re-raising or not is really context dependent so below is just an example. But the point is that the exception you are seeing has more data that PyCharm by default is apparently not showing you.
from itertools import repeat
from multiprocessing import Pool
import traceback
def process(a,b):
print(a,b)
raise Exception("not good")
if __name__ == '__main__':
with Pool(5) as pool:
try:
results = pool.starmap(process, zip([1,2,3,4,5], repeat('A')))
except Exception as ex:
print("starmap failure:")
for error_line in traceback.format_exception(ex, ex, ex.__traceback__):
error_line = error_line.strip()
if not error_line:
continue
print(f" {error_line}")
raise # re-raise if we do not consider this handled.
Gives me this output:
starmap failure:
multiprocessing.pool.RemoteTraceback:
"""
Traceback (most recent call last):
File "C:\Users\...\multiprocessing\pool.py", line 125, in worker
result = (True, func(*args, **kwds))
File "C:\Users\...\multiprocessing\pool.py", line 51, in starmapstar
return list(itertools.starmap(args[0], args[1]))
File "...\starmap_exception.py", line 7, in process
raise Exception("not good")
Exception: not good
"""
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "...\starmap_exception.py", line 12, in <module>
results = pool.starmap(process, zip([1,2,3,4,5], repeat('A')))
File "C:\Users\...\multiprocessing\pool.py", line 372, in starmap
return self._map_async(func, iterable, starmapstar, chunksize).get()
File "C:\Users\...\multiprocessing\pool.py", line 771, in get
raise self._value
Exception: not good
So when I run this... the error is on this line bomb=pd.DataFrame(here,0) but the trace shows me a bunch of code from the pandas library to get to the error.
import traceback,sys
import pandas as pd
def error_handle(err_var,instance_name=None): #err_var list of variables, instance_name
print(traceback.format_exc())
a= sys._getframe(1).f_locals
for i in err_var: # selected var for instance
t= a[instance_name]
print i,"--->",getattr(t,i.split(".")[1])
here=['foo']
err_var = ['self.needthisone','self.constant2']
class test:
def __init__(self):
self.constant1 = 'hi1'
#self.constant2 = 'hi2'
#self.needthisone = ':)'
for i in err_var:
setattr(self, i.split('.')[1], None)
def other_function(self):
self.other_var=5
def testing(self):
self.other_function()
vars=[self.constant1,self.constant2]
try:
for i in vars:
bomb=pd.DataFrame(here,0)
except:
error_handle(err_var,'self')
t=test()
t.testing()
How do I suppress all that and have the error just look like this:
Traceback (most recent call last):
File "C:\Users\Jason\Google Drive\python\error_handling.py", line 34, in testing
bomb=pd.DataFrame(here,0)
TypeError: Index(...) must be called with a collection of some kind, 0 was passed
I just want what's relevant to me and the last line of code that I wrote which was bad.
This is the original:
Traceback (most recent call last):
File "C:\Users\Jason\Google Drive\python\error_handling.py", line 35, in testing
bomb=pd.DataFrame(here,0)
File "C:\Python27\lib\site-packages\pandas\core\frame.py", line 330, in __init__
copy=copy)
File "C:\Python27\lib\site-packages\pandas\core\frame.py", line 474, in _init_ndarray
index, columns = _get_axes(*values.shape)
File "C:\Python27\lib\site-packages\pandas\core\frame.py", line 436, in _get_axes
index = _ensure_index(index)
File "C:\Python27\lib\site-packages\pandas\core\indexes\base.py", line 3978, in _ensure_index
return Index(index_like)
File "C:\Python27\lib\site-packages\pandas\core\indexes\base.py", line 326, in __new__
cls._scalar_data_error(data)
File "C:\Python27\lib\site-packages\pandas\core\indexes\base.py", line 678, in _scalar_data_error
repr(data)))
TypeError: Index(...) must be called with a collection of some kind, 0 was passed
self.needthisone ---> None
self.constant2 ---> None
You can define how far back a traceback goes using the sys.traceback variable. If your code is only 3 levels deep, (a function in a class in a file), then you can define this appropriately with the code:
sys.tracebacklimit = 3
at the top of your file. BE CAREFUL WITH THIS: As you write more code, the portion that you've written will become deeper and deeper, and you may sometime soon find that an error is a result of something deeper in the traceback. As a general rule, I would avoid using the variable and just deal with a longer traceback for the time being.
Please, don't ever think about limiting the stack trace. It is very important.
Only at this moment, in this small example of yours, the error really is in your code.
But in an infinite other cases, an error could be triggered much deeper than that. It could be in the framework, or even out of the code whatsoever, like a configuration error, or it could be in the platform, like an Out of Memory error, etc.
The stack trace is there to help you. It lists all frames the compiler was executing, to give you all the info you need to understand what was going on.
I would highly encourage you not to limit your traceback output, because it is bad practice. You feel like there is too much info; but this is only because you already looked at it already and you know what error to look for.
In most cases, the problem may be hiding elsewhere. So there has to be a better way to achieve what you look for.
Why not wrap your function call in a try except clause and print the exception message? Take this scenario for example:
def f():
a = 0
i = 1
print i/a
def another_func():
print 'this is another func'
return f()
def higher_level_func():
print 'this is higher level'
return another_func()
if __name__ == '__main__':
try:
higher_level_func()
except Exception as e:
print 'caught the exception: {}-{}'.format(type(e)__name__, e.message)
When called, this is the output:
this is higher level
this is another func
caught the exception: ZeroDivisionError-integer division or modulo by zero
This prints only the relevant exception in your code, hiding any information about the traceback, but the traceback is still available and you can print it as well (just raise the exception from your except block).
Compared to this, if I remove the try except block:
this is higher level
this is another func
caught the exception: integer division or modulo by zero
Traceback (most recent call last):
File "test.py", line 17, in <module>
higher_level_func()
File "test.py", line 12, in higher_level_func
return another_func()
File "test.py", line 8, in another_func
return f()
File "test.py", line 4, in f
print i/a
ZeroDivisionError: integer division or modulo by zero
You better use this technique to capture the relevant exception, rather than limiting the tracebacks. If you want your program to stop, just add sys.exit(1) in the except block.
The issue
The Phantom Menace
Say i wrote a function decorator which takes the function, and wraps it in another function like so:
# File example-1.py
from functools import wraps
def decorator(func):
# Do something
#wraps(func)
def wrapper(*args, **kwargs):
# Do something
return func(*args, **kwargs)
# Do something
# Do something
return wrapper
Now lets suppose the function I'm decorating raises an exception:
#decorator
def foo():
raise Exception('test')
The result of running foo() will print out the following traceback (In any Python version):
Traceback (most recent call last):
File "./example-1.py", line 20, in <module>
foo()
File "./example-1.py", line 11, in wrapper
return func(*args, **kwargs)
File "./example-1.py", line 18, in foo
raise Exception('test')
Exception: test
Attack of the Clones
OK, now i look at my traceback and i see it goes through the wrapper function. What if I wrapped the function multiple times(presumably with a slightly more sophisticated decorator object which receives arguments in its constructor)? What if I use this decorator often in my code(I use it for logging, or profiling, or whatever)?
Traceback (most recent call last):
File "./example-1.py", line 20, in <module>
foo()
File "./example-1.py", line 11, in wrapper
return func(*args, **kwargs)
File "./example-1.py", line 11, in wrapper
return func(*args, **kwargs)
File "./example-1.py", line 11, in wrapper
return func(*args, **kwargs)
File "./example-1.py", line 11, in wrapper
return func(*args, **kwargs)
File "./example-1.py", line 18, in foo
raise Exception('test')
Exception: test
I don't want it "polluting" my traceback when i know from the function definition that the wrapper is there, and i don't want it showing up multiple times when the code snippet it displays is the unhelpful return func(*args, **kwargs)
Python 2
Revenge of the Sith
In Python-2, as this answer to a different question points out, the following trick does the job:
# In file example-2.py
def decorator(func):
# Do something
#wraps(func)
def wrapper(*args, **kwargs):
# Do something
info = None
try:
return func(*args, **kwargs)
except:
info = sys.exc_info()
raise info[0], info[1], info[2].tb_next
finally:
# Break the cyclical reference created by the traceback object
del info
# Do something
# Do something
return wrapper
By directly wrapping the call to the wrapped function with this idiom in the same block as the function I want to elide from the traceback, I effectively remove the current layer from the traceback and let the exception keep propagating. Every time the stack unwinding goes through this function, it removes itself from the traceback so this solution works perfectly:
Traceback (most recent call last):
File "./example-2.py", line 28, in <module>
foo()
File "./example-2.py", line 26, in foo
raise Exception('test')
Exception: test
(Note however that you can not encapsulate this idiom in another function, since as soon the stack will unwind from that function back into wrapper, it will still be added to the traceback)
Python 3
A New Hope
Now that we have this covered, lets move along to Python-3. Python-3 introduced this new syntax:
raise_stmt ::= "raise" [expression ["from" expression]]
which allows chaining exceptions using the __cause__ attribute of the new exception. This feature is uninteresting to us, since it modifies the exception, not the traceback. Our goal is to be a completely transparent wrapper, as far as visibility goes, so this won't do.
Alternatively, we can try the following syntax, which promises to do what we want (code example taken from the python documentation):
raise Exception("foo occurred").with_traceback(tracebackobj)
Using this syntax we may try something like this:
# In file example-3
def decorator(func):
# Do something
#wraps(func)
def wrapper(*args, **kwargs):
# Do something
info = None
try:
return func(*args, **kwargs)
except:
info = sys.exc_info()
raise info[1].with_traceback(info[2].tb_next)
finally:
# Break the cyclical reference created by the traceback object
del info
# Do something
# Do something
return wrapper
The Empire Strikes Back
But, unfortunately, this does not do what we want:
Traceback (most recent call last):
File "./example-3.py", line 29, in <module>
foo()
File "./example-3.py", line 17, in wrapper
raise info[1].with_traceback(info[2].tb_next)
File "./example-3.py", line 27, in foo
raise Exception('test')
Exception: test
As you can see, the line executing the raise statement shows up in the traceback. This seems to come from the fact that while the Python-2 syntax sets the traceback from the third argument to raise as the function is being unwound, and thus it is not added to the traceback chain(as explained in the docs under Data Model), the Python-3 syntax on the other hand changes the traceback on the Exception object as an expression inside the functions context, and then passes it to the raise statement which adds the new location in code to the traceback chain (the explanation of this is very similar in Python-3).
A workaround that comes to mind is avoiding the "raise" [ expression ] form of the statement, and instead use the clean raise statement to let the exception propagate as usual but modify the exception objects __traceback__ attribute manually:
# File example-4
def decorator(func):
# Do something
#wraps(func)
def wrapper(*args, **kwargs):
# Do something
info = None
try:
return func(*args, **kwargs)
except:
info = sys.exc_info()
info[1].__traceback__ = info[2].tb_next
raise
finally:
# Break the cyclical reference created by the traceback object
del info
# Do something
# Do something
return wrapper
But this doesn't work at all!
Traceback (most recent call last):
File "./example-4.py", line 30, in <module>
foo()
File "./example-4.py", line 14, in wrapper
return func(*args, **kwargs)
File "./example-4.py", line 28, in foo
raise Exception('test')
Exception: test
Return of the Jedi(?)
So, what else can i do? It seems like using the "traditional" way of doing this just won't work because of the change in syntax, and I wouldn't want to start messing with the traceback printing mechanism (using the traceback module) at the project level. This is because it'll be hard if not impossible to implement in an extensible which won't be disruptive to any other package that tries to change the traceback, print the traceback in a custom format at the top level, or otherwise do anything else related to the issue.
Also, can someone explain why in fact the last technique fails completely?
(I tried these examples on python 2.6, 2.7, 3.4, 3.6)
EDIT: After thinking about it for a while, in my opinion the python 3 behavior makes more sense, to the point that the python 2 behavior almost looks like a design bug, but I still think that there should be a way to do this kinda stuff.
The simple answer is that you shouldn't do that. Hiding things from the traceback is dangerous. You may think you don't want to show that line because it's trivial or "just a wrapper", but in general you wouldn't write the wrapper function if it didn't do something. Next thing you know there is a bug in the wrapper function which is now unfindable because the wrapper function has erased itself from the traceback.
Just deal with the extra lines in the traceback, or, if you really want, override sys.excepthook and filter them out at the top level. If you're worried about someone else overriding sys.excepthook too, then wrap all your code in a top-level function that does the exception printing itself. It isn't and shouldn't be easy to hide levels from the traceback.
The internal _testcapi module contains a binding for the PyErr_SetExcInfo function, which makes this fairly easy to implement. Only tested on cpython 3.9; there's no guarantee that it'll continue working in the future.
import functools
import typing as T
A_Callable = T.TypeVar("A_Callable", bound=T.Callable[..., T.Any])
try:
from sys import exc_info
from _testcapi import set_exc_info
def silent_wrapper(f: A_Callable) -> A_Callable:
#functools.wraps(f)
def wrapper(*args: T.Any, **kwargs: T.Any) -> T.Any:
try:
return f(*args, **kwargs)
except:
tp, exc, tb = exc_info()
# Drop two frames, one for silent_wrapper itself and one for the
# wrapped function
set_exc_info(tp, exc, tb.tb_next.tb_next) # type: ignore[union-attr]
del tp, exc, tb
raise
return T.cast(A_Callable, wrapper)
except ImportError:
def silent_wrapper(f: A_Callable) -> A_Callable:
return f
In the following stacktrace I miss the upper frames.
Who called callback() in ioloop.py line 458?
The stacktrace comes from a unittest TestCase. All tests pass but this traceback is in the logs and reproducible.
I can't see in which test of the TestCase the exception was raised.
ERROR [25950] Exception in callback <functools.partial object at 0x5358368>
Traceback (most recent call last):
File "/home/modwork_foo_dtg/lib/python2.7/site-packages/tornado/ioloop.py", line 458, in _run_callback
callback()
File "/home/modwork_foo_dtg/lib/python2.7/site-packages/tornado/stack_context.py", line 331, in wrapped
raise_exc_info(exc)
File "/home/modwork_foo_dtg/lib/python2.7/site-packages/tornado/stack_context.py", line 302, in wrapped
ret = fn(*args, **kwargs)
File "/home/modwork_foo_dtg/src/websocketrpc/websocketrpc/client.py", line 71, in connect
self.ws = websocket_connect(self.args.url)
File "/home/modwork_foo_dtg/src/websocketrpc/websocketrpc/client.py", line 179, in websocket_connect
conn = websocket.WebSocketClientConnection(io_loop, request)
File "/home/modwork_foo_dtg/lib/python2.7/site-packages/tornado/websocket.py", line 777, in __init__
raise Exception('%s %s' % (request, request.url))
Exception: <tornado.httpclient._RequestProxy object at 0x535cb10> None
How could I use tornado to see the upper stacktrace frames?
The exception itself is not the problem.
You can use IOLoop.handle_callback_exception to print from sys.exc_info to see what specifically is breaking.
The callback was invoked by ioloop.py:458, just like it says. No outer stack frames are shown because the exception didn't escape that frame. The thing that's confusing you is that the callback goes on to re-raise an exception that was captured earlier.
In Python 2, preserving tracebacks to re-raise later is messy (it gets better in Python 3). Tornado usually does the right thing here, but there are some gaps where a traceback will get truncated. The main problem I'm aware of in current versions is that AsyncHTTPClient tends to throw away tracebacks (and there are some annoying backwards-compatibility issues with fixing this).
As a crude workaround while debugging, you can try printing traceback.format_stack just before throwing an exception (at least where it's feasible to modify the code, as you've done here to add an exception to websocket.py).
When I get a runtime exception from the standard library, it's almost always a problem in my code and not in the library code. Is there a way to truncate the exception stack trace so that it doesn't show the guts of the library package?
For example, I would like to get this:
Traceback (most recent call last):
File "./lmd3-mkhead.py", line 71, in <module>
main()
File "./lmd3-mkhead.py", line 66, in main
create()
File "./lmd3-mkhead.py", line 41, in create
headver1[depotFile]=rev
TypeError: Data values must be of type string or None.
and not this:
Traceback (most recent call last):
File "./lmd3-mkhead.py", line 71, in <module>
main()
File "./lmd3-mkhead.py", line 66, in main
create()
File "./lmd3-mkhead.py", line 41, in create
headver1[depotFile]=rev
File "/usr/anim/modsquad/oses/fc11/lib/python2.6/bsddb/__init__.py", line 276, in __setitem__
_DeadlockWrap(wrapF) # self.db[key] = value
File "/usr/anim/modsquad/oses/fc11/lib/python2.6/bsddb/dbutils.py", line 68, in DeadlockWrap
return function(*_args, **_kwargs)
File "/usr/anim/modsquad/oses/fc11/lib/python2.6/bsddb/__init__.py", line 275, in wrapF
self.db[key] = value
TypeError: Data values must be of type string or None.
update: added an answer with the code, thanks to the pointer from Alex.
The traceback module in Python's standard library lets you emit error tracebacks in a way that accords to your liking, while an exception is propagating. You can use this power either in the except leg of a try/except statement, or in a function you've installed as sys.excepthook, which gets called if and when an exception propagates all the way; quoting the docs:
In an interactive session this happens
just before control is returned to the
prompt; in a Python program this
happens just before the program exits.
The handling of such top-level
exceptions can be customized by
assigning another three-argument
function to sys.excepthook.
Here's a simple, artificial example:
>>> import sys
>>> import traceback
>>> def f(n):
... if n<=0: raise ZeroDivisionError
... f(n-1)
...
>>> def excepthook(type, value, tb):
... traceback.print_exception(type, value, tb, 3)
...
>>> sys.excepthook = excepthook
>>> f(8)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in f
File "<stdin>", line 3, in f
ZeroDivisionError
as you see, without needing a try/except, you can easily limit the traceback to (for example) the first three levels -- even though we know by design that there were 9 nested levels when the exception was raised.
You want something more sophisticated than a simple limit on levels, so you'll need to call traceback.format_exception, which gives you a list of lines rather than printing it, then "prune" from that list the lines that are about modules you never want to see in your tracebacks, and finally emit the remaining lines (typically to sys.stderr, but, whatever!-).
Thanks to the pointer from Alex, here's teh codez:
def trimmedexceptions(type, value, tb, pylibdir=None, lev=None):
"""trim system packages from the exception printout"""
if pylibdir is None:
import traceback, distutils.sysconfig
pylibdir = distutils.sysconfig.get_python_lib(1,1)
nlev = trimmedexceptions(type, value, tb, pylibdir, 0)
traceback.print_exception(type, value, tb, nlev)
else:
fn = tb.tb_frame.f_code.co_filename
if tb.tb_next is None or fn.startswith(pylibdir):
return lev
else:
return trimmedexceptions(type, value, tb.tb_next, pylibdir, lev+1)
import sys
sys.excepthook=trimmedexceptions
# --- test code ---
def f1(): f2()
def f2(): f3()
def f3():
import xmlrpclib
proxy = xmlrpclib.ServerProxy('http://nosuchserver')
proxy.f()
f1()
Which yields this stack trace:
Traceback (most recent call last):
File "./tsttraceback.py", line 47, in <module>
f1()
File "./tsttraceback.py", line 40, in f1
def f1(): f2()
File "./tsttraceback.py", line 41, in f2
def f2(): f3()
File "./tsttraceback.py", line 45, in f3
proxy.f()
gaierror: [Errno -2] Name or service not known
The Traceback library is probably what you want. Here's one example that might help:
import traceback
try:
your_main()
except:
lines = traceback.format_exc()
print lines[:lines.find('File "/usr')]
(This obviously won't work if there's an exception outside the library, and might not exactly fit your needs, but it's one way of using the traceback library)
Put an unqualified try...except at the top of your code (ie: in your "main") or set sys.excepthook. You can then format the stack trace however you'd like.