From 925414488beb84ec7ed6ca9632d52a6e919e6de6 Mon Sep 17 00:00:00 2001 From: Daniel Valdenegro Ibarra Date: Thu, 1 Aug 2024 00:08:42 +0100 Subject: [PATCH] Update 10-defensive.md complete change of the lesson to cover exceptions and try except pattern --- episodes/10-defensive.md | 596 +++++++++++---------------------------- 1 file changed, 157 insertions(+), 439 deletions(-) diff --git a/episodes/10-defensive.md b/episodes/10-defensive.md index c945a9627..c376d308c 100644 --- a/episodes/10-defensive.md +++ b/episodes/10-defensive.md @@ -6,11 +6,9 @@ exercises: 10 ::::::::::::::::::::::::::::::::::::::: objectives -- Explain what an assertion is. -- Add assertions that check the program's state is correct. -- Correctly add precondition and postcondition assertions to functions. -- Explain what test-driven development is, and use it when creating new functions. -- Explain why variables should be initialized using actual data values rather than arbitrary constants. +- Understand the importance of input validation. +- Learn how to use exceptions and raise errors in Python. +- Practice writing defensive code to validate inputs. :::::::::::::::::::::::::::::::::::::::::::::::::: @@ -21,513 +19,233 @@ exercises: 10 :::::::::::::::::::::::::::::::::::::::::::::::::: Our previous lessons have introduced the basic tools of programming: -variables and lists, -file I/O, -loops, -conditionals, -and functions. -What they *haven't* done is show us how to tell -whether a program is getting the right answer, -and how to tell if it's *still* getting the right answer -as we make changes to it. - -To achieve that, -we need to: - -- Write programs that check their own operation. -- Write and run tests for widely-used functions. -- Make sure we know what "correct" actually means. - -The good news is, -doing these things will speed up our programming, -not slow it down. -As in real carpentry --- the kind done with lumber --- the time saved -by measuring carefully before cutting a piece of wood -is much greater than the time that measuring takes. - -## Assertions - -The first step toward getting the right answers from our programs -is to assume that mistakes *will* happen -and to guard against them. -This is called [defensive programming](../learners/reference.md#defensive-programming), -and the most common way to do it is to add -[assertions](../learners/reference.md#assertion) to our code -so that it checks itself as it runs. -An assertion is simply a statement that something must be true at a certain point in a program. -When Python sees one, -it evaluates the assertion's condition. -If it's true, -Python does nothing, -but if it's false, -Python halts the program immediately -and prints the error message if one is provided. -For example, -this piece of code halts as soon as the loop encounters a value that isn't positive: +- Variables and lists, +- File I/O, +- Loops, +- Conditionals, +- Functions. -```python -numbers = [1.5, 2.3, 0.7, -0.001, 4.4] -total = 0.0 -for num in numbers: - assert num > 0.0, 'Data should only contain positive values' - total += num -print('total is:', total) -``` - -```error ---------------------------------------------------------------------------- -AssertionError Traceback (most recent call last) - in () - 2 total = 0.0 - 3 for num in numbers: -----> 4 assert num > 0.0, 'Data should only contain positive values' - 5 total += num - 6 print('total is:', total) - -AssertionError: Data should only contain positive values -``` +Now it's time to learn how to ensure that our programs always work as intended. Defensive programming is a practice used in software development to ensure a program functions correctly even under unexpected conditions. A key aspect of defensive programming is input validation, which involves checking input data to prevent errors and ensure the program behaves as expected. -Programs like the Firefox browser are full of assertions: -10-20% of the code they contain -are there to check that the other 80–90% are working correctly. -Broadly speaking, -assertions fall into three categories: +## Why is Input Validation Important? -- A [precondition](../learners/reference.md#precondition) - is something that must be true at the start of a function in order for it to work correctly. +- **Reliability**: Ensures that the program receives the correct type and range of input data. +- **Security**: Prevents malicious input from causing harm. +- **Data Integrity**: Maintains the consistency and accuracy of the data. -- A [postcondition](../learners/reference.md#postcondition) - is something that the function guarantees is true when it finishes. +In general, input validation makes our lives easier by preventing the program from running with invalid input and delivering invalid results. -- An [invariant](../learners/reference.md#invariant) - is something that is always true at a particular point inside a piece of code. - -For example, -suppose we are representing rectangles using a [tuple](../learners/reference.md#tuple) -of four coordinates `(x0, y0, x1, y1)`, -representing the lower left and upper right corners of the rectangle. -In order to do some calculations, -we need to normalize the rectangle so that the lower left corner is at the origin -and the longest side is 1.0 units long. -This function does that, -but checks that its input is correctly formatted and that its result makes sense: +The general pattern of input validation usually follows these steps: ```python -def normalize_rectangle(rect): - """Normalizes a rectangle so that it is at the origin and 1.0 units long on its longest axis. - Input should be of the format (x0, y0, x1, y1). - (x0, y0) and (x1, y1) define the lower left and upper right corners - of the rectangle, respectively.""" - assert len(rect) == 4, 'Rectangles must contain 4 coordinates' - x0, y0, x1, y1 = rect - assert x0 < x1, 'Invalid X coordinates' - assert y0 < y1, 'Invalid Y coordinates' - - dx = x1 - x0 - dy = y1 - y0 - if dx > dy: - scaled = dx / dy - upper_x, upper_y = 1.0, scaled - else: - scaled = dx / dy - upper_x, upper_y = scaled, 1.0 - - assert 0 < upper_x <= 1.0, 'Calculated upper X coordinate invalid' - assert 0 < upper_y <= 1.0, 'Calculated upper Y coordinate invalid' - - return (0, 0, upper_x, upper_y) +def some_func(x): + if x does not meet the condition: + Stop the program and print an error message + do something with x ``` +As you can see above, input validation is one of the first, if not the first, things a function does. Different programming languages have different ways of expressing the steps above, with different keywords and built-in functions. In Python, we use the `if` keyword to check conditions, and the `raise` keyword to print error messages. Additionally, Python enforces the use of built-in error classes, called `exceptions`. There are many built-in exceptions (see: [Python Documentation: Errors and Exceptions](https://docs.python.org/3/library/exceptions.html) ), but we will focus on the most common ones: `ValueError` and `TypeError`. -The preconditions on lines 6, 8, and 9 catch invalid inputs: - -```python -print(normalize_rectangle( (0.0, 1.0, 2.0) )) # missing the fourth coordinate -``` +A `ValueError` is raised when a function receives an argument of the right type but an inappropriate value. For example, a function expects a positive integer, but a negative one is passed. -```error ---------------------------------------------------------------------------- -AssertionError Traceback (most recent call last) - in -----> 1 print(normalize_rectangle( (0.0, 1.0, 2.0) )) # missing the fourth coordinate +A `TypeError` is raised when an operation or function is applied to an object of inappropriate type. For example, a function expects a string, but an integer is passed. - in normalize_rectangle(rect) - 4 (x0, y0) and (x1, y1) define the lower left and upper right corners - 5 of the rectangle, respectively.""" -----> 6 assert len(rect) == 4, 'Rectangles must contain 4 coordinates' - 7 x0, y0, x1, y1 = rect - 8 assert x0 < x1, 'Invalid X coordinates' +By focusing on these common exceptions, you can write robust and error-resistant code that handles unexpected inputs gracefully. -AssertionError: Rectangles must contain 4 coordinates -``` +Let's look at a simple example. Let's create a function that takes the square root of a number. This function needs to check for two things. First, that the inputs are indeed numbers, and second, that the inputs are not negative: ```python -print(normalize_rectangle( (4.0, 2.0, 1.0, 5.0) )) # X axis inverted +import math + +def safe_sqrt(x): + if not isinstance(x, (int, float)): + raise TypeError("Input must be a number") + if x < 0: + raise ValueError("Input must be non-negative") + return math.sqrt(x) ``` -```error ---------------------------------------------------------------------------- -AssertionError Traceback (most recent call last) - in -----> 1 print(normalize_rectangle( (4.0, 2.0, 1.0, 5.0) )) # X axis inverted +In this example, the `safe_sqrt` function first checks if the input is a number using isinstance. If the input is not a number, it raises a `TypeError`. Then, it checks if the input is non-negative. If the input is negative, it raises a `ValueError`. If both conditions are met, it returns the square root of the input using the `math.sqrt` function. - in normalize_rectangle(rect) - 6 assert len(rect) == 4, 'Rectangles must contain 4 coordinates' - 7 x0, y0, x1, y1 = rect -----> 8 assert x0 < x1, 'Invalid X coordinates' - 9 assert y0 < y1, 'Invalid Y coordinates' - 10 - -AssertionError: Invalid X coordinates -``` +Notice that the error types allow for the inclusion of a helpful error message. We should always include helpful and descriptive error messages to make it clear what went wrong and why. This practice not only aids in debugging but also helps users of your code understand what input is expected. For example, instead of a generic error message like "Invalid input," a specific message like "Input must be a number" or "Input must be non-negative" provides clear guidance on how to correct the error. Providing detailed error messages is a key aspect of writing maintainable and user-friendly code. -The post-conditions on lines 20 and 21 help us catch bugs by telling us when our -calculations might have been incorrect. -For example, -if we normalize a rectangle that is taller than it is wide everything seems OK: +Now, let's see how we can use this function and evaluate it in Python: ```python -print(normalize_rectangle( (0.0, 0.0, 1.0, 5.0) )) -``` +>>> print(safe_sqrt(9)) # Expected output: 3.0 -```output -(0, 0, 0.2, 1.0) +3.0 ``` -but if we normalize one that's wider than it is tall, -the assertion is triggered: - ```python -print(normalize_rectangle( (0.0, 0.0, 5.0, 1.0) )) -``` +>>> print(safe_sqrt(-1)) # Expected to raise ValueError -```error ---------------------------------------------------------------------------- -AssertionError Traceback (most recent call last) - in -----> 1 print(normalize_rectangle( (0.0, 0.0, 5.0, 1.0) )) - - in normalize_rectangle(rect) - 19 - 20 assert 0 < upper_x <= 1.0, 'Calculated upper X coordinate invalid' ----> 21 assert 0 < upper_y <= 1.0, 'Calculated upper Y coordinate invalid' - 22 - 23 return (0, 0, upper_x, upper_y) - -AssertionError: Calculated upper Y coordinate invalid +Traceback (most recent call last): + File "", line 1, in + File "", line 5, in safe_sqrt +ValueError: Input must be non-negative ``` -Re-reading our function, -we realize that line 14 should divide `dy` by `dx` rather than `dx` by `dy`. -In a Jupyter notebook, you can display line numbers by typing Ctrl\+M -followed by L. -If we had left out the assertion at the end of the function, -we would have created and returned something that had the right shape as a valid answer, -but wasn't. -Detecting and debugging that would almost certainly have taken more time in the long run -than writing the assertion. - -But assertions aren't just about catching errors: -they also help people understand programs. -Each assertion gives the person reading the program -a chance to check (consciously or otherwise) -that their understanding matches what the code is doing. - -Most good programmers follow two rules when adding assertions to their code. -The first is, *fail early, fail often*. -The greater the distance between when and where an error occurs and when it's noticed, -the harder the error will be to debug, -so good code catches mistakes as early as possible. - -The second rule is, *turn bugs into assertions or tests*. -Whenever you fix a bug, write an assertion that catches the mistake -should you make it again. -If you made a mistake in a piece of code, -the odds are good that you have made other mistakes nearby, -or will make the same mistake (or a related one) -the next time you change it. -Writing assertions to check that you haven't [regressed](../learners/reference.md#regression) -(i.e., haven't re-introduced an old problem) -can save a lot of time in the long run, -and helps to warn people who are reading the code -(including your future self) -that this bit is tricky. - -## Test-Driven Development - -An assertion checks that something is true at a particular point in the program. -The next step is to check the overall behavior of a piece of code, -i.e., -to make sure that it produces the right output when it's given a particular input. -For example, -suppose we need to find where two or more time series overlap. -The range of each time series is represented as a pair of numbers, -which are the time the interval started and ended. -The output is the largest range that they all include: - -![](fig/python-overlapping-ranges.svg){alt='Graph showing three number lines and, at the bottom, the interval that they overlap.'} - -Most novice programmers would solve this problem like this: - -1. Write a function `range_overlap`. -2. Call it interactively on two or three different inputs. -3. If it produces the wrong answer, fix the function and re-run that test. - -This clearly works --- after all, thousands of scientists are doing it right now --- but -there's a better way: - -1. Write a short function for each test. -2. Write a `range_overlap` function that should pass those tests. -3. If `range_overlap` produces any wrong answers, fix it and re-run the test functions. - -Writing the tests *before* writing the function they exercise -is called [test-driven development](../learners/reference.md#test-driven-development) (TDD). -Its advocates believe it produces better code faster because: - -1. If people write tests after writing the thing to be tested, - they are subject to confirmation bias, - i.e., - they subconsciously write tests to show that their code is correct, - rather than to find errors. -2. Writing tests helps programmers figure out what the function is actually supposed to do. - -We start by defining an empty function `range_overlap`: - -```python -def range_overlap(ranges): - pass -``` - -Here are three test statements for `range_overlap`: - ```python -assert range_overlap([ (0.0, 1.0) ]) == (0.0, 1.0) -assert range_overlap([ (2.0, 3.0), (2.0, 4.0) ]) == (2.0, 3.0) -assert range_overlap([ (0.0, 1.0), (0.0, 2.0), (-1.0, 1.0) ]) == (0.0, 1.0) -``` - -```error ---------------------------------------------------------------------------- -AssertionError Traceback (most recent call last) - in () -----> 1 assert range_overlap([ (0.0, 1.0) ]) == (0.0, 1.0) - 2 assert range_overlap([ (2.0, 3.0), (2.0, 4.0) ]) == (2.0, 3.0) - 3 assert range_overlap([ (0.0, 1.0), (0.0, 2.0), (-1.0, 1.0) ]) == (0.0, 1.0) +>>> print(safe_sqrt("nine")) # Expected to raise TypeError -AssertionError: +Traceback (most recent call last): + File "", line 1, in + File "", line 3, in safe_sqrt +TypeError: Input must be a number ``` -The error is actually reassuring: -we haven't implemented any logic into `range_overlap` yet, -so if the tests passed, it would indicate that we've written -an entirely ineffective test. +The evaluated results of the `safe_sqrt` function are as follows: -And as a bonus of writing these tests, -we've implicitly defined what our input and output look like: -we expect a list of pairs as input, -and produce a single pair as output. +- `safe_sqrt(9)` returns 3.0, as expected. +- `safe_sqrt(-1)` raises a ValueError with the message "Input must be non-negative". +- `safe_sqrt("nine")` raises a TypeError with the message "Input must be a number". -Something important is missing, though. -We don't have any tests for the case where the ranges don't overlap at all: +Hurray! We managed to create a function that is robust to the most common input errors. These useful error messages will help future users (ourselves included) to input the correct data and get the correct results. However, sometimes we cannot afford to stop the execution of a program just because our function fails. For example, let's imagine our `safe_sqrt` function is part of a bigger program that has to get the square root of many numbers stored in a list, from which we do not have access before doing the processing. -```python -assert range_overlap([ (0.0, 1.0), (5.0, 6.0) ]) == ??? -``` +## The `Try` and `Except` Pattern -What should `range_overlap` do in this case: -fail with an error message, -produce a special value like `(0.0, 0.0)` to signal that there's no overlap, -or something else? -Any actual implementation of the function will do one of these things; -writing the tests first helps us figure out which is best -*before* we're emotionally invested in whatever we happened to write -before we realized there was an issue. +In cases such as the one described above, where we cannot afford the program to stop for a single erroneous input, we would like to have the option to try the function with the unknown input, and if it works, we keep the output. However, if it does not work (e.g., raises an `Exception`), we decide separately what to do, depending on the case. -And what about this case? +Going back to the example, let's recall that we have a program that gets the square root of many numbers stored in a large list. This program uses our safe_sqrt function and stores the results in a list. This program will run indefinitely, so we cannot afford the program to stop every time it finds something weird in the input numbers. Let's create a function `process_list` that uses `safe_sqrt` to process a list of numbers: ```python -assert range_overlap([ (0.0, 1.0), (1.0, 2.0) ]) == ??? +def process_list(numbers): + results = [] + for num in numbers: + result = safe_sqrt(num) + results.append(result) + return results + +numbers = [9, -1, 16, 'twenty', 25, 36, -49, 64, 81, 100] +print(process_list(numbers)) ``` -Do two segments that touch at their endpoints overlap or not? -Mathematicians usually say "yes", -but engineers usually say "no". -The best answer is "whatever is most useful in the rest of our program", -but again, -any actual implementation of `range_overlap` is going to do *something*, -and whatever it is ought to be consistent with what it does when there's no overlap at all. - -Since we're planning to use the range this function returns -as the X axis in a time series chart, -we decide that: - -1. every overlap has to have non-zero width, and -2. we will return the special value `None` when there's no overlap. - -`None` is built into Python, -and means "nothing here". -(Other languages often call the equivalent value `null` or `nil`). -With that decision made, -we can finish writing our last two tests: +When running the above code, it would produce a traceback when it encounters an invalid input. Here is the expected output and traceback: ```python -assert range_overlap([ (0.0, 1.0), (5.0, 6.0) ]) == None -assert range_overlap([ (0.0, 1.0), (1.0, 2.0) ]) == None +Traceback (most recent call last): + File "example.py", line 11, in + print(process_list(numbers)) + File "example.py", line 5, in process_list + result = safe_sqrt(num) + File "example.py", line 6, in safe_sqrt + raise ValueError("Input must be non-negative") +ValueError: Input must be non-negative ``` -```error ---------------------------------------------------------------------------- -AssertionError Traceback (most recent call last) - in () -----> 1 assert range_overlap([ (0.0, 1.0), (5.0, 6.0) ]) == None - 2 assert range_overlap([ (0.0, 1.0), (1.0, 2.0) ]) == None +This traceback occurs because the function `safe_sqrt` encounters a negative number -1 in the list and raises a `ValueError`. -AssertionError: -``` +In its current form, the function `process_list` is sub-optimal. Remember, our program needs to run indefinitely or until it finishes with the whole list of numbers. In the case above, it stopped at the second number. We need to find a way to manage the exceptions and allow the program to keep running. -Again, -we get an error because we haven't written our function, -but we're now ready to do so: +Here is where the `try` `except` pattern comes in handy. The `try` block lets you test a block of code for errors. The `except` block lets you handle the error. Let's modify our code to handle our exceptions using `try` and `except` in a way that does not stop the program: ```python -def range_overlap(ranges): - """Return common overlap among a set of [left, right] ranges.""" - max_left = 0.0 - min_right = 1.0 - for (left, right) in ranges: - max_left = max(max_left, left) - min_right = min(min_right, right) - return (max_left, min_right) +def process_list(numbers): + results = [] + for num in numbers: + try: + result = safe_sqrt(num) + except Exception as e: + print(f"Error with input {num}: {e}") + result = None # Store None or some other placeholder + results.append(result) + return results + +numbers = [9, -1, 16, 'twenty', 25, 36, -49, 64, 81, 100] +print(process_list(numbers)) ``` -Take a moment to think about why we calculate the left endpoint of the overlap as -the maximum of the input left endpoints, and the overlap right endpoint as the minimum -of the input right endpoints. -We'd now like to re-run our tests, -but they're scattered across three different cells. -To make running them easier, -let's put them all in a function: - -```python -def test_range_overlap(): - assert range_overlap([ (0.0, 1.0), (5.0, 6.0) ]) == None - assert range_overlap([ (0.0, 1.0), (1.0, 2.0) ]) == None - assert range_overlap([ (0.0, 1.0) ]) == (0.0, 1.0) - assert range_overlap([ (2.0, 3.0), (2.0, 4.0) ]) == (2.0, 3.0) - assert range_overlap([ (0.0, 1.0), (0.0, 2.0), (-1.0, 1.0) ]) == (0.0, 1.0) - assert range_overlap([]) == None -``` +In this modified `process_list` function, we use a `try` block to attempt to compute the square root with `safe_sqrt`. If an exception is raised, we catch it with the `except` block, print a helpful error message, and store `None` (or some other placeholder) in the results list. This way, the program continues running even if some inputs are invalid. -We can now test `range_overlap` with a single function call: +In the first case, we used the generic `Exception` to catch any errors. However, since we have different types of exceptions, we can be specific and catch them separately. This allows us to handle different errors in different ways, providing more specific error messages and actions for each case. Here is an example: ```python -test_range_overlap() +def process_list(numbers): + results = [] + for num in numbers: + try: + result = safe_sqrt(num) + except TypeError as e: + print(f"TypeError with input {num}: {e}") + result = None # Store None or some other placeholder + except ValueError as e: + print(f"ValueError with input {num}: {e}") + result = None # Store None or some other placeholder + results.append(result) + return results + +numbers = [9, -1, 16, 'twenty', 25, 36, -49, 64, 81, 100] +print(process_list(numbers)) ``` -```error ---------------------------------------------------------------------------- -AssertionError Traceback (most recent call last) - in () -----> 1 test_range_overlap() - - in test_range_overlap() - 1 def test_range_overlap(): -----> 2 assert range_overlap([ (0.0, 1.0), (5.0, 6.0) ]) == None - 3 assert range_overlap([ (0.0, 1.0), (1.0, 2.0) ]) == None - 4 assert range_overlap([ (0.0, 1.0) ]) == (0.0, 1.0) - 5 assert range_overlap([ (2.0, 3.0), (2.0, 4.0) ]) == (2.0, 3.0) - -AssertionError: -``` +In this code, we use separate `except` blocks to handle `TypeError` and `ValueError` specifically. This way, we can provide more precise error messages and take different actions based on the type of error. -The first test that was supposed to produce `None` fails, -so we know something is wrong with our function. -We *don't* know whether the other tests passed or failed -because Python halted the program as soon as it spotted the first error. -Still, -some information is better than none, -and if we trace the behavior of the function with that input, -we realize that we're initializing `max_left` and `min_right` to 0.0 and 1.0 respectively, -regardless of the input values. -This violates another important rule of programming: -*always initialize from data*. +It is best practice to catch exceptions independently. Using a generic `Exception` or an empty `except` block can lead to invisible errors, making debugging difficult and potentially masking other issues in the code. By specifying the exception types, you ensure that you handle each case appropriately and maintain the clarity and reliability of your code. ::::::::::::::::::::::::::::::::::::::: challenge -## Pre- and Post-Conditions +## Safe Divide -Suppose you are writing a function called `average` that calculates -the average of the numbers in a NumPy array. -What pre-conditions and post-conditions would you write for it? -Compare your answer to your neighbor's: -can you think of a function that will pass your tests but not his/hers or vice versa? +Now that you've learned about input validation, exceptions, and the `try` `except` pattern, here's a challenge for you: -::::::::::::::: solution - -## Solution +1. Write a function `safe_divide` that takes two numbers and returns their division. The function should check for invalid inputs and raise appropriate exceptions if necessary (e.g., division by zero, non-numeric inputs). +2. Modify the `process_list` function to use `safe_divide` to process a list of tuples, where each tuple contains two numbers to be divided. Ensure that the program continues running even if some divisions fail, and handle different exceptions appropriately. +Example input for `process_list`: ```python -# a possible pre-condition: -assert len(input_array) > 0, 'Array length must be non-zero' -# a possible post-condition: -assert numpy.amin(input_array) <= average <= numpy.amax(input_array), -'Average should be between min and max of input values (inclusive)' +numbers = [(10, 2), (3, 0), (5, 'two'), (9, 3)] ``` -::::::::::::::::::::::::: - -:::::::::::::::::::::::::::::::::::::::::::::::::: - -::::::::::::::::::::::::::::::::::::::: challenge +Expected output: +```python +[5.0, None, None, 3.0] +``` -## Testing Assertions +::::::::::::::: solution -Given a sequence of a number of cars, the function `get_total_cars` returns -the total number of cars. +## Solution +1. Writing the `safe_divide` function: ```python -get_total_cars([1, 2, 3, 4]) +def safe_divide(a, b): + if not isinstance(a, (int, float)) or not isinstance(b, (int, float)): + raise TypeError("Both inputs must be numbers") + if b == 0: + raise ValueError("Division by zero is not allowed") + return a / b ``` -```output -10 -``` +2. Modifying the `process_list` function to use `safe_divide`: ```python -get_total_cars(['a', 'b', 'c']) -``` - -```output -ValueError: invalid literal for int() with base 10: 'a' +def process_list(pairs): + results = [] + for a, b in pairs: + try: + result = safe_divide(a, b) + except TypeError as e: + print(f"TypeError with inputs {a} and {b}: {e}") + result = None # Store None or some other placeholder + except ValueError as e: + print(f"ValueError with inputs {a} and {b}: {e}") + result = None # Store None or some other placeholder + results.append(result) + return results + +# Example input +numbers = [(10, 2), (3, 0), (5, 'two'), (9, 3)] + +# Running the function and printing the results +print(process_list(numbers)) ``` -Explain in words what the assertions in this function check, -and for each one, -give an example of input that will make that assertion fail. - +Expected output: ```python -def get_total_cars(values): - assert len(values) > 0 - for element in values: - assert int(element) - values = [int(element) for element in values] - total = sum(values) - assert total > 0 - return total +TypeError with inputs 5 and two: Both inputs must be numbers +ValueError with inputs 3 and 0: Division by zero is not allowed +[5.0, None, None, 3.0] ``` -::::::::::::::: solution - -## Solution - -- The first assertion checks that the input sequence `values` is not empty. - An empty sequence such as `[]` will make it fail. -- The second assertion checks that each value in the list can be turned into an integer. - Input such as `[1, 2, 'c', 3]` will make it fail. -- The third assertion checks that the total of the list is greater than 0. - Input such as `[-10, 2, 3]` will make it fail. - - - ::::::::::::::::::::::::: :::::::::::::::::::::::::::::::::::::::::::::::::: @@ -536,11 +254,11 @@ def get_total_cars(values): :::::::::::::::::::::::::::::::::::::::: keypoints -- Program defensively, i.e., assume that errors are going to arise, and write code to detect them when they do. -- Put assertions in programs to check their state as they run, and to help readers understand how those programs are supposed to work. -- Use preconditions to check that the inputs to a function are safe to use. -- Use postconditions to check that the output from a function is safe to use. -- Write tests before writing code in order to help determine exactly what that code is supposed to do. +- **Input Validation**: Ensuring that functions receive the correct type and range of input data to prevent errors and maintain data integrity. +- **Exceptions**: Using built-in error classes like ValueError and TypeError to handle invalid inputs and unexpected conditions. +- **Try and Except Pattern**: Using try and except blocks to manage exceptions and allow programs to continue running even when errors occur. +- **Specific Exception Handling**: Catching specific exception types separately to provide more precise error messages and actions. +- **Best Practices**: Avoiding the use of generic Exception or empty except blocks to prevent invisible errors and maintain code reliability. ::::::::::::::::::::::::::::::::::::::::::::::::::