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 whatfinally is the guarantee: even if stats.update raises, even if the function returns early, the lock is released.
💡 Python’s
withstatement is syntactic sugar for try/finally with resource management.with open("file") as f:guaranteesf.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 runsExecution order:
- try block runs
- If exception → except block runs
- finally always runs (even after return, even for unhandled exceptions)
- 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