It’s how we deal with them that matters. Same in Python: understanding and handling errors is the key to good code.
Helpfully…
Python will always try to tell you what it thinks went wrong: “I didn’t understand what you meant by this…” or “I’m sorry, I can’t let you do that Dave…”
The challenges are:
Python tends to give you a lot of information about the error: this can be very helpful for programmers dealing with complex problems and totally overwhelming for beginners.
That what Python thinks the problem is doesn’t always line up with where the problem actually is. In cases of syntax, for instance, the problem could be an unclosed parenthesis three lines earlier!
Challenge 1
That the ‘error’ isn’t always the error…
total =0print("About to start loop"for i inrange(1,10): total += iprint(total)
This outputs:
print("About to start loop"... for i inrange(1,10): File "<stdin>", line 2for i inrange(1,10):^SyntaxError: invalid syntax
Dealing with Errors
Errors Have Types
In the same way that variables have types, so do errors:
ModuleNotFoundError
IndexError
KeyError
OSError
…
We can add our own messages:
raiseException("Sorry, I can't let you do that, Dave.")
Custom Errors
We can create our own types (classes) of error:
class CustomError(Exception):pass# We do nothing except create a new type
This can then be triggered with:
raise CustomError("Our custom error")
And (very importantly) this can be caught with:
except CustomError: #... do something with CustomError ...
Why Customise?
Custom exceptions can perform a variety of tasks:
Distinguish between generic issues and those specific to your application.
Triage issues based on your understanding of your application and the severity.
Provide detailed insight based on fuller access to the state of your application.
Perform important tidying-up or rollback operations, etc.
So Errors can be Trapped
Python calls errors exceptions, so this leads to:
try:#... some code that might fail...except<Named_Error_Type>:#... what do it if it fails for a specific reason...except:#... what to do if it fails for any other reason...finally:#... always do this, even if it fails...
You can use any or all of these together: you can have multiple named excepts to handle different types of errors from a single block of code; you do not have to have a catch-all except or a finally.
Trapping Errors
This code fails:
x,y =10,0print(x/y)
And it generates this error:
> Traceback (most recent call last):> File "<stdin>", line 1, in<module>>ZeroDivisionError: division by zero
Trapping Errors (cont’d)
But if you ‘trap’ the error using except then:
x,y =10,0try:print(x/y)exceptZeroDivisionError:print("You can't divide by zero!")except:print("Something has gone very wrong.")finally: print("Division is fun!")
This will print
> You can't divide by zero!
> Division is fun!
Raising Hell
You can trigger your own exceptions using raise.
x,y =10,0try:print(x/y)exceptZeroDivisionError:print("You can't divide by zero!")raiseException("Please don't do that again!")finally: print("Division is fun!")
Understanding Multiple Errors
x,y =10,0try:print(x/y)exceptZeroDivisionError:print("You can't divide by zero!")raiseException("Please don't do that again!")finally: print("Division is fun!")
The code we try triggers the ZeroDivisionError block.
This prints "You can't divide by zero!"
We then raise a new exception that is not caught.
The finally code executes because it always does before Python exits.
Python exits with the message from our newly raised Exception.
Thus: ‘During handling of above (ZeroDivisionError) another exception (our Exception) occurred…’
A Debugging Manifesto!1
Understanding exceptions is critical to fixing problems, instead of being overwhelmed by them!
Test-Based Development
We can actually think of exceptions as a way to develop our code.
Here’s some ‘pseudo-code’:
# Testing the 'addition' operatortest(1+1, 2) # Should equal 2test(1+'1', TypeError) # Should equal TypeErrortest('1'+'1', '11') # Should equal '11'test(-1+1, 0) # Should equal 0
Our test(A,B) function takes an input (A) and the expected output (B) and then compares them. The test returns True if A==B and False otherwise.
Unit Tests
Each test is a Unit Test because it tests one thing and one thing only. So if you had three functions to ‘do stuff’ then you’d need at least three unit tests.
A Unit Test may be composed of one or more assertions. Our pseudo-code on the previous slide contained 4 assertions.
A Unit Test does not mean that your code is correct or will perform properly under all circumstances. It means that your code returns the expected value for a specified input.
Python considers this approach so important that it’s built in.
This approach uses the ‘docstring’ (the bits between """) to test the results of the function. This is intended to encourage good documentation of functions using examples:
def square(x):"""Return the square of x. >>> square(2) 4 >>> square(-2) 4 >>> square(-1) 2 """return x * xif__name__=='__main__':import doctest doctest.testmod()
Collaboration & Continuous Integration
The Unit Test approach is often used on collaborative projects, especially in the Open Source world. PySAL, for instance, asks for unit tests with every new feature or integration.
The running of all tests for multiple components is called ‘integration testing’.
A commit, merge, or pull on GitHub can trigger the unit testing process for the entire software ‘stack’. This is known as Continuous Integration because you are always checking that the code works as expected, rather than leaving testing to the end.