Skip to content

Doctest blocks in one document do not share a namespace, unlike pytest text files #83

Description

@tony

Filed against git-pull/gp-libs v0.0.19, CPython 3.14, pytest 8.4.2.

What happens

Every doctest-bearing node in a document becomes its own doctest.DocTest, and CPython gives each DocTest a private copy of the globals. A name bound in one fenced block is therefore invisible in the next, so a narrative page cannot build state across the prose that explains it.

This diverges from pytest's own text-file doctest collection, which parses the whole file as a single DocTest and lets state flow.

Root cause

DocutilsDocTestFinder._find() walks the parsed document and emits one test per matching node:

src/doctest_docutils.py#L385-L402

for idx, node in enumerate(findall(doc)(condition)):
    ...
    test = self._get_test(
        string=node.astext(),
        name=test_name,
        filename=name,
        globs=globs,
        source_lines=[str(node.line)],
    )

The same globs mapping is passed to each call, but it never stays shared, because get_doctest hands it to DocTest:

src/doctest_docutils.py#L416

and CPython copies it unconditionally in DocTest.__init__:

cpython/Lib/doctest.py#L562

self.globs = globs.copy()

So the number of independent namespaces equals the number of nodes matched by condition().

How pytest differs

DoctestTextfile.collect() calls get_doctest exactly once, on the entire file text:

pytest/src/_pytest/doctest.py#L443-L446

parser = doctest.DocTestParser()
test = parser.get_doctest(text, globs, name, filename, 0)
if test.examples:
    yield DoctestItem.from_parent(

One DocTest, one namespace, state flows across the document. A reader who knows --doctest-glob semantics will expect the same from a .md or .rst page and get something different.

Reproduction

1. Two fences, one document

doc.md:

# Title

```python
>>> greeting = "hello"
>>> greeting
'hello'
```

Narrative prose between the two blocks.

```python
>>> greeting.upper()
'HELLO'
```
$ pytest --doctest-docutils-modules doc.md -q --no-header
.F                                                                       [100%]
012 >>> greeting.upper()
UNEXPECTED EXCEPTION: NameError("name 'greeting' is not defined")
1 failed, 1 passed in 0.24s

2. The same two blocks under pytest's own collector

doc.txt:

Title
=====

    >>> greeting = "hello"
    >>> greeting
    'hello'

Narrative prose between the two blocks.

    >>> greeting.upper()
    'HELLO'
$ pytest --doctest-glob='*.txt' doc.txt -q --no-header
.                                                                        [100%]
1 passed in 0.01s

Same two examples, same intervening prose, opposite outcome.

3. A shared Sphinx group does not help

groups is read from the node, but only to name the test:

src/doctest_docutils.py#L388-L392

test_name = node.get("groups")
if isinstance(test_name, list):
    test_name = test_name[0]
if test_name is None or test_name == "default":
    test_name = f"{name}[{idx}]"

grp.rst:

Title
=====

.. doctest:: shared

    >>> greeting = "hello"
    >>> greeting
    'hello'

Narrative prose.

.. doctest:: shared

    >>> greeting.upper()
    'HELLO'
$ pytest --doctest-docutils-modules grp.rst -q --no-header
FAILED grp.rst::shared
1 failed, 1 passed in 0.03s

Both blocks declare the same group, both still get their own namespace. In sphinx.ext.doctest a group is precisely the unit that shares state, so reading the attribute and then not honouring it is the surprising part.

Why it matters

A page that explains as it goes has to choose between three bad options:

  1. Repeat the setup in every fence, which is noise the reader did not ask for.
  2. Put the whole example in one fence, which forces the explanatory sentences inside the code block, where they render as code.
  3. Split the narrative into separate pages.

Option 2 is the one people reach for, and it is silently wrong: a bare prose line inside a fence is accepted by the doctest parser as narrative between examples, so the suite stays green while the rendered page shows English sentences in a syntax-highlighted code block.

What would fix it

Any one of these, roughly in order of how closely it matches existing expectations:

  • Share one namespace per document. Closest to pytest's text-file behaviour and to what --doctest-glob users expect. Would change existing behaviour for anyone relying on isolation.
  • Honour groups. Nodes declaring the same group share a namespace; ungrouped nodes stay isolated. Matches sphinx.ext.doctest, is opt-in, and the attribute is already being read.
  • Make it configurable, e.g. an ini option choosing per-node or per-document, defaulting to today's behaviour.

The second seems the best fit: it is backward compatible, it gives page authors an explicit way to say "these blocks belong together", and it makes an attribute that is currently decorative actually mean something.

Notes

Whichever route, per-node granularity is worth keeping for reporting: naming tests page.md[0], page.md[1] makes failures easy to locate, and that is independent of which namespace they execute in.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions