Generator-based context managers can't skip __exit__

Hi everyone, Here is a simplification of a problem that's been happening in my code: import contextlib @contextlib.contextmanager def f(): print('1') try: yield finally: print('2') g = f() g.__enter__() This code prints 1 and then 2, not just 1 like you might expect. This is because when the generator is garbage-collected, it gets `GeneratorExit` sent to it. This has been a problem in my code since in some instances, I tell a context manager not to do its `__exit__` function. (I do this by using `ExitStack.pop_all()`. However the `__exit__` is still called here. I worked around this problem by adding `except GeneratorExit: raise` in my context manager, but that's an ugly solution. Do you think that something could be done so I won't have to add `except GeneratorExit: raise` to each context manager to get the desired behavior? Thanks, Ram.

On 6 November 2016 at 14:46, Ram Rachum <ram@rachum.com> wrote:
I worked around this problem by adding `except GeneratorExit: raise` in my context manager, but that's an ugly solution.
Adding `except GeneratorExit: raise` to a try statement with a finally clause won't prevent the finally clause from executing. Selective non-idempotent cleanup behaviour really isn't a good idea, so the language is fighting you for a reason here - the meaning of a "finally" clause is that the code it contains *will* get executed, and you have to try incredibly hard to keep that from happening since it's an implicit part of cleaning up unfinished generators, and we don't let you switch the nominal class of generator objects at runtime. Indeed, yield inside try/finally was prohibited for years prior to PEP 342, as the runtime previously couldn't guarantee that the finally clause would actually execute. If you *don't* want the code to execute unconditionally, then you need to be more explicit about when you *do* want it to execute with some combination of "except" and "else" clauses. For example: >>> @contextmanager ... def cm(): ... print("enter") ... try: ... yield ... except Exception: ... print("Normal exception, not GeneratorExit, KeyboardInterrupt or SystemExit") ... else: ... print("No exception") ... >>> cm().__enter__() enter Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

Sorry, I was wrong at quoting the workaround I do, it's actually this: try: yield except GeneratorExit: raise except: cleanup() raise else: cleanup() This works, but it's ugly! And I basically need to do this to every generator-based context manager that I want to be able to run just the `__enter__` of without the `__exit__`. I wish there was a better solution than including this in my code. On Sun, Nov 6, 2016 at 7:51 AM, Nick Coghlan <ncoghlan@gmail.com> wrote:

Heh, I just played with this, and found a workaround. If I do something like this after creating the generator: sys.g = g Then it wouldn't get closed when Python finishes, and the cleanup won't happen, which is what I want. This is still a bit ugly but now it's something I can do automatically for every generator-based context manager that I create, so it's not as bad as the previous workaround in my opinion. (I wouldn't create a new attribute for each context manager, but just have a list with a mangled name on `sys` that'll hold all of them.) On Sun, Nov 6, 2016 at 8:02 AM, Ram Rachum <ram@rachum.com> wrote:

On 6 November 2016 at 16:07, Ram Rachum <ram@rachum.com> wrote:
The interpreter goes to significant lengths to make sure that finally clauses get executed prior to or during interpreter shutdown, and any means you find by which they don't get executed is considered a bug (not always a fixable bug, but a bug nonetheless). If you rely on those bugs and limitations to get your program to perform the way you want it to you're going to run into problems later when upgrading to new CPython versions, or trying out different interpreter implementations. There's still something seriously odd going in relation to your overall resource management architecture if "cleanup, maybe, unless I decide to tell you not to" is a behaviour you regularly need. Cleanup functions in a garbage collected environment should be idempotent, so it doesn't matter if you redundantly call them again later. However, if you *do* need that pattern regularly, then the pattern itself can be encapsulated in a context manager: class callback_unless_exit: def __init__(self, callback): self.callback = callback def __enter__(self): return self def __exit__(self, exc_type, exc_value, exc_tb): if issubclass(exc_type, GeneratorExit): return self.callback() and then do: with callback_unless_exit(cleanup): yield in the context managers where you want that behaviour. Regards, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On Sun, Nov 6, 2016 at 8:53 AM, Nick Coghlan <ncoghlan@gmail.com> wrote:
I understand, and I agree with the reasoning. Still, I think I'll take my chances. There's still something seriously odd going in relation to your
Well, you think it's weird that I want a `finally` clause to not be called in some circumstances. Do you think it's equally weird to want an `__exit__` method that is not called in some circumstances?
Thanks for the workaround but I feel it's even less elegant than my original workaround.

