Manage Resources with Python with Statements
Use with to bound resource lifetime and create a context manager that releases a file even when the body raises.
Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons
Video lesson: Manage Resources with Python with Statements
The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 00:00
Many Python programs acquire something they must release: a file handle, a lock, a database transaction, or a temporary resource. We might remember to close a file after writing a note, but an exception can jump over that close call. The result is a handle that stays open longer than intended. A context manager gives us a structured way to pair setup with cleanup. The familiar `with open(...) as file:` form closes the file when the block exits, including when an error occurs inside the block. We will first run a broken manual pattern so the resource state is visible. Then we will use a built-in file context manager and build our own `note_writer(path_text)` with `contextlib.contextmanager`.
- 00:46
The custom helper will create parent folders, open a UTF-8 text file in write mode, yield the open handle, and close it in a finally block. The companion lesson has runnable examples, an automatically checked task, a quiz, and common mistakes. We will use a temporary directory so the demo does not depend on files already present on your machine or browser. The important design question is not simply where to put `close()`. It is who owns the resource and how that owner guarantees cleanup when the body takes an unexpected path. The line after the deliberate RuntimeError is unreachable, so the intended `handle.close()` is skipped.
- 01:27
Inside the exception handler, `handle.closed` is false. The finally block in this demonstration closes it so we do not leave an open file behind. Notice that `TemporaryDirectory` is already a context manager and will remove its directory when we leave its outer with block. Even so, it does not excuse us from closing the file handle we opened inside it. The operating system might eventually release a leaked handle when a process exits, but that is not a reliable cleanup strategy, especially in a long-running program. Another risk is that buffered output may not be flushed when you expect, or a later operation may fail because the file is still held open.
- 02:11
The simplest repair for a single file is to let `with path.open(...) as handle:` own that handle. Python will call the file object's exit logic whether the block finishes normally or because an exception leaves it. For a reusable setup sequence that includes parent-directory creation and a file open, we can wrap the process in our own context manager. First, let us verify the built-in behavior before adding custom code. The RuntimeError has not disappeared; the caller still handles it. What changed is the file's lifetime. Leaving the inner with block caused the file to close before execution reached our handler.
- 02:52
We can inspect `handle.closed` afterward, and the text we wrote is visible when we read the file again. This illustrates the entry-and-exit structure. A context manager provides a value for the `as` target when the block starts, and then performs exit behavior when the block ends. The exit behavior can run after normal completion or after an exception. Some context managers can suppress particular exceptions, but a plain file context manager does not hide this RuntimeError. This distinction matters: automatic cleanup is not the same as pretending everything succeeded. A try/finally block can provide the same cleanup guarantee if written carefully. `with` packages the pattern so callers do not have to repeat it.
- 03:39
Our custom helper will need to create a directory before opening the file, then yield an open file handle to the caller's with block. Because we use `@contextmanager`, the code before `yield` is setup and the finally block after it performs cleanup. The generator must yield exactly once for each use. The parent folder did not exist before we entered note_writer. `mkdir(parents=True, exist_ok=True)` creates the needed path without an error if it already exists. Then we open the file explicitly as UTF-8 text and yield the handle. At that point, the caller's with block runs and writes the note.
- 04:20
When the block ends, execution resumes after the yield and enters the finally block, closing the handle. This works even if the caller's block raises an exception, which we will test next. This helper opens in write mode, so an existing file at that path will be replaced. That is part of the current task's contract; a real application should choose between write, append, or exclusive-create mode based on its requirements. We are not silently writing to an arbitrary persistent directory in the demo; TemporaryDirectory gives us an isolated workspace that disappears after the outer block exits. On PythonLessonLab, implement note_writer with the same function name and test it in the browser's temporary filesystem.
- 05:07
The checker looks for parent-directory creation, a usable open handle during the block, and closure afterward. The key placement is the `yield` inside the try block and `handle.close()` in finally. If you place close immediately before yield, the caller receives a closed file. If you put close only after yield without finally, an exception can skip it. During each with block, the yielded file is open and writable. After a normal exit, it is closed and the first note has been saved. On the second entry, write mode replaces the earlier content, and the deliberate RuntimeError exits the block. The caller catches that error, while the helper's finally block still closes the handle.
- 05:51
The final read confirms the second write and the overwrite behavior. These tests check resource state and output; merely checking that no exception leaked would miss an open-handle bug. Try a variant on the website that uses a different nested folder name. Then complete the practical task and quiz. Think about whether your own file-writing utility should overwrite, append, or refuse to replace an existing file, and make that choice visible in its API. Context managers also appear in database transactions, locks, and temporary directories, but their exit behavior can differ. Some commit, some roll back, and some suppress exceptions.
- 06:33
Read each manager's contract instead of assuming every with block behaves like a file. The general pattern remains powerful: acquire a resource, use it within a limited block, and guarantee the corresponding release when the block ends. That keeps cleanup close to setup and makes exceptional paths easier to reason about.
Understand the concept
A file, lock, or temporary directory needs a clear lifetime. The with statement enters a context manager, runs its indented block, and exits the context afterward. A file object closes when its with block ends, including when an exception escapes. Reading after the block fails because the file is closed; that is evidence that the lifetime has ended, not a reason to keep it open forever.
A context manager implements __enter__ and __exit__, or can be created with contextlib.contextmanager. In a generator-based context manager, code before yield prepares the resource, yield passes a value to the with block, and code in finally releases it. The generator must yield exactly once during a normal entry. An exception inside the block is injected at the yield point, so finally still runs.
Closing a resource is different from swallowing an error. Our note writer closes a file but lets an exception from the with body continue to its caller. This keeps failure visible while preventing a leaked file handle. contextlib.suppress deliberately hides selected exceptions; use it only when ignoring a specific error is part of the intended behavior, not as a general cleanup mechanism.
The exercise combines Path and contextmanager. It creates missing parent folders, opens a UTF-8 file for writing, yields the writable handle, and closes it in finally. Test a normal write, a nested path, Unicode text, and an exception raised by the with body. The browser's Python filesystem is temporary; the same pattern works with persistent local files in a normal Python program.
- with defines a resource lifetime
- __enter__ and __exit__ implement the protocol
- @contextmanager uses one yield
- try/finally guarantees cleanup without hiding errors
See it step by step
Read the code, predict the output, then compare it with the result.
01. See a file close after its with block
from tempfile import TemporaryFile
with TemporaryFile(mode='w+', encoding='utf-8') as note:
note.write('saved')
note.seek(0)
print(note.read())
print(note.closed)
print(note.closed)saved
False
TrueThe handle is usable inside with and closed when the block exits.
02. Build a small manager with contextmanager
from contextlib import contextmanager
@contextmanager
def marked(log):
log.append('enter')
try:
yield 'ready'
finally:
log.append('exit')
events = []
with marked(events) as status:
print(status)
print(events)ready
['enter', 'exit']Preparation occurs before yield; cleanup occurs in finally after the with block.
03. Clean up and still report an exception
from contextlib import contextmanager
@contextmanager
def guard(log):
log.append('open')
try:
yield
finally:
log.append('close')
events = []
try:
with guard(events):
raise ValueError('bad input')
except ValueError as error:
print(str(error))
print(events)bad input
['open', 'close']finally runs during the exceptional exit, and the ValueError remains visible to the caller.
A closer look
Follow the reasoning, inspect each result, then try the suggested changes in the console below.
Give a file an explicit lifetime
A file is an external resource with a start and an end. The with statement enters the file's context manager, binds the open handle, and exits it after the indented body. The exit happens on a normal path and when the body raises. This is easier to reason about than placing close() at the end of a long function, where an earlier return or exception may skip it.
TemporaryFile creates a real temporary file for this demonstration. The handle is open in the block, so seek moves back to the beginning and read returns the text. After the block, closed becomes True. Avoid reading the handle after that point. In your own program, `with open(path, 'w', encoding='utf-8') as handle:` uses the same lifetime pattern on a named file. Closing a file is not the same as deleting it; a named file remains unless you remove it separately.
from tempfile import TemporaryFile
with TemporaryFile(mode='w+', encoding='utf-8') as handle:
handle.write('a short note')
handle.seek(0)
print(handle.read())
print(handle.closed)
print(handle.closed)a short note
False
True- The handle is open and usable during the with body.
- Leaving the block invokes the file's exit behavior.
- The closed flag confirms the resource lifetime has ended.
Make a context manager with one yield
contextlib.contextmanager turns a generator function into a with-compatible context manager. Code before yield prepares a resource; the yielded value is bound after as; code after yield performs teardown. The reliable pattern puts yield inside try and teardown in finally. When an exception occurs inside with, it is delivered at the yield point and finally still executes.
The event list makes the lifecycle observable without touching the filesystem. This manager yields a ready label and records open and close around the body. Because the manager does not catch and suppress errors, a problem raised by the body can still be handled by its caller. A generator context manager must yield exactly once during normal entry. Returning before yield or yielding a second time breaks the protocol.
from contextlib import contextmanager
@contextmanager
def marked(events):
events.append('open')
try:
yield 'ready'
finally:
events.append('close')
events = []
with marked(events) as status:
print(status)
print(events)
print(events)ready
['open']
['open', 'close']- The setup code executes before the body receives ready.
- The body sees only the open event while it runs.
- finally records close when the block ends.
Close a note even when writing fails
The checked note_writer exercise uses the same generator pattern for a real text file. It creates parent folders first, opens a UTF-8 handle, yields that handle, and closes it in finally. If the body raises, closing still occurs. The exception should continue to its caller because an unexpected failure is useful information. The example below deliberately raises after writing and catches the error outside the with block so we can inspect the closed flag.
Opening with mode 'w' replaces any existing file contents. Choose append mode only when adding to an existing file is intended. A relative path is interpreted from the current working directory, which can differ from the script location. The example uses a short predictable file name and deletes it at the end; in the browser console, files live in the runtime's temporary filesystem. When adapting this pattern for an application, decide where persistent files belong before writing them.
from contextlib import contextmanager
from pathlib import Path
@contextmanager
def note_writer(path_text):
path = Path(path_text)
path.parent.mkdir(parents=True, exist_ok=True)
handle = path.open('w', encoding='utf-8')
try:
yield handle
finally:
handle.close()
path = Path('pll-context-guide-note.txt')
try:
with note_writer(path) as note:
note.write('saved')
raise ValueError('stop')
except ValueError as error:
print(str(error))
print(note.closed)
print(path.read_text(encoding='utf-8'))
path.unlink()stop
True
saved- The file receives saved before the deliberate error.
- finally closes the handle while the error travels outward.
- The caller catches the visible error and can still read the saved file afterward.
Try it in Python
Edit the example and run it. Python starts in your browser the first time you click Run.
Python console
Ready to runNeed input()? Add one value per line
Your output appears here.
Need a hint?
The setup and yield are already present. Replace pass in finally with handle.close(). The finally block executes after both normal and exceptional exits.
Complete the task and select Check task to verify your code.
Quick quiz
Three questions. You can change your answers and try again.
Typical mistakes
Everyone meets these errors. See what causes them and how to fix them.
Opening a file without a bounded lifetime
note = open('note.txt', 'w', encoding='utf-8')
note.write('draft')with open('note.txt', 'w', encoding='utf-8') as note:
note.write('draft')What happens: The handle may remain open if later code raises before close().
with asks the file's context manager to close the handle on either exit path.
Forgetting finally in a generator manager
@contextmanager
def managed(log):
log.append('open')
yield
log.append('close')@contextmanager
def managed(log):
log.append('open')
try:
yield
finally:
log.append('close')What happens: If the with body raises, the line after yield is skipped and cleanup is lost.
Put cleanup in finally so it runs during exception propagation.
Silencing every failure as cleanup
with suppress(Exception):
write_important_note()with note_writer('note.txt') as note:
note.write('important')What happens: A real write error can disappear without the caller noticing.
Resource cleanup and error handling are separate decisions; allow unexpected errors to propagate.