The idea of the with
statement is to make "doing the right thing" the path of least resistance. While the file example is the simplest, threading locks actually provide a more classic example of non-obviously buggy code:
try:
lock.acquire()
# do stuff
finally:
lock.release()
This code is broken - if the lock acquisition ever fails, either the wrong exception will be thrown (since the code will attempt to release a lock that it never acquired), or, worse, if this is a recursive lock, it will be released early. The correct code looks like this:
lock.acquire()
try:
# do stuff
finally:
# If lock.acquire() fails, this *doesn't* run
lock.release()
By using a with
statement, it becomes impossible to get this wrong, since it is built into the context manager:
with lock: # The lock *knows* how to correctly handle acquisition and release
# do stuff
The other place where the with
statement helps greatly is similar to the major benefit of function and class decorators: it takes "two piece" code, which may be separated by an arbitrary number of lines of code (the function definition for decorators, the try
block in the current case) and turns it into "one piece" code where the programmer simply declares up front what they're trying to do.
For short examples, this doesn't look like a big gain, but it actually makes a huge difference when reviewing code. When I see lock.acquire()
in a piece of code, I need to scroll down and check for a corresponding lock.release()
. When I see with lock:
, though, no such check is needed - I can see immediately that the lock will be released correctly.
Best Answer
I don't know why no one has mentioned this yet, because it's fundamental to the way
with
works. As with many language features in Python,with
behind the scenes calls special methods, which are already defined for built-in Python objects and can be overridden by user-defined classes. Inwith
's particular case (and context managers more generally), the methods are__enter__
and__exit__
.Remember that in Python everything is an object -- even literals. This is why you can do things like
'hello'[0]
. Thus, it does not matter whether you use the file object directly as returned byopen
:or store it first with a different name (for example to break up a long line):
Because the end result is that
the_file
,infile
, and the return value ofopen
all point to the same object, and that's whatwith
is calling the__enter__
and__exit__
methods on. The built-in file object's__exit__
method is what closes the file.