Contents
  1. I. Error Handling
  2. 1.1 try
  3. 1.2 Logging Errors
  4. 1.3 Raising Errors
  5. II. Debugging
  6. 2.1 print
  7. 2.2 Assertions (assert)
  8. 2.3 logging
  9. 2.4 Step Debugging and Breakpoints
  10. III. Testing
  11. 3.1 Unit Tests
  12. 3.2 Document testing

I. Error Handling

1.1 try

Python includes a built-in try...except...finally... error-handling mechanism.

When we think certain code might fail, we can run it inside try. If an error occurs, the rest of that block does not run; execution jumps straight to the error-handling code in the except block. After except finishes, if there is a finally block, the finally block runs, and then execution is done.

try:
    print('try...')
    r = 10 / 0
    print('result:', r)
except ZeroDivisionError as e:
    print('except:', e)
finally:
    print('finally...')
print('END')

The code above triggers a division error when computing 10 / 0:

try...
except: division by zero
finally...
END

There are many kinds of errors. Different except blocks should handle different error types.

Also, after the except block, you can add else; when no error occurs, the else statement runs automatically.

For common exception types and their inheritance hierarchy, see https://docs.python.org/3/library/exceptions.html#exception-hierarchy

1.2 Logging Errors

Python’s built-in logging module makes it easy to record error messages for analysis while letting the program keep running:

import logging

def foo(s):
    return 10 / int(s)

def bar(s):
    return foo(s) * 2

def main():
    try:
        bar('0')
    except Exception as e:
        logging.exception(e)

main()
print('END')

Even though an error occurs, the program prints the error information and then continues:

ERROR:root:division by zero
Traceback (most recent call last):
  File "err_logging.py", line 13, in main
    bar('0')
  File "err_logging.py", line 9, in bar
    return foo(s) * 2
  File "err_logging.py", line 6, in foo
    return 10 / int(s)
ZeroDivisionError: division by zero
END

1.3 Raising Errors

def foo(s):
    n = int(s)
    if n==0:
        raise ValueError('invalid value: %s' % s)
    return 10 / n

def bar():
    try:
        foo('0')
    except ValueError as e:
        print('ValueError!')
        raise

bar()

In bar(), after catching the error and printing ValueError!, the code re-raises it with raise. This pattern is common: you catch an error mainly to log it for later tracing, but because the current function does not know how to handle it, the right move is to propagate it upward and let the top-level caller decide.

If raise is used without arguments, it re-raises the current exception unchanged.

II. Debugging

As everyone knows, the chance of writing a program once and having it run correctly on the first try is essentially zero. Bugs show up in all shapes and sizes, so you need a full set of debugging techniques to fix them.

2.1 print

The simplest, most brute-force approach is to use print() to dump variables that might be wrong. The biggest downside of print() is that you have to remove those calls later.

2.2 Assertions (assert)

Anywhere you would use print() to inspect values, you can use an assertion instead: assert 表达式, '打印的信息'. If the assertion expression is True, nothing happens and execution continues. If the assertion expression fails, the message is printed.

def foo(s):
    n = int(s)
    assert n != 0, 'n is zero!'
    return 10 / n

def main():
    foo('0')

assert means the expression n != 0 should be True; otherwise the message is printed.

When an assertion fails, assert itself raises AssertionError. If assert statements are scattered everywhere, they are not much better than print(). However, when starting the Python interpreter you can use the -O flag to disable assert:

$ python -O err.py

2.3 logging

Compared with assert, logging does not raise errors and can write to files:

import logging
logging.basicConfig(level=logging.INFO)

s = '0'
n = int(s)
logging.info('n = %d' % n)
print(10 / n)

You can choose the level of messages to record: debug, info, warning, error, and so on. When you set level=INFO, logging.debug has no effect. Likewise, with level=WARNING, both debug and info are suppressed. That way you can emit messages at different levels without deleting them, and control the output level in one place.

