The with Statement
Core Python Iteration and Files

Python with Statement

1The with Statement: overview

The with statement runs setup code before a block and guaranteed cleanup code after it, even when the block fails, by using an object called a context manager.

GoalBy the end you can use with for files and other resources, explain what enter and exit do, write your own context manager as a class or with @contextmanager, and use suppress, StringIO and redirect_stdout.

Before this lesson: Python Exceptions: try, except, else and finally, Python File Handling

2The with Statement: diagram

Python with Statementwith open("log.txt") as f:__enter__ runs: the file is openedsetup doneindented block runsread, write, or raise an errorblock ends or fails__exit__ always runsfile closed, success or failurecleanup donenext line after the blockan uncaught error keeps travelling up
A with statement is a promise: whatever happens inside the block, the cleanup in exit runs before Python moves on. An error that nobody handles is passed on only after the cleanup has finished.

3The with Statement: explained step by step

Some jobs come in pairs: open a file and close it, lock a resource and release it, start a transaction and commit it. If your code forgets the second half, or an error jumps over it, files stay open and resources leak. A context manager is an object that does the setup and guarantees the cleanup. The with statement is how you use one.

with open("notes.txt", "w") as f:
    f.write("hello")

What with really does

When Python meets with something as name: it follows three steps:

  1. It calls the object's __enter__() method (the setup). Whatever __enter__ returns is stored in the name after as.
  2. It runs the indented block.
  3. It calls __exit__() (the cleanup) no matter how the block ended: normally, through return or break, or because an exception escaped.

Written by hand, the same promise needs try/finally:

f = open("notes.txt", "w")
try:
    f.write("hello")
finally:
    f.close()

Every caller would have to remember that pattern. With a context manager the rule lives inside the object, and with is the short, readable form.

A file is closed even when the block fails

Every file object has a closed attribute, which lets you watch the cleanup happen.

Python
with open("todo.txt", "w") as f:
    f.write("Call Sam\n")
    print("inside the block:", f.closed)
print("after the block:", f.closed)

try:
    f.write("one more line")
except ValueError as err:
    print("ValueError:", err)

The name f still exists after the block, but the file behind it is closed. Do all the reading and writing inside the block. The same closing happens if the block raises an error, as the first example below shows.

Several context managers in one with

Separate them with commas. They are entered from left to right and exited in the opposite order.

Python
with open("names.txt", "w") as f:
    f.write("asha\nben\n")

with open("names.txt") as source, open("loud.txt", "w") as target:
    for line in source:
        target.write(line.upper())

with open("loud.txt") as f:
    print(f.read().split())

Writing your own with a class

A context manager is any object with an __enter__ and an __exit__ method. __exit__ receives the exception type, value and traceback; all three are None when the block succeeded. What it returns matters: True swallows the exception, while False (or None) lets it continue upward.

Python
class Section:
    def __init__(self, title):
        self.title = title

    def __enter__(self):
        print("start", self.title)
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        problem = exc_type.__name__ if exc_type else "none"
        print("end", self.title, "- error:", problem)
        return False

with Section("import"):
    print("working")

try:
    with Section("export"):
        1 / 0
except ZeroDivisionError:
    print("the error was not hidden")

Returning True is powerful and dangerous. If you ever use it, check exc_type so that you hide only the one error you expect.

The shortcut: @contextmanager

A class is a lot of code for a small job. The decorator contextlib.contextmanager turns a generator function into a context manager: the code before yield is the setup, the yielded value is what as receives, and the code after is the cleanup. Wrap the yield in try/finally. An error inside the with block is raised at the yield, so without finally the cleanup lines would be skipped.

Python
from contextlib import contextmanager

settings = {"mode": "normal"}

@contextmanager
def temporary_mode(name):
    old = settings["mode"]
    settings["mode"] = name
    try:
        yield
    finally:
        settings["mode"] = old

with temporary_mode("debug"):
    print("inside:", settings["mode"])
print("outside:", settings["mode"])

Ready-made helpers

contextlib and io already contain handy ones. suppress(*errors) ignores only the errors you list. redirect_stdout(target) sends everything print() writes into a file-like object. io.StringIO is a text file that lives in memory: it has write(), read() and getvalue(), and it is perfect for tests.

