Statement of Completion#8313bfbf
Control Flow
medium
Exception Handling in Python: From Basics to Best Practices
Resolution
Activities
Project.ipynb
Exception Handling and Debugging in Python¶
Activities¶
Activity 1: Catch the Bug 🐛¶
In [1]:
# Write your code here ...
def divide_numbers(a, b):
if b == 0:
raise ZeroDivisionError("Cannot divide by zero")
else:
return a/b
In [3]:
divide_numbers(10, 0)
--------------------------------------------------------------------------- ZeroDivisionError Traceback (most recent call last) Cell In[3], line 1 ----> 1 divide_numbers(10, 0) Cell In[1], line 5, in divide_numbers(a, b) 3 def divide_numbers(a, b): 4 if b == 0: ----> 5 raise ZeroDivisionError("Cannot divide by zero") 6 else: 7 return a/b ZeroDivisionError: Cannot divide by zero
Activity 2. Handle Invalid Input 🧮¶
In [10]:
def parse_input(input_val):
try:
return int(input_val)
except ValueError:
return "Invalid input"
In [11]:
parse_input("a")
Out[11]:
'Invalid input'
Activity 3. Safe List Access 🔢¶
In [14]:
def get_element(lst, index):
try:
return lst[index]
except IndexError:
return "Index out of range"
In [18]:
get_element(["a", "b", "c"], 3)
Out[18]:
'Index out of range'
Activity 4. Use Assertions for Debug Checks 🚨¶
In [29]:
def process_input(name):
assert name is not None, "Name must not be None"
return len(name)
In [30]:
process_input(None)
--------------------------------------------------------------------------- AssertionError Traceback (most recent call last) Cell In[30], line 1 ----> 1 process_input(None) Cell In[29], line 2, in process_input(name) 1 def process_input(name): ----> 2 assert name is not None, "Name must not be None" 3 return len(name) AssertionError: Name must not be None
Activity 5. Log Exception Type 🔍¶
In [32]:
def log_exception():
try:
return 10 / 0
except ZeroDivisionError as e:
return type(e).__name__
In [33]:
log_exception()
Out[33]:
'ZeroDivisionError'
Activity 6. Use raise from
to Track Root Cause 🔗¶
In [36]:
def double_wrap_exception():
try:
return int("abc")
except ValueError as e:
raise RuntimeError("Failed to convert input") from e
In [37]:
double_wrap_exception()
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) Cell In[36], line 3, in double_wrap_exception() 2 try: ----> 3 return int("abc") 4 except ValueError as e: ValueError: invalid literal for int() with base 10: 'abc' The above exception was the direct cause of the following exception: RuntimeError Traceback (most recent call last) Cell In[37], line 1 ----> 1 double_wrap_exception() Cell In[36], line 5, in double_wrap_exception() 3 return int("abc") 4 except ValueError as e: ----> 5 raise RuntimeError("Failed to convert input") from e RuntimeError: Failed to convert input
Activity 7. Trace Specific Error with traceback 📜¶
In [41]:
import traceback
def trace_zero_division():
try:
return 10/0
except Exception:
tb_str = traceback.format_exc()
last_line = tb_str.strip().split('\n')[-1]
return last_line
In [42]:
trace_zero_division()
Out[42]:
'ZeroDivisionError: division by zero'
Activity 8. Catch Multiple Exception Types 🧠¶
In [47]:
def handle_multiple_errors(val):
try:
val += int("5")
return val
except (ValueError, TypeError):
return "Handled"
In [49]:
handle_multiple_errors("q")
Out[49]:
'Handled'
Activity 9. Return Custom Traceback Info 🔍¶
In [51]:
def custom_trace():
try:
open("missing.txt", "r")
except Exception as e:
return type(e).__name__
In [52]:
custom_trace()
Out[52]:
'FileNotFoundError'
Activity 10. Use Assertions for Unit Testing 🔧¶
In [54]:
def check_result(num):
result = num * 10
assert result >= 0, "Result must be non-negative"
return result
In [56]:
check_result(-3)
--------------------------------------------------------------------------- AssertionError Traceback (most recent call last) Cell In[56], line 1 ----> 1 check_result(-3) Cell In[54], line 3, in check_result(num) 1 def check_result(num): 2 result = num * 10 ----> 3 assert result >= 0, "Result must be non-negative" 4 return result AssertionError: Result must be non-negative
Activity 11. Create and Raise a Custom Debug Exception ⚠️¶
In [60]:
class DebugException(Exception):
pass
def trigger_debug():
raise DebugException("Debugging error")
In [61]:
trigger_debug()
--------------------------------------------------------------------------- DebugException Traceback (most recent call last) Cell In[61], line 1 ----> 1 trigger_debug() Cell In[60], line 5, in trigger_debug() 4 def trigger_debug(): ----> 5 raise DebugException("Debugging error") DebugException: Debugging error
Activity 12. Capture and Compare Error Messages 🔤¶
In [63]:
def error_text():
try:
raise ValueError("Something went wrong")
except ValueError as e:
return str(e)
In [65]:
error_text()
Out[65]:
'Something went wrong'
Activity 13. Avoid Bare except:
Statements 🚫¶
In [67]:
def safe_divide(a, b):
try:
return a / b
except ZeroDivisionError:
return "Division by zero not allowed"
In [68]:
safe_divide(10, 0)
Out[68]:
'Division by zero not allowed'
Activity 14. Keep try
Block Minimal¶
In [70]:
def precise_try(a, b):
result = None
try:
result = a / b
except ZeroDivisionError:
result = "Fail"
return result
In [71]:
precise_try(10, 0)
Out[71]:
'Fail'
Activity 15. Avoid Repeating Code¶
In [74]:
def format_name(name):
try:
formatted = name.strip().title()
except AttributeError:
formatted = "Invalid input"
return formatted
In [75]:
format_name("AsVDd")
Out[75]:
'Asvdd'
Activity 16. Use Custom Exception for Logic Errors¶
In [80]:
class InvalidAgeError(Exception):
pass
def check_age(age):
if age < 18:
raise InvalidAgeError("Must be 18 or older")
else:
return "Access granted"
In [81]:
check_age(13)
--------------------------------------------------------------------------- InvalidAgeError Traceback (most recent call last) Cell In[81], line 1 ----> 1 check_age(13) Cell In[80], line 6, in check_age(age) 4 def check_age(age): 5 if age < 18: ----> 6 raise InvalidAgeError("Must be 18 or older") 7 else: 8 return "Access granted" InvalidAgeError: Must be 18 or older
Activity 17. Cleanup Resources with finally
¶
In [86]:
def close_file_safely():
check_file = None
try:
check_file = open("file.txt", "w")
check_file.write("aaaaaa")
finally:
if check_file:
check_file.close()
return "done"
In [87]:
close_file_safely()
Out[87]:
'done'
In [ ]:
Author.ipynb
In [2]:
exec(open("utils.py").read())
Control Flow Management For Exception Handling¶
Introduction¶
Write Smarter, Safer Code with Exception Handling¶
Every programmer hits a wall at some point—your code crashes, your logic breaks, or worse, something works… but not quite right. That’s where exception handling comes in. But handling exceptions isn’t just about stopping your code from crashing—it's about doing it the right way.
In this special practice project, we’ll focus on two crucial objectives every developer must master:
🔹 Debugging with Exception Handling
🔹 Best Practices for Exception Handling
These are the skills that separate beginner code from professional-grade software. Think of this as your training ground for writing code that not only runs—but survives unexpected inputs, handles failures gracefully, and can be debugged with ease when things go wrong. 🧠💻
💡 Why This Lab Matters¶
In real-world projects, exception handling isn’t just a feature—it’s a core foundation for building apps that users can trust. From web applications to data pipelines, everything relies on well-thought-out error handling. In this project, you'll learn how to:
✅ Write clean and specific try-except blocks
✅ Avoid common mistakes like over-catching or swallowing errors
✅ Use logging instead of print statements for traceability
✅ Apply finally for safe cleanup of resources
✅ Leverage debugging tools like traceback, assert, and breakpoint()
✅ Use strategic exception flows to pinpoint and fix bugs
🚀 By the End of This Practice Project…¶
You won’t just know how to handle exceptions—you’ll understand how to do it effectively, professionally, and with confidence. You’ll gain critical thinking skills for debugging, tools to prevent silent failures, and the discipline to keep your code clean and manageable.
So if you're ready to take your Python coding to the next level—where your programs don’t just run, but run smart—then let’s dive into the best practices and debugging essentials of exception handling. This is where good code becomes great. 💥🐍
Activities¶
Basics of Debugging with Exception Handling¶
Activity 1. Catch the Bug 🐛¶
Write a function that divides two numbers. If the denominator is zero, catch the ZeroDivisionError and return a custom error message as Cannot divide by zero
.
In [3]:
def divide_numbers(a, b):
try:
result = a / b
except ZeroDivisionError:
result = "Cannot divide by zero"
return result
# Assertion:
assert_student_function_name_equals('divide_numbers', (6, 2), expected_value=3.0)
assert divide_numbers(10, 0) == "Cannot divide by zero", "Test failed for ZeroDivisionError handling"
Activity 2. Handle Invalid Input 🧮¶
Define a function parse_input()
that takes input_val
as input. Use a try block to convert input_val
to an integer and return it. If a ValueError occurs (indicating invalid input for integer conversion), use an except block to catch it and return the string "Invalid input".
In [ ]:
def parse_input(input_val):
try:
return int(input_val)
except ValueError:
return "Invalid input"
assert parse_input("abc") == "Invalid input", "Test failed for ValueError handling"
assert_student_function_name_equals('parse_input', ("42"), expected_value=42)
Activity 3. Safe List Access 🔢¶
Define a function get_element()
that takes a list lst and an index. Use a try block to access and return the element at last index. If an IndexError
occurs (meaning the index is invalid for the list), use an except block to catch it and return the string "Index out of range".
In [ ]:
def get_element(lst, index):
try:
return lst[index]
except IndexError:
return "Index out of range"
# Assertion
assert get_element([1, 2, 3], 5) == "Index out of range", "Test failed for IndexError handling"
assert_student_function_name_equals('get_element', ([1, 2, 3], 1), expected_value=2)
Activity 4. Use Assertions for Debug Checks 🚨¶
Define a function process_input that accepts data. Use an assert statement to check that data is not None, providing the message "Data must not be None" for the assertion. If the assertion passes, return the length of data.
In [8]:
def process_input(data):
assert data is not None, "Data must not be None"
return len(data)
In [ ]:
# Assertion
assert_student_function_name_equals('process_input', ("hello"), expected_value=5)
try:
process_input(None)
except AssertionError as e:
assert str(e) == "Data must not be None", "Test failed for invalid AssertionError handling"
Activity 5. Log Exception Type 🔍¶
Define a function log_exception()
. Next, inside a try block, attempt to divide 10
by zero to intentionally cause an error. Use except Exception as e to catch the exception. Within the except block, return the name of the caught exception's type using type(e).__name__
.
In [ ]:
def log_exception():
try:
10 / 0
except Exception as e:
return type(e).__name__
# Assertion
assert log_exception() == "ZeroDivisionError", "Test failed for ZeroDivisionError handling"
Advanced Debugging with Exception Handling¶
Activity 6. Use raise from to Track Root Cause 🔗¶
Define a function double_wrap_exception. Inside a try block, attempt an operation that causes an initial error, such as int("abc"). Catch the resulting ValueError as e. In the except block, raise a new RuntimeError with the message "Failed to convert input", using the from e syntax to link it explicitly to the original caught exception e.
In [16]:
def double_wrap_exception():
try:
int("abc")
except ValueError as e:
raise RuntimeError("Failed to convert input") from e
In [17]:
# Assertion
try:
double_wrap_exception()
except RuntimeError as e:
# assert isinstance(e.__cause__, ValueError) == True, "Test failed for double wrapping exception handling"
assert str(e) == "Failed to convert input", "Test failed for invalid exception handling"
Activity 7. Trace Specific Error with traceback 📜¶
First, import the traceback module. Define a function trace_zero_division. Use a try block to perform an operation that causes an error (e.g., 10 / 0). In the except Exception: block, get the formatted traceback string using traceback.format_exc(). Finally, process this string to extract and return only the last line of the traceback.
The
breakpoint()
function in Python is a built-in debugging tool that allows you to set a breakpoint in your code.
breakpoint()
is particularly useful for debugging exceptions because it allows you to inspect the state of your program just before an exception occurs
In [4]:
import traceback
def trace_zero_division():
try:
10 / 0
except Exception:
tb = traceback.format_exc()
return tb.strip().splitlines()[-1]
trace_zero_division()
Out[4]:
'ZeroDivisionError: division by zero'
In [ ]:
assert "ZeroDivisionError" in trace_zero_division() == True, "Test failed for ZeroDivisionError traceback handling"
Activity 8. Catch Multiple Exception Types 🧠¶
Define a function handle_multiple_errors that accepts val. In a try block, attempt to convert val to an integer and then add it to the string "5" (an operation that might cause ValueError or TypeError). Use a single except block that catches both ValueError and TypeError (using a tuple). Inside this block, return the string "Handled".
In [12]:
def handle_multiple_errors(val):
try:
return int(val) + "5"
except (ValueError, TypeError):
return "Handled"
In [ ]:
# Assertion
assert handle_multiple_errors("10") == "Handled", "Test failed for invalid error handling"
Activity 9. Return Custom Traceback Info 🔍¶
Define a function custom_trace. Inside a try block, attempt to open a non-existent file like "missing.txt" to trigger an error. Use except Exception as e to catch the exception. Within the except block, return the name of the caught exception's type using type(e).__name__
.
In [ ]:
def custom_trace():
try:
open("missing.txt")
except Exception as e:
return type(e).__name__
In [ ]:
# Assertion
assert custom_trace() == "FileNotFoundError", "Test failed for FileNotFoundError handling"
Activity 10. Use Assertions for Unit Testing 🔧¶
Define a function square that takes num. Calculate the square and store it in result. Use an assert statement to check if result is non-negative (result >= 0), providing the message "Result must be non-negative". If the assertion passes, return the result.
In [ ]:
def square(num):
result = num * num
assert result >= 0, "Result must be non-negative"
return result
In [ ]:
# Assertion
assert_student_function_name_equals('square', (3), expected_value=9)
try:
square(-1)
except AssertionError as e:
assert str(e) == "Result must be non-negative", "Test failed for invalid AssertionError handling"
Activity 11. Create and Raise a Custom Debug Exception ⚠️¶
First, define a custom exception class named DebugException that inherits from Exception, using pass for its body. Then, define a function trigger_debug. Inside this function, use the raise keyword to throw an instance of your DebugException, providing the message "Debugging error".
In [ ]:
class DebugException(Exception):
pass
def trigger_debug():
raise DebugException("Debugging error")
In [ ]:
# Assertion
try:
trigger_debug()
except DebugException as e:
assert str(e) == "Debugging error", "Test failed for DebugException handling"
Activity 12. Capture and Compare Error Messages 🔤¶
Define a function error_text()
. Inside a try block, use raise to throw a ValueError with the specific message Something went wrong
. Use except ValueError
as e
to catch this exception. Within the except block, convert the caught exception object e to a string using str(e) and return the resulting message.
In [ ]:
def error_text():
try:
raise ValueError("Something went wrong")
except ValueError as e:
return str(e)
In [ ]:
# Assertion
assert error_text() == "Something went wrong", "Test failed for ValueError handling"
Best Practices for Exception Handling¶
Activity 13. Avoid Bare Except¶
Refactor the code to catch a specific exception (ZeroDivisionError) instead of using a generic except:
block. Your task is to create a function named safe_divide()
safely divide two numbers and return a meaningful message if a division by zero occurs.
def safe_divide(a, b):
try:
return a / b
except ZeroDivisionError:
return "Division by zero not allowed"
Why it’s a Best Practice:¶
Catching all exceptions blindly (except:)
can suppress important errors, including programming errors like KeyboardInterrupt
or SystemExit
, making debugging much harder. Always catch the specific exceptions you expect.
In [ ]:
# Assertion
assert safe_divide(10, 0) == "Division by zero not allowed", "Test failed for ZeroDivisionError handling"
Activity 14. Keep try Block Minimal¶
Refactor the code to ensure that the try block contains only the statement that may raise an exception.
Why it’s a Best Practice:¶
Minimizing the code inside a try block helps pinpoint where errors are likely to occur. This improves readability and reduces the risk of masking unrelated bugs.
In [ ]:
def precise_try(a, b):
result = None
try:
result = a / b
except ZeroDivisionError:
result = "Fail"
return result
In [ ]:
# Assertion
assert precise_try(10, 0) == "Fail", "Test failed for ZeroDivisionError handling"
Activity 15. Avoid Repeating Code Inside try¶
Refactor the function to avoid repeating logic in both the normal and exception flows. Apply a consistent structure to return a formatted name.
Why it’s a Best Practice: Duplicating code in both try and except blocks leads to maintainability issues. DRY (Don’t Repeat Yourself) is a key principle in writing clean code.
In [ ]:
def format_name(name):
try:
formatted = name.strip().title()
except AttributeError:
formatted = "Invalid input"
return formatted
# Assertion
assert format_name(None) == "Invalid input", "Test failed for AttributeError handling"
assert_student_function_name_equals('format_name', (" bob "), expected_value="Bob")
Activity 16. Use Custom Exception for Logic Errors¶
Create and raise a custom exception when a business rule (e.g., age must be 18+) is violated.
Explanation: Custom exceptions improve clarity and separate logic errors from system errors. This makes the code more self-documenting and maintainable.
In [ ]:
class InvalidAgeError(Exception):
pass
def check_age(age):
if age < 18:
raise InvalidAgeError("Must be 18 or older")
return "Access granted"
In [ ]:
# Assertion:
try:
check_age(15)
except InvalidAgeError as e:
assert str(e) == "Must be 18 or older", "Test failed for InvalidAgeError handling"
assert 'InvalidAgeError' in globals(), "Class 'InvalidAgeError' is not declared globally."
Activity 17. Cleanup Resources with finally¶
Use a finally block to ensure that a file is closed even if an error occurs during file operations.
Why it’s a Best Practice:¶
Using finally guarantees that resources like files, sockets, or database connections are always cleaned up, preventing resource leaks.
In [ ]:
def close_file_safely():
check_file = None
try:
check_file = open("file.txt", "w")
check_file.write("data")
finally:
if check_file:
check_file.close()
return "done"
In [ ]:
# Assertion
assert close_file_safely() == "done", "Test failed for file closing handling"