On 2016-11-06 00:18, Ram Rachum wrote:
It's weird to not want the __exit__ to be called if it's defined as a finally block, which is what you're doing with the way you're using contextlib.contextmanager. The __exit__ block there is effectively whatever is after the yield in the generator function you write. If there's code you don't want to always be run, don't put that code inside a finally where the yield is in the try. -- Brendan Barnwell "Do not follow where the path may lead. Go, instead, where there is no path, and leave a trail." --author unknown

On 6 November 2016 at 17:18, Ram Rachum <ram@rachum.com> wrote:
Yes, as the whole point of __exit__ is that the interpreter goes to great lengths to make sure it always gets called, no matter what else happens with the currently executing frame (whether it finishes normally, returns early, breaks out of a loop, continues with the next iteration, raises an exception, or gets suspended without ever resuming normal execution). If you don't want that behaviour, then __exit__ likely isn't the right tool (although it may provide the technical basis for a selective cleanup framework). Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On Sun, Nov 6, 2016 at 9:38 AM, Nick Coghlan <ncoghlan@gmail.com> wrote:
I understand your point of view. I see that Python does allow you to not call `__exit__` if you don't want to, so I wish it'll have the same approach to not calling `generator.close()` if you don't want to. (This is what it's really about, not `finally`.)

On 6 November 2016 at 17:44, Ram Rachum <ram@rachum.com> wrote:
No, as that's like asking that Python not call close() on files automatically, or not wait for non-daemon threads to terminate when it's shutting down. When Python is discarding a frame that was previously suspended and never finished normally, it throws an exception into it in order to give it a chance to release any resources it might be holding. If you want to deliberately make it leak resources in such cases instead of cleaning them up, you're going to have to leak them deliberately and explicitly, just as you would in normal synchronous code. Regards, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On 7 November 2016 at 12:25, Ethan Furman <ethan@stoneleaf.us> wrote:
It involves wrapping the context manager in another context manager that deliberately doesn't delegate the call to __exit__ in some cases (cf contextlib.ExitStack.pop_all()). By contrast, if __del__ is defined (as it is on generators), if you don't keep the context manager itself alive, you can only prevent the cleanup happening if you can define a subclass to use instead, and that's not always possible (deliberately so, in the case of generator cleanup). So the odd part of Ram's request isn't wanting to have conditional resource cleanup - the recipes in the contextlib docs gives some examples of where conditional local resource management is useful and how to achieve it using ExitStack. The odd part is wanting to make the resource cleanup implicitly unreliable, rather than having it be reliable by default and folks having to explicitly opt in to disabling it, since the easiest way to obtain non-deterministic resource management is to just avoid using the context management features in the first place. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On 11/6/2016 2:18 AM, Ram Rachum wrote:
Without a deeper understanding of why you want to do so, I would. The automatic exit cleanup typically the main purpose of a context manager and 'with' block. If, for instance, I want a file only maybe closed, I would just call 'open' instead of 'with open' and then conditionally (with 'if' or 'try') call 'close'. -- Terry Jan Reedy

On 11/06/2016 12:18 AM, Ram Rachum wrote:
Well, you think it's weird that I want a `finally` clause to not be called in some circumstances.
Yes I (we) do.
Do you think it's equally weird to want an `__exit__` method that is not called in some circumstances?
Yes I (we) do. -- ~Ethan~

