This article contains 20 Python exception handling exercises, from beginner to expert level, covering topics from try/except/finally, custom exceptions, raising errors to multiple exception handling, raising exceptions and assertion statements to helping you write more efficient and Pythonic code.
Each coding challenge includes a Practice Problem, Hint, Solution code, and detailed Explanation, ensuring you don’t just copy code, but genuinely practice and understand how and why it works.
- All solutions have been fully tested on Python 3.
- Use our Online Code Editor to solve these exercises in real time.
- Read Python exception handling for any help
+ Table of Contents (20 Exercises)
Table of contents
- Exercise 1: Basic Try-Except
- Exercise 2: Division Safety
- Exercise 3: File Not Found
- Exercise 4: Index Out of Range
- Exercise 5: Key Error Handling
- Exercise 6: Type Error Guard
- Exercise 7: Multiple Exceptions
- Exercise 8: Finally Block
- Exercise 9: Else Clause
- Exercise 10: Nested Try-Except
- Exercise 11: Re-raising Exceptions
- Exercise 12: Custom Exception Class
- Exercise 13: Exception Chaining
- Exercise 14: Validate User Input Loop
- Exercise 15: Context Manager with Exceptions
- Exercise 16: Exception Hierarchy
- Exercise 17: Logging Exceptions
- Exercise 18: Retry Decorator
- Exercise 19: Thread-Safe Exception Handling
- Exercise 20: Exception in Generator
Exercise 1: Basic Try-Except
Problem Statement: Write a Python program that asks the user to enter a number. If the input is not a valid integer, raise a ValueError and display a helpful error message instead of crashing.
Purpose: This exercise introduces the fundamental try/except pattern in Python. Handling invalid user input gracefully is one of the most common real-world uses of exception handling, and mastering it is a key step toward writing robust, user-friendly programs.
Given Input: User enters "hello" when prompted for a number.
Expected Output: Error: That was not a valid integer. Please enter a number.
▼ Hint
- Use
input()to read user input, then wrap the conversion attempt in atryblock. - Use
int()to convert the string to an integer. This raisesValueErrorautomatically if the string is not numeric. - Catch the exception with
except ValueError as e:and print your custom message inside that block.
▼ Solution & Explanation
Explanation:
try:: Opens the guarded block. Python attempts to run every statement inside it. If any line raises an exception, execution jumps immediately to the matchingexceptblock.int(user_input): Converts the string to an integer. When the string contains non-numeric characters, Python raises aValueErrorautomatically, so you do not need to raise it manually here.except ValueError:: Catches onlyValueErrorexceptions. Using a specific exception type instead of a bareexceptis best practice because it avoids silently swallowing unrelated errors.- Print in the except block: Any code inside the
exceptblock runs only when the named exception is raised. Code after the entiretry/exceptstructure continues normally either way.
Exercise 2: Division Safety
Problem Statement: Create a function safe_divide(a, b) that divides a by b. If b is zero, handle the ZeroDivisionError and return None instead of letting the program crash.
Purpose: Division by zero is a classic runtime error that appears in financial calculations, data pipelines, and scientific code. This exercise teaches you to wrap risky arithmetic in exception handling and return a sentinel value when a valid result cannot be produced.
Given Input: Call safe_divide(10, 0) and safe_divide(10, 2).
Expected Output: None for the first call and 5.0 for the second.
▼ Hint
- Place the division
a / binside atryblock inside the function. - Catch
ZeroDivisionErrorin theexceptclause and usereturn Nonethere. - If no exception occurs, return the result of the division normally.
▼ Solution & Explanation
Explanation:
result = a / b: Performs the division. Whenbis zero, Python raisesZeroDivisionErrorbefore assigning anything toresult.except ZeroDivisionError:: Intercepts the error only whenbis zero. All other exceptions (e.g.,TypeErrorif non-numbers are passed) propagate normally, which is the correct behaviour.return None: Returns Python’s built-in null value as a safe sentinel. The caller can checkif result is None:to detect the failure without catching an exception itself.- Normal path: When
bis non-zero, theexceptblock is skipped entirely andreturn resultexecutes as usual.
Exercise 3: File Not Found
Problem Statement: Write a program that tries to open a file that does not exist on disk. Catch the FileNotFoundError exception and display a clear, user-friendly message instead of showing a raw Python traceback.
Purpose: File I/O is one of the most error-prone operations in any program. Learning to handle missing files gracefully makes your scripts production-ready and prevents cryptic crashes when the environment differs from what you expect.
Given Input: Attempt to open "missing_file.txt" for reading.
Expected Output: Error: The file 'missing_file.txt' was not found. Please check the filename and path.
▼ Hint
- Use
open("missing_file.txt", "r")inside atryblock. - Catch
FileNotFoundErrorand print a descriptive message that includes the filename so the user knows exactly what went wrong. - You can embed the filename directly in the message string, or access it from the exception object using
except FileNotFoundError as e:and thene.filename.
▼ Solution & Explanation
Explanation:
with open(filename, "r") as f:: Thewithstatement is the recommended way to open files because it automatically closes the file handle even if an exception occurs. The"r"flag opens the file in read-only mode.FileNotFoundError: A subclass ofOSErrorraised specifically when the path does not point to an existing file. Catching this precise type avoids masking other I/O errors such as permission denied.- f-string in the message: Embedding
{filename}in the error message gives the user actionable information without requiring them to search the traceback. - Alternative: You can also use
except FileNotFoundError as e: print(e.filename)to extract the filename directly from the exception object if you prefer not to store it in a variable beforehand.
Exercise 4: Index Out of Range
Problem Statement: Given a fixed list, ask the user to enter an index and print the element at that position. If the index is outside the valid range, catch the IndexError and display an informative message.
Purpose: Accessing list elements by index is something Python developers do constantly. This exercise builds the habit of validating or protecting index-based access, which is critical in data processing, game development, and any code that works with sequences of unknown length.
Given Input: items = ["apple", "banana", "cherry"] and the user enters index 5.
Expected Output: Error: Index 5 is out of range. The list has 3 items (valid indices: 0 to 2).
▼ Hint
- First convert the user input to an integer with
int(). You may want a nestedtry/exceptor a single block that catches bothValueErrorandIndexError. - Access the list with
items[index]inside thetryblock. Python raisesIndexErrorautomatically when the index is too large or too small. - Use
len(items)in the error message to tell the user the valid range dynamically.
▼ Solution & Explanation
Explanation:
int(input(...)): Combines input reading and conversion in one step. If the user types a non-integer string,ValueErroris raised before the list access is ever attempted.items[index]: TriggersIndexErrorwhenindexis greater than or equal tolen(items), or less than-len(items). Python supports negative indices (e.g.,-1for the last element), so values like-10on a three-item list also cause this error.- Multiple
exceptclauses: You can stack as manyexceptblocks as needed after a singletry. Python checks them in order and runs only the first matching one. len(items) - 1: Computes the last valid index dynamically so the error message stays accurate even if the list changes size later.
Exercise 5: Key Error Handling
Problem Statement: Given a dictionary of country capitals, ask the user to enter a country name and print the corresponding capital. If the country is not in the dictionary, catch the KeyError and display a helpful message.
Purpose: Dictionary lookups are everywhere in Python: configuration parsing, JSON processing, API responses, and more. Knowing how to handle a missing key cleanly, rather than letting a KeyError crash your program, is an essential skill for working with real-world data.
Given Input: capitals = {"France": "Paris", "Japan": "Tokyo", "Brazil": "Brasilia"} and the user enters "Germany".
Expected Output: Error: 'Germany' was not found in the dictionary.
▼ Hint
- Access the dictionary with
capitals[country]inside atryblock. Python raisesKeyErrorautomatically when the key is absent. - Catch
KeyError as eand print the missing key. The exception objecteholds the key as its first argument, sostr(e)ore.args[0]gives you the key name as a string. - As an alternative approach, you could use
dict.get(key)which returnsNoneinstead of raising an exception. Consider which style is cleaner for your use case.
▼ Solution & Explanation
Explanation:
capitals[country]: Direct bracket access raisesKeyErrorimmediately when the key does not exist. This is the idiomatic way to look up a dictionary value when you expect the key to be present and want to treat its absence as an error.except KeyError:: Catches the missing-key error and lets you respond with a custom message. The exception object contains the missing key value, so you can reference it in the message for clarity.- Alternative –
dict.get(): Usingcapitals.get(country)returnsNonesilently when the key is missing, avoiding the need for atry/exceptblock entirely. Chooseget()when absence is a normal, expected condition, and use bracket access with exception handling when absence should be treated as an error worth reporting. - Case sensitivity: Dictionary keys are case-sensitive in Python. Entering
"france"instead of"France"would also trigger aKeyError. You could normalise input withcountry.title()orcountry.capitalize()to improve robustness.
Exercise 6: Type Error Guard
Problem Statement: Write a function add_values(a, b) that returns the sum of two values. If the arguments are of incompatible types (for example, a string and an integer), catch the TypeError and return a descriptive error message instead of crashing.
Purpose: Python is dynamically typed, which means type mismatches only surface at runtime. This exercise teaches you to guard operations that assume specific types, a habit that becomes critical when processing data from external sources such as APIs, CSV files, or user forms where the shape of the data is not always guaranteed.
Given Input: Call add_values(10, "20") and add_values(10, 20).
Expected Output: Error: incompatible types for addition. for the first call and 30 for the second.
▼ Hint
- Place
return a + binside atryblock. Python raisesTypeErrorautomatically when+is applied to incompatible types such asintandstr. - Catch
TypeErrorin theexceptclause and return a string message from there. - You can also use
except TypeError as e:and includestr(e)in the returned message for the exact Python error text.
▼ Solution & Explanation
Explanation:
return a + b: The+operator works for numbers, strings, and lists, but mixing types that do not support addition together (such asintandstr) causes Python to raiseTypeErrorimmediately.except TypeError:: Catches the type mismatch error specifically. Keeping the exception type narrow means other unexpected errors, such as aMemoryError, will still propagate and not be silently swallowed.returnfrom the except block: Returning a value from inside anexceptclause is perfectly valid. The caller receives either the computed result or the error string and can check which one was returned.- Note on
"20"vs20:add_values(10, "20")fails because Python will not silently coerce a string to an integer. If you want to support that, you would explicitly convert withint(b)orfloat(b)before adding, and handle the resultingValueErrorseparately.
Exercise 7: Multiple Exceptions
Problem Statement: Write a function parse_and_divide(value, divisor) that converts value to a float and then divides it by divisor. Use a single try block that handles ValueError (bad conversion) and ZeroDivisionError (division by zero) with separate messages for each.
Purpose: Real programs rarely face only one kind of failure. This exercise shows you how to stack multiple except clauses after one try block so each error type gets its own tailored response, keeping error-handling logic clear and maintainable.
Given Input: Call parse_and_divide("abc", 2) and parse_and_divide("10", 0) and parse_and_divide("10", 2).
Expected Output:
Error: 'abc' cannot be converted to a number. Error: Cannot divide by zero. 5.0
▼ Hint
- Put both
float(value)and the division on separate lines inside the sametryblock. Python stops at the first line that raises an exception and jumps to the matchingexcept. - Add two
exceptclauses below: one forValueErrorand one forZeroDivisionError. - You can also catch multiple exceptions in a single clause using a tuple:
except (ValueError, ZeroDivisionError) as e:, but here you want separate handling, so keep them apart.
▼ Solution & Explanation
Explanation:
float(value): Attempts to convert the string to a floating-point number. When the string is not numeric (e.g.,"abc"), Python raisesValueErrorand the division line is never reached.number / divisor: Only executes if the conversion succeeded. Ifdivisoris zero at this point,ZeroDivisionErroris raised and the secondexceptblock takes over.- Order of
exceptclauses: Python tests each clause from top to bottom and runs only the first match. For unrelated exception types, the order does not matter practically, but it is good practice to list more specific exceptions before broader ones. - Tuple shorthand: If you want identical handling for multiple types, you can write
except (ValueError, ZeroDivisionError):in a single clause. Here they are kept separate because the error messages differ.
Exercise 8: Finally Block
Problem Statement: Write a function read_file(filename) that opens a file manually (without a with statement), reads its contents, and uses a finally block to ensure the file handle is always closed, whether the read succeeds or an exception is raised.
Purpose: The finally block is Python’s mechanism for guaranteed cleanup. Understanding it is essential for managing resources such as file handles, database connections, and network sockets correctly, especially in older codebases or situations where the with statement is not available.
Given Input: Call read_file("hello.txt") where the file exists, and then read_file("missing.txt") where it does not.
Expected Output:
File closed. Hello from the file! --- File closed. Error: 'missing.txt' not found.
▼ Hint
- Open the file with
f = open(filename, "r")before thetryblock, or initialisef = Nonebefore and open inside thetryso thefinallyblock can safely checkif f:before callingf.close(). - Put
f.read()inside thetryblock and catchFileNotFoundErrorin theexceptclause. - Place
f.close()insidefinally:. This block always runs, making it the right place for resource cleanup.
▼ Solution & Explanation
Explanation:
f = None: Initialisingfbefore thetryblock ensures the variable exists in thefinallyscope. Ifopen()itself raises an exception (e.g.,FileNotFoundError),fwould never be assigned, so checkingif f:before callingf.close()prevents a secondaryAttributeError.finally:: This block runs unconditionally after thetryand any matchingexceptclause, whether the code succeeded, raised a caught exception, or raised an uncaught one. It is the correct place for cleanup logic.f.close(): Releases the operating system file handle. Leaving files open wastes resources and can cause data corruption or lock issues on Windows systems.- Why not just use
with?: Thewith open(...) as f:statement handles closing automatically and is the modern preferred approach. This exercise intentionally avoids it to demonstrate whatwithdoes under the hood and to prepare you for codebases that do not use it.
Exercise 9: Else Clause
Problem Statement: Write a function safe_sqrt(value) that converts user input to a float and calculates its square root. Use the try-except-else pattern so that a success message is printed only when no exception is raised, keeping success and error paths cleanly separated.
Purpose: The else clause on a try block is one of Python’s less-known but very useful features. It keeps the “happy path” code out of the try block itself, which means only the risky operation is guarded and success logic is not accidentally shielded from other exceptions it might raise.
Given Input: Call safe_sqrt("25") and safe_sqrt("abc").
Expected Output:
Success: the square root of 25.0 is 5.0 Error: 'abc' is not a valid number.
▼ Hint
- Put only the risky conversion
number = float(value)inside thetryblock. - Add an
except ValueError:clause that prints the error message. - Add an
else:block after theexcept. Code inelseruns only when thetryblock completes without raising any exception. Place your square root calculation and success print statement there. - You can compute the square root with
number ** 0.5or by importingmath.sqrt.
▼ Solution & Explanation
Explanation:
trycontains only the risky line: Onlyfloat(value)sits insidetry. This is intentional. Ifmath.sqrt()were also insidetryand it raised an error (e.g., on a negative number), that error would be caught by theexcept ValueErrorclause, which would be misleading.else:: Runs if and only if thetryblock completed without raising any exception. It is the correct place for code that depends on the success of the guarded operation but should not itself be protected by the sameexceptclause.math.sqrt(number): Returns the square root as a float. It raisesValueErrorfor negative inputs, but since it lives in theelseblock, that error is not caught here and would propagate to the caller – which is the correct and transparent behaviour.- Clause order: The full order of clauses on a
trystatement is:try–except–else–finally. You can combine all four in a single structure when needed.
Exercise 10: Nested Try-Except
Problem Statement: Write a function load_and_parse(filename, key) that opens a JSON file (outer try) and then looks up a key in the parsed data (inner try). Handle FileNotFoundError at the outer level and KeyError at the inner level so each failure is reported with an appropriate message.
Purpose: Complex workflows often have multiple distinct failure points that require different recovery strategies. Nested try blocks let you apply fine-grained handling close to the source of each potential error, keeping the logic easier to reason about than a single flat block that catches everything.
Given Input: A file data.json containing {"name": "Alice", "age": 30}. Call load_and_parse("data.json", "name"), then load_and_parse("data.json", "email"), then load_and_parse("missing.json", "name").
Expected Output:
Alice Error: key 'email' not found in the data. Error: file 'missing.json' does not exist.
▼ Hint
- In the outer
try, open the file and parse it withjson.load(f). CatchFileNotFoundErrorin the outerexcept. - Inside the outer
try, after parsing succeeds, open an innertryblock that doesdata[key]. CatchKeyErrorin the innerexcept. - Remember to
import jsonat the top of your script.
▼ Solution & Explanation
Explanation:
- Outer
try– file access: Wraps the file open and JSON parse operations. If either fails due to a missing file, the outerexcept FileNotFoundErrorcatches it and the inner block never runs. This prevents a cascade of secondary errors when the prerequisite (the file) does not exist. - Inner
try– key lookup: Only reached when the file loaded successfully. It isolates the dictionary access so that a missing key is handled specifically without interfering with the outer error handler. json.load(f): Parses the entire file contents into a Python dictionary. It can raisejson.JSONDecodeErrorif the file is not valid JSON. That exception is not caught here and would propagate up, which is intentional: the exercise focuses on nesting, not exhaustive handling.- When to nest vs. stack: Use nested
tryblocks when later operations depend on the success of earlier ones and each needs its own handler. Use stacked (sequential)tryblocks when operations are independent. Deeply nested structures (three or more levels) are usually a sign to refactor into separate functions.
Exercise 11: Re-raising Exceptions
Problem Statement: Write a function process_data(value) that converts a string to an integer. Catch any ValueError, log a message to the console, and then re-raise the same exception so the calling code can handle it at a higher level.
Purpose: Catching an exception purely to re-raise it is a common pattern in layered applications. A lower-level function may need to record a log entry or clean up state without fully swallowing the error, while still allowing the caller to decide how to respond. This exercise shows exactly how to do that with a bare raise statement.
Given Input: Call process_data("abc") from a wrapping try block in the main code.
Expected Output:
[log] process_data failed: invalid literal for int() with base 10: 'abc' [main] Caught re-raised exception: invalid literal for int() with base 10: 'abc'
▼ Hint
- Inside the
except ValueError as e:block, print your log message usingstr(e)to include the original error text. - After printing, use a bare
raisestatement (with no argument) to re-raise the exact same exception object. This preserves the original traceback, which is important for debugging. - In the calling code, wrap
process_data()in its owntry/except ValueErrorblock to demonstrate that the re-raised exception can be caught there.
▼ Solution & Explanation
Explanation:
except ValueError as e:: Binds the exception object to the namee, giving access to the error message viastr(e)or directly inside an f-string.- Bare
raise: Re-raises the current active exception without modifying it. This is different fromraise e, which would create a new traceback starting at that line. A bareraisepreserves the full original traceback, which is critical for diagnosing where the error actually originated. - Layered handling: After
process_datalogs and re-raises, execution resumes in the caller’sexceptblock. The caller sees the same exception type and message, so it can respond independently – for example, by returning a default value, displaying a user-facing message, or logging again at a higher severity level. - When to use this pattern: Re-raising is appropriate when a function has side effects to perform on failure (logging, metrics, cleanup) but is not the right place to make the final recovery decision. Avoid it when you actually want to suppress the error entirely – use a plain
exceptwithoutraisefor that.
Exercise 12: Custom Exception Class
Problem Statement: Define a custom exception class InsufficientFundsError. Then create a BankAccount class with a withdraw(amount) method that raises InsufficientFundsError when the withdrawal amount exceeds the current balance. Handle the exception in the calling code.
Purpose: Built-in exceptions are generic. Custom exceptions make your code self-documenting and allow callers to catch exactly the error your library or class can produce, without accidentally catching unrelated errors of the same built-in type. This pattern is standard in professional Python packages and frameworks.
Given Input: Create a BankAccount with a balance of 100. Try to withdraw 150.
Expected Output:
Error: Cannot withdraw 150. Available balance: 100.
▼ Hint
- Define
class InsufficientFundsError(Exception):– inheriting fromExceptionis the standard way to create a custom exception. You can leave the body aspassfor a minimal version, or add an__init__to store extra context. - In
BankAccount.withdraw(), compareamounttoself.balanceand callraise InsufficientFundsError(...)with a descriptive message when the check fails. - In the calling code, use
except InsufficientFundsError as e:to catch only your custom exception and printstr(e).
▼ Solution & Explanation
Explanation:
class InsufficientFundsError(Exception):: Inheriting from the built-inExceptionclass is the correct base for most custom exceptions. The custom__init__stores the attempted amount and current balance as attributes, making them available for programmatic inspection in the caller, not just as a string message.super().__init__(...): Passes the human-readable message up to the baseExceptionclass so thatstr(e)andprint(e)work as expected without any extra effort from the caller.raise InsufficientFundsError(amount, self.balance): Raises the custom exception with contextual data baked in. The caller can catch it by name and accesse.amountore.balancefor fine-grained error recovery, such as suggesting a valid withdrawal amount.- Why not raise
ValueError?: You could raiseValueErrorhere, but then callers cannot distinguish an insufficient-funds error from any other value error in the system. A custom exception type creates a precise, unambiguous signal that belongs to your domain.
Exercise 13: Exception Chaining
Problem Statement: Write a function load_config(filename) that reads a JSON config file. If the file is missing, catch the FileNotFoundError and raise a higher-level ConfigurationError using raise ConfigurationError(...) from original_exception to preserve the original cause in the chain.
Purpose: Exception chaining lets you translate low-level technical errors into meaningful domain-level errors without discarding the debugging context. A caller that receives a ConfigurationError knows exactly what went wrong in business terms, while the full original traceback is still accessible via __cause__ for developers who need it.
Given Input: Call load_config("config.json") when the file does not exist.
Expected Output:
ConfigurationError: Could not load config file 'config.json'. Caused by: [Errno 2] No such file or directory: 'config.json'
▼ Hint
- Define
class ConfigurationError(Exception): passas a minimal custom exception. - Inside the
except FileNotFoundError as e:block, writeraise ConfigurationError(f"Could not load config file '{filename}'.") from e. Thefrom epart attaches the original exception as the__cause__of the new one. - In the calling code, catch
ConfigurationError as eand print botheande.__cause__to show both levels of the chain.
▼ Solution & Explanation
Explanation:
raise NewException(...) from e: Thefromkeyword explicitly chains the new exception to the original one. Python setsnew_exception.__cause__ = eand marks the chain as explicit. When the traceback is printed in full, Python shows both exceptions with the message “The above exception was the direct cause of the following exception.”e.__cause__: The attribute holding the original exception. Accessing it programmatically lets you log or display the root cause without relying on the full traceback printout.- Implicit chaining vs. explicit chaining: If you raise a new exception inside an
exceptblock withoutfrom, Python still attaches the original exception as__context__(implicit chaining). Usingfrom eis the explicit form and signals intent clearly. Usingfrom Nonesuppresses the chain entirely when you do not want the original cause exposed. - Domain translation:
FileNotFoundErroris an OS-level detail.ConfigurationErroris a business-level concept. Translating between them at the boundary of your module keeps the public API clean while still preserving the technical root cause for debugging.
Exercise 14: Validate User Input Loop
Problem Statement: Write a program that repeatedly prompts the user to enter a positive integer. If the input is not a valid integer or is not positive, catch the exception or condition, display an error message, and ask again. The loop should only exit when valid input is received.
Purpose: Real applications rarely get clean input on the first attempt. This exercise combines exception handling with a control loop to build a robust input routine – a pattern found in CLI tools, configuration wizards, and any program that interacts directly with a human user.
Given Input: The user types abc, then -5, then 7 across three prompts.
Expected Output:
Enter a positive integer: abc Error: 'abc' is not a valid integer. Try again. Enter a positive integer: -5 Error: -5 is not positive. Try again. Enter a positive integer: 7 You entered: 7
▼ Hint
- Use a
while True:loop. Inside it, wrapint(input(...))in atryblock and catchValueErrorto handle non-integer input. - After a successful conversion, check whether the number is positive using an
ifstatement. If it is not, print an error and usecontinueto restart the loop iteration. - When both checks pass, print the success message and use
breakto exit the loop.
▼ Solution & Explanation
Explanation:
while True:: Creates an infinite loop that runs until abreakstatement is reached. This is the standard Python idiom for “keep trying until success” input loops.except ValueError: ... continue: When the conversion fails, the error is printed andcontinuesends execution back to the top of the loop immediately, skipping the positivity check and thebreak.if number <= 0: ... continue: Handles the case where the input was a valid integer but fails the business rule. Using a plainifhere (rather than raising an exception) is appropriate because this is an expected, normal condition rather than a programming error.break: Exits the loop only after both the type check and the value check have passed. Placingbreakat the end of the loop body after all validations makes the success condition easy to identify at a glance.- Storing
rawseparately: Saving the raw input string inrawbefore converting allows the error message to echo back exactly what the user typed, which is more helpful than a generic “invalid input” message.
Exercise 15: Context Manager with Exceptions
Problem Statement: Implement a custom context manager class SuppressError using __enter__ and __exit__ that silently suppresses a specific exception type passed at construction time. Demonstrate it suppressing a ValueError while letting a TypeError propagate normally.
Purpose: Understanding what happens inside a with statement unlocks a powerful Python design pattern. Context managers are used everywhere – in file handling, database transactions, threading locks, and test frameworks. Knowing how __exit__ controls exception propagation lets you build your own resource-management and error-suppression utilities.
Given Input: Use SuppressError(ValueError) as a context manager around a block that raises ValueError, then demonstrate that a TypeError raised inside a second block is not suppressed.
Expected Output:
Entering context... ValueError suppressed. Outside context - execution continued. Entering context... TypeError: unsupported operand type(s) for +: 'int' and 'str'
▼ Hint
- Define
__enter__(self)to print the entry message and returnself(or any value you want bound to theasvariable). __exit__(self, exc_type, exc_val, exc_tb)receives three arguments when an exception occurs: the exception class, the instance, and the traceback. If no exception occurred, all three areNone.- Return
Truefrom__exit__to suppress the exception, orFalse(orNone) to let it propagate. Checkexc_typeagainstself.exception_typeusingissubclassto decide which to do.
▼ Solution & Explanation
Explanation:
__enter__(self): Called at the start of thewithblock. Whatever it returns is bound to the variable afteras. Here it returnsself, but it could return any object – a file handle, a database cursor, orNone.__exit__(self, exc_type, exc_val, exc_tb): Called when thewithblock exits, whether normally or due to an exception. When no exception occurred, all three parameters areNone. When an exception did occur, they hold the type, value, and traceback respectively.issubclass(exc_type, self.exception_type): Usingissubclassrather thanexc_type == self.exception_typemeans the suppression also applies to subclasses of the target exception, which is consistent with howexceptclauses work.- Return value of
__exit__: ReturningTrueis the signal to Python that the exception has been handled and should be suppressed. ReturningFalseorNonelets the exception propagate as if thewithblock were not there. This is the single most important rule of context manager exception handling.
Exercise 16: Exception Hierarchy
Problem Statement: Define a three-level exception hierarchy: AppError as the base, DatabaseError inheriting from it, and ConnectionError inheriting from DatabaseError. Raise a ConnectionError and show that catching DatabaseError or AppError also intercepts it, while catching an unrelated exception type does not.
Purpose: Exception hierarchies are the backbone of professional Python library design. They let users of your code choose how broadly or narrowly to catch errors. A caller that wants to handle all database problems catches DatabaseError; one that wants only connectivity failures catches ConnectionError. This exercise makes that mechanism concrete.
Given Input: Raise ConnectionError("host unreachable") and catch it three times – once with each level of the hierarchy – to show that all three work.
Expected Output:
Caught as ConnectionError: host unreachable Caught as DatabaseError: host unreachable Caught as AppError: host unreachable
▼ Hint
- Define each class with a single-line body of
pass:class AppError(Exception): pass, thenclass DatabaseError(AppError): pass, thenclass ConnectionError(DatabaseError): pass. - Write three separate
try/exceptblocks, each raisingConnectionErrorand catching a different level of the hierarchy, to demonstrate the relationship. - Note that Python has a built-in
ConnectionError. Prefix your class names with your module or app name (e.g.,AppConnectionError) in real projects to avoid shadowing built-ins.
▼ Solution & Explanation
Explanation:
- Inheritance chain:
AppConnectionErroris aDatabaseError, which is anAppError, which is anException. Python’sexceptclause usesisinstancesemantics, so catching any ancestor in the chain will intercept the raised exception. passas body: For simple marker exceptions that carry no extra data beyond a message string, a body ofpassis perfectly valid. The inherited__init__fromExceptionalready handles storing and displaying the message.- Catching broadly vs. narrowly: Catching
AppErrorin a top-level handler is a common pattern for “catch all application errors and log them.” Lower layers catch specific subclasses to attempt targeted recovery. This mirrors how logging frameworks and web frameworks handle request-level errors. - Avoid shadowing built-ins: Python has a built-in
ConnectionError(a subclass ofOSError). Defining a class with the same name in module scope shadows it. Prefixing with your app or module name (as done here withAppConnectionError) prevents subtle bugs where code expecting the built-in gets yours instead.
Exercise 17: Logging Exceptions
Problem Statement: Configure Python’s logging module to write to a file called app.log. Write a function divide(a, b) that catches a ZeroDivisionError and uses logging.exception() to record the full traceback to the log file before returning None.
Purpose: Printing errors to the console is fine for learning, but production code needs persistent, structured logs. Python’s built-in logging module provides log levels, file rotation, formatters, and – crucially – the ability to capture full tracebacks automatically. This exercise builds the habit of logging exceptions properly from the start.
Given Input: Call divide(10, 0).
Expected Output:
Console: divide(10, 0) returned None app.log: ERROR:root:Division failed - ZeroDivisionError with full traceback
▼ Hint
- Call
logging.basicConfig(filename="app.log", level=logging.ERROR)once at the top of your script to configure the file handler. - Inside the
exceptblock, uselogging.exception("your message")rather thanlogging.error(). The difference is thatlogging.exception()automatically appends the full traceback to the log entry without any extra work. - Open
app.login a text editor after running the script to inspect the full traceback that was written.
▼ Solution & Explanation
Explanation:
logging.basicConfig(...): Sets up the root logger with a file handler, minimum log level, and a format string. The%(asctime)stoken adds a timestamp to every entry, making it much easier to trace when errors occurred in a running system.logging.exception(msg): Logs atERRORlevel and automatically appends the current exception’s full traceback to the log entry. It must be called from inside an activeexceptblock so Python knows which exception to attach. Using this instead oflogging.error(msg, exc_info=True)is simply more concise – they produce identical output.- Log levels: Python’s logging hierarchy from lowest to highest is:
DEBUG,INFO,WARNING,ERROR,CRITICAL. Settinglevel=logging.ERRORmeans onlyERRORandCRITICALmessages are written. Adjust the level toDEBUGduring development to capture more detail. - Why log and return
None?: The function handles the error internally (logging it) and returns a safe sentinel value. The caller does not need to know an exception occurred unless it checks the return value. This pattern suits library functions where the caller may not always want to write their owntry/except.
Exercise 18: Retry Decorator
Problem Statement: Write a @retry(times=3) decorator factory that re-runs the decorated function up to times attempts if it raises an exception. If all attempts fail, re-raise the last exception. Demonstrate it on a function that fails twice and succeeds on the third call.
Purpose: Transient failures – network timeouts, temporary service unavailability, rate-limit responses – are a fact of life in distributed systems. A retry decorator is one of the most reused tools in a Python developer’s toolkit. Building it from scratch cements understanding of decorators, closures, and exception handling all at once.
Given Input: A function flaky_call() that fails on the first two calls and succeeds on the third.
Expected Output:
Attempt 1 failed: temporary failure Attempt 2 failed: temporary failure Attempt 3 succeeded. Result: success
▼ Hint
retrymust be a function that acceptstimesand returns a decorator (a function that accepts the target function). This two-level nesting is the standard pattern for decorators that take arguments.- Inside the innermost wrapper, use a
for attempt in range(1, times + 1):loop. Wrap the function call intry/except Exceptionand store the exception. Break out of the loop on success. - After the loop, if the function never succeeded, re-raise the stored exception with a bare
raise last_exception.
▼ Solution & Explanation
Explanation:
- Three-level nesting:
retry(times)is the factory that captures the argument.decorator(func)is the actual decorator that wraps the target function.wrapper(*args, **kwargs)is the replacement function that runs at call time. Each level closes over the variables of the level above it. @functools.wraps(func): Copies the original function’s__name__,__doc__, and other metadata ontowrapper. Without it, debugging tools and introspection would showwrapperinstead of the real function name, making tracebacks harder to read.last_exception = e: Stores the most recent exception so it can be re-raised after all attempts are exhausted. Without this, the variable would go out of scope when the loop ends and the exception would be lost.- Catching
Exceptionbroadly: The decorator catches allExceptionsubclasses by design, since it cannot know which specific errors the decorated function might raise. In production you would typically add anexceptionsparameter so callers can restrict which error types trigger a retry – for example,@retry(times=3, on=requests.Timeout).
Exercise 19: Thread-Safe Exception Handling
Problem Statement: Launch five threads that each perform a division. Some will raise a ZeroDivisionError. Catch exceptions inside each thread and store them in a shared list protected by a threading.Lock. After all threads complete, iterate the list and report all failures.
Purpose: Exceptions raised in a thread do not propagate to the main thread automatically – they simply terminate the thread silently unless you handle them explicitly. This exercise teaches the correct pattern for capturing and surfacing thread-level errors in concurrent Python programs.
Given Input: Five threads dividing 10 by the values [2, 0, 5, 0, 1].
Expected Output:
Thread-1: 10 / 2 = 5.0 Thread-3: 10 / 5 = 2.0 Thread-5: 10 / 1 = 10.0 Errors collected: Thread-2: division by zero Thread-4: division by zero
▼ Hint
- Create a
errors = []list and alock = threading.Lock()before spawning threads. Both are shared across all thread functions via closure or by passing them as arguments. - Inside the thread function, wrap the division in
try/except ZeroDivisionError. In theexceptblock, usewith lock:to acquire the lock before appending toerrors. - After starting all threads, call
t.join()on each to wait for completion before reading theerrorslist in the main thread.
▼ Solution & Explanation
Explanation:
- Exceptions do not cross thread boundaries: A
ZeroDivisionErrorraised inside athreading.Threadtarget function terminates that thread silently. It never reaches the main thread’s exception handlers. The only way to surface it is to catch it inside the thread and communicate it through a shared data structure. with lock:before appending: Theerrorslist is a shared mutable object. Multiple threads could attempt to append at the same moment, and while CPython’s GIL often makes individual list appends safe in practice, using aLockis the correct and portable approach that works regardless of the Python implementation.t.join(): Blocks the main thread until the target thread finishes. Callingjoin()on every thread before readingerrorsguarantees that all threads have completed and all exceptions have been appended before the report is printed.- Alternative –
concurrent.futures: TheThreadPoolExecutorfromconcurrent.futureshandles exception collection automatically. Callingfuture.result()re-raises the exception in the main thread. For new code that does not need direct thread control, this is the preferred approach.
Exercise 20: Exception in Generator
Problem Statement: Write a generator function file_lines(filename) that yields lines from a file one at a time, handles IOError gracefully if the file cannot be opened, and supports receiving injected exceptions from outside using generator.throw(). Demonstrate injecting a RuntimeError mid-iteration to cancel reading early.
Purpose: Generators are lazy pipelines, and like any pipeline they need to handle both internal errors (bad file) and external signals (cancel this operation). The throw() method is the standard way to push an exception into a running generator, and understanding it is essential for building cooperative async-style systems and robust data pipelines.
Given Input: A file sample.txt with three lines. Yield the first line normally, then inject a RuntimeError before the second line is yielded.
Expected Output:
Line 1: Hello from line 1 Generator cancelled: operation aborted Done.
▼ Hint
- Open the file inside a
try/except IOErrorblock at the top of the generator. If the file cannot be opened, print an error and return (which stops the generator immediately). - Wrap the
yield linestatement in its owntry/except RuntimeError. Whengenerator.throw(RuntimeError, "message")is called, Python resumes the generator at theyieldand immediately raisesRuntimeErrorthere, which your innerexceptcan catch. - After catching the injected exception, use
returnto stop the generator cleanly, or re-raise if you want the caller to see it.
▼ Solution & Explanation
Explanation:
try/except IOErroraroundopen(): Guards the file open attempt at the very start of the generator. If it fails, the generator prints the error and exits viareturn, which raisesStopIterationto the caller – the same signal as a generator that ran to completion.yield line.rstrip("\n")insidetry: The key insight is thatyieldis a suspension point. When the generator is paused atyield, callinggen.throw(ExcType, msg)resumes it and immediately raises the given exception at that exact line. The surroundingtry/except RuntimeErrorcatches it there.returnafter catching the injected exception: Usingreturninside a generator raisesStopIteration, signalling the caller that the generator is done. The caller wrapsgen.throw()in atry/except StopIterationto handle this cleanly.generator.throw()vs.generator.close():close()is a convenience method that callsthrow(GeneratorExit). It is the mechanism Python uses to clean up generators when they are garbage-collected.throw()with a custom exception type lets you inject any signal you define – making it a flexible cancellation and communication channel in data pipelines.

Leave a Reply