Can you write this from memory?
Write a try/except skeleton that catches Exception. Use pass in both blocks.
Everyone writes Python's try/except. Not everyone writes it well.
Catching too broadly hides bugs. Forgetting else bloats your try block. Swallowing errors without logging makes debugging a nightmare. And most tutorials skip exception chaining (raise ... from ...)—the pattern that actually preserves context when translating errors.
These exercises cover specific exception types, else clauses, cleanup with finally and context managers, exception chaining, and custom domain exceptions.
When an exception is raised, Python walks up the call stack looking for a matching except clause. If none is found, the program crashes with a traceback.
Your job is to catch exceptions at the right level — the Facade design pattern often serves as that natural boundary, catching low-level errors and exposing a clean, simplified interface to callers:
- Too low: You handle errors before you have enough context to respond properly
- Too high: Errors propagate far from their source, making debugging harder
- Just right: Catch where you can take meaningful action (retry, log, convert, recover)
Python idiomatically prefers EAFP ("Easier to Ask Forgiveness than Permission") over LBYL ("Look Before You Leap").
# LBYL: check first, then act
if key in my_dict:
value = my_dict[key]
else:
value = default
# EAFP: try it, handle failure
try:
value = my_dict[key]
except KeyError:
value = default
Why prefer EAFP?
-
Avoids race conditions: The file might exist when you check, but be deleted before you open it.
try/excepthandles this atomically. -
Faster when errors are rare: In Python 3.11+, entering a
tryblock costs essentially nothing. You only pay when an exception is raised. -
Cleaner code: One path for the common case, explicit handling for the exceptional case.
# LBYL has a race condition:
if os.path.exists(filename):
# File could be deleted here by another process!
with open(filename) as f:
data = f.read()
# EAFP is atomic:
try:
with open(filename) as f:
data = f.read()
except FileNotFoundError:
data = ""
When LBYL is better: When the check is cheap and failure is common (e.g., checking if items: before iterating), or when the "asking forgiveness" has side effects you want to avoid.
Most tutorials show try/except. Here's the complete structure:
try:
result = risky_operation() # Only code that might raise
except SpecificError as e:
handle_error(e) # Recovery or re-raise
else:
use_result(result) # Only runs if no exception
finally:
cleanup() # Always runs
Why else matters: It keeps your try block minimal. Code in else only runs on success, and exceptions there aren't caught by the preceding except. For a printable reference of common patterns, see the Python error handling cheat sheet.
Why finally matters: It runs whether you succeed, fail, or even if you return from inside the try block. Use it for cleanup that must happen.
| Situation | Catch | Why |
|---|---|---|
| User input / parsing | ValueError, TypeError, json.JSONDecodeError | Invalid data is expected |
| File operations | FileNotFoundError, PermissionError, IsADirectoryError | I/O failures are recoverable |
| Network / DB | ConnectionError, TimeoutError, library-specific | Transient failures happen |
| "Should never happen" | Don't catch—fix the bug | Catching hides logic errors |
| Top-level boundary | except Exception + log + convert | Last line of defense |
Rule of thumb: Catch the most specific exception that makes sense. You can always catch multiple:
except (FileNotFoundError, PermissionError) as e:
...
When you translate a low-level exception into a domain exception, chain them explicitly:
try:
data = json.loads(raw)
except json.JSONDecodeError as e:
raise ConfigError("Invalid config format") from e
The traceback shows both exceptions with "The above exception was the direct cause of the following exception." This makes the causal relationship clear.
Without from, Python still preserves the original via implicit chaining (__context__), but the traceback says "During handling of the above exception, another exception occurred"—which is less clear about intent:
raise ConfigError("Invalid config format") # __context__ keeps original, but relationship is ambiguous
Using raise ... from e signals that the translation is deliberate.
Suppress chaining explicitly (rare):
raise ConfigError("...") from None # Hides the original intentionally
Both ensure cleanup happens. Prefer context managers when available:
# Context manager (preferred)
with open("file.txt") as f:
data = f.read()
# f.close() called automatically
# Equivalent finally block
f = open("file.txt")
try:
data = f.read()
finally:
f.close()
Context managers:
- Combine setup and cleanup in one statement
- Work with
async withfor async code - Are harder to get wrong (no forgetting the finally)
Use finally for custom cleanup that doesn't fit the context manager pattern.
Create custom exceptions when you need to:
- Distinguish your errors from built-in ones
- Add domain-specific information
- Create a hierarchy for different error types
class AppError(Exception):
"""Base class for application errors."""
pass
class ConfigError(AppError):
"""Configuration is invalid or missing."""
pass
class ValidationError(AppError):
"""User input failed validation."""
def __init__(self, field: str, message: str):
self.field = field
super().__init__(f"{field}: {message}")
Now callers can catch AppError for all your errors, or specific subclasses for targeted handling.
Never silently swallow exceptions. At minimum, log them:
import logging
try:
process(data)
except Exception:
logging.exception("Processing failed") # Includes full traceback
raise # Re-raise to let caller handle
# Or if you're recovering:
except ValueError as e:
logging.warning(f"Invalid data, using default: {e}")
data = default_value
logging.exception() automatically includes the traceback. Use logging.error(..., exc_info=True) if you need more control.
For transient failures (network timeouts, rate limits), retry with exponential backoff:
import time
def fetch_with_retry(url, max_attempts=3):
for attempt in range(max_attempts):
try:
return fetch(url)
except (ConnectionError, TimeoutError) as e:
if attempt == max_attempts - 1:
raise # Final attempt failed, propagate
wait = 2 ** attempt # 1s, 2s, 4s...
logging.warning(f"Attempt {attempt + 1} failed: {e}. Retrying in {wait}s")
time.sleep(wait)
Key points:
- Only retry on transient errors (network, rate limits)—not on
ValueErroror logic errors - Use exponential backoff to avoid hammering a failing service
- Set a max attempts limit to avoid infinite loops
- Re-raise on final failure so callers know something went wrong
For production code, consider the tenacity library which handles this pattern elegantly. JavaScript handles async errors differently from Python's synchronous try/except — see Python vs JavaScript error handling for the key differences.
For concurrent code that can have multiple failures:
async def fetch_all(urls):
results, errors = [], []
async with asyncio.TaskGroup() as tg:
for url in urls:
tg.create_task(fetch(url))
# If any tasks failed, raises ExceptionGroup
try:
await fetch_all(urls)
except* ConnectionError as eg:
# eg.exceptions is a tuple of all ConnectionErrors
for e in eg.exceptions:
log.warning(f"Connection failed: {e}")
except* ValueError as eg:
for e in eg.exceptions:
log.error(f"Invalid response: {e}")
The except* syntax lets you handle different exception types from the group separately. Rust takes a fundamentally different approach with Result types instead of exceptions — see Python vs Rust error handling for the comparison.
1. Catching too broad, too early
# Bad: hides all bugs
try:
result = complex_operation()
except Exception:
result = None # Masks logic errors!
# Better: catch what you expect
try:
result = complex_operation()
except (ValueError, KeyError) as e:
logging.warning(f"Expected error: {e}")
result = None
2. Empty except block
# Bad: silently swallows everything
try:
send_email()
except:
pass
# Better: at least log
try:
send_email()
except Exception as e:
logging.warning(f"Email failed: {e}")
3. Losing exception context
# Bad: original error lost
except DatabaseError:
raise AppError("Database failed")
# Good: chain the exception
except DatabaseError as e:
raise AppError("Database failed") from e
4. Too much in the try block
# Bad: catching errors from process() too
try:
data = load_file()
result = process(data)
save(result)
except FileNotFoundError:
...
# Good: only wrap what can raise that error
try:
data = load_file()
except FileNotFoundError:
data = {}
result = process(data) # Errors here propagate normally
save(result)
When to Use Python Exception Handling: try/except/else/finally, raise from, custom exceptions
- Wrap I/O, parsing, or user input that can fail.
- Convert low-level exceptions into meaningful domain errors (with `raise ... from ...`).
- Ensure cleanup with `finally` or context managers (`with` statement).
- Keep try blocks minimal—use `else` for success-path code.
Check Your Understanding: Python Exception Handling: try/except/else/finally, raise from, custom exceptions
Read a JSON config file, handle missing file and malformed JSON, and return a default config.
Use try/except with specific exceptions (FileNotFoundError, json.JSONDecodeError), use `else` for the success path, and consider `raise ConfigError(...) from e` if you need to translate to a domain exception while preserving context.
What You'll Practice: Python Exception Handling: try/except/else/finally, raise from, custom exceptions
Common Python Exception Handling: try/except/else/finally, raise from, custom exceptions Pitfalls
- Bare `except:` catches system-exiting exceptions—use `except Exception:` at minimum
- Catching too broadly hides bugs—catch specific exceptions when possible
- `except Exception` at every level masks bugs—reserve it for top-level boundaries
- Too much code in try block—use `else` for success-path code
- Silencing errors without logging—at least log before swallowing
- Forgetting `raise` in except block—you handle it or re-raise it, not ignore it
- Losing context when translating exceptions—use `raise NewError() from original`
Python Exception Handling: try/except/else/finally, raise from, custom exceptions FAQ
What's the difference between except Exception and except BaseException?
Exception is the base class for most errors your code should handle. BaseException also includes system-exiting exceptions like KeyboardInterrupt and SystemExit—catching it broadly can prevent clean exits and break Ctrl+C.
When do I use else with try/except?
Use `else` for code that should only run if no exception occurred. It keeps the try block minimal (only the risky code) and makes it clear what depends on success.
When should I use raise ... from ...?
Use it when translating a low-level error into a domain-specific one while preserving the original context. The chained exception shows up in tracebacks as 'The above exception was the direct cause of the following exception.'
Should I use finally or a context manager (with)?
Prefer context managers when available—they handle setup and cleanup together and are harder to get wrong. Use `finally` for custom cleanup logic that doesn't fit the context manager pattern.
Why is bare except: bad?
Bare `except:` catches everything including KeyboardInterrupt and SystemExit, which usually indicates bugs or prevents clean shutdown. At minimum, use `except Exception:` to catch normal errors only.
How do I print the full stack trace of an exception?
Use `logging.exception("message")` to log with traceback, or `traceback.print_exc()` to print to stderr. For a string, use `traceback.format_exc()`. Inside an except block, `logging.exception()` is usually the cleanest option.
Is try/except slow? Should I avoid it for performance?
No—entering a try block is essentially free in Python 3.11+. The cost only comes when an exception is actually raised. Use try/except freely for expected error cases; don't add if-checks just to avoid exceptions.
What is EAFP vs LBYL in Python?
EAFP ("Easier to Ask Forgiveness than Permission") means using try/except instead of checking conditions first. LBYL ("Look Before You Leap") means checking before acting. Python idiomatically prefers EAFP—it's faster when errors are rare and avoids race conditions.
What is the difference between try/except and try/catch in Python?
Python uses `try/except`, not `try/catch`. The `except` keyword works like `catch` in JavaScript or Java. Python also adds `else` (runs if no exception) and `finally` (always runs), giving it a four-part structure: try/except/else/finally.
When should you use else in try/except?
Use `else` for code that should only run when the try block succeeds without raising an exception. It keeps the try block minimal—only the risky code goes inside try, and success-path code goes in else. Exceptions raised in else are not caught by the preceding except.
Python Exception Handling: try/except/else/finally, raise from, custom exceptions Syntax Quick Reference
try:
data = parse(user_input) # Only risky code
except ValueError as e:
log.warning(f"Bad input: {e}")
data = default
else:
save(data) # Only runs if no exception
finally:
cleanup() # Always runstry:
with open("config.json") as f:
config = json.load(f)
except FileNotFoundError:
config = {} # Missing file is OK
except json.JSONDecodeError as e:
raise ConfigError(f"Malformed config: {e.msg}") from etry:
user = db.get_user(user_id)
except DatabaseError as e:
raise UserNotFoundError(f"Could not load user {user_id}") from e
# Traceback shows both exceptionswith open("data.txt") as f:
data = f.read()
# File closed automatically, even on exceptionclass ConfigError(Exception):
"""Raised when configuration is invalid."""
pass
class ValidationError(ConfigError):
"""Raised when a config value fails validation."""
passimport logging
try:
process(data)
except Exception:
logging.exception("Processing failed") # Includes traceback
raise # Re-raise after loggingimport time
for attempt in range(3):
try:
result = fetch_data(url)
break # Success, exit loop
except ConnectionError:
if attempt == 2:
raise # Final attempt, give up
time.sleep(2 ** attempt) # Exponential backofftry:
results = process_batch(items) # May raise ExceptionGroup
except* ValueError as eg:
print(f"Value errors: {eg.exceptions}")
except* TypeError as eg:
print(f"Type errors: {eg.exceptions}")Python Exception Handling: try/except/else/finally, raise from, custom exceptions Sample Exercises
Write the finally block that closes the file object f
finally:
f.close()Write a try/except/finally skeleton. Use pass in each block and catch Exception.
try:
pass
except Exception:
pass
finally:
passWhat does this code print? List each line of output.
cleanup
active+ 28 more exercises
Copy-ready syntax examples for quick lookup