PEP Idea: Matching Exceptions with 3.10's Structural Pattern Matching Syntax
Dear The PEP Community, Happy New Year! I'm assuming there's been discussions on this already! I checked the current PEP list and issue tracker, but to no avail. *Can someone point me to wherever it has or is taking place?* The functionality that I'm thinking about is: match (named_tuple_object.*missing_attribute*, a_random_string): case *AttributeError*, "Catching an attribute error": print("Catches as attribute error") case *err:= AttributeError*, "Assigns an attribute error as err": print(f"This is the captured attribute error: {*err*}") Sincerely, elvis kahoro <https://www.warp.dev/about-us> he/him/his DevX @ Warp <https://blog.warp.dev/how-warp-works/> Warp Blog <https://blog.warp.dev/> Warp Docs <https://docs.warp.dev/> Warp Twitter <https://twitter.com/warpdotdev>
Hi Elvis, On Sat, Jan 01, 2022 at 12:59:32AM -0500, elvis kahoro wrote:
The functionality that I'm thinking about is:
match (named_tuple_object.*missing_attribute*, a_random_string): case *AttributeError*, "Catching an attribute error": print("Catches as attribute error") case *err:= AttributeError*, "Assigns an attribute error as err": print(f"This is the captured attribute error: {*err*}")
Reading between the lines, I *think* that you want the match statement to catch the exception that you get when the attribute lookup fails, am I right? The problem here is that exceptions are values that can already be matched, and the regular pattern matching rules apply: >>> spam = (AttributeError, "eggs") >>> match spam: ... case (Exception, str): ... print("matched") ... matched So `case Exception` is going to match the exception as a class or instance. We would need new syntax to match a *raised* exception. I propose: match expression: except exceptions: block # regular cases follow after the except block which will only catch exceptions raised when evaluating the match expression. That is, equivalent to: try: temp = expression except exceptions: block else: match temp: # regular cases follow here except that there is no actual "temp" variable created. To be clear, the except block must come first, ahead of all the cases. -- Steve
Steven D'Aprano writes:
I propose:
match expression: except exceptions: block # regular cases follow after the except block
I probably would rarely use this syntax (preferring the explicit temporary, and possibly encapsulating the exception handling in a separate function) because in my common use cases for match (currently using if-elif-else), the exceptions are somebody else's issue that I have to defend against and there are a lot of them which almost all need some special casing, and what I care about are the match cases, which I want as close to the expression as possible. Ie, I would do something like def compute_specific_expression() try: return expression except OneException as e: handle_it_1(e) except TwoException as e: handle_it_2(e) expression_value = compute_specific_expression() match expression_value: case ... where the long names are intended to express the meaning of the expression in some detail, in preference to match expression: except OneException as e: handle_it_1(e) except TwoException as e: handle_it_2(e) case ... Also, we don't have to assume that match has try semantics (ie, a prologue of code which raises exceptions for match's handlers to catch). We could define match handlers to only catch exceptions from the expression, and just say that if you want to handle exceptions from all the cases in a uniform way, wrap the match in a try. This allows you to distinguish expression exceptions from case exceptions in the same way your syntax does, but allows me to satisfy my desire to have the cases at the top. Yes this would be confusing to people who would assume that handlers apply to all code in the match's suite, and I don't blame them for being confused. But IMO the benefits of putting the handlers at the end outweigh the costs of learning that the syntax means what it means. Steve
Reading between the lines, I *think* that you want the match statement to catch the exception that you get when the attribute lookup fails, am I right?
Yes! I was hoping there could be some syntax to extend pattern matching to handle exceptions such that we could handle patterns with multiple types of exceptions like so: match *this_raises_an_exception*, *this_raises_another_exception*: case *AttributeError*, *TypeError*: print("catches attribute and type errors") case *AttributeError*, *AttributeError*: print("catches attribute and attribute") case *Exception*, *Exception*: print("catches the remaining exceptions") case *x*, *y*: print(f"{x} and {y}") case *_*, *_*: print("everything else") Any thoughts on this kind of syntax? Maybe the author could explicitly distinguish that an exception might be raised by using *with Exception *like so: match *this_raises_an_exception*, *this_raises_another_exception** with Exception:* On Mon, Jan 3, 2022 at 7:25 PM Steven D'Aprano <steve@pearwood.info> wrote:
Hi Elvis,
On Sat, Jan 01, 2022 at 12:59:32AM -0500, elvis kahoro wrote:
The functionality that I'm thinking about is:
match (named_tuple_object.*missing_attribute*, a_random_string): case *AttributeError*, "Catching an attribute error": print("Catches as attribute error") case *err:= AttributeError*, "Assigns an attribute error as err": print(f"This is the captured attribute error: {*err*}")
Reading between the lines, I *think* that you want the match statement to catch the exception that you get when the attribute lookup fails, am I right?
The problem here is that exceptions are values that can already be matched, and the regular pattern matching rules apply:
>>> spam = (AttributeError, "eggs") >>> match spam: ... case (Exception, str): ... print("matched") ... matched
So `case Exception` is going to match the exception as a class or instance. We would need new syntax to match a *raised* exception.
I propose:
match expression: except exceptions: block # regular cases follow after the except block
which will only catch exceptions raised when evaluating the match expression. That is, equivalent to:
try: temp = expression except exceptions: block else: match temp: # regular cases follow here
except that there is no actual "temp" variable created.
To be clear, the except block must come first, ahead of all the cases.
-- Steve _______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-leave@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/WUUGAQ... Code of Conduct: http://python.org/psf/codeofconduct/
On Tue, Jan 4, 2022 at 3:32 PM elvis kahoro <elvis@warp.dev> wrote:
Reading between the lines, I *think* that you want the match statement to catch the exception that you get when the attribute lookup fails, am I right?
Yes!
I was hoping there could be some syntax to extend pattern matching to handle exceptions such that we could handle patterns with multiple types of exceptions like so:
match this_raises_an_exception, this_raises_another_exception: case AttributeError, TypeError: print("catches attribute and type errors") case AttributeError, AttributeError: print("catches attribute and attribute") case Exception, Exception: print("catches the remaining exceptions") case x, y: print(f"{x} and {y}") case _, _: print("everything else")
Any thoughts on this kind of syntax? Maybe the author could explicitly distinguish that an exception might be raised by using with Exception like so:
match this_raises_an_exception, this_raises_another_exception with Exception:
Question: Why? What's wrong with the existing syntax? Just because match/case syntax exists, that doesn't mean it has to be used for everything that selects different options. Exceptions are almost exclusively matched by type, nothing else, and we already have a very good syntax for doing that. The only situation where you'd want any other sort of exception matching is when it's a single type that carries multiple causes of error: try: ... except OSError[errno.EEXIST]: ... But as you can see here, this could easily be accomplished using the existing syntax, if we needed a way to do it (the use-cases are uncommon, given that we have eg FileNotFoundError). When, in production code, have you *ever* needed to match on two different exceptions at once? ChrisA
On Mon, Jan 03, 2022 at 11:31:30PM -0500, elvis kahoro wrote:
I was hoping there could be some syntax to extend pattern matching to handle exceptions such that we could handle patterns with multiple types of exceptions like so:
match *this_raises_an_exception*, *this_raises_another_exception*: case *AttributeError*, *TypeError*: print("catches attribute and type errors") case *AttributeError*, *AttributeError*: print("catches attribute and attribute")
Can you explain why you want to do that? Right now, to do something like that you would need something like this: err1 = err2 = None try: x = this_raises_an_exception except AttributeError as e: err1 = e try: y = this_raises_another_exception except (AttributeError, TypeError) as e: err2 = e if err1 is err2 is None: # handle the case where neither expression raised print(x, y) elif err1 is None or err2 is None: # only one expression raised print("everything else") elif isinstance(err1, AttributeError): if isinstance(err2, AttributeError): print("two Attribute Errors") elif isinstance(err2, TypeError): print("Attribute Error and Type Error") else: print("everything else") else: print("everything else") or something equally convoluted. I agree that if you are currently writing code this horrible, the idea of using a match statement would seem attractive. But... are you actually writing this sort of horrible, convoluted, complex, complicated mess of code? Why??? Please explain your actual concrete use-case for this. Otherwise it looks to me like an over-generalisation. I think I would need to see a sketch of *real* code to understand why you want this. As I see it, there are three realistic use-cases, all which can be handled with existing syntax: * protect the match expression in a try...except: try: x = expression except SomeError: ... match x: ... * protect a case block in a try...except: match expression: case something: try: block except SomeError: ... * protect the whole construct in a try...except: try: match expression: case something: block except: log(something) raise Anything else seems to me to be YAGNI; too complex and complicated to care about. Convince me that I'm wrong. -- Steve
Thanks for the feedback everyone. I've only encountered the use case once, like you've mentioned it's probably not worth the effort given that it's such a small problem space. Thanks again! On Tue, Jan 4, 2022, 00:18 Steven D'Aprano <steve@pearwood.info> wrote:
On Mon, Jan 03, 2022 at 11:31:30PM -0500, elvis kahoro wrote:
I was hoping there could be some syntax to extend pattern matching to handle exceptions such that we could handle patterns with multiple types of exceptions like so:
match *this_raises_an_exception*, *this_raises_another_exception*: case *AttributeError*, *TypeError*: print("catches attribute and type errors") case *AttributeError*, *AttributeError*: print("catches attribute and attribute")
Can you explain why you want to do that?
Right now, to do something like that you would need something like this:
err1 = err2 = None try: x = this_raises_an_exception except AttributeError as e: err1 = e try: y = this_raises_another_exception except (AttributeError, TypeError) as e: err2 = e
if err1 is err2 is None: # handle the case where neither expression raised print(x, y)
elif err1 is None or err2 is None: # only one expression raised print("everything else")
elif isinstance(err1, AttributeError): if isinstance(err2, AttributeError): print("two Attribute Errors")
elif isinstance(err2, TypeError): print("Attribute Error and Type Error")
else: print("everything else")
else: print("everything else")
or something equally convoluted. I agree that if you are currently writing code this horrible, the idea of using a match statement would seem attractive.
But... are you actually writing this sort of horrible, convoluted, complex, complicated mess of code? Why??? Please explain your actual concrete use-case for this. Otherwise it looks to me like an over-generalisation.
I think I would need to see a sketch of *real* code to understand why you want this.
As I see it, there are three realistic use-cases, all which can be handled with existing syntax:
* protect the match expression in a try...except:
try: x = expression except SomeError: ... match x: ...
* protect a case block in a try...except:
match expression: case something: try: block except SomeError: ...
* protect the whole construct in a try...except:
try: match expression: case something: block except: log(something) raise
Anything else seems to me to be YAGNI; too complex and complicated to care about.
Convince me that I'm wrong.
-- Steve _______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-leave@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/RNMCIY... Code of Conduct: http://python.org/psf/codeofconduct/
participants (4)
-
Chris Angelico
-
elvis kahoro
-
Stephen J. Turnbull
-
Steven D'Aprano