Catch the error you can actually handle, and keep the try block narrow enough to know which operation failed. The patterns below cover specific catches, cleanup, raising, retries, and the broad catch that makes programs fail silently.
🎙️ Published & recorded: ·
Put only the operation expected to fail inside try. Catch ValueError when converting user text to a number. Bind the exception as e when its details help the user. Separate handlers when a timeout should retry but a lost connection should alert someone.
try:
age = int(user_input)
except ValueError:
print("that wasn't a number")
try:
data = json.loads(text)
except json.JSONDecodeError as e:
print(f"bad JSON at line {e.lineno}: {e.msg}")
try:
resp = requests.get(url, timeout=5)
except requests.Timeout:
retry()
except requests.ConnectionError:
alert_ops()
except (ValueError, KeyError) as e:
log(e)
A tuple of exception classes is right only when those failures receive identical handling. If the recovery differs, keep separate except clauses so the policy stays visible.
Else runs only when try succeeded. It keeps later processing outside the protected block, so a bug in process is not mistaken for an open-file failure. Finally runs whether the operation succeeds, fails, or returns early, which makes it suitable for releasing locks and other unconditional cleanup.
try:
f = open(path, encoding="utf-8")
except FileNotFoundError:
print("no such file")
else:
process(f.read())
f.close()
finally:
release_lock()
For files, a with statement is usually safer than manual close. Finally matters most when the resource has no context manager or when cleanup spans several operations.
Raise ValueError when a caller supplied a value your function cannot accept. A bare raise inside except sends the same exception upward with its original traceback intact. For retries, catch only transient failures, wait longer after each failure, and re-raise after the final attempt.
def set_age(age):
if age < 0:
raise ValueError(f"age can't be negative, got {age}")
try:
charge_card(order)
except PaymentError:
log_incident(order)
raise # same error and traceback
class QuotaExceeded(Exception):
pass
for attempt in range(3):
try:
result = flaky_api_call()
break
except requests.Timeout:
if attempt == 2:
raise
time.sleep(2 ** attempt) # 1 second, then 2
Do not retry validation errors, permission failures, or a card decline. Repeating a permanent failure adds delay and can duplicate side effects. Retry policy belongs to the operation that knows whether another attempt is safe.
The classic symptom is, my code runs but does nothing. Search for except colon pass or except Exception colon pass. Those lines discard the real traceback, including messages such as NameError: name user underscore id is not defined. Remove the catch while debugging, or log the traceback with logger dot exception.
NameError: name 'user_id' is not defined
try:
save_user(user_id)
except Exception:
pass # hides the NameError
# If a top-level worker truly must stay alive:
try:
run_one_job()
except Exception:
logger.exception("job failed") # records full traceback
logger.exception(...); unlike logger.error(e), it includes the traceback.A bare except is broader still: it also catches signals such as KeyboardInterrupt. Do not wrap a whole function to be safe. Let an error rise until some layer can make a real decision: retry, report it to the user, or stop.