Python programs fail at runtime when operations like file access, parsing input, or network calls raise exceptions. Handling these with try
/except
keeps your app responsive, preserves useful context, and guarantees cleanup.
Before you start — core rules
- Catch the most specific exception types you expect (for example,
ValueError
,FileNotFoundError
), not a blanket catch-all. - Use
else
for the success path andfinally
for code that must run no matter what. - Prefer context managers (
with
) for files and other resources to simplify cleanup. - Let unexpected bugs propagate; log them and re-raise instead of silently swallowing them.
- Reserve catch-all handlers (
except Exception
orexcept BaseException
) for top-level boundaries where you can fail fast or report clearly.
Method 1 — Catch specific exceptions (recommended)
Step 1: Identify one risky operation to protect.
raw = input("Select a fruit number (0-2): ")
Step 2: Wrap the operation in try
/except
and handle explicit exception types.
try:
selection = ["apple", "pear", "banana"][int(raw)]
print("You chose:", selection)
except ValueError:
print("Not a number.")
except IndexError:
print("Choice out of range.")
Step 3: Group related exceptions when the recovery is identical.
try:
selection = ["apple", "pear", "banana"][int(raw)]
except (ValueError, IndexError) as e:
print("Invalid selection:", type(e).__name__)
Common specific exceptions include OSError
family (files), ValueError
and TypeError
(data), ZeroDivisionError
(math), and KeyError
/IndexError
(collections).
Method 2 — Use else and finally to separate success and cleanup
Step 1: Add else
for code that should run only if nothing failed.
try:
f = open("config.json", "r", encoding="utf-8")
except FileNotFoundError:
print("No config file found.")
else:
data = f.read()
print("Loaded", len(data), "bytes.")
f.close()
Step 2: Add finally
for cleanup that must happen whether the try succeeded or not.
f = None
try:
f = open("config.json", "r", encoding="utf-8")
data = f.read()
except OSError as err:
print("OS error:", err)
finally:
if f:
f.close()
Method 3 — Guard your entry point to fail fast and log
Step 1: Move your workflow into a main()
function.
def main():
# core workflow
# ...
return 0
Step 2: Wrap the call in a top-level try
/except
to log unexpected errors and stop the program.
import logging, sys
logging.basicConfig(level=logging.INFO)
if __name__ == "__main__":
try:
code = main()
except Exception:
logging.exception("Unhandled error")
sys.exit(1)
sys.exit(code)
Step 3: Return a non‑zero exit code on failure to signal automation tools that the run failed.
# in main(), return 1 or raise an exception when a critical step fails
Method 4 — Manage resources with with
(preferred over manual finally)
Step 1: Replace manual open/close with a context manager to enforce timely release.
from pathlib import Path
try:
with Path("data.txt").open("r", encoding="utf-8") as f:
print(f.readline().strip())
except FileNotFoundError:
print("Create data.txt first.")
Step 2: Keep only the risky operations inside the protected block to avoid catching unrelated errors.
try:
with open("image.png", "rb") as fh:
blob = fh.read()
except OSError as e:
print("File access failed:", e)
# decode/process blob in a separate block
Method 5 — Raise, re-raise, and chain exceptions for clear error paths
Step 1: Raise a specific exception when a precondition fails.
def parse_age(s: str) -> int:
if not s.isdigit():
raise ValueError("age must contain only digits")
age = int(s)
if age < 0:
raise ValueError("age cannot be negative")
return age
Step 2: Re-raise unexpected errors after logging so callers can also handle them.
import logging
try:
do_risky_thing()
except OSError as err:
logging.error("OS error: %s", err)
raise
Step 3: Chain exceptions with raise ... from e
to preserve the root cause while adding context.
from pathlib import Path
import json
def load_json(path: str):
try:
text = Path(path).read_text(encoding="utf-8")
except OSError as e:
raise RuntimeError(f"Failed to read {path}") from e
return json.loads(text)
Method 6 — Catch-all handling at boundaries (use sparingly)
Step 1: Use except Exception as e
to capture any non–system-exiting error, log details, and decide whether to stop or retry.
import traceback
try:
risky()
except Exception as e:
print(f"Unexpected {type(e).__name__}: {e}")
traceback.print_exc() # full stack trace
# decide: raise, return error, or abort
Step 2: Only catch BaseException
if you intentionally intercept KeyboardInterrupt
or SystemExit
, and then re-raise immediately.
try:
service_loop()
except BaseException as e:
print(f"Caught {type(e).__name__}; shutting down cleanly.")
raise # never swallow interrupts or SystemExit
Method 7 — Handle multiple failures with ExceptionGroup and except* (Python 3.11+)
Step 1: Raise an ExceptionGroup
when aggregating errors from parallel or batched work.
def run_tests(tests):
errors = []
for t in tests:
try:
t.run()
except Exception as e:
e.add_note(f"Test {t.name} failed")
errors.append(e)
if errors:
raise ExceptionGroup("Batch failures", errors)
Step 2: Use except*
to handle only matching exception types within the group.
try:
run_tests(tests)
except* (ValueError, TypeError):
print("Some data errors occurred.")
except* OSError:
print("Some OS errors occurred.")
Quick troubleshooting patterns
- Input parsing: wrap
int()
orfloat()
withexcept ValueError
and reprompt the user. - File I/O: catch
FileNotFoundError
,PermissionError
, or genericOSError
, and switch to a fallback path. - Networking/API calls: catch library-specific timeouts/connection errors and implement a bounded retry with backoff.
- Always log the exception type and message; for full traces, use
logging.exception()
ortraceback.print_exc()
.
A few focused patterns like specific except
clauses, else
/finally
, and top-level guards will make your Python programs more resilient without hiding real bugs.
Member discussion