Python & AI Tutorials Logo
Python Programming

25. Handling Exceptions Gracefully

In Chapter 24, we learned how to read and understand exceptions when they occur. Now we'll learn how to handle exceptions gracefully, allowing our programs to recover from errors instead of crashing. This is essential for writing robust, user-friendly programs that can deal with unexpected situations.

When an exception occurs in Python, the normal flow of the program stops immediately. But what if we could catch that exception before it crashes our program? What if we could respond to the error, perhaps by asking the user to try again, or by using a default value, or by logging the problem and continuing? That's exactly what exception handling allows us to do.

25.1) Using try and except Blocks

25.1.1) The Basic Structure of try and except

A try-except block is Python's way of saying "try to do this, and if an exception occurs, do this instead." The basic structure looks like this:

python
try:
    # Code that might raise an exception
    risky_operation()
except:
    # Code that runs if ANY exception occurs
    print("Something went wrong!")

The try block contains code that might raise an exception. If an exception occurs anywhere in the try block, Python immediately stops executing the try block and jumps to the except block. If no exception occurs, the except block is skipped entirely.

Let's see a concrete example. Remember from Chapter 24 that trying to convert an invalid string to an integer raises a ValueError:

python
# Without exception handling - program crashes
user_input = "hello"
number = int(user_input)  # ValueError: invalid literal for int() with base 10: 'hello'
print("This line never executes")

Now let's handle this exception gracefully:

python
# With exception handling - program continues
user_input = "hello"
 
try:
    number = int(user_input)
    print(f"You entered: {number}")
except:
    print("That's not a valid number!")
    number = 0  # Use a default value
 
print(f"Using number: {number}")

Output:

That's not a valid number!
Using number: 0

The program didn't crash! When int(user_input) raised a ValueError, Python jumped to the except block, printed our error message, set a default value, and then continued with the rest of the program.

Here's what happens step by step:

No

Yes

Start try block

Execute int conversion

Exception raised?

Continue in try block

Jump to except block

Skip except block

Execute except code

Continue after try-except

Understanding the "Jump" - What Really Happens

When we say Python "jumps" to the except block, we mean it abandons the normal sequential execution. This is a fundamental change in how your program flows - not just a simple branch like an if statement. Let's see this in detail with a concrete example:

python
# Watching execution flow with exceptions
print("1. Starting program")
 
try:
    print("2. Entered try block")
    number = int("hello")  # Exception happens HERE
    print("3. After conversion")   # This line NEVER executes
    result = number * 2            # This line NEVER executes
    print("4. After calculation")  # This line NEVER executes
except ValueError:
    print("5. In except block - handling the error")
 
print("6. After try-except block")

Output:

1. Starting program
2. Entered try block
5. In except block - handling the error
6. After try-except block

Notice that lines 3 and 4 never execute! The moment int("hello") raises a ValueError, Python:

  1. Stops executing the try block immediately - right at the line where the exception occurred
  2. Searches for a matching except clause that can handle this type of exception
  3. Jumps to that except block, skipping all remaining code in the try block
  4. Continues after the try-except structure once the except block completes

This is fundamentally different from normal program flow. In normal execution, Python runs each line sequentially. With an exception, Python abandons the current path and takes a completely different route. Without exception handling, the program would crash at line 2 and terminate. With exception handling, the program recovers and continues.

Why This Matters:

Understanding this "jump" behavior is crucial because:

  • Any code after the exception in the try block is skipped - you can't assume later lines in the try block executed
  • Variables might not be initialized if the exception occurs before their assignment
  • You need to plan for what state your program is in when the except block runs

25.1.2) Handling User Input Safely

One of the most common uses of exception handling is validating user input. Users can type anything, and we need to handle invalid input gracefully. Here's a practical example of a program that asks for a user's age:

python
# Safe age input with exception handling
print("Please enter your age:")
user_input = input()
 
try:
    age = int(user_input)
    print(f"You are {age} years old.")
    
    # Calculate birth year (assuming current year is 2024)
    birth_year = 2024 - age
    print(f"You were born around {birth_year}.")
except:
    print("Invalid input! Age must be a number.")
    print("Using default age of 0.")
    age = 0

If the user enters "25", the output is:

Please enter your age:
25
You are 25 years old.
You were born around 1999.

If the user enters "twenty-five", the output is:

Please enter your age:
twenty-five
Invalid input! Age must be a number.
Using default age of 0.

