← Home

080. Finally Block

Guarantee cleanup code always runs, regardless of success or failure

080. Finally Block

He adds a threading lock so only one goroutine writes to the stats dict at a time. If the write raises an exception, the lock is never released — the next collection attempt hangs forever.

import threading
_lock = threading.Lock()

# Dangerous — lock held forever if an exception occurs
def update_stats(stats, new_data):
    _lock.acquire()
    stats.update(new_data)   # what if this raises?
    _lock.release()          # never reached on error!

# Safe — lock always released
def update_stats(stats, new_data):
    _lock.acquire()
    try:
        stats.update(new_data)
    finally:
        _lock.release()   # runs no matter what

finally is the guarantee: even if stats.update raises, even if the function returns early, the lock is released.

💡 Python’s with statement is syntactic sugar for try/finally with resource management. with open("file") as f: guarantees f.close() even on exception. Under the hood, it is exactly try/finally.

Learning objectives

  • Add finally blocks to guarantee cleanup code runs
  • [object Object]
  • Recognize when finally is appropriate

Key concepts

  • finally
  • cleanup
  • exception handling
  • try/except/finally

Try it

Concept detail

finally: always executes — no matter what happened in try/except.

try:
    risky()
    return result          # finally still runs before the return!
except SomeError:
    handle()
    return fallback        # finally still runs before this return too!
finally:
    cleanup()              # ALWAYS runs

Execution order:

  1. try block runs
  2. If exception → except block runs
  3. finally always runs (even after return, even for unhandled exceptions)
  4. Then the return value is delivered / exception propagates

WHY finally:

  • Lock released even on error
  • File closed even on error
  • Database connection returned to pool even on error
  • Metrics logged even on error

Common pattern — resource cleanup: conn = db.connect() try: conn.execute(query) except DBError as e: log_error(e) finally: conn.close() # always released

Python’s “with” statement is built on this exact pattern: with open(“file.txt”) as f: data = f.read() # f.close() is called automatically in a finally block under the hood

Solution

log = []
status = "idle"

def process_data(items):
    global status, log
    status = "processing"
    try:
        results = [int(x) * 2 for x in items]
        status = "done"
        return results
    except ValueError:
        status = "error"
        return []
    finally:
        log.append("cleanup")

Tests

def test_process_success():
    global log, status
    log = []
    result = process_data(["1", "2", "3"])
    assert result == [2, 4, 6]
    assert status == "done"

def test_cleanup_on_success():
    global log
    log = []
    process_data(["1", "2"])
    assert "cleanup" in log

def test_process_error():
    global log, status
    log = []
    result = process_data(["1", "bad", "3"])
    assert result == []
    assert status == "error"

def test_cleanup_on_error():
    global log
    log = []
    process_data(["bad"])
    # Without finally, cleanup is NOT appended on error path
    assert "cleanup" in log

def test_cleanup_always_once():
    global log
    log = []
    process_data(["1", "2"])
    process_data(["bad"])
    # Each call should add exactly one "cleanup" entry
    assert log.count("cleanup") == 2

Resources