Compound with .. else statement

Hi, In Python, there are multiple [compound statements](https://docs.python.org/3/reference/compound_stmts.html) with the `else` keyword. For example: ``` for x in iterable: if x == sentinel: break else: print("Sentinel not found.") ``` or: ``` try: do_something_sensitive() except MyError: print("Oops!") else: print("We're all safe.") ``` In my situation, I would like to mix the `with` statement with `else`. In this case I would like that if no exception is raised within the `with` to run the `else` part. For example: ``` with my_context(): do_something_sensitive() else: print("We're all safe.") ``` Now imagine that in my `try .. except` block I have some heavy setup to do before `do_something_sensitive()` and some heavy cleanup when the exception occurs. I'd like my context manager to do the preparation work, execute the body, and cleanup. Or execute my else block only if there is no exception. Is there already a way to accomplish this in Python or can this be a nice to have? Regards, Jimmy

On Mon, 30 Mar 2020 23:27:19 +0200 Jimmy Thrasibule <jimmy.thrasibule@gmail.com> wrote:
Is there already a way to accomplish this in Python or can this be a nice to have?
Perhaps you could use try/finally: try: prepare() do_something_sensitive() finally: cleanup() Whether the call to prepare goes inside or outside the try block depends on many things, mostly its coupling to the cleanup procedure (e.g., do they need to share objects? is cleanup idempotennt?). HTH, Dan -- “Atoms are not things.” – Werner Heisenberg Dan Sommers, http://www.tombstonezero.net/dan

Well I actually would like to run the else block in case an exception did occurred. Let me provide an example from my use case which is the management of a database transaction: with savepoint(transaction_manager): # Let's try to add into the database with some constraints. obj = db.add(data) db.flush() else: # Object already in database. obj = db.get(data) With the following context manager: class savepoint(object): def __init__(self): self._sp = None def __enter__(self, tm): self._sp = tm.savepoint() def __exit__(self, exc_ty, exc_val, tb): if exc_ty is not None and issubclass(ecx_ty, IntegrityError): self._sp.rollback() # We have an exception, execute else block. return False # All good, we commit our transaction. self._sp.commit() return True I find it quite a pretty, try and fail back way that I can easily replicate in my code without having to prepare and clean up each time with a try/catch.

On Mon, Mar 30, 2020 at 3:19 PM Serhiy Storchaka <storchaka@gmail.com> wrote:
In case Serhiy's answer wasn't clear: context managers can be written to handle exceptions (within their context) in any way you see fit. that is: the method: __exit__(self, exc_type, exc_value, exc_traceback): get the exception, and information about it, of one is raised, so you can handle it anyway you want. -CHB
-- Christopher Barker, PhD Python Language Consulting - Teaching - Scientific Software Development - Desktop GUI and Web Development - wxPython, numpy, scipy, Cython

31.03.20 01:32, Christopher Barker пише:
Actually I meant the opposite. Sorry for being unclear. In normal case the context manager does not silence a raised exception, so control flow is never passed to the statement past the with block if an exception is raised inside the with block. But if you use a context manager which silences the exception, like contextlib.suppress() or unittest.TestCase.assertRaises(), it is easy to do too. was_not_raised = False with my_context(): do_something_sensitive() was_not_raised = True if was_not_raised: print("We're all safe.") You do not need a special syntax for this. was_not_raised will never be set to True if do_something_sensitive() raises an exception.

On Mar 31, 2020, at 03:06, Jimmy Thrasibule <jimmy.thrasibule@gmail.com> wrote:
It might help your proposal to just show a small concrete and realistic example of how this workaround parallels the workaround for not having for/else, and how your proposed change would let you improve your code’s readability in exactly the same way as for/else. At least for me, it’s always been easier to show a newcomer to Python the point of for/else with a nice example than to try to explain the semantics and why they’re useful, and I assume the same would be true here.

On Tue, Mar 31, 2020 at 2:41 AM Serhiy Storchaka <storchaka@gmail.com> wrote:
Actually I meant the opposite.
I think I was thrown by the use of the example "my_context" -- that is, if you are writing a conrtect manger, you can handle exceptions any way you like. If you are using an existing one, then, yes:
but we don't need special syntax for "else" on a for or while loop (or try block) either, you could always set a sentinel for those too. which to me is a case for adding else to a "with" block as well, for all the same reasons it's there for the other block construct. Though I don't think I'd advocate it in this case, as the Exception is not really a clear part of the context manger API, like "break" is to the loops. -CHB -- Christopher Barker, PhD Python Language Consulting - Teaching - Scientific Software Development - Desktop GUI and Web Development - wxPython, numpy, scipy, Cython

but we don't need special syntax for "else" on a for or while loop (or try block) either, you could always set a sentinel for those too. which to me is a case for adding else to a "with" block as well, for all the same reasons it's there for the other block construct.
That's part of my opinion too, afaik, only `with` is currently missing an `else` part.
Though I don't think I'd advocate it in this case, as the Exception is not really a clear part of the context manger API, like "break" is to the loops.
Well part of the __exit__ documentation, you can read the following: Returning a true value from this method will cause the with statement to suppress the exception and continue execution with the statement immediately following the with statement. Otherwise the exception continues propagating after this method has finished executing. So exceptions are quite part of the context manager API. Based on that I actually created a context manager using this property: class savepoint(object): def __init__(self, tm): self._sp = None self._tm = tm def __enter__(self) -> bool: """Save the current state of the transaction.""" try: self._sp = self._tm.savepoint() except TypeError: return False else: return self._sp.valid def __exit__(self, exc_type, exc_val: ty.Any, exc_tb): """Save the sub-transaction or roll it back in case an error occurred.""" if exc_type is not None and issubclass(exc_type, DatabaseError): self._sp.rollback() return False elif exc_type is not None: raise exc_type(exc_val) return True The issue is that I have to enclose usage of the manager into a try/catch block: try: with savepoint(request.tm) as sp: if not sp: raise TypeError('Unsupported database.') url = insert_url(request.db, u_url) request.db.flush() except IntegrityError: url = lookup_url(request.db, u_url) Having with/else I could do the following instead: with savepoint(request.tm) as sp: if not sp: raise TypeError('Unsupported database.') url = insert_url(request.db, u_url) request.db.flush() else: url = lookup_url(request.db, u_url) I therefore save some lines and indentations.

Hi, I was just about to make exactly the suggestion. Seem like a nice idea to have this, because it simplifies error handling a lot. Axel

On Mon, 30 Mar 2020 23:27:19 +0200 Jimmy Thrasibule <jimmy.thrasibule@gmail.com> wrote:
Is there already a way to accomplish this in Python or can this be a nice to have?
Perhaps you could use try/finally: try: prepare() do_something_sensitive() finally: cleanup() Whether the call to prepare goes inside or outside the try block depends on many things, mostly its coupling to the cleanup procedure (e.g., do they need to share objects? is cleanup idempotennt?). HTH, Dan -- “Atoms are not things.” – Werner Heisenberg Dan Sommers, http://www.tombstonezero.net/dan

Well I actually would like to run the else block in case an exception did occurred. Let me provide an example from my use case which is the management of a database transaction: with savepoint(transaction_manager): # Let's try to add into the database with some constraints. obj = db.add(data) db.flush() else: # Object already in database. obj = db.get(data) With the following context manager: class savepoint(object): def __init__(self): self._sp = None def __enter__(self, tm): self._sp = tm.savepoint() def __exit__(self, exc_ty, exc_val, tb): if exc_ty is not None and issubclass(ecx_ty, IntegrityError): self._sp.rollback() # We have an exception, execute else block. return False # All good, we commit our transaction. self._sp.commit() return True I find it quite a pretty, try and fail back way that I can easily replicate in my code without having to prepare and clean up each time with a try/catch.

On Mon, Mar 30, 2020 at 3:19 PM Serhiy Storchaka <storchaka@gmail.com> wrote:
In case Serhiy's answer wasn't clear: context managers can be written to handle exceptions (within their context) in any way you see fit. that is: the method: __exit__(self, exc_type, exc_value, exc_traceback): get the exception, and information about it, of one is raised, so you can handle it anyway you want. -CHB
-- Christopher Barker, PhD Python Language Consulting - Teaching - Scientific Software Development - Desktop GUI and Web Development - wxPython, numpy, scipy, Cython

31.03.20 01:32, Christopher Barker пише:
Actually I meant the opposite. Sorry for being unclear. In normal case the context manager does not silence a raised exception, so control flow is never passed to the statement past the with block if an exception is raised inside the with block. But if you use a context manager which silences the exception, like contextlib.suppress() or unittest.TestCase.assertRaises(), it is easy to do too. was_not_raised = False with my_context(): do_something_sensitive() was_not_raised = True if was_not_raised: print("We're all safe.") You do not need a special syntax for this. was_not_raised will never be set to True if do_something_sensitive() raises an exception.

On Mar 31, 2020, at 03:06, Jimmy Thrasibule <jimmy.thrasibule@gmail.com> wrote:
It might help your proposal to just show a small concrete and realistic example of how this workaround parallels the workaround for not having for/else, and how your proposed change would let you improve your code’s readability in exactly the same way as for/else. At least for me, it’s always been easier to show a newcomer to Python the point of for/else with a nice example than to try to explain the semantics and why they’re useful, and I assume the same would be true here.

On Tue, Mar 31, 2020 at 2:41 AM Serhiy Storchaka <storchaka@gmail.com> wrote:
Actually I meant the opposite.
I think I was thrown by the use of the example "my_context" -- that is, if you are writing a conrtect manger, you can handle exceptions any way you like. If you are using an existing one, then, yes:
but we don't need special syntax for "else" on a for or while loop (or try block) either, you could always set a sentinel for those too. which to me is a case for adding else to a "with" block as well, for all the same reasons it's there for the other block construct. Though I don't think I'd advocate it in this case, as the Exception is not really a clear part of the context manger API, like "break" is to the loops. -CHB -- Christopher Barker, PhD Python Language Consulting - Teaching - Scientific Software Development - Desktop GUI and Web Development - wxPython, numpy, scipy, Cython

but we don't need special syntax for "else" on a for or while loop (or try block) either, you could always set a sentinel for those too. which to me is a case for adding else to a "with" block as well, for all the same reasons it's there for the other block construct.
That's part of my opinion too, afaik, only `with` is currently missing an `else` part.
Though I don't think I'd advocate it in this case, as the Exception is not really a clear part of the context manger API, like "break" is to the loops.
Well part of the __exit__ documentation, you can read the following: Returning a true value from this method will cause the with statement to suppress the exception and continue execution with the statement immediately following the with statement. Otherwise the exception continues propagating after this method has finished executing. So exceptions are quite part of the context manager API. Based on that I actually created a context manager using this property: class savepoint(object): def __init__(self, tm): self._sp = None self._tm = tm def __enter__(self) -> bool: """Save the current state of the transaction.""" try: self._sp = self._tm.savepoint() except TypeError: return False else: return self._sp.valid def __exit__(self, exc_type, exc_val: ty.Any, exc_tb): """Save the sub-transaction or roll it back in case an error occurred.""" if exc_type is not None and issubclass(exc_type, DatabaseError): self._sp.rollback() return False elif exc_type is not None: raise exc_type(exc_val) return True The issue is that I have to enclose usage of the manager into a try/catch block: try: with savepoint(request.tm) as sp: if not sp: raise TypeError('Unsupported database.') url = insert_url(request.db, u_url) request.db.flush() except IntegrityError: url = lookup_url(request.db, u_url) Having with/else I could do the following instead: with savepoint(request.tm) as sp: if not sp: raise TypeError('Unsupported database.') url = insert_url(request.db, u_url) request.db.flush() else: url = lookup_url(request.db, u_url) I therefore save some lines and indentations.

Hi, I was just about to make exactly the suggestion. Seem like a nice idea to have this, because it simplifies error handling a lot. Axel
participants (6)
-
Andrew Barnert
-
axelheider@gmx.de
-
Christopher Barker
-
Dan Sommers
-
Jimmy Thrasibule
-
Serhiy Storchaka