← Home

079. Multiple Except Clauses

Handle different failure modes with different recovery strategies

079. Multiple Except Clauses

He wraps all process-record parsing in a bare except: to avoid crashes. Then a process starts eating 100% CPU and he can’t Ctrl-C out — except: swallowed the KeyboardInterrupt.

The fix: specific except clauses, one per failure mode.

def parse_process_record(record):
    try:
        name  = record["name"]          # KeyError if field missing
        pid   = int(record["pid"])      # ValueError if not a number
        rss   = float(record["rss_mb"]) # TypeError if record is not a dict
        return {"name": name, "pid": pid, "rss_mb": rss}
    except KeyError as e:
        return {"error": f"missing field: {e}"}
    except ValueError as e:
        return {"error": f"bad value: {e}"}
    except TypeError as e:
        return {"error": f"wrong type: {e}"}
    # KeyboardInterrupt is NOT caught — Ctrl-C still works

Now the logs show exactly which field failed and why — instead of a useless “something went wrong”.


💡 Fun fact: Python’s bare except: clause is so dangerous that the CPython interpreter itself uses it in only a tiny handful of places, and even then only in bootstrap code. The KeyboardInterrupt that except: swallows is generated by the OS sending SIGINT — so a hung program with a bare except: can become literally impossible to stop without kill -9.

⚠️ Watch out: Beginners often catch Exception thinking it is “specific enough.” But Exception still catches SystemExit, GeneratorExit, and memory-related exceptions — things you almost never want to swallow. Stick to the exact exception types you actually expect, like (KeyError, ValueError).

🤔 Think about it: Python tries except clauses top-to-bottom and runs the first match. If you put except Exception before except ValueError, will the ValueError clause ever run? What does this imply about the order you should arrange your except blocks?

Learning objectives

  • Write multiple except clauses for different error types
  • Catch multiple exceptions in a single except clause
  • Access the exception message with “as e”

Key concepts

  • multiple except
  • exception specificity
  • KeyError
  • ValueError
  • TypeError

Try it

Concept detail

Multiple except clauses handle different errors with different recovery logic.

try:
    ...
except KeyError:
    # missing dict key
except ValueError:
    # bad value (e.g., int("abc"))
except TypeError:
    # wrong type (e.g., None["key"])

Python tries each clause top-to-bottom; runs the FIRST match, then skips the rest.

Catch multiple exceptions in one clause: except (IndexError, TypeError): return None

Access the exception object: except ValueError as e: return {“error”: str(e)} # str(e) gives the error message

Order matters — put more specific exceptions before more general ones: except FileNotFoundError: # specific — check first … except OSError: # general — catches FileNotFoundError too …

WHY bare except: is almost always wrong: except: # catches KeyboardInterrupt, SystemExit, ALL errors except Exception: # catches most errors but not system exits (better) except (ValueError, KeyError): # catches exactly what you expect (best)

Catching too broadly hides bugs: try: result = complex_calculation() except Exception: result = None # maybe calculation has a typo — you’ll never know

Solution

def parse_record(data):
    try:
        name  = data["name"]
        age   = int(data["age"])
        score = float(data["score"])
        return {"name": name, "age": age, "score": score}
    except KeyError as e:
        return {"error": f"missing field: {e}"}
    except ValueError as e:
        return {"error": f"invalid value: {e}"}
    except TypeError as e:
        return {"error": f"wrong type: {e}"}

def safe_lookup(items, index):
    try:
        return items[index]
    except (IndexError, TypeError):
        return None

Tests

def test_parse_valid():
    result = parse_record({"name": "Alice", "age": "30", "score": "95.5"})
    assert result["name"] == "Alice"
    assert result["age"] == 30
    assert result["score"] == 95.5

def test_parse_missing_field():
    result = parse_record({"name": "Alice"})
    assert "error" in result
    assert "missing" in result["error"]

def test_parse_invalid_value():
    result = parse_record({"name": "Bob", "age": "not_a_number", "score": "88"})
    assert "error" in result
    assert "invalid" in result["error"]

def test_parse_wrong_type():
    # data is not a dict — causes TypeError on data["name"]
    result = parse_record(None)
    assert "error" in result
    assert "wrong type" in result["error"]

def test_safe_lookup_valid():
    assert safe_lookup([10, 20, 30], 1) == 20
    assert safe_lookup([10, 20, 30], 0) == 10

def test_safe_lookup_out_of_range():
    assert safe_lookup([10, 20, 30], 99) is None

def test_safe_lookup_none_list():
    assert safe_lookup(None, 0) is None

Resources