On Sun, Nov 06, 2016 at 06:46:40AM +0200, Ram Rachum wrote:
I expect it to print 2. After all, its in a finally clause. And why are you calling g.__enter__() directly? Calling dunder methods by hand is nearly always the wrong thing to do.
Right. That's what they're designed to do. Later, in another thread you say: "Well, you think it's weird that I want a `finally` clause to not be called in some circumstances. Do you think it's equally weird to want an `__exit__` method that is not called in some circumstances?" Yes to both. It is seriously weird. You might as well be complaining that Python calls your __iter__ method when you call iter(my_instance). That's the whole point of __iter__, and the whole point of finally clauses and the __exit__ method is that they are unconditionally called, always, when you leave the with block. But judging from your code above, it looks like you're not even using a with block. In that case, instead of abusing the __enter__ and __exit__ methods, why not just create a class with non-dunder enter() and exit() methods and call them by hand? g = f() # implementation of f is left as an exercise g.enter() if condition: g.exit() I'm having a lot of difficulty in understanding your use-case here, and so maybe I've completely misunderstood something.
Have you considered something like: def f(): print('1') try: yield finally: if f.closing: print('2') You can then write a decorator to set f.closing to True or False as needed. But again, I don't understand why you would want this feature, or how you are using it, so I might have this completely wrong. -- Steve

On 06.11.2016 09:07, Steven D'Aprano wrote:
I'm having a lot of difficulty in understanding your use-case here, and so maybe I've completely misunderstood something.
Although, this thread is dead for a week or so, I am still curious to hear the real-world use-case. I am equally puzzled by the fact that somebody really wants to use a context manager not to work like a context manager; without even considering not to use context managers at all and using regular functions instead. Cheers, Sven

Sure, here are a couple of use cases: 1. I'm making a program that lets people lease machines. They can issue a command to lease 7 machines. When they do, my program leases them one by one and adds them all to an exit stack, so in case there aren't 7 machines available, all the machines we leased get released and the situation is back to normal. If everything goes fine, I do pop_all on the exit stack so it doesn't get exited and the machines stay leased, then the command exits and the user gets his machines. 2. I have a test suite that creates a temporary folder to put files that are used by the test. The temporary folder is created by a context manager that deletes it at the end. But, if the test fails I want to move the temporary folder away into a dedicated folder for the user to be able to examine those files later to figure out why the test fails. So I want to tell the temporary folder context manager to not delete the folder, because it'll fail since it was moved away so it's not at the expected location. (If you're replying please keep me in "to" because the Gmail filter I set up isn't smart enough to let the messages in otherwise.) On Nov 18, 2016 21:27, "Sven R. Kunze" <srkunze@mail.de> wrote:

On 11/18/2016 12:42 PM, Ram Rachum wrote:
So you're using the contextlib.ExitStack context manager?
My first thought is to make a custom context manager for this use-case, but if you really want to use a generator instead can't you just put in an existence check for the directory and only delete if it is still there? -- ~Ethan~

Ram Rachum wrote:
Seems to me that would be done more easily and clearly without involving a context manager at all: machines = [] try: for i in range(num_machines_required): machines.append(lease_machine()) except AllMachinesInUseError: for machine in machines: release_machine(machine) del machines[:] A context manager is a tool to be used if it helps. If it doesn't help, don't be afraid to not use it!
Again, don't use a context manager, just write a try-except that does what you want. -- Greg

On 19 November 2016 at 07:00, Greg Ewing <greg.ewing@canterbury.ac.nz> wrote:
ExitStack() handles that case by design - doing "pop_all() gives you a fresh ExitStack() instance, while making close() and __exit__() on the original no-ops. Since ExitStack() deliberately never cleans up the stack implicitly, you end up being able to do: machines = [] with contextlib.ExitStack() as cleanup: for i in range(num_machines_required): machines.append(lease_machine()) cleanup.callback(release_machine, machine) cleanup.pop_all() # It worked, so don't clean anything up yet return machines However, ExitStack *itself* can't readily be written using contextlib.contextmanager - it's not impossible, but it's convoluted enough not to be worth the hassle, since you'd need to do something like: class _ExitStack: # Like the current ExitStack, but without __enter__/__exit__ @contextmanager def exit_stack() state = _ExitStack() try: yield state finally: state.close() Externally, the visible differences would be that: - "es_cm = exit_stack()" and "es_state = exit_stack().__enter__()" would give different results - it would be slower and use more memory due to the additional layer of indirection Since the extra layer of indirection doesn't buy you any new capabilities and has a runtime performance cost, there's no real reason to do it. The general case of that pattern is to yield a state variable that just has a single attribute "needs_cleanup": @contextmanager def my_cm() ... # Do setup operations here state = types.SimpleNamespace(needs_cleanup=True) try: yield state finally: if state.needs_cleanup: ... # Do cleanup operations here However, as with ExitStack, at that point, you're usually going to be better off just implementing __enter__ and __exit__ yourself, and not worrying about using the generator format. This kind of difference in flexibility isn't really specific to context managers though - it's a particular instance of the general pattern that custom classes are often more convenient than closures when you actually *want* to expose externally mutable state. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On 6 November 2016 at 14:46, Ram Rachum <ram@rachum.com> wrote:
I worked around this problem by adding `except GeneratorExit: raise` in my context manager, but that's an ugly solution.
Adding `except GeneratorExit: raise` to a try statement with a finally clause won't prevent the finally clause from executing. Selective non-idempotent cleanup behaviour really isn't a good idea, so the language is fighting you for a reason here - the meaning of a "finally" clause is that the code it contains *will* get executed, and you have to try incredibly hard to keep that from happening since it's an implicit part of cleaning up unfinished generators, and we don't let you switch the nominal class of generator objects at runtime. Indeed, yield inside try/finally was prohibited for years prior to PEP 342, as the runtime previously couldn't guarantee that the finally clause would actually execute. If you *don't* want the code to execute unconditionally, then you need to be more explicit about when you *do* want it to execute with some combination of "except" and "else" clauses. For example: >>> @contextmanager ... def cm(): ... print("enter") ... try: ... yield ... except Exception: ... print("Normal exception, not GeneratorExit, KeyboardInterrupt or SystemExit") ... else: ... print("No exception") ... >>> cm().__enter__() enter Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

Sorry, I was wrong at quoting the workaround I do, it's actually this: try: yield except GeneratorExit: raise except: cleanup() raise else: cleanup() This works, but it's ugly! And I basically need to do this to every generator-based context manager that I want to be able to run just the `__enter__` of without the `__exit__`. I wish there was a better solution than including this in my code. On Sun, Nov 6, 2016 at 7:51 AM, Nick Coghlan <ncoghlan@gmail.com> wrote:

Heh, I just played with this, and found a workaround. If I do something like this after creating the generator: sys.g = g Then it wouldn't get closed when Python finishes, and the cleanup won't happen, which is what I want. This is still a bit ugly but now it's something I can do automatically for every generator-based context manager that I create, so it's not as bad as the previous workaround in my opinion. (I wouldn't create a new attribute for each context manager, but just have a list with a mangled name on `sys` that'll hold all of them.) On Sun, Nov 6, 2016 at 8:02 AM, Ram Rachum <ram@rachum.com> wrote:

On 6 November 2016 at 16:07, Ram Rachum <ram@rachum.com> wrote:
The interpreter goes to significant lengths to make sure that finally clauses get executed prior to or during interpreter shutdown, and any means you find by which they don't get executed is considered a bug (not always a fixable bug, but a bug nonetheless). If you rely on those bugs and limitations to get your program to perform the way you want it to you're going to run into problems later when upgrading to new CPython versions, or trying out different interpreter implementations. There's still something seriously odd going in relation to your overall resource management architecture if "cleanup, maybe, unless I decide to tell you not to" is a behaviour you regularly need. Cleanup functions in a garbage collected environment should be idempotent, so it doesn't matter if you redundantly call them again later. However, if you *do* need that pattern regularly, then the pattern itself can be encapsulated in a context manager: class callback_unless_exit: def __init__(self, callback): self.callback = callback def __enter__(self): return self def __exit__(self, exc_type, exc_value, exc_tb): if issubclass(exc_type, GeneratorExit): return self.callback() and then do: with callback_unless_exit(cleanup): yield in the context managers where you want that behaviour. Regards, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On Sun, Nov 6, 2016 at 8:53 AM, Nick Coghlan <ncoghlan@gmail.com> wrote:
I understand, and I agree with the reasoning. Still, I think I'll take my chances. There's still something seriously odd going in relation to your
Well, you think it's weird that I want a `finally` clause to not be called in some circumstances. Do you think it's equally weird to want an `__exit__` method that is not called in some circumstances?
Thanks for the workaround but I feel it's even less elegant than my original workaround.

On 2016-11-06 00:18, Ram Rachum wrote:
It's weird to not want the __exit__ to be called if it's defined as a finally block, which is what you're doing with the way you're using contextlib.contextmanager. The __exit__ block there is effectively whatever is after the yield in the generator function you write. If there's code you don't want to always be run, don't put that code inside a finally where the yield is in the try. -- Brendan Barnwell "Do not follow where the path may lead. Go, instead, where there is no path, and leave a trail." --author unknown

