Skip to content

Context Managers & with

TypeScript has no with keyword. When you need to acquire a resource and guarantee it is released — a file handle, a database connection, a lock — you write try/finally. You set up the resource before the try, and you clean it up in the finally block. This works, but it requires you to remember the pattern and write it correctly every time.

Python’s with statement encapsulates the setup and teardown inside the object itself. The object defines what happens on entry and what happens on exit, and Python guarantees the exit runs — even if an exception is raised inside the block. The cleanup logic lives in the resource, not scattered across every call site.

Opening a file is the most common example. TypeScript has no language-level file API, but the pattern looks like this:

TypeScript
// TypeScript: manual try/finally for resource cleanup
import * as fs from "fs";
function readFile(path: string): string {
const fd = fs.openSync(path, "r");
try {
const buf = Buffer.alloc(1024);
const bytesRead = fs.readSync(fd, buf, 0, 1024, 0);
return buf.toString("utf8", 0, bytesRead);
} finally {
fs.closeSync(fd); // always runs
}
}
Python
# Python: with statement handles open/close automatically
def read_file(path):
with open(path, "r") as f:
return f.read()
# f.close() is called automatically — even if read() raises

The with block calls f.close() automatically on exit, whether it exits normally or because of an exception — no duplicated try/finally required.

A class becomes a context manager by implementing two dunder methods: __enter__ and __exit__. Python calls __enter__ when execution enters the with block and __exit__ when it leaves — for any reason.

TypeScript
// TypeScript: try/finally for guaranteed cleanup
class ManagedResource {
constructor(private name: string) {}
acquire(): this {
console.log(`Acquiring ${this.name}`);
return this;
}
release(): void {
console.log(`Releasing ${this.name}`);
}
use(): void {
console.log(`Using ${this.name}`);
}
}
const resource = new ManagedResource("database connection");
resource.acquire();
try {
resource.use();
} finally {
resource.release(); // must remember to call this
}
Python
# Python: with statement handles acquire and release automatically
class ManagedResource:
def __init__(self, name):
self.name = name
def __enter__(self):
print(f"Acquiring {self.name}")
return self # bound to the 'as' variable
def __exit__(self, exc_type, exc_val, exc_tb):
print(f"Releasing {self.name}")
return False # do not suppress exceptions
with ManagedResource("database connection") as resource:
print(f"Using {resource.name}")
# release is guaranteed even if an exception occurs

The value returned by __enter__ is bound to the variable after as. The as clause is optional — you can write with ManagedResource("x"): if you do not need a reference to the resource inside the block.

contextlib: generators as context managers

Section titled “contextlib: generators as context managers”

Writing a full class for a simple context manager is often overkill. The contextlib module provides a @contextmanager decorator that lets you write a context manager as a generator function. Everything before yield is the setup; everything after yield is the teardown.

TypeScript
// TypeScript: a utility wrapper function with try/finally
function withTimer<T>(label: string, fn: () => T): T {
console.log(`[${label}] starting`);
try {
return fn();
} finally {
console.log(`[${label}] done`);
}
}
const result = withTimer("my block", () => {
let total = 0;
for (let i = 0; i < 1000; i++) total += i;
console.log(`sum = ${total}`);
return total;
});
Python
from contextlib import contextmanager
@contextmanager
def timer(label):
print(f"[{label}] starting")
yield # execution of the with block happens here
print(f"[{label}] done")
with timer("my block"):
total = sum(range(1000))
print(f"sum = {total}")
from contextlib import contextmanager
class ManagedResource:
def __init__(self, name):
self.name = name
def __enter__(self):
print(f"Acquiring {self.name}")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print(f"Releasing {self.name}")
return False
with ManagedResource("database connection") as resource:
print(f"Using {resource.name}")
@contextmanager
def timer(label):
print(f"[{label}] starting")
yield
print(f"[{label}] done")
with timer("my block"):
total = sum(range(1000))
print(f"sum = {total}")
What two dunder methods must a class implement to work as a context manager?
What value is bound to the variable after "as" in a with statement?
In a @contextmanager function, where does the body of the with block execute?
How does __exit__ suppress an exception that occurred inside the with block?
What is the correct syntax to use two context managers together on one line, each bound to its own name?