Documenting and Testing Pythonic Code with Doctest
Daniel Hayes
Full-Stack Engineer · Leapcell

Introduction
In the fast-paced world of software development, ensuring code quality and maintaining up-to-date documentation are paramount. Often, these two crucial aspects are treated as separate, and sometimes even competing, concerns. Developers might write extensive test suites that live apart from the code, and then craft documentation that needs constant synchronization. This traditional approach can lead to documentation becoming stale and tests becoming less effective as the codebase evolves.
Imagine a world where your documentation is your test. A world where examples shown to users are automatically verified for correctness, and where the act of documenting your code inherently reinforces its reliability. This isn't a pipe dream; it's a reality made possible in Python thanks to its powerful doctest
module. By seamlessly integrating test cases directly into docstrings, doctest
offers a unique and highly effective way to create self-documenting and self-testing code. This article will delve into the principles, implementation, and practical benefits of using doctest
to elevate your Python development workflow.
The Synergy of Documentation and Testing
Before diving into the specifics of doctest
, let's clarify some core concepts that underpin its philosophy.
-
Docstrings: In Python, a docstring is a string literal that occurs as the first statement in a module, function, class, or method definition. It serves as inline documentation, explaining what the code does, its arguments, and what it returns. They are accessible via the
__doc__
attribute and are widely used by tools like Sphinx for generating API documentation. -
Test-Driven Development (TDD): A software development process where tests are written before the actual code. This approach ensures that the code meets specified requirements and helps in designing robust, modular components.
-
Unit Testing: The practice of testing individual units or components of a software application in isolation. The goal is to validate that each unit of the software performs as designed.
doctest
bridges the gap between docstrings and unit testing by enabling you to embed example usage, along with their expected outputs, directly within your function or module docstrings. These examples then serve as executable tests.
How Doctest Works
The doctest
module works by searching for text that looks like interactive Python sessions in docstrings. Specifically, it looks for lines starting with >>>
(the Python interactive prompt) and treats the subsequent lines as the expected output for that interactive command.
When doctest
is run, it executes the code following the >>>
prompt and compares the actual output with the expected output provided in the docstring. If they match, the test passes; otherwise, it fails.
Let's illustrate this with a simple example:
def add(a, b): """ Adds two numbers and returns their sum. >>> add(2, 3) 5 >>> add(10, -5) 5 >>> add(0, 0) 0 """ return a + b
In this add
function, the docstring contains three distinct test cases. Each case demonstrates how add
should be called and what its corresponding return value should be.
To run these tests, you can add a simple block at the end of your script:
import doctest def add(a, b): """ Adds two numbers and returns their sum. >>> add(2, 3) 5 >>> add(10, -5) 5 >>> add(0, 0) 0 """ return a + b if __name__ == "__main__": doctest.testmod()
When you run this script: python your_module.py
, doctest.testmod()
will automatically find and execute all the doctests in the current module. The output will look something like this if all tests pass:
$ python your_module.py
(No output indicates success by default)
If a test fails, doctest
will report the discrepancy:
Let's intentionally break one:
def add(a, b): """ Adds two numbers and returns their sum. >>> add(2, 3) 6 """ return a + b if __name__ == "__main__": doctest.testmod()
Running this would yield:
$ python your_module.py
**********************************************************************
File "your_module.py", line 5, in __main__.add
Failed example:
add(2, 3)
Expected:
6
Got:
5
**********************************************************************
1 items had failures:
1 of 1 in __main__.add
***Test Failed*** 1 failures.
Advanced Features and Directives
doctest
offers more than just basic prompt-output matching. You can use special directives to handle situations like exceptions, unordered output, or floating-point comparisons.
-
ELLIPSIS
: Useful when you expect parts of the output to vary (e.g., memory addresses or object IDs). Use...
in your expected output to match any substring.def get_object_info(obj): """ Returns a string representation of an object's type and ID. >>> get_object_info(1) "<class 'int'> at 0x..." """ return f"{type(obj)} at {hex(id(obj))}"
To enable this, you need to pass
optionflags=doctest.ELLIPSIS
todoctest.testmod()
. -
Expected Exceptions: If a test case is expected to raise an exception, you can specify that directly.
def divide(a, b): """ Divides two numbers. >>> divide(10, 2) 5.0 >>> divide(10, 0) Traceback (most recent call last): ... ZeroDivisionError: division by zero """ return a / b
-
NORMALIZE_WHITESPACE
: Ignores differences in whitespace (spaces, tabs, newlines) when comparing expected and actual output.def format_list(items): """ Formats a list of items into a string. >>> format_list(['apple', 'banana', 'cherry']) # doctest: +NORMALIZE_WHITESPACE "Items: - apple - banana - cherry" """ return "Items:\n" + "\n".join(f"- {item}" for item in items)
You can enable these flags globally for testmod()
or locally using doctest: +FLAG
.
Application Scenarios
doctest
shines in several scenarios:
- Examples in Documentation: The primary use case. It ensures that any example usage shown to users or fellow developers in your docstrings is always correct and up-to-date. This builds trust in your documentation.
- Simple Function Testing: For small, well-defined functions,
doctest
can be a quick and effective way to capture their expected behavior without setting up a separateunittest
orpytest
suite. - Educational Material: When writing tutorials or teaching Python,
doctest
ensures that code snippets provided for students are runnable and produce the advertised results. - Quick Regression Checks: When a bug is fixed, a new
doctest
can be added to the relevant function's docstring to prevent the bug from reappearing, serving as a simple-yet-effective regression test.
Best Practices and Limitations
While powerful, doctest
isn't a replacement for comprehensive unit testing frameworks like unittest
or pytest
.
When to use doctest
:
- For simple, illustrative examples.
- To test the public API of functions, methods, and classes.
- When the test case naturally fits into an executable example.
Limitations and when to use other frameworks:
- Complex Setup:
doctest
is not ideal for tests that require extensive setup (e.g., mocking databases, external services, or complex object graphs). - Fixture Management: Other frameworks offer robust fixture management for initializing test environments.
- Parametrized Tests: Running the same test logic with different inputs is easier with
pytest
's parametrization. - Performance Testing:
doctest
is not designed for performance or stress testing. - Error Reporting: While
doctest
reports failures, the detailed reporting capabilities ofpytest
orunittest
are often superior for larger projects.
A common approach is to use doctest
for clear, public-facing examples and use pytest
or unittest
for internal, complex, or edge-case testing.
Conclusion
doctest
embodies the Pythonic philosophy of "batteries included" by providing a lightweight yet effective way to blend documentation and testing. By embedding executable examples directly within your docstrings, you create code that is inherently self-validating and easier to understand. This unique approach not only boosts code quality by catching regressions early but also dramatically improves maintainability by ensuring your documentation always reflects the true behavior of your code. Embrace doctest
, and let your documentation be the guardian of your code's correctness.