Python
import io
import os
from contextlib import suppress, redirect_stdout

with suppress(FileNotFoundError):
    os.remove("missing.txt")
print("no crash")

buffer = io.StringIO()
with redirect_stdout(buffer):
    print("captured text")
print(repr(buffer.getvalue()))

When to use with

Use it whenever something has to be undone or released: files, locks, database connections and transactions, temporary changes to a setting, or output you want to capture. If a task needs "do this, then always do that", ask whether it should be a context manager. The files in this lesson live in the browser's temporary file system, but on your own computer the same code works on real files.

Quick reference: The with Statement

4The with Statement: syntax

with open("data.txt") as f:        # __enter__ runs; f is what it returned
    text = f.read()                # do all the work inside the block
                                   # __exit__ runs here, always

with open("in.txt") as src, open("out.txt", "w") as dst:   # two managers
    dst.write(src.read())

class Managed:                     # class-based context manager
    def __enter__(self):
        ...                        # setup
        return self                # bound to the name after "as"
    def __exit__(self, exc_type, exc_value, traceback):
        ...                        # cleanup (all three are None if no error)
        return False               # True would swallow the exception

from contextlib import contextmanager, suppress, redirect_stdout

@contextmanager
def managed():                     # generator-based version
    ...                            # setup
    try:
        yield value                # value is bound by "as"
    finally:
        ...                        # cleanup, even after an error

with suppress(FileNotFoundError):  # ignore only this error
    os.remove("old.tmp")
with redirect_stdout(buffer):      # print() writes into buffer
    print("captured")
buffer = io.StringIO()             # in-memory text file: write(), getvalue()

5The with Statement: run the example

Press Run (or Ctrl + Enter). The output (step 6) appears directly below the code. Change anything and run it again.

Interactive example
with open("scores.txt", "w") as f:
    f.write("Asha,82\nBen,oops\nChen,91\n")

total = 0
try:
    with open("scores.txt") as f:
        for line in f:
            name, score = line.strip().split(",")
            total += int(score)
except ValueError as err:
    print("Bad data:", err)

print("Total before the error:", total)
print("File closed?", f.closed)
Expected outputBad data: invalid literal for int() with base 10: 'oops'
Total before the error: 82
File closed? True

7The with Statement: how the example works

Hover or tap a step to highlight its lines in the example above.

  1. Line 1-2A file is written inside a with block and closed automatically when the block ends. The middle line, Ben,oops, has a score that is not a number.
  2. Line 4total collects the scores that are read successfully.
  3. Line 5-6The try block opens the file for reading. __enter__ returns the file object, which is stored in f.
  4. Line 7-8The loop reads one line at a time. strip() removes the newline and split(",") separates the name from the score.
  5. Line 9int(score) converts the text and adds it to total. Asha's 82 works, but on the second line int("oops") raises a ValueError. The error leaves the loop and the with block, and on its way out __exit__ closes the file.
  6. Line 10-11The except block catches the error and prints its message. Chen's score was never reached.
  7. Line 13Only Asha's score made it into total: 82.
  8. Line 14f.closed is True: the file was closed even though an error escaped from inside the block. That guarantee is the whole point of with.

8The with Statement: common mistakes

Using the file after the with block

✗ Wrong

with open("scores.txt", "w") as f:
    f.write("Asha,90\n")

with open("scores.txt") as f:
    print("Reading the file...")
text = f.read()
print(text)
ValueError: I/O operation on closed file.

✓ Right

with open("scores.txt", "w") as f:
    f.write("Asha,90\n")

with open("scores.txt") as f:
    print("Reading the file...")
    text = f.read()
print(text)

The file is closed the moment the indented block ends. Move every read or write that needs the file inside the block (indent text = f.read()).

Cleanup without try/finally in @contextmanager

✗ Wrong

from contextlib import contextmanager

locked = []

@contextmanager
def lock(name):
    locked.append(name)
    yield
    locked.remove(name)

try:
    with lock("report"):
        raise RuntimeError("crash")
except RuntimeError:
    pass
print(locked)
No error, but it prints ['report']: the lock was never released

