Exceptions and Exit Codes
What You'll Learn
How Python's exception system works, how to catch and raise errors correctly, and how to communicate failure to the outside world via exit codes.
How Exceptions Work
When something goes wrong, Python raises an exception — an object that describes the error. If nothing catches it, the program crashes with a traceback:
Traceback (most recent call last):
File "script.py", line 5, in <module>
result = 10 / 0
ZeroDivisionError: division by zero
You can catch exceptions with try/except to handle them gracefully:
try:
result = 10 / 0
except ZeroDivisionError:
print("Cannot divide by zero")
result = 0
print(f"Result: {result}")
try / except / else / finally
import json
def load_config(path: str) -> dict:
try:
with open(path, encoding="utf-8") as f:
return json.load(f)
except FileNotFoundError:
# File doesn't exist
print(f"Config file not found: {path}")
return {}
except json.JSONDecodeError as e:
# File exists but has bad JSON
print(f"Invalid JSON in {path}: {e}")
return {}
except OSError as e:
# Permissions error, disk error, etc.
print(f"Could not read {path}: {e}")
return {}
else:
# Only runs if NO exception occurred
print("Config loaded successfully")
finally:
# ALWAYS runs — use for cleanup
print("Done loading config")
Catching Multiple Exceptions
# Catch multiple types in one except block
try:
value = int(user_input)
except (ValueError, TypeError) as e:
print(f"Invalid input: {e}")
# Catch a broad base class (use carefully)
try:
risky_operation()
except Exception as e:
print(f"Unexpected error: {type(e).__name__}: {e}")
raise # re-raise after logging — don't swallow unknown errors
Exception Hierarchy
Python's built-in exceptions form a hierarchy. Catching a parent catches all children:
BaseException
└── Exception
├── ValueError ← wrong value (int("abc"))
├── TypeError ← wrong type ("a" + 1)
├── KeyError ← missing dict key
├── IndexError ← list index out of range
├── AttributeError ← object has no attribute
├── NameError ← variable not defined
├── OSError ← file/network errors
│ ├── FileNotFoundError
│ ├── PermissionError
│ └── IsADirectoryError
├── RuntimeError ← general runtime failure
├── StopIteration ← iterator exhausted
└── ArithmeticError
└── ZeroDivisionError
Catch the most specific exception type you can:
# ❌ Too broad — hides bugs
except Exception:
pass
# ✅ Specific — handles exactly what you expect
except FileNotFoundError:
...
except json.JSONDecodeError:
...
Raising Exceptions
Use raise to signal that something is wrong:
def divide(a: float, b: float) -> float:
if b == 0:
raise ValueError(f"Divisor cannot be zero (got a={a}, b={b})")
return a / b
def process_age(age: int) -> str:
if not isinstance(age, int):
raise TypeError(f"age must be int, got {type(age).__name__}")
if age < 0 or age > 150:
raise ValueError(f"age must be 0–150, got {age}")
return "adult" if age >= 18 else "minor"
Re-raising after logging:
try:
result = risky_operation()
except Exception as e:
log.error("Operation failed: %s", e)
raise # propagate the original exception
Context Managers for Safe Cleanup
Use with statements to guarantee cleanup even when exceptions occur:
# ✅ File automatically closed even if an exception occurs
with open("data.txt", encoding="utf-8") as f:
data = f.read()
# ✅ Multiple context managers
with open("input.txt") as fin, open("output.txt", "w") as fout:
fout.write(fin.read().upper())
Exit Codes
Exit codes communicate success or failure to the shell, CI/CD, and orchestration systems:
- 0 = success
- 1 = generic failure
- 2 = usage error (wrong arguments)
import sys
def main() -> int:
try:
result = do_work()
print(f"Done: {result}")
return 0 # success
except FileNotFoundError as e:
print(f"Error: {e}", file=sys.stderr)
return 1 # failure
except KeyboardInterrupt:
print("\nInterrupted", file=sys.stderr)
return 130 # conventional for Ctrl+C
if __name__ == "__main__":
sys.exit(main())
Check in the shell:
python3 script.py
echo "Exit code: $?" # 0 for success, 1 for failure
In shell scripts:
python3 script.py || echo "Script failed!"
Writing Errors to stderr
Error messages belong on stderr, not stdout:
import sys
# ✅ Errors → stderr
print("Error: file not found", file=sys.stderr)
# Output → stdout
print("Result: 42")
This lets users pipe stdout while still seeing errors:
python3 script.py | grep "processed" # errors still appear on screen
Common Mistakes
| Mistake | Consequence | Fix |
|---|---|---|
except Exception: pass | Silently hides all errors | At minimum, log the error |
Catching BaseException | Catches KeyboardInterrupt too | Use Exception instead |
| Not re-raising unknown errors | Hides bugs | Add raise after logging |
| Printing to stdout for errors | Mixes output and errors | Use file=sys.stderr |
| No exit code | CI/CD can't detect failure | Return 0/1 from main() |
Quick Reference
# try/except
try:
...
except SpecificError as e:
handle(e)
except (TypeA, TypeB) as e:
handle(e)
else:
# no exception occurred
...
finally:
# always runs
cleanup()
# Raise
raise ValueError("message")
raise TypeError(f"expected int, got {type(x).__name__}")
# Re-raise
except Exception as e:
log.error("%s", e)
raise
# Exit codes
import sys
sys.exit(0) # success
sys.exit(1) # failure
sys.exit(main())
# Errors to stderr
print("Error: ...", file=sys.stderr)