Skip to main content

Testing Python Apps with Pytest and Doctest: A Symbiotic Approach

· 5 min read
Serhii Hrekov
software engineer, creator, artist, programmer, projects founder

Testing Python Apps with Pytest and Doctest: A Symbiotic Approach

How to test an app using both pytest and doctest in a complementary (symbiotic) way:


You know, I’ve spent the last couple months revisiting my whole approach to testing in Python. I was primarily using pytest, like most of us, but then I stumbled into a few projects that were very documentation-driven. That pushed me to rethink the role of doctest, and how it might be paired intelligently with pytest. So, after some real-world use, experimentation, mistakes, and realizations, I want to lay out how I now think about using these two together—and when it actually makes sense to use them in harmony.


Why Pytest and Doctest?

First things first: pytest is my daily driver. It’s powerful, flexible, and has a ridiculous plugin ecosystem that makes almost anything possible—parametrization, fixtures, mocking, coverage, you name it.

But doctest… that’s a different beast. It’s not meant to replace unit testing. It’s not even meant for testing at scale, really. What it is meant for, is making sure that your documentation is alive—that your examples don’t silently rot.

When you think about that for a second, it’s kind of beautiful.

So here's my take: don’t try to make doctest something it’s not. It’s not a full-blown test framework. It's a documentation correctness tool. That’s its power.


The Symbiosis: How I Use Them Together

Here's the model I arrived at:

  • Use doctest for core modules and reusable logic, where the function is small, pure, and has examples in the docstring.
  • Use pytest for everything else: actual test cases, edge conditions, I/O validation, performance guards, exception handling, etc.

Example: Doctest for Small, Pure Functions

def sanitize_vegetable_name(name):
"""
Removes trailing whitespace and capitalizes the vegetable name.

>>> sanitize_vegetable_name(" carrot ")
'Carrot'
>>> sanitize_vegetable_name("BROCCOLI")
'Broccoli'
"""
return name.strip().capitalize()

Boom. That’s the perfect use case for doctest. The logic is readable, there are a couple of examples, and they double as executable documentation. This kind of thing is gold in internal libraries and helper modules that get reused a lot.

Example: Pytest for Broader Test Coverage

import pytest
from mylib import sanitize_vegetable_name

@pytest.mark.parametrize("input_value,expected", [
("carrot ", "Carrot"),
("LETTUCE", "Lettuce"),
(" broccoli ", "Broccoli"),
])
def test_sanitize_vegetable_name(input_value, expected):
assert sanitize_vegetable_name(input_value) == expected

Here, you’re getting parametrization, validation of different inputs, and the freedom to scale up your test logic. You wouldn’t want all this jammed into a docstring.


When Not to Use Doctest

If I’m being honest, I’ve seen devs go overboard. They try to stick a doctest on every function. That’s a trap.

Rules I follow:

  • Only use doctest for deterministic, side-effect-free functions with tight logic and meaningful input/output examples.
  • Don’t use doctest where setup is needed (e.g. database access, mocking, complex objects).
  • Don’t put tests in docstrings unless they improve the understanding of the function.

Honestly, most functions in a real-world app don’t meet that bar. So for me, maybe 10-20% of functions deserve a doctest. The rest live in the tests/ directory under the warm embrace of pytest.


How I Set It Up

Here’s the simplest setup I keep in my projects:

pyproject.toml

[tool.pytest.ini_options]
addopts = "--doctest-modules"

This line tells pytest to run all your doctests too. One runner, two test styles. Easy.

Then I write pytest tests in the tests/ folder and keep doctests inline only when useful.

Bonus: Pre-commit Hook for Pytest

If you're using pre-commit, add a hook for pytest:

- repo: https://github.com/pre-commit/mirrors-pytest
rev: v7.4.2
hooks:
- id: pytest

Now you’ll never push broken doc examples or failing tests.


Editor Setup for VS Code and Gitpod

For doctest:

  • Make sure your Python extension is up to date.
  • Use doctest syntax highlighters like doctest-vscode to color the docstring examples.
  • Run pytest in the terminal to catch both test types.

For test exploration:

  • The Python extension will automatically discover pytest tests and show them in the test explorer view.
  • Gitpod works the same—just make sure you’ve got your requirements.txt or pyproject.toml specifying pytest.

Conclusion: Should You Use Doctest at All?

If you're asking that question, you're probably writing code for other developers to consume. In that case, yes. doctest is a gift. It forces you to think about example-driven documentation, and in return, gives you auto-validation of those examples. That’s a beautiful feedback loop.

But don’t abandon pytest. It’s the workhorse, the backbone of your test strategy. doctest is the poet. It speaks in example, in clarity, and in correctness over time.

Used right, they complement each other like prose and proof.



Let me know if you want this exported as .txt or styled with markdown.