Notice how the program handles the error gracefully instead of crashing with a traceback. This is much better for the user experience.

25.1.3) Handling Multiple Operations in a try Block

You can put multiple operations in a single try block. If any of them raises an exception, Python jumps to the except block immediately. Let's start with a simple example:

python
# Two operations in try block
print("Enter a number:")
user_input = input()
 
try:
    number = int(user_input)      # First operation - could raise ValueError
    result = 100 / number          # Second operation - could raise ZeroDivisionError
    print(f"100 / {number} = {result}")
except:
    print("Something went wrong!")

If the user enters "hello", the exception occurs at the first operation (conversion). If the user enters "0", the exception occurs at the second operation (division). Either way, our single except block catches it.

Now let's extend this to three operations:

python
# Multiple operations in try block
print("Enter two numbers to divide:")
numerator_input = input("Numerator: ")
denominator_input = input("Denominator: ")
 
try:
    numerator = int(numerator_input)      # Could raise ValueError
    denominator = int(denominator_input)  # Could raise ValueError
    result = numerator / denominator      # Could raise ZeroDivisionError
    print(f"{numerator} / {denominator} = {result}")
except:
    print("Something went wrong with the calculation!")
    print("Make sure you enter valid numbers and don't divide by zero.")

If the user enters "10" and "2":

Enter two numbers to divide:
Numerator: 10
Denominator: 2
10 / 2 = 5.0

If the user enters "10" and "zero":

Enter two numbers to divide:
Numerator: 10
Denominator: zero
Something went wrong with the calculation!
Make sure you enter valid numbers and don't divide by zero.

If the user enters "10" and "0":

Enter two numbers to divide:
Numerator: 10
Denominator: 0
Something went wrong with the calculation!
Make sure you enter valid numbers and don't divide by zero.

In this example, three different things could go wrong: the numerator conversion could fail, the denominator conversion could fail, or the division could fail (if denominator is zero). Our single except block catches all of these cases. However, this approach has a limitation: we can't tell which specific error occurred. We'll address this in the next section.

25.1.4) The Problem with Bare except Clauses

Using except: without specifying an exception type is called a bare except clause. While it catches all exceptions, this is often too broad and can hide unexpected problems. Consider this example:

python
# Bare except catches EVERYTHING - even things we don't expect
numbers = [10, 20, 30]
 
try:
    index = 5  # We expect IndexError if index is out of range
    value = numbers[index]
    print(f"Value at index {index}: {value}")
except:
    print("Could not access the list element.")

This seems reasonable - we're trying to access a list element that might not exist. But what if there's a typo in our code?

python
# What if there's a typo in our code?
numbers = [10, 20, 30]
 
try:
    index = 2
    value = numbrs[index]  # Typo: 'numbrs' instead of 'numbers'
    print(f"Value at index {index}: {value}")
except:
    print("Could not access the list element.")

Output:

Could not access the list element.

The bare except catches the NameError from the typo and prints "Could not access the list element" - giving us the wrong information about what went wrong! We think the index is out of range, but actually we have a typo in our variable name.

A bare except also catches KeyboardInterrupt (when the user presses Ctrl+C) and SystemExit (when the program tries to exit), which usually shouldn't be caught. For these reasons, it's better to catch specific exceptions, which we'll learn about next.

25.2) Catching Specific Exceptions

25.2.1) Specifying Exception Types

Instead of catching all exceptions with a bare except, we can specify which exception types we want to handle. This makes our code more precise and helps us respond appropriately to different errors:

python
# Catching a specific exception type
user_input = "hello"
 
try:
    number = int(user_input)
    print(f"You entered: {number}")
except ValueError:
    print("That's not a valid number!")
    number = 0
 
print(f"Using number: {number}")

Output:

That's not a valid number!
Using number: 0

Now our except clause only catches ValueError exceptions. If a different type of exception occurs (like a NameError from a typo), it won't be caught, and we'll see the full traceback - which is actually helpful for debugging!

The syntax is: except ExceptionType: where ExceptionType is the name of the exception class you want to catch (like ValueError, TypeError, ZeroDivisionError, etc.).

Common Mistake: Catching the Wrong Exception Type

What happens if you specify an exception type that doesn't match what actually occurs? Let's see:

python
# Catching the wrong exception type
user_input = "hello"
 
try:
    number = int(user_input)  # This raises ValueError
    print(f"You entered: {number}")