Another benefit of logging is that with simple configuration, one statement can write to multiple destinations at once, such as the console and a file.

2.4 Step Debugging and Breakpoints

(1) pdb

Start Python’s debugger, pdb, to run the program step by step and inspect state at any time.

$ python -m pdb err.py

With -m pdb, pdb stops at the next line to execute.

  • Enter l to list code
  • Enter n to execute the next line
  • Enter p 变量名 to inspect a variable
  • Enter q to end debugging and exit the program

(2) pdb.set_trace()

This approach also uses pdb, but without stepping from the start. Just import pdb and place pdb.set_trace() where an error might occur to set a breakpoint.

When you run the code, execution pauses automatically at pdb.set_trace() and enters the pdb environment. Use p to inspect variables or c to continue.

(3) IDE

An IDE with debugging support makes it easier to set breakpoints and step through code.

For everyday use, Visual Studio Code is a good choice: https://code.visualstudio.com/. Install the Python extension.

For large projects, PyCharm is recommended: http://www.jetbrains.com/pycharm/.

III. Testing

3.1 Unit Tests

A unit test checks whether a module, function, or class behaves correctly.

For example, for abs(), you might write these test cases:

  1. Positive inputs such as 1, 1.2, and 0.99 should return the same value;
  2. Negative inputs such as -1, -1.2, and -0.99 should return the opposite value;
  3. Input 0 should return 0;
  4. Non-numeric inputs such as None, [], and {} should raise TypeError.

Put those cases in a test module and you have a complete unit test.

(1) Writing unit tests

When writing unit tests, define a test class that inherits from unittest.TestCase.

Methods whose names start with test are test methods. Methods that do not start with test are not treated as tests and are not run.

import unittest

class TestStudent(unittest.TestCase):

    def test_80_to_100(self):
        s1 = Student('Bart', 80)
        s2 = Student('Lisa', 100)
        self.assertEqual(s1.get_grade(), 'A')
        self.assertEqual(s2.get_grade(), 'A')

    def test_60_to_80(self):
        s1 = Student('Bart', 60)
        s2 = Student('Lisa', 79)
        self.assertEqual(s1.get_grade(), 'B')
        self.assertEqual(s2.get_grade(), 'B')

    def test_0_to_60(self):
        s1 = Student('Bart', 0)
        s2 = Student('Lisa', 59)
        self.assertEqual(s1.get_grade(), 'C')
        self.assertEqual(s2.get_grade(), 'C')

    def test_invalid(self):
        s1 = Student('Bart', -1)
        s2 = Student('Lisa', 101)
        with self.assertRaises(ValueError):
            s1.get_grade()
        with self.assertRaises(ValueError):
            s2.get_grade()

if __name__ == '__main__':
    unittest.main()

Write one test_xxx() method for each category of test. unittest.TestCase provides many built-in checks; call them to assert that output matches expectations. The most common assertion is assertEqual():

self.assertEqual(abs(-1), 1) # 断言函数返回的结果与1相等

(2) Running unit tests

The simplest way to run tests:

if __name__ == '__main__':
    unittest.main()

Another option is to run unit tests from the command line with -m unittest:

$ python -m unittest mydict_test

3.2 Document testing

Python’s built-in “document testing” (doctest) module can directly extract code from comments and execute tests.

When we write comments, if we write comments like this:

def fact(n):
    '''
    Calculate 1*2*...*n
    
    >>> fact(1)
    1
    >>> fact(10)
    3628800
    >>> fact(-1)
    Traceback (most recent call last):
        ...
    ValueError
    '''
    if n < 1:
        raise ValueError()
    if n == 1:
        return 1
    return n * fact(n - 1)

That makes the expected inputs and outputs much clearer to callers.

To run doctests:

if __name__=='__main__':
    import doctest
    doctest.testmod()

Run the test program:

$ python test.py

If there is no output, all doctests passed.