On 6 November 2016 at 17:18, Ram Rachum <ram@rachum.com> wrote:
Yes, as the whole point of __exit__ is that the interpreter goes to great lengths to make sure it always gets called, no matter what else happens with the currently executing frame (whether it finishes normally, returns early, breaks out of a loop, continues with the next iteration, raises an exception, or gets suspended without ever resuming normal execution). If you don't want that behaviour, then __exit__ likely isn't the right tool (although it may provide the technical basis for a selective cleanup framework). Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On Sun, Nov 6, 2016 at 9:38 AM, Nick Coghlan <ncoghlan@gmail.com> wrote:
I understand your point of view. I see that Python does allow you to not call `__exit__` if you don't want to, so I wish it'll have the same approach to not calling `generator.close()` if you don't want to. (This is what it's really about, not `finally`.)

On 6 November 2016 at 17:44, Ram Rachum <ram@rachum.com> wrote:
No, as that's like asking that Python not call close() on files automatically, or not wait for non-daemon threads to terminate when it's shutting down. When Python is discarding a frame that was previously suspended and never finished normally, it throws an exception into it in order to give it a chance to release any resources it might be holding. If you want to deliberately make it leak resources in such cases instead of cleaning them up, you're going to have to leak them deliberately and explicitly, just as you would in normal synchronous code. Regards, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On 7 November 2016 at 12:25, Ethan Furman <ethan@stoneleaf.us> wrote:
It involves wrapping the context manager in another context manager that deliberately doesn't delegate the call to __exit__ in some cases (cf contextlib.ExitStack.pop_all()). By contrast, if __del__ is defined (as it is on generators), if you don't keep the context manager itself alive, you can only prevent the cleanup happening if you can define a subclass to use instead, and that's not always possible (deliberately so, in the case of generator cleanup). So the odd part of Ram's request isn't wanting to have conditional resource cleanup - the recipes in the contextlib docs gives some examples of where conditional local resource management is useful and how to achieve it using ExitStack. The odd part is wanting to make the resource cleanup implicitly unreliable, rather than having it be reliable by default and folks having to explicitly opt in to disabling it, since the easiest way to obtain non-deterministic resource management is to just avoid using the context management features in the first place. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On 11/6/2016 2:18 AM, Ram Rachum wrote:
Without a deeper understanding of why you want to do so, I would. The automatic exit cleanup typically the main purpose of a context manager and 'with' block. If, for instance, I want a file only maybe closed, I would just call 'open' instead of 'with open' and then conditionally (with 'if' or 'try') call 'close'. -- Terry Jan Reedy

On 11/06/2016 12:18 AM, Ram Rachum wrote:
Well, you think it's weird that I want a `finally` clause to not be called in some circumstances.
Yes I (we) do.
Do you think it's equally weird to want an `__exit__` method that is not called in some circumstances?
Yes I (we) do. -- ~Ethan~

On Sun, Nov 06, 2016 at 06:46:40AM +0200, Ram Rachum wrote:
I expect it to print 2. After all, its in a finally clause. And why are you calling g.__enter__() directly? Calling dunder methods by hand is nearly always the wrong thing to do.
Right. That's what they're designed to do. Later, in another thread you say: "Well, you think it's weird that I want a `finally` clause to not be called in some circumstances. Do you think it's equally weird to want an `__exit__` method that is not called in some circumstances?" Yes to both. It is seriously weird. You might as well be complaining that Python calls your __iter__ method when you call iter(my_instance). That's the whole point of __iter__, and the whole point of finally clauses and the __exit__ method is that they are unconditionally called, always, when you leave the with block. But judging from your code above, it looks like you're not even using a with block. In that case, instead of abusing the __enter__ and __exit__ methods, why not just create a class with non-dunder enter() and exit() methods and call them by hand? g = f() # implementation of f is left as an exercise g.enter() if condition: g.exit() I'm having a lot of difficulty in understanding your use-case here, and so maybe I've completely misunderstood something.
Have you considered something like: def f(): print('1') try: yield finally: if f.closing: print('2') You can then write a decorator to set f.closing to True or False as needed. But again, I don't understand why you would want this feature, or how you are using it, so I might have this completely wrong. -- Steve