except TypeError:  # But we're catching TypeError!
    print("That's not a valid number!")
    number = 0
 
print(f"Using number: {number}")

Output:

Traceback (most recent call last):
  File "example.py", line 4, in <module>
    number = int(user_input)
ValueError: invalid literal for int() with base 10: 'hello'

The program crashed! Why? Because int("hello") raises a ValueError, but our except clause only catches TypeError. Since there's no matching except clause, the exception isn't caught, and the program terminates.

This is actually helpful during development - if you catch the wrong exception type, you'll see the full traceback and realize your mistake. This is one reason why catching specific exceptions is better than using bare except.

How to avoid this mistake:

  1. Read the traceback to see what exception type actually occurred
  2. Use that specific exception type in your except clause
  3. If you're unsure, run the code and let it crash - the traceback will tell you!

25.2.2) Handling Different Exceptions Differently

You can have multiple except clauses to handle different exception types in different ways. This is extremely useful when different errors require different responses:

python
# Different handling for different exceptions
print("Enter two numbers to divide:")
numerator_input = input("Numerator: ")
denominator_input = input("Denominator: ")
 
try:
    numerator = int(numerator_input)
    denominator = int(denominator_input)
    result = numerator / denominator
    print(f"{numerator} / {denominator} = {result}")
except ValueError:
    print("Error: Both inputs must be valid integers.")
    print("You entered something that isn't a number.")
except ZeroDivisionError:
    print("Error: Cannot divide by zero.")
    print("The denominator must be a non-zero number.")

If the user enters "10" and "abc":

Enter two numbers to divide:
Numerator: 10
Denominator: abc
Error: Both inputs must be valid integers.
You entered something that isn't a number.

If the user enters "10" and "0":

Enter two numbers to divide:
Numerator: 10
Denominator: 0
Error: Cannot divide by zero.
The denominator must be a non-zero number.

Python checks each except clause in order. When an exception occurs, Python finds the first except clause that matches the exception type and executes that block. The other except clauses are skipped.

Yes

No

Yes

No

Yes

No

Exception raised in try block

Matches first except?

Execute first except block

Matches second except?

Execute second except block

Matches third except?

Execute third except block

Exception not caught - propagates up

Continue after try-except

25.2.3) Catching Multiple Exception Types in One Clause

Sometimes you want to handle several different exception types in the same way. Instead of writing multiple identical except blocks, you can catch multiple exception types in a single clause by putting them in parentheses as a tuple:

python
# Catching multiple exception types together
print("Enter a number:")
user_input = input()
 
try:
    number = int(user_input)
    result = 100 / number
    print(f"100 divided by {number} is {result}")
except (ValueError, ZeroDivisionError):
    print("Invalid input or division by zero.")
    print("Please enter a non-zero number.")

If the user enters "hello":

Enter a number:
hello
Invalid input or division by zero.
Please enter a non-zero number.

If the user enters "0":

Enter a number:
0
Invalid input or division by zero.
Please enter a non-zero number.

Both ValueError (from invalid conversion) and ZeroDivisionError (from dividing by zero) are handled by the same except clause. This is useful when different errors should trigger the same response.

25.2.4) Accessing Exception Information

Sometimes you need to know more details about the exception that occurred. You can capture the exception object using the as keyword. But first, let's understand what an exception object actually is.

What Is an Exception Object?

When Python raises an exception, it doesn't just signal that something went wrong - it creates an object that contains information about the error. This exception object is like a detailed error report that includes:

  • The error message: A description of what went wrong
  • The exception type: What kind of error occurred (ValueError, TypeError, etc.)
  • Additional attributes: Specific information depending on the exception type

Think of an exception object as a container that holds all the information about an error. Just like a list object contains items and has methods like append(), an exception object contains error information and has attributes you can access.

When you write except ValueError as error:, you're telling Python: "If a ValueError occurs, create a variable called error and put the exception object in it so I can examine it."

Let's explore what's inside an exception object:

python
# Examining exception object contents
try:
    number = int("hello")
except ValueError as error:
    print("Exception caught! Let's examine it:")
    print(f"Type: {type(error)}")
    print(f"String representation: {error}")
    print(f"Args tuple: {error.args}")

Output:

Exception caught! Let's examine it:
Type: <class 'ValueError'>
String representation: invalid literal for int() with base 10: 'hello'
Args tuple: ("invalid literal for int() with base 10: 'hello'",)

