🧪 A super practical introduction to Python document testing (doctest)

Have you ever encountered this "famous scene":

  • I have worked so hard to write the interface document, but when I copied the sample code, I got an error;
  • After modifying the code, I forgot to update the examples in the document, and the user followed them and generated a bunch of exceptions;
  • In order to verify the example, I wrote another set of unit tests. As a result, I was exhausted both physically and mentally due to repeated maintenance on both sides.

Don't worry, Python comes with a "hidden trump card" - the doctest module, which embeds executable test cases directly into the document string (docstring), making it look like a real Python interactive conversation. It can be viewed as a document and can be used to run tests with one click. True "write once, use twice"!


Why choose doctest?

Doctest is not a universal testing framework, but on the track of "lightweight, readable, and zero dependencies", it has almost no rivals:

  1. ✅Self-documenting code The best tutorials are code that you can run and verify. doctest allows your examples to be test cases themselves, eliminating the need to maintain documentation and tests separately.

  2. 🔄 Force document synchronization Once the code logic changes, if the documentation examples do not keep up, the test will fail immediately, and you no longer have to worry about "outdated documentation".

  3. 📦 works out of the box, zero dependencies Python comes with the standard library, so you don’t need to install pytest or unittest, so you can write it easily.

  4. 🤝 Seamless document generator Tools such as Sphinx and MkDocs can directly extract doctest examples in docstrings, generate beautiful API documents, and automatically run tests.


🔧Basic gameplay: three steps to get started

1. Write "simulation dialogue" in the docstring of the function/class

The syntax of doctest is very simple, just like moving the operations in the Python interpreter into multi-line comments:

  • by>>>The beginning indicates test input;
  • The next line is expected output (leave empty if there is no output, but keep the input/output order consistent with the real interaction);
  • If the test is abnormal, keep the first and last lines of traceback, and use the middle part...Omit.

① Simple function test

Here is an absolute value function with its own documentation and testing:

def my_abs(n):
    """
    返回数字的绝对值

    日常使用示例:
    >>> my_abs(1)
    1
    >>> my_abs(-100)
    100
    >>> my_abs(0)  # 边界值也测一下
    0
    """
    return n if n >= 0 else (-n)

② Class test with exception-handling

Here’s another gadget that can “access dictionary values ​​like attributes”:

class AttrDict(dict):
    """
    支持 .key 和 ['key'] 两种访问方式的字典

    常用操作:
    >>> ad = AttrDict(name='Bob', age=25)
    >>> ad.name
    'Bob'
    >>> ad['age']
    25
    >>> ad.city = 'Beijing'
    >>> ad['city']
    'Beijing'

    exception-handling:
    >>> ad.nonexistent_key
    Traceback (most recent call last):
        ...
    AttributeError: 'AttrDict' object has no attribute 'nonexistent_key'
    """

    def __getattr__(self, key):
        try:
            return self[key]
        except KeyError:
            raise AttributeError(f"'AttrDict' object has no attribute '{key}'")

    def __setattr__(self, key, value):
        self[key] = value

Tip:...It will help us omit irrelevant details in the middle of the traceback. As long as the first and last information match, the test will pass.


2. Run doctest in two ways

Method 1: Call inside the module (suitable for quick self-test)

Add these two lines of code to the bottom of the Python file:

if __name__ == '__main__':
    import doctest
    doctest.testmod(verbose=True)   # verbose=True 会打印详细的测试过程

Then run this file directly:

python my_module.py

Do not addverboseAs for parameters, follow the Unix philosophy of "pass without output" - if all tests pass, it will be quiet, and errors will only be reported.

Method 2: Direct call from command line (suitable for batch/temporary testing)

Don’t modify any code, just one command:

# 测试单个文件,-v 显示详细信息
python -m doctest my_module.py -v

You can also directly pass in a section of tape>>>text string, or used with pipes, very flexible.


🚀 Advanced skills: Handling "disobedient" output

When writing doctest, you will always encounter some situations where the output is "not very honest", such as:

  • The memory address is different every time it is run;
  • Printing time changes with the system;
  • Random numbers cannot be predicted.

Don't be too rigid, doctest has prepared some thoughtful marking instructions.

1. Dynamic/unpredictable output? useELLIPSIS

