Syntax Cache
BlogMethodFeaturesHow It WorksBuild a Game
  1. Home
  2. Python
  3. Python Exception Handling Practice: try/except/else/finally, raise from, custom exceptions
Python31 exercises

Python Exception Handling Practice: try/except/else/finally, raise from, custom exceptions

Practice Python exception handling: try/except/else/finally, catching specific exceptions, exception chaining (raise ... from ...), custom exceptions, and cleanup with context managers.

Common ErrorsQuick ReferencePractice
Warm-up1 / 2

Can you write this from memory?

Write a try/except skeleton that catches Exception. Use pass in both blocks.

On this page
  1. 1Exceptions flow up until caught
  2. 2EAFP vs LBYL: the Python way
  3. 3try/except/else/finally: the full picture
  4. 4Which exception should I catch?
  5. 5Exception chaining: explicit vs implicit
  6. 6Context managers vs finally
  7. 7Custom exceptions: when and how
  8. 8The logging pattern
  9. 9Retry pattern
  10. 10ExceptionGroup (Python 3.11+)
  11. 11Common anti-patterns
  12. 1. Catching too broad, too early
  13. 2. Empty except block
  14. 3. Losing exception context
  15. 4. Too much in the try block
  16. 12References
Exceptions flow up until caughtEAFP vs LBYL: the Python waytry/except/else/finally: the full pictureWhich exception should I catch?Exception chaining: explicit vs implicitContext managers vs finallyCustom exceptions: when and howThe logging patternRetry patternExceptionGroup (Python 3.11+)Common anti-patternsReferences

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.

Related Python Topics
Python Function Arguments: defaults, *args, keyword-only, **kwargsPython Classes: self, super(), __repr__ vs __str__, inheritancePython Decorators: wraps, factories, stacking order & lru_cache

Catch exceptions where you can take meaningful action—not too low (insufficient context) and not too high (far from the source).

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)

Exception propagation through the call stack — ValueError bubbles up from parse_date() through validate_input() to process_order() where it is caught


Ready to practice?

Start practicing Python Exception Handling: try/except/else/finally, raise from, custom exceptions with spaced repetition

Python prefers try/except over checking conditions first. It's faster when errors are rare and avoids race conditions with file and network operations.

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?

  1. Avoids race conditions: The file might exist when you check, but be deleted before you open it. try/except handles this atomically.

  2. Faster when errors are rare: In Python 3.11+, entering a try block costs essentially nothing. You only pay when an exception is raised.

  3. 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.


Use else for success-only code (keeps the try block minimal). Use finally for cleanup that must happen regardless of success or failure.

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

try/except/else/finally control flow — try leads to except or else, both converge at finally

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.


SituationCatchWhy
User input / parsingValueError, TypeError, json.JSONDecodeErrorInvalid data is expected
File operationsFileNotFoundError, PermissionError, IsADirectoryErrorI/O failures are recoverable
Network / DBConnectionError, TimeoutError, library-specificTransient failures happen
"Should never happen"Don't catch—fix the bugCatching hides logic errors
Top-level boundaryexcept Exception + log + convertLast line of defense

Python exception hierarchy — BaseException branches into SystemExit, KeyboardInterrupt, GeneratorExit, and Exception. Exception branches into ValueError, TypeError, LookupError (KeyError), OSError (FileNotFoundError, ConnectionError), and RuntimeError

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.

Exception chaining — explicit (raise...from) sets cause, implicit chaining still preserves context but with a less clear traceback message

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 with for 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:

  1. Distinguish your errors from built-in ones
  2. Add domain-specific information
  3. 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 ValueError or 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)

  • Python Tutorial: Errors and Exceptions
  • Built-in Exceptions
  • PEP 3134 – Exception Chaining
  • PEP 654 – Exception Groups
  • logging.exception() documentation

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

Prompt

Read a JSON config file, handle missing file and malformed JSON, and return a default config.

What a strong answer looks like

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

try/except/else/finally blocks (keeping try blocks small)Catching specific exceptions (ValueError, FileNotFoundError, etc.)Exception hierarchy (BaseException vs Exception)Raising exceptions with raiseException chaining with raise ... from ...Custom exception classesContext managers for cleanup (with statement)Logging exceptions with traceback (print stack trace)Retry logic with exponential backoffEAFP vs LBYL patternsExceptionGroup and except* (Python 3.11+)

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/except/else/finally
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 runs
Catch specific exceptions
try:
    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 e
Exception chaining (raise from)
try:
    user = db.get_user(user_id)
except DatabaseError as e:
    raise UserNotFoundError(f"Could not load user {user_id}") from e
# Traceback shows both exceptions
Context manager cleanup
with open("data.txt") as f:
    data = f.read()
# File closed automatically, even on exception
Custom exception class
class ConfigError(Exception):
    """Raised when configuration is invalid."""
    pass

class ValidationError(ConfigError):
    """Raised when a config value fails validation."""
    pass
Logging with traceback
import logging

try:
    process(data)
except Exception:
    logging.exception("Processing failed")  # Includes traceback
    raise  # Re-raise after logging
Retry pattern
import 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 backoff
ExceptionGroup (Python 3.11+)
try:
    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

Example 1Difficulty: 2/5

Write the finally block that closes the file object f

finally:
    f.close()
Example 2Difficulty: 2/5

Write a try/except/finally skeleton. Use pass in each block and catch Exception.

try:
    pass
except Exception:
    pass
finally:
    pass
Example 3Difficulty: 3/5

What does this code print? List each line of output.

cleanup
active

+ 28 more exercises

Quick Reference
Python Exception Handling: try/except/else/finally, raise from, custom exceptions Cheat Sheet →

Copy-ready syntax examples for quick lookup

Related Design Patterns

Facade Pattern

Also in Other Languages

JavaScript Error Handling PracticeRust Error Handling Practice: Result, Option, ? Operator

Start practicing Python Exception Handling: try/except/else/finally, raise from, custom exceptions

Free daily exercises with spaced repetition. No credit card required.

← Back to Python Syntax Practice
Syntax Cache

Build syntax muscle memory with spaced repetition.

Product

  • Pricing
  • Our Method
  • Daily Practice
  • Design Patterns
  • Interview Prep

Resources

  • Blog
  • Compare
  • Cheat Sheets
  • Vibe Coding
  • Muscle Memory

Languages

  • Python
  • JavaScript
  • TypeScript
  • Rust
  • SQL
  • GDScript

Legal

  • Terms
  • Privacy
  • Contact

© 2026 Syntax Cache

Cancel anytime in 2 clicks. Keep access until the end of your billing period.

No refunds for partial billing periods.