On 06.11.2016 09:07, Steven D'Aprano wrote:
I'm having a lot of difficulty in understanding your use-case here, and so maybe I've completely misunderstood something.
Although, this thread is dead for a week or so, I am still curious to hear the real-world use-case. I am equally puzzled by the fact that somebody really wants to use a context manager not to work like a context manager; without even considering not to use context managers at all and using regular functions instead. Cheers, Sven

Sure, here are a couple of use cases: 1. I'm making a program that lets people lease machines. They can issue a command to lease 7 machines. When they do, my program leases them one by one and adds them all to an exit stack, so in case there aren't 7 machines available, all the machines we leased get released and the situation is back to normal. If everything goes fine, I do pop_all on the exit stack so it doesn't get exited and the machines stay leased, then the command exits and the user gets his machines. 2. I have a test suite that creates a temporary folder to put files that are used by the test. The temporary folder is created by a context manager that deletes it at the end. But, if the test fails I want to move the temporary folder away into a dedicated folder for the user to be able to examine those files later to figure out why the test fails. So I want to tell the temporary folder context manager to not delete the folder, because it'll fail since it was moved away so it's not at the expected location. (If you're replying please keep me in "to" because the Gmail filter I set up isn't smart enough to let the messages in otherwise.) On Nov 18, 2016 21:27, "Sven R. Kunze" <srkunze@mail.de> wrote:

On 11/18/2016 12:42 PM, Ram Rachum wrote:
So you're using the contextlib.ExitStack context manager?
My first thought is to make a custom context manager for this use-case, but if you really want to use a generator instead can't you just put in an existence check for the directory and only delete if it is still there? -- ~Ethan~

Ram Rachum wrote:
Seems to me that would be done more easily and clearly without involving a context manager at all: machines = [] try: for i in range(num_machines_required): machines.append(lease_machine()) except AllMachinesInUseError: for machine in machines: release_machine(machine) del machines[:] A context manager is a tool to be used if it helps. If it doesn't help, don't be afraid to not use it!
Again, don't use a context manager, just write a try-except that does what you want. -- Greg

On 19 November 2016 at 07:00, Greg Ewing <greg.ewing@canterbury.ac.nz> wrote:
ExitStack() handles that case by design - doing "pop_all() gives you a fresh ExitStack() instance, while making close() and __exit__() on the original no-ops. Since ExitStack() deliberately never cleans up the stack implicitly, you end up being able to do: machines = [] with contextlib.ExitStack() as cleanup: for i in range(num_machines_required): machines.append(lease_machine()) cleanup.callback(release_machine, machine) cleanup.pop_all() # It worked, so don't clean anything up yet return machines However, ExitStack *itself* can't readily be written using contextlib.contextmanager - it's not impossible, but it's convoluted enough not to be worth the hassle, since you'd need to do something like: class _ExitStack: # Like the current ExitStack, but without __enter__/__exit__ @contextmanager def exit_stack() state = _ExitStack() try: yield state finally: state.close() Externally, the visible differences would be that: - "es_cm = exit_stack()" and "es_state = exit_stack().__enter__()" would give different results - it would be slower and use more memory due to the additional layer of indirection Since the extra layer of indirection doesn't buy you any new capabilities and has a runtime performance cost, there's no real reason to do it. The general case of that pattern is to yield a state variable that just has a single attribute "needs_cleanup": @contextmanager def my_cm() ... # Do setup operations here state = types.SimpleNamespace(needs_cleanup=True) try: yield state finally: if state.needs_cleanup: ... # Do cleanup operations here However, as with ExitStack, at that point, you're usually going to be better off just implementing __enter__ and __exit__ yourself, and not worrying about using the generator format. This kind of difference in flexibility isn't really specific to context managers though - it's a particular instance of the general pattern that custom classes are often more convenient than closures when you actually *want* to expose externally mutable state. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia
participants (8)
-
Brendan Barnwell
-
Ethan Furman
-
Greg Ewing
-
Nick Coghlan
-
Ram Rachum
-
Steven D'Aprano
-
Sven R. Kunze
-
Terry Reedy