TIL: What Happens When Using the Context Manager Outside its with-Statement
Tags: python, todayilearned
Published: 2020-01-25 20:17:16

The other day I learned two things about context managers. The first one is that the variable, created when using `with ... as x` exist outside the context-block. This isn't really surprising, I just never tried it before. The second is that what happens when calling a method on the context manager outside the `with`-statement, really depends on the implementation. Which for some reason surprised me a lot more than it maybe should. Anyway, let us get into how I came across this.

So at work, I came across some code that I was surprised worked at all. It is related to using the `flask.testing.FlaskClient` as a context manager, which is shown in the tutorial: https://flask.palletsprojects.com/en/1.1.x/testing/. I can not share the code but I will re-create the scenario with a minimal example. 

To start with I have an example Flask app _my_flask_app.py_, which contains the code:
```python
from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello_world():
    return "Hello World!"
```

I also created the file _test_app.py_, which I will use for a small test for the app. The file looks like this:

```python
import pytest
import my_flask_app

def test_helloworld():
    with my_flask_app.app.test_client() as client:
        pass
    resp = client.get("/")
    assert resp.get_data(as_text=True) == "Hello World!"
```

If you know how context managers work, you probably can see the issue. I am using the `client` context variable outside the context-block. I use a `pass` statement here, but in reality, it was some more setup code. It was an easy mistake to make, having the wrong indentation level on a single line of code. What was so surprising is that the test actually works.

This caused me to dig around and I came across this StackOverflow question: https://stackoverflow.com/questions/52028124/why-use-flasks-app-test-client-over-requests-session-for-testing. So what is happening here is that the context manager gives you access to the request-stack inside the context but the actual communication is not affected by this. Therefore, when you exit the context the client still worked as expected for what we needed in the test.

I could contrast this with the most common context manager, the file returned from `open`. So what we usually do is this:
```python
with open("my_file.txt", "r") as f:
    f.read()
```
If we would do the read outside the context-block, like this:
```python
with open("my_file.txt", "r") as f:
    pass
f.read()
```
Then we get `ValueError: I/O operation on closed file.`, which is expected as the idea of this context manager is to automatically close the file when finished.

To summarize what I take with me from this discovery. Instead of blindly using a context manager, try to understand what context it actually sets up. Also, I think this can be a lesson for when you are designing your own context managers. Make sure the intent is well documented and think about how you want the object to behave when used outside the context. Maybe an error will be raised because a resource has been released. In that case, make sure the error message is clear and easy to understand.