Watch out for this (async) generator cleanup pitfall in Python

mCoding · Beginner ·🔍 RAG & Vector Search ·1y ago

Key Takeaways

The video discusses a common pitfall in Python async generators, where the cleanup code may not run as expected if the generator is not exhausted, and provides solutions such as manual cleanup using the `close` method and `contextlib.closing`.

Full Transcript

hello and welcome back to M coding I'm James Murphy today we're talking about generators specifically generators and async generators that used the width statement or a try finally and a common Pitfall that you might fall into suppose we have a generator that needs some kind of resource it could be something like talking to a database where you need to start a transaction and then commit at the end of it or you need to grab a lock and then release the lock at the end of it or you need to open a file read some data from the file and then close the file at the end of it for rtion purposes this resource is just a dummy resource that has a name and prints out its own name on cleanup so that we can see when the cleanup happens but in real life it could be basically anything the normal way that we would use a generator like this is just to Loop over it so we would see yield zero and then got zero yield one and then got one yield two and then got two then we would exit the loop we would see the resource cleanup happen and then we would see the print statement after the loop and if we actually run it that's indeed what we see 0 1 2 clean up and then the stuff after the loop in this case the resource got cleaned up because we ran the generator past the point of when resource clean up would happen within the generator we exited the for Loop we exited the width block and we reached the end of the generator but here's a question for you does the resource still get cleaned up if we don't run the generator to completion we either stop it with a break or there's an exception that gets thrown and we never actually run the generator to the point where it would exit the WID block take a moment to think about it will this code still print the cleanup message answer coming in 3 2 1 interestingly yes the cleanup still happens we never got to iteration two in our for Loop but there's the cleanup message this behavior is possible because when a generator is garbage collected it automatically has its close method called the close method actually throws an exception inside the generator and then continues to run it until completion so instead of continuing with the for Loop it will exit the for Loop and exit the width statement which is why the cleanup happens we can actually observe this Behavior by surrounding the yield statement in a tri accept that catches a base exception and then printing it out before re raising it this allows us to kind of spy on the exception being raised but still raisee it so that it can do its job now when we run the code we see Loop zero Loop one and then indeed our Tri accept catches this generator exit exception but here's the tricky part and the source of the common Pitfall remember what I said causes this cleanup to actually happen it's the garbage collection of the generator object that triggers the cleanup in cpython if there are no more references to an object it'll get cleaned up immediately so in this case because we know garbage collection happens here we know that our resource will be cleaned up before anything that happens after the loop but what if we did some refactoring and pulled the generator out into a variable since we still maintain a reference to it even after the loop the generator won't be garbage collected here but rather at the end of the main function therefore our resource doesn't get cleaned up at the same place it gets cleaned up at the end of main this could already be a problem if the stuff after the loop expected that resource to have been cleaned up like if it was holding a lock and the stuff afterwards also needs to use the lock even if we didn't Factor things out into a separate variable like we did here other implementations of python don't guarantee the same behavior with garbage collection and they may not garbage collect something immediately even if its ref count is zero this non-determinism in generator resource cleanup is especially bad when you have nested resources just the way the code looks programmers generally tend to assume that inner resources will get cleaned up before outer resources but in this case that's not true the outer resource is cleaned up at the end of the width block but the inner resource isn't cleaned up until the generator is garbage collected which happens after the WID block at the end of main you can guarantee the cleanup happens earlier by manually triggering it by calling the generator's close function technically though this still isn't good enough because something in here could throw an exception and we'd miss that close call in order to guarantee clean up of the inner resources before the outer ones you can use something like contextlib closing which is a context manager that will just call that generator close function with this approach the place where the generator is garbage collected becomes irrelevant to the cleanup of the resources but be warned code in the wild very rarely actually does this it's super annoying to have an extra width statement around every for Loop that uses a generator so in the wild you typically just see people depending on the garbage collection behavior of cpython remember in cpython which is the python that 99% of people people are using the one that you download from python.org if you just don't keep a reference to this generator then it will be garbage collected and the cleanup of the inner thing will happen before the outer thing it's just very delicate and Flaky code where if someone comes along and does a slight refactor it might break and for normal generators that's kind of a good enough solution and the end of the story just don't hold on to a reference to a generator or if you do use context li. closing to ensure it gets cleaned up but if you happen to be using async generators then the problem gets much worse let's go back up to our generator and make this an async def function we could be using an async width block or width block or Tri finally it doesn't really matter here whether the resource is async or not what's going to make this worse is just that the generator is async and then back in main let's go ahead and make main an async depth function and use an async for Loop to Loop over the elements in the generator and we now run our main function using async i.run so this is the async equivalent of the problem we had before and the generator is garbage collected at this point but when we run the code we unfortunately see that the outer resource is cleaned up before the inner one the inner one does still get cleaned up in a similar way as in the synchronous case a special exception gets thrown inside the generator and then it's run until completion which causes the WID block to eventually exit but for some reason in this async case the garbage collection isn't triggering the resource to be cleaned up just after the for Loop like it was in the synchronous case and as you could probably guess the delaying has something to do with it being async we can get a little hint about what's really going on here by reaching into the internals of the event Loop that's running and looking at what the ready tasks are this print statement just before the break is sort of our Baseline then right after the break it will go to this print statement which is outside the for Loop the first one is just before the generator is garbage collected and the second one is just after the generator is garbage collected when we run the code and see the before and after we see that there's this extra thing in the after and it doesn't take a huge leap to figure out that yes this is the task that closes the generator and there in lies the Crux of the problem an async generator is async but the garbage collector is not async so even though the generator is garbage collected here the best the garbage collector could do is schedule the task that causes the generator to be closed it can't actually await that task though if we manually sleep a few times we give the event Loop enough time to run any pending tasks now when we run the code we see that indeed the inner one gets cleaned up before the outer one which is what we want so how do we get around this obviously I'm not suggesting that you add sleep statements after every for Loop similar to the synchronous case the most correct thing to do that almost nobody actually does is to surround any async for Loop using a generator in an async with block where you use context Libs a closing all this does is await the generator's a close method on the way out just like with the synchronous case this makes the point where the generator is garbage collected irrelevant to when the cleanup happens but this is kind of unsatisfying for a number of reasons first off I mean good luck convinc everyone to do this in the first place but it's also unsatisfying because it breaks a level of abstraction a closing only works for async generators not arbitrary async iterables so immediately I now have to keep track of whether this is an async generator or just an async iterable that's something else and second since the pragmatic thing to do is not to put an async width around every async for Loop but only to put it around the ones that need it then in addition to keeping track of whether or not this is an async generator you also need to know about the implementation of the generator and whether or not that implementation uses any Tri finales width statements or async width statements not to mention the additional refactoring cost because remember whenever a generator uses a tri finally a width statement or an asyn width statement then you need to wrap all of its usages in an async WID block but now all those callers have an async WID block so if any of them are generators you now have to wrap all their colors in asyn blocks and this process can get out of hand very quickly the real solution is of course to completely ban asnc for just kidding but yeah unfortunately this is sort of just the State of Affairs right now if you care about asnc generator clean up you just have to use a closing there is a pep Number 533 that proposes a solution to this though the idea is pretty simple basically just add two new dunders a iter close and iter close and change the meaning of for Loops have an async for Loop await a it close at the end of the for loop after a regular exit breaking or an exception and have an normal for Loop call inter close again when you either Exit normally exit with a break or exit with an exception this is a pretty good idea and if python was being written for the very first time then this might have made it in but as you can imagine changing the semantics of iteration is probably a no-go at this point not to mention how hard it would be to implement and it would be a breaking change to any code that intends to use the current semantics where it has a break statement but then continues to iterate over the same generator or iterable afterwards in this case we open a file process some header lines in the file and then process the rest of the lines in a different way so we would break and then continue processing the rest of the lines technically open doesn't return a generator so lines in this case is not a generator but just imagine you've written your own kind of open wrapper that uses a generator in that case it's perfectly reasonable that the developer intended it to be this way that they didn't want lines to be closed after the first for Loop so anyway I don't think this a itose and itose pep will ever be added into python aside from outright Banning the use of async generators in your code which I guess technically is a fine solution the best thing to do is to just be aware of it and add this to your list of quirks of python that you need to watch out for once again I'm James Murphy and this is M coding I'm available for code reviews Contracting and Consulting check out my company at M coding. and thank you to my patrons and donors for helping to support the channel don't forget to leave a comment subscribe and slap that like button in odd number times see you in the next one

