with open("data.txt") as f:
contents = f.read()
# file is guaranteed to be closed here, even if read() raised an exception
with guarantees cleanup runs — this is the same problem try/finally solves, but for the very common case of "acquire a resource, use it, release it no matter what." Writing a custom one only needs two dunder methods:
class Timer:
def __enter__(self):
self.start = time.perf_counter()
return self # becomes the "as x" value
def __exit__(self, exc_type, exc_val, exc_tb):
elapsed = time.perf_counter() - self.start
print(f"Elapsed: {elapsed:.4f}s")
return False # False (or None) means: don't suppress the exception, if any
with Timer():
time.sleep(0.2)
# "Elapsed: 0.2001s"
__exit__ receives the exception info (if the with block raised one) as its three arguments — returning True from __exit__ swallows that exception silently, which is occasionally intentional but is also a common source of bugs when done by accident. The equivalent, shorter way to write the same thing uses contextlib.contextmanager with a single yield:
from contextlib import contextmanager
@contextmanager
def timer():
start = time.perf_counter()
yield # code inside "with timer():" runs here
print(f"Elapsed: {time.perf_counter() - start:.4f}s")
Everything before yield is __enter__; everything after is __exit__.