✓ Right

from contextlib import contextmanager

locked = []

@contextmanager
def lock(name):
    locked.append(name)
    try:
        yield
    finally:
        locked.remove(name)

try:
    with lock("report"):
        raise RuntimeError("crash")
except RuntimeError:
    pass
print(locked)

An error in the with block is raised at the yield, so the lines after it never run. Put the yield inside try and the cleanup inside finally.

An __exit__ method with the wrong parameters

✗ Wrong

class Timer:
    def __enter__(self):
        print("start")
        return self

    def __exit__(self):
        print("stop")

with Timer():
    print("working")
TypeError: Timer.__exit__() takes 1 positional argument but 4 were given

✓ Right

class Timer:
    def __enter__(self):
        print("start")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("stop")

with Timer():
    print("working")

Python always passes the exception type, value and traceback to __exit__, so it needs three parameters besides self, even if you ignore them.

Returning True from __exit__ and hiding every error

✗ Wrong

class Quiet:
    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("cleaning up")
        return True

with Quiet():
    number = int("abc")
print("finished")
No error is shown: the ValueError disappears and the program happily prints finished

✓ Right

class Quiet:
    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("cleaning up")
        return False

try:
    with Quiet():
        number = int("abc")
except ValueError as err:
    print("Handled:", err)
print("finished")

Returning True tells Python the exception was handled. Return False (or nothing) to let errors through, or return True only for the one exception you really want to hide, for example return exc_type is ValueError.

Using with on something that is not a context manager

✗ Wrong

with "notes.txt" as f:
    print(f.read())
TypeError: 'str' object does not support the context manager protocol

✓ Right

with open("notes.txt", "w") as f:
    f.write("remember the milk\n")

with open("notes.txt") as f:
    print(f.read())

with needs an object that has __enter__ and __exit__. A file name is just a string; call open() to get a file object, which is a context manager.

9The with Statement: exercises

Write your answer, then press Check. The checks run in your browser.

1. Save and load a name list

Write code Easy

Write two functions, both using a with block:

  • save_names(path, names) writes each name on its own line (a later call replaces the old content).
  • load_names(path) returns the names as a list of strings, without the newline characters.

The code then saves and loads the starter list and prints the result.

Your code
def save_names(path, names):
    # write one name per line
    pass

def load_names(path):
    # return a list of names without the newlines
    return []

save_names("team.txt", ["Asha", "Ben", "Chen"])
print(load_names("team.txt"))

2. Predict the guard

Predict the output Medium

Read the class and the two with blocks, then type exactly what the program prints. Think about what __exit__ returns in each case. Then press Check.

Read this code
class Guard:
    def __enter__(self):
        print("enter")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("exit:", exc_type.__name__ if exc_type else "clean")
        return exc_type is KeyError

with Guard():
    print("body one")

with Guard():
    {}["missing"]
    print("body two")

print("after")

2 more exercises → 6 more examples

Finished this lesson?
Progress is saved in this browser — create a free account to keep it across devices.

Questions learners ask

Do I still need to call f.close() when I use with open(...)?

No. Leaving the with block closes the file for you, even if an error happens inside the block. Calling close() as well does no harm, but it is unnecessary. If you open a file without with, you must close it yourself, and you must make sure that happens even when an error occurs.

What are the three arguments of __exit__, and what does returning True do?

They are the exception type, the exception value and the traceback. All three are None when the block finished without an error. If exit returns True, Python treats the exception as handled and carries on after the with block; if it returns False or None, the exception continues upward. Return True only for a specific error you intend to hide, because it can bury real bugs.

Is the with statement only for files?

No. Any object that has enter and exit works: locks, database connections and transactions, temporary directories, temporary changes to a setting, and tools like suppress() and redirect_stdout(). Files are just the most common example because forgetting to close them is such an easy mistake.

Further reading

10Next lesson

Up nextPython pathlibpathlib's Path is an object that represents a file or folder location, with operators and methods to join, take apart, …

Related:
Python Exceptions: try, except, else and finallyPython File HandlingPython pathlibPython Class Methods and Static Methods

← Python File Handling · All Iteration and Files lessons · Practise in the IDE