The exception object has:

  • A type (ValueError class) - this tells you what kind of error occurred
  • A string representation (the error message) - this is what you see in tracebacks
  • An args attribute (tuple containing the message and any other arguments) - this provides structured access to error details

Why This Matters:

Different exception types have different attributes that provide specific information. Understanding the structure of exception objects helps you extract useful information for debugging or user feedback:

python
# Different exceptions have different attributes
numbers = [10, 20, 30]
 
try:
    value = numbers[10]
except IndexError as error:
    print(f"IndexError message: {error}")
    print(f"Exception args: {error.args}")
 
# Now try with a dictionary
grades = {"Alice": 95}
 
try:
    grade = grades["Bob"]
except KeyError as error:
    print(f"KeyError message: {error}")
    print(f"Missing key: {error.args[0]}")

Output:

IndexError message: list index out of range
Exception args: ('list index out of range',)
KeyError message: 'Bob'
Missing key: Bob

Notice how KeyError includes the actual key that was missing in its message. Different exception types provide different useful information that you can access through the exception object.

25.3) Using else and finally with try Blocks

25.3.1) The else Clause: Code That Runs Only on Success

The else clause in a try-except block runs only if no exception occurred in the try block. This is useful for code that should only execute when the risky operation succeeds:

python
# Using else for success-only code
print("Enter a number:")
user_input = input()
 
try:
    number = int(user_input)
except ValueError:
    print("That's not a valid number!")
else:
    # This only runs if int(user_input) succeeded
    print(f"Successfully converted: {number}")
    squared = number ** 2
    print(f"The square of {number} is {squared}")

If the user enters "5":

Enter a number:
5
Successfully converted: 5
The square of 5 is 25

If the user enters "hello":

Enter a number:
hello
That's not a valid number!

Why use else instead of just putting the code at the end of the try block? There are two important reasons:

  1. Clarity: The else clause makes it explicit that this code only runs on success
  2. Exception scope: Exceptions raised in the else clause are not caught by the preceding except clauses

Here's an example showing why the second point matters:

python
# Demonstrating why else is useful for exception scope
try:
    number_1 = int(input("Enter a number_1: "))
except ValueError:
    print("Invalid input!")
else:
    # If an error occurs here, it won't be caught by the except above
    # This helps distinguish between input errors and processing errors
    number_2 = int(input("Enter a number_2: ")) # Could raise ValueError

If we put number_2 = int(input(...)) in the try block along with number_1, any ValueError from either input would be caught by the same except ValueError clause. This makes it impossible to tell which input caused the problem.

By putting number_2 = int(input(...)) in the else block, we separate the error handling. The except clause only catches errors from number_1, while errors from number_2 will raise an uncaught exception with a full traceback - making it clear that the second input failed, not the first.

25.3.2) The finally Clause: Code That Always Runs

The finally clause contains code that runs no matter what - whether an exception occurred or not, whether it was caught or not. This is essential for cleanup operations that must always happen:

python
# Using finally for cleanup
print("Enter a number:")
user_input = input()
 
try:
    number = int(user_input)
    result = 100 / number
    print(f"Result: {result}")
except ValueError:
    print("Invalid number!")
except ZeroDivisionError:
    print("Cannot divide by zero!")
finally:
    print("Calculation attempt completed.")

If the user enters "5":

Enter a number:
5
Result: 20.0
Calculation attempt completed.

If the user enters "hello":

Enter a number:
hello
Invalid number!
Calculation attempt completed.

If the user enters "0":

Enter a number:
0
Cannot divide by zero!
Calculation attempt completed.

The finally block runs in all three cases! This is the key behavior of finally: it always executes, regardless of what happened in the try block.

No

Yes

Yes

No

Yes

No

Start try block

Exception raised?

Complete try block

Exception caught?

Execute else block if present

Execute matching except block

Exception propagates

Execute finally block

Was exception caught?

Continue after try-except-finally

Exception continues propagating

25.3.3) Combining try, except, else, and finally

You can use all four clauses together to create comprehensive exception handling:

python
# Complete exception handling structure
print("Enter a number to calculate its reciprocal:")
user_input = input()
 
try:
    # Risky operations
    number = int(user_input)
    reciprocal = 1 / number
except ValueError:
    # Handle conversion errors
    print("Error: Input must be a valid integer.")
except ZeroDivisionError:
    # Handle division by zero
    print("Error: Cannot calculate reciprocal of zero.")
