TIL: What Happens When Using the Context Manager Outside its with-Statement
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: 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:
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:
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: stackoverflow.com/questions/52028124/why-us... 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:
with open("my_file.txt", "r") as f:
f.read()
If we would do the read outside the context-block, like this:
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.