How it works
The explanation, with examples you can run.
Raising and catching
Exceptions are classes in a hierarchy rooted at BaseException. Almost everything you would catch derives from Exception; KeyboardInterrupt and SystemExit deliberately do not, so that except Exception: lets Ctrl-C and sys.exit() through. Catching a base class catches its subclasses: except OSError handles FileNotFoundError and PermissionError too.
A try statement has up to four parts. except handles matching errors, else runs only if nothing was raised, and finally runs no matter how the block is left, including by return:
def check(): try: print("try") return "from try" finally: print("finally runs before the function returns")print(check()) # try / finally runs before the function returns / from tryPython style leans towards "easier to ask forgiveness than permission" (EAFP): try the dictionary lookup and catch KeyError, rather than checking first. It avoids a race between the check and the use, and the fast path stays fast.
Chaining
When you translate a low-level error into one that makes sense to your caller, keep the original:
import json def load_config(text): try: return json.loads(text) except json.JSONDecodeError as e: raise ValueError("config is not valid JSON") from e try: load_config("{oops")except ValueError as e: print(e, "| caused by:", type(e.__cause__).__name__)# config is not valid JSON | caused by: JSONDecodeErrorfrom e stores the original in __cause__ and the traceback prints both. If an exception is raised while handling another without from, Python still records the first one in __context__ and prints "During handling of the above exception, another exception occurred". raise ... from None hides the context when it is just noise. Since 3.11 you can also attach extra lines to a traceback with e.add_note("...").
Context managers
with open(path) as f: is shorthand for a try/finally that calls two methods: __enter__ when the block starts (its return value is bound to f) and __exit__(exc_type, exc, tb) when it ends. If the block raised, __exit__ receives the exception and may return True to suppress it. Files close, locks release and database transactions commit or roll back this way.
The easiest way to write one is a generator with contextlib.contextmanager. Everything before yield is the setup, everything after it is the cleanup:
from contextlib import contextmanagerimport time @contextmanagerdef timer(label): start = time.perf_counter() try: yield finally: print(f"{label}: {time.perf_counter() - start:.2f}s") with timer("sleep"): time.sleep(0.05) # sleep: 0.05sThe try/finally around yield matters: if the with body raises, the exception is thrown into the generator at the yield, and without finally the cleanup would be skipped. contextlib also has suppress, closing, ExitStack (for a variable number of resources) and async versions for async with.
Many errors at once
Concurrent code can fail in several places at the same time. Python 3.11 added ExceptionGroup and the except* syntax, which matches each kind of exception inside a group separately. asyncio.TaskGroup raises one when more than one task fails:
try: raise ExceptionGroup("two failures", [ValueError("bad"), TypeError("worse")])except* ValueError as eg: print("values:", eg.exceptions) # values: (ValueError('bad'),)except* TypeError as eg: print("types:", eg.exceptions) # types: (TypeError('worse'),)Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.