else:
    # Success-only code
    print(f"The reciprocal of {number} is {reciprocal}")
    print(f"Verification: {number} × {reciprocal} = {number * reciprocal}")
finally:
    # Cleanup code that always runs
    print("Reciprocal calculation completed.")

If the user enters "4":

Enter a number to calculate its reciprocal:
4
The reciprocal of 4 is 0.25
Verification: 4 × 0.25 = 1.0
Reciprocal calculation completed.

If the user enters "hello":

Enter a number to calculate its reciprocal:
hello
Error: Input must be a valid integer.
Reciprocal calculation completed.

If the user enters "0":

Enter a number to calculate its reciprocal:
0
Error: Cannot calculate reciprocal of zero.
Reciprocal calculation completed.

The execution flow is:

  1. try block always executes first
  2. If an exception occurs, the matching except block executes
  3. If no exception occurs, the else block executes (if present)
  4. The finally block always executes last, regardless of what happened

25.4) Raising Exceptions Deliberately with raise

25.4.1) Why Raise Exceptions?

So far, we've been catching exceptions that Python raises automatically. But sometimes you need to raise an exception deliberately in your own code. This is useful when:

  1. You detect an invalid situation that your code can't handle
  2. You want to enforce rules or constraints
  3. You want to signal an error to code that called your function

Raising an exception is Python's way of saying "I can't continue - something is wrong, and whoever called me needs to deal with it."

The syntax is simple: raise ExceptionType("error message")

Here's a basic example:

python
# Raising an exception deliberately
age = -5
 
if age < 0:
    raise ValueError("Age cannot be negative!")
 
print(f"Age: {age}")  # This line never executes

Output:

Traceback (most recent call last):
  File "example.py", line 5, in <module>
    raise ValueError("Age cannot be negative!")
ValueError: Age cannot be negative!

When Python encounters raise, it immediately creates an exception and starts looking for an except block to handle it. If there isn't one, the program terminates with a traceback.

25.4.2) Raising Exceptions in Functions

Raising exceptions is particularly useful in functions to validate inputs and enforce constraints:

python
# Function that validates input by raising exceptions
def calculate_discount(price, discount_percent):
    """Calculate discounted price.
    
    Args:
        price: Original price (must be positive)
        discount_percent: Discount percentage (must be 0-100)
    
    Returns:
        Discounted price
    
    Raises:
        ValueError: If inputs are invalid
    """
    if price < 0:
        raise ValueError("Price cannot be negative!")
    
    if discount_percent < 0 or discount_percent > 100:
        raise ValueError("Discount must be between 0 and 100!")
    
    discount_amount = price * (discount_percent / 100)
    return price - discount_amount
 
# Using the function
try:
    final_price = calculate_discount(100, 20)
    print(f"Final price: ${final_price}")
except ValueError as error:
    print(f"Error: {error}")

Output:

Final price: $80.0

Now let's try with invalid inputs:

python
# Invalid price
try:
    final_price = calculate_discount(-50, 20)
    print(f"Final price: ${final_price}")
except ValueError as error:
    print(f"Error: {error}")

Output:

Error: Price cannot be negative!
python
# Invalid discount
try:
    final_price = calculate_discount(100, 150)
    print(f"Final price: ${final_price}")
except ValueError as error:
    print(f"Error: {error}")

Output:

Error: Discount must be between 0 and 100!

By raising exceptions, the function clearly communicates what went wrong. The calling code can then decide how to handle the error - maybe by asking the user for new input, using default values, or logging the error.

25.4.3) Choosing the Right Exception Type

Python has many built-in exception types, and choosing the right one makes your code clearer. Here are the most commonly used exceptions for validation:

  • ValueError: Use when a value has the right type but an inappropriate value (e.g., negative age, invalid percentage)
  • TypeError: Use when a value is the completely wrong type (e.g., string instead of number)
  • KeyError: Use when a dictionary key doesn't exist
  • IndexError: Use when a sequence index is out of range

Here's an example showing different exception types:

