1

Possible Duplicate:
How can I modify a Python traceback object when raising an exception?

Consider this toy example:

def twice(n):
    _validate_twice_args(n)
    return 2*n

def _validate_twice_args(n):
    if type(n) != int:
        raise TypeError('n must be an int')

twice(None)
--
Traceback (most recent call last):
  File "demo.py", line 9, in <module>
    twice(None)
  File "demo.py", line 2, in twice
    _validate_twice_args(n)
  File "demo.py", line 7, in _validate_twice_args
    raise TypeError('n must be an int')
TypeError: n must be an int

Even though the locus of the error is the call twice(None), the traceback refers to code that the person responsible for the error is not even aware of, ("_validate who? I never called that! I don't even know what it is!"), and unnecessarily exposes code that should be "behind the API". Even in the absence of "private" helper functions like _validate_twice_args, the stacktrace printed in response to bad arguments unnecessarily exposes internal code, and obscures the locus of the error.

For example, if one inlines the code of _validate_twice_args, the stack trace looks like this:

Traceback (most recent call last):
  File "demo.py", line 10, in <module>
    twice(None)
  File "demo.py", line 3, in twice
    raise TypeError('n must be an int')
TypeError: n must be an int

To get an idea of what the stack trace for this sort of error should look like, here's the one resulting from a very similar type of error, the call twice() instead of twice(None), but one that is raised by Python, before control is passed to twice:

Traceback (most recent call last):
  File "demo.py", line 9, in <module>
    twice()
TypeError: twice() takes exactly 1 argument (0 given)

In this case too, the error lies in a call to twice with an invalid arguments list. Accordingly the stack trace points directly to the locus of the error (and not to the line in the underlying [most likely C] code where the error was first detected, thank goodness). This is as it should be, IMO1.

How can I modify twice_args and/or _validate_twice_args so that the last line of the stacktrace refers to the source code line of the buggy call to twice?


1I realize that opinions on what the stacktrace should say in such cases differ from mine, but these philosophical considerations are not the subject of this thread.

Edit: emphasized remarks in the original post, but apparently overlooked by the first two responders.

Community
  • 1
  • 1
kjo
  • 33,683
  • 52
  • 148
  • 265
  • 1
    BTW, if you want people to understand you more clearly, you could simplify your question - I'm not sure why you brought `_validate_twice_args` into this at all, now that I understand it isn't actually relevant to what you're trying to do. – Blorgbeard Sep 12 '12 at 04:25
  • `_validate_twice_args` shows the more extreme version of the problem. In the end, it's damn if you don't, damn if you don't: if I make my question succinct, people complain of not enough information; if I put a lot of information in it, people don't read *what I actually wrote*. – kjo Sep 12 '12 at 04:30
  • 4
    I would think that you'd be more polite to the people who are _still_ trying to answer your question after you repeatedly insult them. – Mu Mind Sep 12 '12 at 04:36
  • Rather than this approach, you may be interested in one of the contract decorator libraries available, such as http://andreacensi.github.com/contracts/ – Keith Sep 12 '12 at 04:41
  • 3
    The real question here is: what do you achieve from this? If the exceptions as they exist aren't relevant to the end user _then trap them at a higher level, log them and give the user a non-code-leaking error message instead_. This isn't pointless philosophical bitching, this is fundamental good design. – Matthew Trevor Sep 12 '12 at 04:46

4 Answers4

3

Well, you can change twice to this:

def twice(n):
    try:
        _validate_twice_args(n)
    except Exception, e:
        raise e
    return 2*n

But of course, if you're going to put a try/except block in twice, you might as well do the actual check in there. Which is, of course, what you really should do if you want the exception raised there, despite your footnote. An exception happens where it happens. You can never call a function and expect that any exception will occur in the actual body of that particular function. Functions call other functions all the time. It's fine to have validator functions that do stuff like that, but if so, you just have to accept that the exceptions are going to be raised there and not somewhere else.

In response to your edit: It may seem like you are "exposing private code" when you raise the exception where it occurs, but doing it the other way hides bugs, which is way, way worse. The traceback easily allows you to see up the stack and look for the place where you called the function. Doing what you describe, however, would chop off the traceback too high, making it impossible to see where the error actually happened. If code could raise an exception somewhere else, buggy code could hide itself very easily.

