Python Testing: doctest (Complete Guide)
Mastering inline documentation tests for executable specifications
Table of Contents
1. doctest: Inline Testing
Basic Syntax & Execution
def add(a, b): """ Adds two numbers. >>> add(2, 3) 5 >>> add(-1, 1) 0 """ return a + b
python -m doctest module.py |
Run tests (silent unless failures) |
python -m doctest -v module.py |
Verbose output (show all tests) |
python -m doctest *.txt |
Test standalone text files |
Advanced Formatting
def format_data(data):
"""
>>> format_data(['a', 'b']) # doctest: +NORMALIZE_WHITESPACE
['a',
'b']
>>> import random
>>> random.random() # doctest: +ELLIPSIS
0...
>>> print("First line\\n\\nThird line") # doctest: +REPORT_NDIFF
First line
Third line
"""
return data
Edge Cases
def divide(a, b):
"""
>>> divide(1.0, 3.0) # doctest: +ELLIPSIS
0.333...
>>> import datetime
>>> datetime.datetime.now() # doctest: +SKIP
datetime.datetime(2023, 1, 1, 12, 0)
>>> 1 / 0
Traceback (most recent call last):
ZeroDivisionError: division by zero
"""
return a / b
2. doctest: Documentation Tests
Best Practices
- Keep docstring examples realistic and readable
- Place basic usage examples first, edge cases after
- Use doctest for API examples, unittest/pytest for complex scenarios
Integration Methods
project/
├── module.py # Module-level doctests
├── tests/
│ ├── doctests/ # Standalone .txt files
│ └── test_doctest.py # unittest integration
├── module.py # Module-level doctests
├── tests/
│ ├── doctests/ # Standalone .txt files
│ └── test_doctest.py # unittest integration
Execution Control
Directive | Purpose |
---|---|
# doctest: +SKIP |
Skip this test |
# doctest: +ELLIPSIS |
Enable ... wildcard matching |
# doctest: +NORMALIZE_WHITESPACE |
Ignore whitespace differences |
# doctest: +IGNORE_EXCEPTION_DETAIL |
Match exceptions loosely |
3. Advanced doctest Features
Test Fixtures
def setup_test():
"""
>>> import tempfile
>>> fd, path = tempfile.mkstemp()
>>> with open(path, 'w') as f:
... _ = f.write("test data\\n")
>>> # Test code using the temp file
>>> import os
>>> os.remove(path)
"""
pass
Customization
from doctest import DocTestRunner, OutputChecker
class MyOutputChecker(OutputChecker):
def check_output(self, want, got, optionflags):
# Custom comparison logic
return super().check_output(want, got, optionflags)
runner = DocTestRunner(checker=MyOutputChecker())
Performance Considerations
- Use
doctest.testmod(verbose=False)
in production - Move complex tests to separate files
- Skip computationally expensive examples
4. Real-World Patterns
Documentation-Driven Development
def sort_items(items):
"""
Sorts items according to business rules.
Specification Example:
>>> sort_items([3, 1, 2])
[1, 2, 3]
Real-world Usage:
>>> data = load_production_data()
>>> results = sort_items(data) # doctest: +SKIP
>>> assert len(results) == len(data) # doctest: +SKIP
"""
return sorted(items)
Debugging Techniques
def failing_function():
"""
>>> failing_function() # Expected: 42
24
"""
return 24
if __name__ == "__main__":
import doctest
doctest.testmod() # Run with -v to see failures
5. Limitations & Alternatives
When Not to Use Doctest
- Complex test setups requiring fixtures
- Data-driven tests with many variations
- Performance benchmarking
- UI or integration tests
Complementary Tools
# unittest integration example
import unittest
import doctest
import mymodule
def load_tests(loader, tests, ignore):
tests.addTests(doctest.DocTestSuite(mymodule))
return tests
Integration Tip: Combine doctest with pytest using pytest --doctest-modules
to run both doctests and regular tests together.
6. Complete Example
def factorial(n):
"""
Compute the factorial of n.
Examples:
>>> factorial(3)
6
>>> factorial(5)
120
Edge cases:
>>> factorial(0)
1
>>> factorial(-1)
Traceback (most recent call last):
ValueError: n must be >= 0
Floating point approximation:
>>> factorial(20.0) # doctest: +ELLIPSIS
2.43290200...e+18
Documentation example:
>>> print("5! =", factorial(5))
5! = 120
"""
if n < 0:
raise ValueError("n must be >= 0")
return 1 if n == 0 else n * factorial(n-1)
if __name__ == "__main__":
import doctest
doctest.testmod(verbose=True)
Performance Warning: For recursive functions like factorial, consider adding a # doctest: +SKIP
directive for very large values to avoid slowing down documentation builds.