Add after the statement# doctest: +ELLIPSIS, then use...Instead of the uncertain parts, doctest will "accommodatingly" match only what you wrote, ignoring the changed parts:

import sys

def show_python_version():
    """
    返回 Python 主版本号

    >>> show_python_version()  # doctest: +ELLIPSIS
    3...
    """
    return sys.version_info.major

In the above example, as long as the output starts with3Begins, followed by zero or more characters, and the test passes—whether it's 3.8, 3.11, or 3.12.

2. No need for test output at all? useSKIP

Some examples are purely for readers, such as random number display, and there is no way to fix the expected output. Add directly# doctest: +SKIPSkip verification:

import random

def get_random_num():
    """
    返回 [0, 1) 之间的随机浮点数

    仅示意,无需测试:
    >>> get_random_num()  # doctest: +SKIP
    0.123456789
    """
    return random.random()

3. How to align multi-line output?

When doctest matches multi-line output, it only cares about the spaces at the beginning of each line and the text content, so as long as you copy the real output "as is", there is no need to worry about the absolute position. For exampleprint()Scenarios for typing multiple paragraphs of text:

def print_greeting(name):
    """
    打印多行问候语

    >>> print_greeting('Alice')
    Hello, Alice!
    How are you today?
    Have a nice day!
    """
    print(f"Hello, {name}!")
    print("How are you today?")
    print("Have a nice day!")

🎯 Comprehensive practice: a fully covered factorial function

Next, we combine regular use cases, boundary values, and exception tests to write a complete and robust factorial function. This example covers almost all the knowledge points mentioned earlier:

def factorial(n):
    """
    计算正整数的阶乘(n! = 1 × 2 × … × n)

    常规测试:
    >>> factorial(1)
    1
    >>> factorial(5)
    120
    >>> factorial(10)
    3628800

    非法输入测试:
    >>> factorial(0)
    Traceback (most recent call last):
        ...
    ValueError: n must be a positive integer
    >>> factorial(-3)
    Traceback (most recent call last):
        ...
    ValueError: n must be a positive integer
    """
    if not isinstance(n, int) or n < 1:
        raise ValueError("n must be a positive integer")
    if n == 1:
        return 1
    return n * factorial(n - 1)

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

Small exercise: Try saving the above code asfactorial.py, run it and see what the output of doctest looks like.


💡 Best Practices

  1. ✅ Keep tests small and precise each>>>The block only tests one function point, which is clear and easy to maintain. There is no need to squeeze all the scenarios into one sentence.

  2. ⚠️ Be sure to measure the boundary value Minimum values, maximum values, empty input, special characters... these are often where bugs hide.

  3. ❌ Avoid tests with side effects Do not change global variables, read and write disks, or connect to databases in doctest. This type of operation will make the test results unstable and even pollute the environment.

  4. 🤝 Use with unit testing framework Doctest is good at "document example verification" and is not suitable for scenarios such as complex business logic and performance testing. Use pytest / unittest decisively wherever you should.

  5. 📝 Write examples that are truly useful to users Don't just test1 + 1, the value of the document can be maximized by showing the typical usage that users will actually encounter.


📚 Connect with mainstream document generation tools

Another major advantage of doctest is that it can be seamlessly integrated with tools such as Sphinx and MkDocs Material, making "writing documents" and "running tests" one thing.

Sphinx configuration example

In the Sphinx projectconf.pyEnable related extensions in:

extensions = [
    'sphinx.ext.doctest',   # 核心:自动运行 doctest
    'sphinx.ext.autodoc',   # 自动提取 docstring
    # 其他扩展...
]

Then run in the project root directory:

make doctest

This command will not only test the docstrings in all Python modules, but also.rstHandwritten in the document>>>Examples will also be verified together, truly achieving "documentation as testing".


🎬 Summary

Python's doctest is a lightweight tool that is "extremely cost-effective":

  • The syntax is simple and no additional learning is required;
  • Solve the two pain points of "document example synchronization" and "quick self-test" at the same time;
  • It can also be linked with professional document tools to make documents truly "alive".

Next time you finish writing a function, don’t rush to close the file. Spend two minutes adding a few lines to the docstring.>>>Let’s give an example. ** Let you write examples so that you will never cheat yourself, let alone users! **