Web Analytics

Try/Except

Intermediate ~30 min read

The try/except block is Python's primary mechanism for handling exceptions. Instead of crashing when an error occurs, you can catch the exception, handle it gracefully, and keep your program running. This is essential for building robust applications that handle unexpected situations - from invalid user input to network failures!

Basic Try/Except

The basic structure is simple: code that might fail goes in the try block, and the error handling code goes in the except block. When an exception occurs in the try block, Python immediately jumps to the except block. If no exception occurs, the except block is skipped entirely.

Output
Click Run to execute your code
How Try/Except Works:
1. Python executes code in the try block
2. If an exception occurs, execution jumps to the matching except block
3. If no exception occurs, the except block is skipped
4. Either way, code after try/except continues normally

Key insight: Only lines after the exception are skipped in try block - lines before it still run!

Multiple Except Clauses

Different errors require different handling. You can have multiple except clauses, each catching a specific exception type. Python checks them in order and runs the first matching handler. You can also catch multiple exception types in a single clause using a tuple.

Output
Click Run to execute your code
Order Matters! Put specific exceptions before general ones. If you catch Exception first, more specific handlers will never run because Exception catches everything. Python uses the first matching handler, not the most specific one.

Accessing Exception Details

Use as to capture the exception object and access its details. Different exception types have different attributes - FileNotFoundError has filename, KeyError has args[0] for the missing key. You can also re-raise exceptions after logging them.

Output
Click Run to execute your code
Useful Exception Attributes:
str(e) - Human-readable error message
e.args - Tuple of arguments passed to exception
type(e).__name__ - Exception class name as string
repr(e) - Debug representation with class name
e.__context__ - Original exception if this was raised during handling another

Common Error Handling Patterns

Exception handling isn't just about catching errors - it's about handling them intelligently. These patterns appear frequently in production code: safe file reading, type conversion with defaults, retry logic for flaky operations, and user-friendly error messages.

Output
Click Run to execute your code
Pattern Summary:
Safe File Reading - Return None or default on file errors
Safe Type Conversion - Convert with fallback: safe_int("x", 0)
Retry Pattern - Retry flaky operations N times before giving up
User-Friendly Messages - Convert technical errors to helpful messages
Cleanup Pattern - Use finally to ensure cleanup always happens

Common Mistakes

1. Using bare except

# Wrong - catches EVERYTHING including Ctrl+C!
try:
    risky_operation()
except:  # Bare except - NEVER do this!
    pass  # Silently swallows all errors

# Wrong - too broad, hides bugs
try:
    risky_operation()
except Exception:
    pass  # Still catches too much

# Correct - catch specific exceptions
try:
    risky_operation()
except ValueError:
    print("Invalid value")
except ConnectionError:
    print("Network failed")

2. Catching exceptions in wrong order

# Wrong - FileNotFoundError is never reached!
try:
    open("missing.txt")
except OSError:
    print("OS error")  # This catches FileNotFoundError too
except FileNotFoundError:
    print("File not found")  # NEVER RUNS - dead code!

# Correct - specific exceptions first
try:
    open("missing.txt")
except FileNotFoundError:
    print("File not found")  # Specific first
except OSError:
    print("Other OS error")  # General second

3. Swallowing exceptions silently

# Wrong - error disappears, debugging nightmare!
def get_user(user_id):
    try:
        return database.fetch(user_id)
    except:
        return None  # What went wrong? Nobody knows!

# Correct - at least log the error
import logging

def get_user(user_id):
    try:
        return database.fetch(user_id)
    except DatabaseError as e:
        logging.error(f"Failed to fetch user {user_id}: {e}")
        return None

4. Using exceptions for flow control

# Wrong - slow and unclear, exceptions aren't for flow control
def find_item(items, target):
    try:
        return items.index(target)
    except ValueError:
        return -1

# Also wrong for dictionaries
try:
    value = my_dict[key]
except KeyError:
    value = default

# Correct - use built-in methods
def find_item(items, target):
    return items.index(target) if target in items else -1

# Correct for dictionaries
value = my_dict.get(key, default)

5. Too much code in try block

# Wrong - too much in try, unclear what might fail
try:
    data = load_config()
    processed = process_data(data)
    result = calculate(processed)
    save_result(result)
    send_notification()
except Exception as e:
    print(f"Something failed: {e}")  # Which operation?

# Correct - minimal try blocks, clear error handling
try:
    data = load_config()
except FileNotFoundError:
    data = default_config()

try:
    processed = process_data(data)
    result = calculate(processed)
except ValueError as e:
    print(f"Invalid data: {e}")
    return

save_result(result)  # Let this fail if it fails

Exercise: Safe Calculator

Task: Create a calculator function that handles all possible errors gracefully.

Requirements:

  • Handle division by zero
  • Handle invalid number inputs
  • Handle unknown operations
  • Return a tuple of (success, result_or_error)
Output
Click Run to execute your code
Show Solution
def safe_calculate(a, b, operation):
    """
    Safely perform calculation with full error handling.
    Returns (True, result) on success, (False, error_msg) on failure.
    """
    # Convert inputs to numbers
    try:
        num_a = float(a)
        num_b = float(b)
    except (ValueError, TypeError) as e:
        return (False, f"Invalid number: {e}")

    # Perform operation
    operations = {
        '+': lambda x, y: x + y,
        '-': lambda x, y: x - y,
        '*': lambda x, y: x * y,
        '/': lambda x, y: x / y,
        '**': lambda x, y: x ** y,
    }

    if operation not in operations:
        return (False, f"Unknown operation: {operation}")

    try:
        result = operations[operation](num_a, num_b)
        return (True, result)
    except ZeroDivisionError:
        return (False, "Cannot divide by zero")
    except OverflowError:
        return (False, "Result too large")


# Test the calculator
test_cases = [
    (10, 5, '+'),      # Normal addition
    (10, 0, '/'),      # Division by zero
    ('abc', 5, '+'),   # Invalid input
    (10, 5, '%'),      # Unknown operation
    (2, 1000, '**'),   # Large exponent (might overflow)
    ('10', '3', '-'),  # String numbers (should work)
]

print("=== Safe Calculator Tests ===\n")
for a, b, op in test_cases:
    success, result = safe_calculate(a, b, op)
    status = "OK" if success else "ERR"
    print(f"{a} {op} {b} => [{status}] {result}")

Summary

  • try/except: Code in try runs; if it fails, except handles the error
  • Specific exceptions: Always catch specific types, not bare except
  • Multiple except: Handle different errors differently, specific first
  • as keyword: Capture exception: except ValueError as e
  • Exception attributes: str(e), e.args, type(e).__name__
  • Tuple syntax: except (TypeError, ValueError) for multiple types
  • Re-raise: Use bare raise to re-raise after logging
  • Keep try blocks small: Only wrap code that might fail
  • Don't swallow errors: At minimum, log what went wrong

What's Next?

Now you know how to catch exceptions, but what about cleanup code that must always run? And what if you want to do something only when no exception occurs? Next, we'll learn about finally for guaranteed cleanup and else for success-only code - completing your exception handling toolkit!