TypeError
and a ValueError
?isinstance
do?Sometimes, the programmer calling our function passes in the wrong information. It could be wrong because:
open
function a file that doesn't exist and asking it to open the file for reading)In these cases, you need to alert the user of your function that bad information was given to the function. Since the user of your function may be another programmer and not the person running the program, you cannot just print an error to the screen. For example, this is the wrong way to alert the "user" that the wrong value was given:
It's primarily wrong because you're alerting the wrong person that num
was wrong. You're alerting the person running the program, but in most cases the person running the program isn't capable of fixing the problem. A secondary reason this is wrong is because the function returns None
on bad input and a number on good input. While this might not be a bad thing, it's possible for a sloppy programmer to not check whether factorial
returned None
.
The prefered way to deal with bad input to a function is with exceptions. You can raise an exception on bad input to a function by using the raise
statement. The raise statement raises an exception. We've already seen how to catch these exceptions, now we're seeing how to raise (also called "throw") them.
Now, if a negative value is passed into factorial
, an exception is raised instead of None
being returned. When an exception is raised, the function stops where it is and Python starts looking for the exception handling environment to catch this exception. The exception will propagate back up to the calling function and if there is no exception handling environment to catch it, it'll go to that function's calling function. This will continue until either an exception handling environment is found, or it reaches the top and the program crashes. Here's an example:
Code | Output |
---|---|
if num < 0: raise ValueError('The number must be positive') product = 1 for i in range(1, num+1): product *= i return product def user_input_factorial(): val = int(input("Enter a positive integer: ")) try: print("That number's factorial is:", factorial(val)) except ValueError as ve: print(ve) user_input_factorial() user_input_factorial() |
That number's factorial is: 120 Enter a positive integer: The number must be positive |
Code | Output |
---|---|
if num < 0: raise ValueError('The number must be positive') product = 1 for i in range(1, num+1): product *= i return product def get_user_factorial(): val = int(input("Enter a positive integer: ")) fact = factorial(val) print('Got the factorial') return fact def print_user_factorial(): user_fact = get_user_factorial() try print("The factorial of that number is:", user_fact) except ValueError: print('Error with computing factorial') print_user_factorial() print_user_factorial() |
Got the factorial The factorial of that number is: 120 Enter a positive integer: Error with computing factorial |
What's wrong with returning None
on bad input? Why bother with exception handling environments? Exceptions allow us to do more than just indicate whether the function worked correctly or not. Our factorial function actually has two different kinds of bad arguments. The first is negative numbers, which are handled already. The second is that it only works on integers. If the programmer using our function tries to pass in a string or a float, the function will fail. To fix our problem, we need to do another kind of check at the start of the function. We need to check whether the function was given an integer. To do that, we can use the isinstance
function. It takes a value and a data type and returns True if the value is an instance of that data type. For example:
You can pass in a list/tuple of data types if you'd like. Maybe you're just curious if it's a number or not. You can easily do that with:
In a function, we can use it to check whether the correct data type was given. For our factorial function, it only works on positive integers, so we must check whether an integer is given (we're already checking whether it's positive:
Does it matter which check comes first? Can we check whether it's positive before we check whether it's an integer?
How do you know which kind of error to raise? Python provides a lot (and we can even create our own). For now, we'll primarily be using these errors:
The complete list of built-in exceptions can be found in Python's documentation.
Since you may not be the only programmer using your functions, you should document your function. Function documentation is a multi-line comment just after the function header. This comment should:
You should leave a blank line between "paragraphs".
Here's an example:
This "docstring" (or "doc string") belongs to the function. You can access it using __doc__
, as in:
However, the easier way is to just use the help
function:
These docstrings are also helpful for generating documentation, similar to what you can find in Python's Standard Library. Python's pydoc module provides some basic tools for this.
Modules are Python files containing pre-written code. We gain access to them in our programs through the import
statements. So far, we've been using modules provided by Python (e.g. math
, string
, and random
). However, we can write our own modules. You've actually been doing this for the labs and assignments. The .py files you submit could be treated as modules. However, modules often contain just function and class definitions (so, lab 6, part 1 would be a good example of a module that you're writing).
To import one of your modules, you must make sure it's in a location that Python knows about. Python has a list of paths that it will search whenever you try to import something. To view it, import the sys
module and access its path
list. For example:
Your path will likely look different from the one above. The first elements in the list is an empty string. That means to search the current working directory (which we last talked about with files). The rest of the paths are absolute paths to locations that Python has modules installed. If you want to import your own modules, you can:
sys.path
before trying to import the moduleWe've already talked about changing the current working directory, so we won't re-cover it here (use os.chdir). One down side to this option is that you might not want to change your current working directory just to import a file. Maybe the rest of your program assumes you'll be working in a certain location.
Putting your module where Python already knows to look could work if you have access to do that. Unfortunately, those locations are often restricted to administrator access. Additionally, you might not want to share your modules with everyone else on the computer (maybe you're storing passwords in the file.
The third option is very easy to do. We've already talked about appending/inserting into a list. sys.path
is just a list. You just append/insert the path you want to use into sys.path
. Once you've updated the path, import as you normally would. In the example below,
<< Previous Notes | Daily Schedule | Next Notes >> |