Use the Python `with` Statement for Files

Learn how the Python with statement automatically closes files, even when errors occur, and why it is the only safe way to manage file resources in production code.

5 min read

Every article in this section has used the Python with statement to open files, and by now you have seen it enough times that it probably feels like the natural way to work with files in Python. That is by design. The with statement is not a stylistic preference or an optional convenience; it is the mechanism that guarantees your files are closed correctly no matter what happens inside the block. Without it, a single unhandled exception between open and close can leak a file descriptor, corrupt buffered writes, and lock the file so no other program can access it until your process exits. The Python with statement eliminates these risks with a syntax so minimal that the safest pattern is also the shortest one.

The with statement works through a protocol called context management. When Python encounters a with block, it calls a special method named enter on the object you provide. For file objects, enter returns the file object itself, which gets bound to the variable after the as keyword. When the with block ends, whether through normal completion, a return statement, or an unhandled exception, Python calls the exit method on the same object. For file objects, exit calls close, flushing any buffered writes to disk and releasing the file descriptor back to the operating system. This two-phase protocol ensures that cleanup always runs, which is why you have seen with in every code example since the article on opening and closing files in Python.

Why manual open and close fails

To appreciate the with statement, it helps to see the problem it solves. Consider a function that opens a file, processes each line, and closes the file afterward. The straightforward approach uses a manual open and close pair, and at first glance it looks correct. The file opens, the loop runs, the file closes, and the function returns. But there is a gap between the open call and the close call, and any error that occurs in that gap prevents close from ever running.

pythonpython
def count_lines(filename):
    file = open(filename, "r")
    try:
        lines = 0
        for line in file:
            lines += 1
            if "ERROR" in line:
                raise ValueError("Found error line")
    finally:
        file.close()
    return lines

The try-finally block fixes the problem by guaranteeing that close runs even if an exception fires. This pattern works, and it was the standard approach in Python before the with statement existed. The issue is not that it fails; the issue is that it is verbose and easy to forget. Every open call needs a matching try-finally, and in functions that open multiple files, the nesting becomes deep and hard to read. The with statement compresses this entire pattern into two lines that are impossible to write incorrectly.

The with statement pattern

The with statement takes the resource-acquisition pattern and makes it declarative. You write with, an expression that creates a context manager, an optional as clause to capture the result, and a colon. The indented block that follows is your working code. When the block ends, the cleanup happens automatically. There is no finally clause to write and no risk of forgetting it.

pythonpython
with open("journal.txt", "r", encoding="utf-8") as file:
    for line in file:
        print(line.strip())

After this block exits, the file is closed. You do not see the close call because you do not need to see it. The with statement handles the cleanup as reliably as the try-finally pattern but with less code and zero opportunity for the cleanup to be accidentally omitted. This is why every example in this section uses with, and why you should use it in every program you write.

The with statement also makes the scope of file access visually obvious. Any code inside the indented block has access to the file. Any code outside the block does not, because the file is closed. This scoping is enforced by the language, not by convention. If you accidentally reference the file variable after the block, you get a ValueError when you try to read or write because the underlying file handle has been released. This fail-fast behavior catches bugs at the point of misuse rather than letting them surface as corrupted data or resource exhaustion later.

Opening multiple files with a single with

When your program needs to read from one file and write to another, you can open both files in a single with statement by separating the open calls with commas. Python calls enter on each context manager in order, and when the block exits, it calls exit on each one in reverse order. Both files are guaranteed to close, even if an error occurs while reading the input or writing the output.

pythonpython
with open("source.txt", "r", encoding="utf-8") as infile, \
     open("destination.txt", "w", encoding="utf-8") as outfile:
    for line in infile:
        outfile.write(line.upper())

This example reads every line from source.txt, converts it to uppercase, and writes the result to destination.txt. Both files are opened before the loop starts, and both are closed when the block ends. If writing to the output file fails on line fifty, the input file is still closed correctly, and any buffered data that was successfully written is flushed. The backslash after the comma is a line continuation character that lets you split the long with statement across multiple lines for readability.

You can also nest with statements, which is useful when the second file can only be opened after reading data from the first. For example, a script might read a configuration file to determine which data file to process, then open that data file inside the outer with block. Each level of nesting adds a level of indentation, but the cleanup order remains predictable: the inner file closes first, then the outer file closes, and both close even if an exception fires at any level.

Beyond files: the context manager protocol

The with statement is not specific to files. Any Python object that implements the context manager protocol, meaning it defines enter and exit methods, works with with. This includes database connections that need to be returned to a connection pool, network sockets that need to be shut down, thread locks that need to be released, and temporary directories that need to be cleaned up. The same mental model applies: acquire the resource, use it inside the block, and trust that cleanup happens when the block ends.

Python's standard library is full of context managers. The threading.Lock class supports with, and acquiring a lock inside a with block guarantees it is released. The decimal.localcontext function temporarily changes decimal precision settings and restores them afterward. The contextlib module provides utilities for creating your own context managers, including the contextmanager decorator that turns a generator function into a context manager with minimal code.

The with statement is one of those Python features that pays off more the longer you use it. The first time it saves a file you forgot to close, or guarantees a lock is released that would have deadlocked your program, it has already justified the two extra words of syntax. When you move from file handling into error handling in the article on handling file errors in Python, the with pattern will already be second nature, and you will be able to focus on the error logic rather than worrying about whether your files are properly closed.

Rune AI

Rune AI

Key Insights

  • The with statement automatically closes files when the block exits, even during exceptions.
  • Every file object returned by open() supports the context manager protocol that with relies on.
  • Use with open(...) as file: as your default pattern and only deviate if you have a specific reason.
  • Multiple files can be opened in a single with statement by separating open calls with commas.
  • The with statement is not file-specific; it works with any object that implements enter and exit.
RunePowered by Rune AI

Frequently Asked Questions

Why is the with statement better than calling open() and close() manually?

The with statement guarantees that close() is called when the block exits, even if an exception occurs inside the block. With manual open and close, an exception between the two calls skips close() entirely, leaking a file descriptor and potentially losing unwritten data. The with statement eliminates this entire class of bugs with zero extra effort.

Can I open multiple files in a single with statement?

Yes. You can separate multiple open calls with commas inside the with statement, like with open('in.txt') as infile, open('out.txt', 'w') as outfile:. This opens both files and guarantees both are closed when the block exits. Nested with statements are also valid and sometimes clearer when the second file depends on data read from the first.

Does the with statement work with files opened in binary mode?

Yes. The with statement works identically with all file modes: text, binary, read, write, append, and every plus variant. The context manager protocol that with relies on is built into the file object itself and does not depend on the mode used to open it.

Conclusion

The Python with statement is the correct way to open files in every program you write from this point forward. It replaces manual open and close pairs with a scoped block that guarantees cleanup. The syntax is minimal, the safety benefit is enormous, and the habit will serve you well not just for files but for every Python resource that supports the context manager protocol: database connections, network sockets, thread locks, and more.