What happens if function from another module calls failing_external_function()? Will you wrap it in another_module.Error?
This sounds like viral boilerplate: any function that can fail forces all other functions in the stack trace to be wrapped. Seems quite unpythonic, the whole point of exceptions is to avoid this boilerplate by propagating.
Are you just writing boilerplate to assign exceptions to modules? But this information is already present in the stack trace, what is the point? It's too generic to actually handle exceptions, and too redundant to provide debugging value.
> What happens if function from another module calls failing_external_function()? Will you wrap it in another_module.Error?
Yes?
If you call a function in another module, generally you’ll always have to catch and handle errors specific to that module. If that module does not wrap its errors (like socket.error, FileNotFound, etc.), you’ll have to handle those, as well. And, like Parthenon points out, if that module ever changes its internal implementation, it will suddenly raise different errors, which your code does not catch. On the other hand, if the module wraps its errors, you’ll have a guarantee that you’ll be able to catch them.
I don’t know why you keep bringing up debugging; Python’s “raise from” keeps the original exception intact and available.
> Are you just writing boilerplate to assign exceptions to modules?
Wait, are you talking about monkey-patching? I’m not doing that. I’m talking about the case where you write your own module for something, and then use that module in another program (possibly itself a module).
Your code example essentially renames a regular wildcard Exception to module-specific wildcard exception.
But here's a thing:
For direct callers of your module, regular wildcard and module wildcard are exactly the same. There's absolutely no difference for them whether they are catching module.Error or Exception, because your code is structured so that mean exactly the same thing.
For indirect callers higher in the stack, your custom wildcard exception makes error handling harder, because instead of familiar exceptions they will see your custom one. Your wrapper makes it harder to access what happened, but easier to access where it happened. This is just a bad tradeoff. In some cases for you where might be more valuable than what. But you are in no position to assume that this is how it's going to be for your customers.
Examples where this practice might be useful (e.g. explicitly shifting the blame to a 3rd party) are simply too rare to justify those wrappers. You could still achieve the same results by inspecting the stack, without impeding the ability of your customers to deal with known exceptions.
> For direct callers of your module, regular wildcard and module wildcard are exactly the same.
Well, no. I usually only wrap exceptions which I know to possibly expect, and only wrap ‘Exception’ in code sections where I know I want to catch any exception, no matter what (which is rare, but happens). Unexpected exceptions (either of an unexpected type or in an unexpected place) will still propagate upwards. This allows the code calling my module to catch any reasonably expected exceptions (since I will wrap them in my module.Error, or, really, a more specifc exception class inhereting from module.Error), while still allowing unexpected exceptions to be shown.
I realize that I was unclear in my initial description; I do not catch ‘Exception’ all the time, but only occasionally. I most often list the exceptions which I know that the called function could raise in failure states which I know how to handle.
> Your wrapper makes it harder to access what happened
How? With “raise from”, Python shows you not only the stack trace of the error, but also the stack trace of the original exception, IIRC.
> How? With “raise from”, Python shows you not only the stack trace of the error, but also the stack trace of the original exception, IIRC.
Because you can't catch the original exception, you're stuck with weird module.Error which is too generic to do something about it. You would have to catch the module.Error and then look at e.__cause__ to actually handle the exception. So you end up with the exact same problem Parthenon is talking about, but with extra steps.
And pray no other dependencies are following your practice, because you would then have to go into e.__cause__.__cause__ and so on.
What exactly is the benefit that module.Error provides to your users compared to letting the original exception propagate?
> Because you can't catch the original exception, you're stuck with weird module.Error which is too generic to do something about it.
To be clear, I most often do not raise plain module.Error; I generally raise semantically informative errors like module.FooError or module.EntityNotFoundError, all of which inherit from module.Error.
> What exactly is the benefit that module.Error provides to your users compared to letting the original exception propagate?
I thought that I (and Parthenon) made that clear; it’s to protect the users of my module from having to know about my implementation details. If I switch from a socket-based approach to an HTTP-based system internally, I don’t want my users to have to know that and switch from catching socket.error to requests.Error or other_http_module.Error. Implementation details should not be a part of the API.
I’ve come to this view from, when investigating a runtime error, too often having to dig into the source code of some third-party module to investigate what kind of exception class I’ve gotten in my traceback, only to discover that it’s not an exception from that module, it’s an exception class from yet another module that the third-party module is itself using, and which I had no way of knowing could be raised. I then have to not only catch that exception in my code (which ties my code to implementation details of the third-party module, i.e. making my code brittle), but I also have to import the module containing the exception (in order to name the exception class in order to catch it), which increases my code’s direct dependencies.
This sounds like viral boilerplate: any function that can fail forces all other functions in the stack trace to be wrapped. Seems quite unpythonic, the whole point of exceptions is to avoid this boilerplate by propagating.
Are you just writing boilerplate to assign exceptions to modules? But this information is already present in the stack trace, what is the point? It's too generic to actually handle exceptions, and too redundant to provide debugging value.