python
# Using appropriate exception types
def get_student_grade(grades, student_name):
    """Get a student's grade from the grades dictionary.
    
    Args:
        grades: Dictionary mapping student names to grades
        student_name: Name of the student
    
    Returns:
        The student's grade
    
    Raises:
        TypeError: If grades is not a dictionary
        KeyError: If student_name is not in grades
        ValueError: If the grade is invalid
    """
    if not isinstance(grades, dict):
        raise TypeError("Grades must be a dictionary!")
    
    if student_name not in grades:
        raise KeyError(f"Student '{student_name}' not found!")
    
    grade = grades[student_name]
    
    if not (0 <= grade <= 100):
        raise ValueError(f"Invalid grade: {grade} (must be 0-100)")
    
    return grade
 
# Test with valid data
grades = {"Alice": 95, "Bob": 87, "Carol": 92}
 
try:
    grade = get_student_grade(grades, "Alice")
    print(f"Alice's grade: {grade}")
except (TypeError, KeyError, ValueError) as error:
    print(f"Error: {error}")

Output:

Alice's grade: 95
python
# Test with missing student
try:
    grade = get_student_grade(grades, "David")
    print(f"David's grade: {grade}")
except (TypeError, KeyError, ValueError) as error:
    print(f"Error: {error}")

Output:

Error: Student 'David' not found!
python
# Test with wrong type
try:
    grade = get_student_grade("not a dict", "Alice")
    print(f"Alice's grade: {grade}")
except (TypeError, KeyError, ValueError) as error:
    print(f"Error: {error}")

Output:

Error: Grades must be a dictionary!

Using the appropriate exception type helps other programmers (and your future self) understand what kind of error occurred.

25.4.4) Re-raising Exceptions

Sometimes you want to catch an exception, do something (like logging), and then let the exception continue propagating. You can do this by using raise without any arguments inside an except block:

python
# Re-raising an exception after logging
def divide_numbers(a, b):
    """Divide two numbers with error logging."""
    try:
        result = a / b
        return result
    except ZeroDivisionError:
        print("ERROR LOG: Division by zero attempted")
        print(f"  Numerator: {a}, Denominator: {b}")
        raise  # Re-raise the same exception
 
# Using the function
try:
    result = divide_numbers(10, 0)
    print(f"Result: {result}")
except ZeroDivisionError:
    print("Cannot divide by zero!")

Output:

ERROR LOG: Division by zero attempted
  Numerator: 10, Denominator: 0
Cannot divide by zero!

The raise statement without arguments re-raises the exception that was just caught. This is useful when you want to:

  1. Log or record the error
  2. Do some cleanup
  3. Let the error propagate to the caller

25.4.5) Raising Exceptions from Exceptions

Sometimes you want to raise a new exception while handling another one, preserving the context of the original error. Python 3 provides the raise ... from ... syntax for this:

python
# Raising a new exception from an existing one
def load_config(config_dict, key):
    """Load configuration value from dictionary."""
    try:
        config_value = config_dict[key]
        
        # Try to parse as integer
        parsed_value = int(config_value)
        return parsed_value
        
    except KeyError as error:
        raise RuntimeError(f"Configuration key missing: {key}") from error
    except ValueError as error:
        raise RuntimeError(f"Invalid configuration format for {key}") from error
 
# Using the function
config = {"timeout": "30", "retries": "5"}
 
try:
    value = load_config(config, "timeout")
    print(f"Config value: {value}")
except RuntimeError as error:
    print(f"Configuration error: {error}")
    print(f"Original cause: {error.__cause__}")

Output:

Config value: 30

If the key doesn't exist:

python
try:
    value = load_config(config, "missing_key")
    print(f"Config value: {value}")
except RuntimeError as error:
    print(f"Configuration error: {error}")
    print(f"Original cause: {error.__cause__}")

Output:

Configuration error: Configuration key missing: missing_key
Original cause: 'missing_key'

The from keyword links the new exception to the original one. This creates a chain of exceptions that helps with debugging - you can see both what went wrong at a high level (configuration error) and what the underlying cause was (key not found).


Exception handling is one of the most important tools for writing reliable programs. By using try-except blocks, you can anticipate problems, handle them gracefully, and provide a better experience for your users. Remember:

  • Use try-except to handle expected errors gracefully
  • Catch specific exception types rather than using bare except
  • Use else for code that should only run on success
  • Use finally for cleanup code that must always run
  • Raise exceptions in your own code to signal problems
  • Choose appropriate exception types to make errors clear
  • Provide helpful error messages that explain what went wrong

In the next chapter, we'll learn defensive programming techniques that combine exception handling with input validation and other strategies to make our programs even more robust.

© 2025. Primesoft Co., Ltd.
support@primesoft.ai