Original Description

Watch out for this common generator pitfall. What happens to a try/finally, with block, or async with block inside a generator if the generator isn't exhausted? Does the cleanup code still run? When and how does it run? In this video we take a look at the answers to these questions and learn how to avoid a common situation where cleanup code doesn't run when you want it to. ― mCoding with James Murphy (https://mcoding.io) PEP 533: https://peps.python.org/pep-0533/ Discussion on GH issue: https://github.com/python/cpython/issues/88684 Source code: https://github.com/mCodingLLC/VideosSampleCode SUPPORT ME ⭐ --------------------------------------------------- Sign up on Patreon to get your donor role and early access to videos! https://patreon.com/mCoding Feeling generous but don't have a Patreon? Donate via PayPal! (No sign up needed.) https://www.paypal.com/donate/?hosted_button_id=VJY5SLZ8BJHEE Want to donate crypto? Check out the rest of my supported donations on my website! https://mcoding.io/donate Top patrons and donors: Laura M, Neel R, Dragos C, Jameson, Matt R, Pi, Vahnekie, Johan A, Mark M, Mutual Information BE ACTIVE IN MY COMMUNITY 😄 --------------------------------------------------- Discord: https://discord.gg/Ye9yJtZQuN Github: https://github.com/mCodingLLC/ Reddit: https://www.reddit.com/r/mCoding/ Facebook: https://www.facebook.com/james.mcoding CHAPTERS --------------------------------------------------- 0:00 Intro 1:21 The cleanup question 5:30 The async case
Watch on YouTube ↗ (saves to browser)
Sign in to unlock AI tutor explanation · ⚡30

Playlist

Uploads from mCoding · mCoding · 0 of 60

← Previous Next →
1 Goodbye, List! Type hinting standard collections - New in Python 3.9
Goodbye, List! Type hinting standard collections - New in Python 3.9
mCoding
2 Python's comma equals ,= operator?
Python's comma equals ,= operator?
mCoding
3 Finding Primes in Python with the Sieve of Eratosthenes
Finding Primes in Python with the Sieve of Eratosthenes
mCoding
4 Find the First Missing Positive Int | Hard Interview Question on LeetCode
Find the First Missing Positive Int | Hard Interview Question on LeetCode
mCoding
5 JSON Tutorial Python | Basic Python Recipes
JSON Tutorial Python | Basic Python Recipes
mCoding
6 Simulating Brownian Motion in Python
Simulating Brownian Motion in Python
mCoding
7 The Single Most Useful Decorator in Python
The Single Most Useful Decorator in Python
mCoding
8 The Fastest Way to Loop in Python - An Unfortunate Truth
The Fastest Way to Loop in Python - An Unfortunate Truth
mCoding
9 Numpy Array Broadcasting In Python Explained
Numpy Array Broadcasting In Python Explained
mCoding
10 Brownian Motion Single Path Zoom
Brownian Motion Single Path Zoom
mCoding
11 Brownian Motion Fractal Zoom
Brownian Motion Fractal Zoom
mCoding
12 Magic Methods - Making Python builtins work with your classes
Magic Methods - Making Python builtins work with your classes
mCoding
13 50 Million Primes In 5 Seconds - Segmented Sieve of Eratosthenes
50 Million Primes In 5 Seconds - Segmented Sieve of Eratosthenes
mCoding
14 The Hottest New Feature Coming In Python 3.10 - Structural Pattern Matching / Match Statement
The Hottest New Feature Coming In Python 3.10 - Structural Pattern Matching / Match Statement
mCoding
15 How Fast is Python's Sort? Performance Testing
How Fast is Python's Sort? Performance Testing
mCoding
16 C++ First Missing Int, faster than 100%!
C++ First Missing Int, faster than 100%!
mCoding
17 [April Fools 2021] Python 4.0! New old print, mandatory static typing, StackOverflow integration
[April Fools 2021] Python 4.0! New old print, mandatory static typing, StackOverflow integration
mCoding
18 Python dataclasses will save you HOURS, also featuring attrs
Python dataclasses will save you HOURS, also featuring attrs
mCoding
19 C++ Sudoku Solver in 7 minutes using Recursive Backtracking
C++ Sudoku Solver in 7 minutes using Recursive Backtracking
mCoding
20 Every PROOF you've seen that .999... = 1 is WRONG
Every PROOF you've seen that .999... = 1 is WRONG
mCoding
21 Python's sharpest corner is ... plus equals? (+=)
Python's sharpest corner is ... plus equals? (+=)
mCoding
22 Binary Search - A Different Perspective | Python Algorithms
Binary Search - A Different Perspective | Python Algorithms
mCoding
23 The Best Way to Check for Optional Arguments in Python
The Best Way to Check for Optional Arguments in Python
mCoding
24 Local and Global Variable Lookup Weirdness in Python
Local and Global Variable Lookup Weirdness in Python
mCoding
25 Efficient Exponentiation
Efficient Exponentiation
mCoding
26 How To Install Python for Data Science
How To Install Python for Data Science
mCoding
27 0.1 + 0.2 is NOT 0.3 in Most Programming Languages
0.1 + 0.2 is NOT 0.3 in Most Programming Languages
mCoding
28 Python 3.10's new type hinting features
Python 3.10's new type hinting features
mCoding
29 Python 3.10's Quality of Life improvements
Python 3.10's Quality of Life improvements
mCoding
30 Introducing mZips! Python Zip and Zip Longest
Introducing mZips! Python Zip and Zip Longest
mCoding
31 Match statement tips
Match statement tips
mCoding
32 Using except: is a HUGE mistake
Using except: is a HUGE mistake
mCoding
33 Python + YouTube API | Automating descriptions
Python + YouTube API | Automating descriptions
mCoding
34 Anaphones, phonetic anagrams
Anaphones, phonetic anagrams
mCoding
35 Cracking passwords using ONLY response times | Secure Python
Cracking passwords using ONLY response times | Secure Python
mCoding
36 Python f-strings can do more than you thought. f'{val=}', f'{val!r}', f'{dt:%Y-%m-%d}'
Python f-strings can do more than you thought. f'{val=}', f'{val!r}', f'{dt:%Y-%m-%d}'
mCoding
37 Diagnose slow Python code. (Feat. async/await)
Diagnose slow Python code. (Feat. async/await)
mCoding
38 Python MD5 implementation
Python MD5 implementation
mCoding
39 Salting, peppering, and hashing passwords
Salting, peppering, and hashing passwords
mCoding
40 x to bool conversion in Python, C++, and C
x to bool conversion in Python, C++, and C
mCoding
41 You should put this in all your Python scripts | if __name__ == '__main__': ...
You should put this in all your Python scripts | if __name__ == '__main__': ...
mCoding
42 Find the Skyline Problem with C++ Solution Explained
Find the Skyline Problem with C++ Solution Explained
mCoding
43 The ONLY C keyword with no C++ equivalent
The ONLY C keyword with no C++ equivalent
mCoding
44 Should you use "not not x" instead of "bool(x)" in Python? (NO!)
Should you use "not not x" instead of "bool(x)" in Python? (NO!)
mCoding
45 Multiple Assignments in Python
Multiple Assignments in Python
mCoding
46 Why I don't like Python's chained comparisons
Why I don't like Python's chained comparisons
mCoding
47 Automated Testing in Python with pytest, tox, and GitHub Actions
Automated Testing in Python with pytest, tox, and GitHub Actions
mCoding
48 You can pip install directly from GitHub
You can pip install directly from GitHub
mCoding
49 __new__ vs __init__ in Python
__new__ vs __init__ in Python
mCoding
50 Metaclasses in Python
Metaclasses in Python
mCoding
51 The easy way to keep your repos tidy.
The easy way to keep your repos tidy.
mCoding
52 Which Python @dataclass is best? Feat. Pydantic, NamedTuple, attrs...
Which Python @dataclass is best? Feat. Pydantic, NamedTuple, attrs...
mCoding
53 Python __slots__ and object layout explained
Python __slots__ and object layout explained
mCoding
54 C++ cache locality and branch predictability
C++ cache locality and branch predictability
mCoding
55 Avoiding import loops in Python
Avoiding import loops in Python
mCoding
56 25 nooby Python habits you need to ditch
25 nooby Python habits you need to ditch
mCoding
57 Python staticmethod and classmethod
Python staticmethod and classmethod
mCoding
58 Building a Python app with Anvil to email me if my website goes down (includes paid features)
Building a Python app with Anvil to email me if my website goes down (includes paid features)
mCoding
59 31 nooby C++ habits you need to ditch
31 nooby C++ habits you need to ditch
mCoding
60 Interviewing the creator of C++, Bjarne Stroustrup
Interviewing the creator of C++, Bjarne Stroustrup
mCoding

The video teaches how to avoid a common pitfall in Python async generators where the cleanup code may not run as expected, and provides solutions to ensure proper cleanup. This is important to prevent resource leaks and ensure the reliability of async generators. By watching this video, viewers will learn how to use async generators with proper cleanup and avoid common mistakes.

Key Takeaways
  1. Use a generator with a cleanup method
  2. Run the generator to completion to see the cleanup happen
  3. Stop the generator with a break or exception to see the cleanup happen despite not running to completion
  4. Surround the yield statement with a try-except block to catch the generator exit exception
  5. Manually call the `close` method on an async generator to trigger cleanup
  6. Use `contextlib.closing` to ensure cleanup of async generators
💡 Async generators in Python have a cleanup pitfall when using break statements, and manual cleanup using the `close` method and `contextlib.closing` can ensure proper cleanup.

Related Reads

📰
A Production RAG Pipeline for PDFs: Relational Parsing, TOC Retrieval, Typed Answers
Learn to build a production-ready RAG pipeline for PDFs, enabling efficient document parsing, question answering, and retrieval
Towards Data Science
📰
How to Cut RAG Token Costs 90% by Caching the Prefix
Cut RAG token costs by 90% using prefix caching, reducing the financial burden of large payloads
Medium · AI
📰
How to Cut RAG Token Costs 90% by Caching the Prefix
Cut RAG token costs by 90% by caching the prefix, reducing the payload size and saving on input prices
Medium · Programming
📰
Why Your Chunking Strategy Matters More Than Your Model
Optimizing your chunking strategy can be more crucial to your app's performance than the model you choose, learn why and how to improve it
Medium · RAG

Chapters (3)

Intro
1:21 The cleanup question
5:30 The async case
Up next
This FREE Tool Turns ANY PDF into Perfect Markdown (MinerU Live Test)
Prompt Engineer
Watch →