{
  "$type": "site.standard.document",
  "canonicalUrl": "https://rednafi.com/python/exitstack/",
  "description": "Master Python's ExitStack for managing multiple context managers, conditional callbacks, request rollbacks, and avoiding nested with statements.",
  "path": "/python/exitstack/",
  "publishedAt": "2022-08-27T00:00:00.000Z",
  "site": "at://did:plc:fgtm2c26vfcj74rfmeggbyqj/site.standard.publication/3mnl6f7ob462z",
  "tags": [
    "Python"
  ],
  "textContent": "Over the years, I've used Python's contextlib.ExitStack in a few interesting ways. The\n[official ExitStack documentation] advertises it as a way to manage multiple context\nmanagers and has a couple of examples of how to leverage it. However, neither in the docs\nnor in [GitHub code search] could I find examples of some of the maybe unusual ways I've\nused it in the past. So, I thought I'd document them here.\n\nEnforcing request level transaction\n\nWhile consuming APIs, it's important to handle errors in a way that prevents database state\ncorruption. In the following example, I'm making two POST requests to an API and rolling\nback to the original state if any one of them fails:\n\nRunning this will print the following output:\n\nHere, the group_create function makes two calls to POST httpbin.org/post endpoint and\nthe maybe_rollback function deletes the created record if any one of the two requests\nfails. In the main function, I've used the ExitStack.callback method to register the\nmaybe_rollback callback. If you change the expected_status_code in the maybe_rollback\nfunction to something like HTTPStatus.FORBIDDEN, you'll be able to see the cleanup\ncallbacks in action:\n\nInvoking conditional event hooks\n\nThe same strategy used in the previous section can be applied to invoke event hooks\nconditionally. For example, let's say you want to run a callback function when some event\nfunction executes. However, you want only a particular type of callback function to be\nexecuted depending on the state of your conditionals or code path. I've found the following\npattern useful in this case:\n\nHere the .on_failure hook will only be called if there's an error in your execution path\nraises an exception.\n\nAvoiding nested context structure\n\nIt can get ugly pretty quickly when you start using multiple nested context managers. For\nexample, if you need to open two files and copy content from one file to the other, you'd\ntypically start two nested context managers and transfer the content like this:\n\nExitStack can help you get away with only one level of nesting here. Here's a complete\nexample:\n\nThis example creates two in-memory temporary file instances with\ntempfile.SpooledTemporaryFile. The SpooledTemporaryFile can be used as a context\nmanager. However, instead of nesting the two instances, I'm using ExitStack.enter_context\nto enter into the context manager without explicitly using the with statement. This\n.enter_context method ensures that the __exit__ method of the respective context\nmanagers will be called properly at the end of the main() function run.\n\nThen in the body of the ExitStack, we're writing some content to the first in-memory file\nand then copying the content to the other in-memory file. If we had to open and manage even\nmore context managers, in this way, we'd be able to that without crating any additional\nnestings.\n\nApplying multiple patches as context managers\n\nPython's unittest.mock.patch can be used as both decorators and context managers. For\ngranular patching and unpatching during tests, the context manager approach gives you more\ncontrol than its decorator counterpart. In this case, ExitStack can help you avoid\nmultiple nestings just like in the previous section:\n\nRunning the above snippet with pytest will reveal that the test passes without any error:\n\nHere, I'm making GET and POST requests with the httpx library and in the test_main\nfunction, the httpx.get and httpx.post callable are patched with the patch context\nmanager. However, ExitStack allows me here to do it without creating additional nested\nwith blocks.\n\n\n\n\n[official ExitStack documentation]:\n    https://docs.python.org/3/library/contextlib.html#contextlib.ExitStack\n\n[github code search]:\n    https://github.com/search?l=Python&q=ExitStack&type=Code",
  "title": "ExitStack in Python"
}