Course Content
Python Essentials for AI Engineer
6 sections · 48 lessons
What is the difference between except and finally?
What you need to know
The full statement
1def load(path):2 try:3 f = open(path, encoding="utf-8")4 except FileNotFoundError:5 print(" except: missing file")6 return None7 else:8 print(" else: opened fine")9 with f:10 return f.read()11 finally:12 print(" finally: always runs")1314open("present.txt", "w", encoding="utf-8").close()15print("case 1"); load("present.txt")16print("case 2"); load("absent.txt")17# case 118# else: opened fine19# finally: always runs20# case 221# except: missing file22# finally: always runsWhat happened in try | except | else | finally |
|---|---|---|---|
| no exception | skipped | runs | runs |
| matching exception | runs | skipped | runs |
| non-matching exception | skipped | skipped | runs, then the exception continues |
return inside try | skipped | skipped | runs before the function returns |
Note that finally ran even though both paths used return.
Why else exists
Code in else is not protected by the except clauses. That is the point: if the success-path code raises its own FileNotFoundError, you do not want the handler meant for the first step to swallow it.
Don't return from finally
A return inside finally replaces whatever was happening — including an exception in flight, which silently disappears. Python 3.14 now emits a SyntaxWarning for return, break or continue in a finally block (PEP 765).
When finally does not run
If the process is killed (kill -9, out-of-memory killer), the machine loses power, or code calls os._exit(), nothing gets a chance to run.
A real-life example
A service limits itself to 4 concurrent LLM calls with a semaphore and records the latency of every call, including failed ones, for its dashboard:
1import threading, time23slots = threading.Semaphore(4)4latencies = []56def timed_call(prompt, fail=False):7 slots.acquire()8 start = time.perf_counter()9 try:10 if fail:11 raise TimeoutError("upstream timeout")12 return f"ok: {prompt}"13 finally:14 latencies.append(round(time.perf_counter() - start, 3)) # failures are measured too15 slots.release() # the slot is always returned1617print(timed_call("a"))18try:19 timed_call("b", fail=True)20except TimeoutError as e:21 print("caught:", e)22free = sum(slots.acquire(blocking=False) for _ in range(4))23print(len(latencies), free) # 2 4 -> both calls timed, all 4 slots freeAn earlier version released the semaphore after the return line, outside any finally. Each timeout leaked one slot; after four timeouts the service stopped making calls entirely and hung until restart. With finally (or with slots:, which does the same), every path gives the slot back. And because latency is recorded in finally, slow failures appear on the dashboard instead of hiding.
Follow-up questions to expect
- "Does
finallyrun iftryreturns?" — Yes. It runs just before the function actually returns. - "What is the
elseclause for?" — Code that should run only when thetrysucceeded, kept outside thetryso its own errors are not caught by the wrong handler. - "
finallyorwith?" — Usewithwhenever the resource supports it (files, locks, many clients); writefinallyfor cleanup that has no context manager.