Defensive programming

Reading time15 min
Principle

Defensive programming makes it possible to develop programs that are able to detect anomalies and make pre-determined responses by assuming that errors can occur and protecting against them.It ensures that a piece of software will continue to function under unforeseen circumstances. Defensive programming practices are often used where high availability, safety or security is required.

It is an approach to improving software and source code in terms of:

  • Overall quality: reducing the number of bugs and problems in the software.
  • Making the source code understandable: readable and understandable so that it can be accepted in a code audit.
  • Making the software behave in a predictable way despite unexpected inputs or user actions.

However, overly defensive programming can protect against bugs that will never occur, resulting in running and maintenance costs.

With defensive programming, we can raise and handle errors to check and respond to program input, and ensure that nothing unexpected has happened. There are two main kinds of errors: syntax errors and exceptions.

Syntax Errors

Syntax errors, also known as parsing errors, are the most common type of complaint you can get when programming. Consider the following example :

while True print('Hello world')

Attempting to run this code will produce the following error message from the Python interpreter on the console:

File “example_SyntaxError.py”, line 1

while True print('Hello world')
           ^

SyntaxError: invalid syntax

The parser repeats the offending line and displays little ‘arrow’s pointing at the token in the line where the error was detected. The error may be caused by the absence of a token before the indicated token. In this example, the error is detected at the function print(), since a colon (’:’) is missing before it. File name and line number are printed so you know where to look in case the input came from a script.

Exceptions

Even if a statement or expression is syntactically correct, it may cause an error when an attempt is made to execute it. Errors detected during execution are called exceptions, and are not unconditionally fatal: we will learn how to handle them in Python programs. However, most exceptions are not handled by programs and result in error messages like the ones shown here:

Output
>>> 10 * (1/0)
File "exception_ErrorMessages.py", line 1, in <module>
   10 * (1/0) 
ZeroDivisionError: division by zero

>>> 4 + spam*3
File "exception_ErrorMessages.py", line 1, in <module>
    4 + spam*3
NameError: name 'spam' is not defined

>>> '2' + 2
File "exception_ErrorMessages.py", line 1, in <module>
   '2' + 2
TypeError: can only concatenate str (not "int") to str

The last line of the error message indicates what happened. Exceptions come in different types, and the type is printed as part of the message: the types in the example are ZeroDivisionError, NameError and TypeError. The string printed as the exception type is the name of the built-in exception that occurred.

Programs can define their own exceptions by creating a new exception class (which should be derived from the Exception class), but we will not describe this in this lesson.

Raising exceptions

The raise statement allows the programmer to force a specified exception to occur. The argument to raise indicates the exception to be raised. This must be either an exception instance or an exception class. For example:

Output
>>> raise NameError('HiThere')

File "raise_exception.py", line 1, in <module>
    raise NameError('HiThere')
NameError: HiThere

>>> raise Exception("Error message here.")

File "raise_exception.py", line 1, in <module>
    raise Exception("Error message here.")
Exception: Error message here.

>>> varName = 'name'
if varName != 'nc':
    raise ValueError("The variable does not have the expected value")

File "raise_exception.py", line 3, in <module>
    raise ValueError("The variable does not have the expected value")
ValueError: (The variable does not have the expected value)

Handling exceptions

It is possible to write programs that handle selected exceptions. This allows the program not to crash when a particular exception occurs. We can use a try-except block to catch and handle the exception. The syntax of the try-except block is as follows :

try:
    <do something>
except Exception:
    <handle the error>

The try statement works as follows:

  • First, the try clause (the statement(s) between the try and except keywords) is executed.
  • If no exception occurs, the except clause is skipped and execution of the try statement is finished.
  • If an exception occurs during execution of the try clause, the rest of the clause is skipped. Then, if its type matches the exception named after the except keyword, the except clause is executed, and then execution continues after the try/except block.
  • If an exception occurs which does not match the exception named in the except clause, it is passed on to outer try statements; if no handler is found, it is an unhandled exception and execution stops with an error message.

The following example, asks the user for input until a valid integer has been entered, but allows the user to interrupt the program (using Control-C).

while True:
    try:
        x = int(input("Please enter a number: "))
        break
    except ValueError:
        print("Oops!  That was no valid number.  Try again...")

A try statement may have more than one except clause, to specify handlers for different exceptions. At most one handler will be executed. An except clause may name multiple exceptions as a parenthesized tuple, for example:

except (RuntimeError, TypeError, NameError):
    pass

The pass statement is used as a placeholder for future code. When the pass statement is executed, nothing happens, but you avoid getting an error when empty code is not allowed. Empty code is not allowed in loops, function definitions, class definitions, or in if statements.

Defining clean-up actions

The try statement has another optional clause which is intended to define clean-up actions that must be executed under all circumstances. If a finally clause is present, the finally clause will execute as the last task before the try statement completes. For example:

try:
    raise KeyboardInterrupt
finally:
    print('Goodbye, world!')
Output

Goodbye, world!

Traceback (most recent call last):

File “finally_clause_example1.py”, line 2, in raise KeyboardInterrupt

KeyboardInterrupt

The finally clause runs whether or not the try statement produces an exception. For example :

def divide(x,y):
    try:
        result= x/y
    except ZeroDivisionError:
        print('division by zero')
    else:
        print('result is:',result)
    finally:
        print('executing finnally clause')
Output

divide(2,1)

result is: 2.0

executing finally clause

divide(2,0)

division by zero

executing finally clause

divide(‘2’,‘1’)

executing finally clause Traceback (most recent call last):

File “finally_clause_example2.py”, line 11, in divide(‘2’,‘1’)

File “finally_clause_example2.py”, line 3, in divide result= x/y

TypeError: unsupported operand type(s) for /: ‘str’ and ‘str’

As we can see in this example, the finally clause is executed in any event. The TypeError raised by dividing two strings is not handled by the except clause and therefore re-raised after the finally clause has been executed.