Python Essentials for AI Engineer

Course Content

Python Essentials for AI Engineer

6 sections · 48 lessons

What is the difference between except and finally?


One LLM call, every path through trytry: take aslot, callthe modelexcept: only ona matching errorelse: onlyif nothingwas raisedfinally: recordlatency, free slotReleasing the slot outside finally froze the service after four timeouts.
finally is the one block every path reaches, so cleanup placed anywhere else is cleanup some path skips.

What you need to know

The full statement

Python
def load(path):    try:        f = open(path, encoding="utf-8")    except FileNotFoundError:        print("  except: missing file")        return None    else:        print("  else: opened fine")        with f:            return f.read()    finally:        print("  finally: always runs")open("present.txt", "w", encoding="utf-8").close()print("case 1"); load("present.txt")print("case 2"); load("absent.txt")# case 1#   else: opened fine#   finally: always runs# case 2#   except: missing file#   finally: always runs
What happened in tryexceptelsefinally
no exceptionskippedrunsruns
matching exceptionrunsskippedruns
non-matching exceptionskippedskippedruns, then the exception continues
return inside tryskippedskippedruns 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:

Python
import threading, timeslots = threading.Semaphore(4)latencies = []def timed_call(prompt, fail=False):    slots.acquire()    start = time.perf_counter()    try:        if fail:            raise TimeoutError("upstream timeout")        return f"ok: {prompt}"    finally:        latencies.append(round(time.perf_counter() - start, 3))   # failures are measured too        slots.release()                                          # the slot is always returnedprint(timed_call("a"))try:    timed_call("b", fail=True)except TimeoutError as e:    print("caught:", e)free = sum(slots.acquire(blocking=False) for _ in range(4))print(len(latencies), free)     # 2 4 -> both calls timed, all 4 slots free

An 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 finally run if try returns?" — Yes. It runs just before the function actually returns.
  • "What is the else clause for?" — Code that should run only when the try succeeded, kept outside the try so its own errors are not caught by the wrong handler.
  • "finally or with?" — Use with whenever the resource supports it (files, locks, many clients); write finally for cleanup that has no context manager.