More generally, what you describe as "the locus of the error" is a red herring. An exception (which may or may not be an "error") should be raised at the point where the exceptional condition is detected. "Where the error is" is a more complex question that is not something you should try to paper over by creating "spaghetti exceptions" that raise themselves somewhere else. You as a human can see that twice(None) is the "place where a mistake was made" in this particular call chain, but that doesn't mean that you should try to make your program clever and have it figure that out. It is likely to go wrong at some point and cause greater confusion.

So to answer what you have emphasized: no, you can't do that. An exception is raised where it is raised. You can't raise an exception in one place and have it show up as if it was raised somewhere else.

BrenBarn
  • 242,874
  • 37
  • 412
  • 384
  • +1 for doing the check in `twice`, but this try/except doesn't actually work - the stack trace seems to still mention `_validate_twice_args`. – lvc Sep 12 '12 at 04:01
  • Plus it ignores what I wrote in the original post. There I specifically said that, even in the absence of a helper function like `_validate_twice_args` (i.e. even if its code is inlined into `twice`) the resulting stack trace 1) exposes private code; and 2) does not point directly to the locus of the error. – kjo Sep 12 '12 at 04:03
  • @lvc: It works for me. What Python version are you using? – BrenBarn Sep 12 '12 at 04:04
  • Just change it to not raise the same exception but a copy of it. Wouldn't this work? – Rod Sep 12 '12 at 04:08
  • @BrenBarn seems to work in Python 2, but not in Python 3 (noting that you need to change it to `except Exception as e:` to be testable in both). – lvc Sep 12 '12 at 04:08
  • I don't know what I must do to prevent answers that I have already ruled out... It is infuriating that no matter how explicitly I rule them out, I invariably get them. Without fail. – kjo Sep 12 '12 at 04:09
  • @kjo: I've edited my answer to give some more info. But basically the answer is "No, you can't do that." – BrenBarn Sep 12 '12 at 04:12
  • 3
    @kjo: Well, one way or another, you have the answer to the original question. – BrenBarn Sep 12 '12 at 04:18
1

Long answer short: without writing a C extension or using ctypes magic, you cannot manipulate a normal Python traceback to pretend that an error came from anywhere but a raise statement in your code.

Exceptions raised from Python will have a raise statement at the very top of their traceback, since that is by definition where the exception originated -- you can't change that (without any of the aforementioned trickery). In your case, you want the exception to look like it came from a function call (as the topmost frame), and I am saying it is impossible from pure Python.

If you still want to see a ctypes or CPython way to do this, I could show you. But you will not get what you want (and you evidently want what you want) by sticking to pure Python code.

EDIT: OK, this isn't 100% accurate. Of course you can always call some extension method that throws an exception -- the exception "comes from" the call to the extension method. Consequently, if you wrote twice as an extension method you could implement the exact semantics you wanted (this is what I mean by "writing a C extension").

Owing to the existence of generator.throw(), you can also cause exceptions to appear to come out of the def line in a generator function, or a yield statement, all from Python code. Neither, of course, will help you implement the semantics you want, but I wanted to mention these for completeness.

nneonneo
  • 171,345
  • 36
  • 312
  • 383
1

See How can I modify a Python traceback object when raising an exception?

There you'll find more advice not to do this, and also a link to some ugly code in jinja2 that shows you how to do it if you really must.

Community
  • 1
  • 1
Mu Mind
  • 10,935
  • 4
  • 38
  • 69
0

You could modify your code to something like this:

def twice(n):
    error = _validate_twice_args(n)
    if error:
        raise error
    return 2*n

def _validate_twice_args(n):
    if type(n) != int:
        return TypeError('n must be an int')
    return None

twice(None)

This way, the error is raised from twice. Unfortunately, it requires a bit more code in the twice method than a simple method call.

Traceback (most recent call last):
  File "<module1>", line 28, in <module>
  File "<module1>", line 25, in main
  File "<module1>", line 16, in twice
TypeError: n must be an int
Blorgbeard
  • 101,031
  • 48
  • 228
  • 272
  • Please see my latest edit to the original post. – kjo Sep 12 '12 at 04:04
  • Not sure what you mean - this generates a stack trace on `raise` - i.e. in `twice`. It doesn't refer to `_validate_twice_args`. – Blorgbeard Sep 12 '12 at 04:09
  • Oh, you want the stack trace to refer to the *call to twice*. Right. OK, I have no idea how you would go about doing that. – Blorgbeard Sep 12 '12 at 04:17