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 and finally 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 or except BaseException) for top-level boundaries where you can fail fast or report clearly.

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() or float() with except ValueError and reprompt the user.
  • File I/O: catch FileNotFoundError, PermissionError, or generic OSError, 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() or traceback.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.