3

In the FastAPI docs it is recommended to set up a SQLAlchemy database session dependency using a generator function like so:

async def get_db():
    db = DBSession()
    try:
        yield db
    finally:
        db.close()

My question is why does the finally block ever get executed? I was under the impression that generator functions pause execution at each yield. Once the session object goes out of scope, shouldn't the execution of get_db be discarded with db.close() never having run?

j_krl
  • 79
  • 1
  • 6

2 Answers2

2

If you throw breakpoint() in your dependency before try and look at the call tree using w option you can see that for generator type function next()is called in contextlib module code:

[0]   c:\users\homeuser\.pyenv\pyenv-win\versions\3.7.9\lib\threading.py(890)_bootstrap()
-> self._bootstrap_inner()
[1]   c:\users\homeuser\.pyenv\pyenv-win\versions\3.7.9\lib\threading.py(926)_bootstrap_inner()
-> self.run()
[2]   c:\users\homeuser\.pyenv\pyenv-win\versions\3.7.9\lib\threading.py(870)run()
-> self._target(*self._args, **self._kwargs)
[3]   c:\users\homeuser\.pyenv\pyenv-win\versions\3.7.9\lib\concurrent\futures\thread.py(80)_worker()
-> work_item.run()
[4]   c:\users\homeuser\.pyenv\pyenv-win\versions\3.7.9\lib\concurrent\futures\thread.py(57)run()
-> result = self.fn(*self.args, **self.kwargs)
[5]   c:\users\homeuser\.pyenv\pyenv-win\versions\3.7.9\lib\contextlib.py(112)__enter__()
-> return next(self.gen)

Notice that this next() call is being done inside __enter__ special function that is used by context managers when using with keyword to open it. Now if step forward through yield statement and wait until your breakpoint will step in the finally block where you can look at call tree using w once again:

[0]   c:\users\homeuser\.pyenv\pyenv-win\versions\3.7.9\lib\threading.py(890)_bootstrap()
-> self._bootstrap_inner()
[1]   c:\users\homeuser\.pyenv\pyenv-win\versions\3.7.9\lib\threading.py(926)_bootstrap_inner()
-> self.run()
[2]   c:\users\homeuser\.pyenv\pyenv-win\versions\3.7.9\lib\threading.py(870)run()
-> self._target(*self._args, **self._kwargs)
[3]   c:\users\homeuser\.pyenv\pyenv-win\versions\3.7.9\lib\concurrent\futures\thread.py(80)_worker()
-> work_item.run()
[4]   c:\users\homeuser\.pyenv\pyenv-win\versions\3.7.9\lib\concurrent\futures\thread.py(57)run()
-> result = self.fn(*self.args, **self.kwargs)
[5]   c:\users\homeuser\.pyenv\pyenv-win\versions\3.7.9\lib\contextlib.py(119)__exit__()
-> next(self.gen)

Now you will notice that next() is called on your generator function when exiting from context manager object.

You're right about pausing, but state of function is also remembered. So when first next() in __enter__ is called it yields your session object, then second next() call resumes function execution thus making it possible to enter inside finally block.

I think @contextmanager decorator is used somewhere in FastAPI implementation that takes care of dependencies thus making it possible to use generators in Depends. Take a look at this article that takes pretty good explanation how Python generators work.

devaerial
  • 2,069
  • 3
  • 19
  • 33
  • I appreciate the answer. I figured there was something going on under the hood with FastAPI dependencies that isn't obvious. I assume they have some kind of custom context manager implementation that runs `next()` on enter and exit. I think there is also another reason why `finally` would get run, more general to generators. I'm going to post my own answer for that. – j_krl Nov 02 '21 at 17:17
2

devaerial posted a good answer about how next() is called during the enter and exit magic methods of the context manager that is implemented by FastAPI generator dependencies. However, there is another more general reason the finally block would execute that I wanted to post here.

I found this answer about generators more broadly. Say we have a generator function...

def gen():
    yield 1
    print("test")

My output will be like so:

>>> print(next(gen()))
1

However, if I modify my function using try...finally

def gen():
    try:
        yield 1
    finally:
        print("test")

I will get this output:

>>> print(next(gen()))
test
1

The finally block is guaranteed to be executed before garbage collection once the generator is destroyed.

j_krl
  • 79
  • 1
  • 6