Handle multiple exceptions¶
The question. A block of code can fail in several ways — a missing file, malformed JSON, a missing field in the parsed data — and each failure needs its own response. You want to write that without either a huge except Exception catch-all or a deeply-nested pile of try/except blocks.
The answer: one try block with one except clause per failure type, ordered specific-to-general. Python evaluates them top-to-bottom and executes the first one that matches.
# One try, several excepts — each failure mode gets its own response.
# Specific types first; a broad fallback last if you need a safety net.
import json
def parse_json_field(raw: str, field: str) -> str | None:
'''Parse a JSON string, extract the named field, or return None on any failure.'''
try:
data = json.loads(raw)
return str(data[field])
except json.JSONDecodeError as exc:
# Malformed input — surface the position, it's the most useful debugging clue.
print(f'Invalid JSON at position {exc.pos}: {exc.msg}')
except KeyError:
# Parsed fine, but field isn't there.
print(f'Field {field!r} not found in parsed data')
except TypeError:
# Parsed value wasn't a dict — can't index it.
print('Parsed data is not a dict; cannot look up fields')
return None
print(parse_json_field('{"name": "Alice"}', 'name')) # → 'Alice'
print(parse_json_field('not json', 'name')) # → JSONDecodeError
print(parse_json_field('{"name": "Alice"}', 'age')) # → KeyError
print(parse_json_field('[1, 2, 3]', 'name')) # → TypeError
Variant: group exception types that share handling¶
When two failures deserve identical handling — say, a missing file and a permissions problem both become 'can't read config' — group them in a tuple. The tuple form keeps the code short; the as exc bind still gives you the actual raised type if you want to inspect it.
def safe_read(path):
try:
with open(path, encoding='utf-8') as f:
return f.read()
except (FileNotFoundError, PermissionError) as exc:
# Both are 'we can't read this file' — identical response.
print(f'cannot read {path}: {type(exc).__name__}: {exc}')
return None
print(safe_read('/definitely/not/a/file'))
Variant: bare raise to log and propagate¶
When you want to record that an exception happened but still let the caller handle it, bare raise (no arguments) re-raises the current exception with its original traceback intact. Do not write raise exc — that resets the traceback and loses the original call chain.
import logging
logger = logging.getLogger('demo')
def process_age(raw):
try:
age = int(raw)
except ValueError:
logger.warning('failed to parse age from %r', raw)
raise # same exception, original traceback
if age < 0:
raise ValueError(f'age must not be negative, got {age}')
return age
try:
process_age('abc')
except ValueError as exc:
print(f'caller received: {exc}')
Variant: chain exceptions with raise ... from ...¶
When you replace a low-level exception with a domain-specific one, use from exc so the original cause is preserved on the new exception's __cause__. Tracebacks show both, connected by 'The above exception was the direct cause of the following exception' — which is usually what you want.
Use from None to suppress the original when it's an implementation detail the caller shouldn't care about.
class ConfigurationError(Exception):
'''Raised when configuration is missing or invalid.'''
def load_port(config):
try:
raw = config['port']
except KeyError as exc:
raise ConfigurationError("'port' is missing from config") from exc
try:
return int(raw)
except ValueError as exc:
raise ConfigurationError(f"'port' must be an integer, got {raw!r}") from exc
try:
load_port({})
except ConfigurationError as exc:
print(f'ConfigurationError: {exc}')
print(f'caused by: {exc.__cause__!r}')
Why this works¶
Python checks except clauses in the order they're written and takes the first whose type matches the raised exception (via isinstance). That's why order matters: a general except Exception before a specific except ValueError would capture the ValueError first and the specific clause would never run.
Each branch does the single most useful thing for that failure — surface the JSON position, or name the missing field, or explain the shape mismatch. That's the shape of 'helpful error handling': the diagnostic message names the proximate cause and points the caller at the fix.
If two or more failure types deserve identical handling, group them in a tuple: except (KeyError, TypeError) as exc:. That's the grouping version — see the extra cells.
Trade-offs¶
Keep try blocks small. Each line inside a try is a potential source of exceptions; a five-line try around 'do everything' makes it hard to tell which call raised. Put only the line(s) that might raise the exception you're catching inside the try — the common-mistakes recipe has the 'try block too big' anti-pattern in detail.
When you need to wrap a low-level exception in a more meaningful one (KeyError → ConfigurationError), use raise NewError(...) from exc — that preserves the original as __cause__. See the extra cells for the chaining pattern.
A final except Exception clause is a safety net for the truly unexpected. Use it sparingly — it can mask bugs, and it should always log what it caught so you can investigate.
Related reading¶
- Create custom exceptions — when to raise your own types from an
exceptblock. - Avoid common error handling mistakes — over-broad catches, silent swallow, the big-try anti-pattern.
- try/except syntax reference — the full grammar and the
else/finallyclauses.