Repository navigation
Description
A PyPy spec can accept a CPython executable named pypy3.11. The returned PythonInfo says CPython, but the lookup reports it as satisfying the PyPy request.
Affected code
src/python_discovery/_discovery.py:478 at the reported commit. The reproducer also returns BUG on current main 004294957ed227fe7e52b2864952ba09fd28b220.
Mechanism
The path-name filter admits the filename and the later acceptance check does not reject the interpreter's different implementation.
Reproduction
Check out the reported commit or current main. Save the files below as named, then build and run:
docker build -t python-discovery-audit -f - . <<'DOCKERFILE'
FROM python:3.15-bookworm
RUN python -m venv /opt/venv
ENV PATH=/opt/venv/bin:$PATH
COPY . /src
WORKDIR /src
ENV SETUPTOOLS_SCM_PRETEND_VERSION=1.9.1
RUN pip install -e . --group test
DOCKERFILE
docker run --rm --network=none --memory=2g -u 0 -w /src \
-v "$PWD/repro:/tmp/repro:ro" -v "$PWD/repro.d:/tmp/repro.d:ro" \
python-discovery-audit /tmp/reprorepro
#!/usr/bin/env python3
import json
import os
import subprocess
import sys
import tempfile
CPYTHON = "/usr/bin/python3.11"
REQUEST = "pypy3.11"
NAMES_ON_PATH = ["python3.11", "pypy3.11", "Python3.11"] # python3.11 is the control
# set up: the stand-in interpreter is a CPython
stand_in = subprocess.run(
[CPYTHON, "-c", "import platform; print(platform.python_implementation())"],
capture_output=True,
text=True,
check=True,
).stdout.strip()
print(f"{CPYTHON} is {stand_in}")
scratch = tempfile.mkdtemp()
# trigger: the same CPython alone on PATH under each name, pypy3.11 requested, one fresh process and cache each
# (fresh processes, because python-discovery also caches interpreter info in memory)
selected = {}
for name in NAMES_ON_PATH:
path_dir = os.path.join(scratch, name + "-bin")
os.mkdir(path_dir)
os.symlink(CPYTHON, os.path.join(path_dir, name))
cache_dir = os.path.join(scratch, name + "-cache")
call = subprocess.run(
[sys.executable, "/tmp/repro.d/find_interpreter.py", REQUEST, path_dir, cache_dir],
capture_output=True,
text=True,
check=True,
)
print(f"\nPATH={path_dir} ({name} -> {CPYTHON}) get_interpreter({REQUEST!r}) log:")
print(call.stderr, end="")
selected[name] = json.loads(call.stdout)
# check: a pypy3.11 request must return a PyPy or None, whatever the file on PATH is called
print()
for name, info in selected.items():
print(f"{name:>10} on PATH -> {info}")
not_pypy = [name for name, info in selected.items() if info is not None and info["implementation"] != "PyPy"]
print("BUG" if not_pypy else "OK")repro.d/find_interpreter.py
import json
import logging
import sys
from pathlib import Path
from python_discovery import DiskCache, get_interpreter
spec, path_dir, cache_dir = sys.argv[1:]
# shows every interrogation argv and the proposed/accepted decisions on stderr
logging.basicConfig(format=" %(message)s")
logging.getLogger("python_discovery").setLevel(logging.DEBUG)
info = get_interpreter(spec, cache=DiskCache(Path(cache_dir)), env={"PATH": path_dir})
if info is None:
print(json.dumps(None))
else:
print(
json.dumps({
"executable": info.executable,
"system_executable": info.system_executable,
"implementation": info.implementation,
"spec": info.spec,
})
)Output on 004294957ed227fe7e52b2864952ba09fd28b220:
/usr/bin/python3.11 is CPython
PATH=/tmp/tmpfvw5s9vi/python3.11-bin (python3.11 -> /usr/bin/python3.11) get_interpreter('pypy3.11') log:
find interpreter for spec PythonSpec(implementation=pypy, major=3, minor=11, free_threaded=False)
get interpreter info via cmd: /usr/local/bin/python3.15 /src/src/python_discovery/_py_info_collect.py f301952094e71991d774bdb741721d9b 6086cf8e271bd3172584765d27535c1f
wrote python info at /tmp/tmpfvw5s9vi/python3.11-cache/py_info/4/755e5f8e9348854516c9513236b846adb15e9a21490fd9dc391537946af27457.json
filesystem is case-sensitive
proposed PythonInfo(spec=CPython3.15.2.final.0-64-x86_64, system=/usr/local/bin/python3.15, exe=/opt/venv/bin/python3, platform=linux, version='3.15.2 (main, Oct 28 2026, 04:34:17) [GCC 12.2.0]', encoding_fs_io=utf-8-utf-8)
discover PATH[0]=/tmp/tmpfvw5s9vi/python3.11-bin
get interpreter info via cmd: /tmp/tmpfvw5s9vi/python3.11-bin/python3.11 /src/src/python_discovery/_py_info_collect.py 89a120178e873ae144e973b7d436a3fd ecf37618ad4e11b62734237932b85d45
wrote python info at /tmp/tmpfvw5s9vi/python3.11-cache/py_info/4/a1586e0a50513ab4d5ca39158fe53d239d45b28a4d4c232f2263117a562cca9e.json
get interpreter info via cmd: /usr/bin/python3.11 /src/src/python_discovery/_py_info_collect.py 6f26471b56546ff7290cdd53c51938ac 8b484f3ebf626ec7e1af6559a7c0e2ad
wrote python info at /tmp/tmpfvw5s9vi/python3.11-cache/py_info/4/ca3ed784184f1b3bb7c3539bfb45e71710cd27667424f92c2d5bb4df9c107c23.json
proposed PathPythonInfo(spec=CPython3.11.2.final.0-64-x86_64, system=/usr/bin/python3.11, exe=/tmp/tmpfvw5s9vi/python3.11-bin/python3.11, platform=linux, version='3.11.2 (main, Oct 7 2026, 12:35:07) [GCC 12.2.0]', encoding_fs_io=utf-8-utf-8)
PATH=/tmp/tmpfvw5s9vi/pypy3.11-bin (pypy3.11 -> /usr/bin/python3.11) get_interpreter('pypy3.11') log:
find interpreter for spec PythonSpec(implementation=pypy, major=3, minor=11, free_threaded=False)
get interpreter info via cmd: /usr/local/bin/python3.15 /src/src/python_discovery/_py_info_collect.py 83b7f186188cd30612c6e4d2d00b9583 e22ea2e6a429f834924ccfca7f06bf7b
wrote python info at /tmp/tmpfvw5s9vi/pypy3.11-cache/py_info/4/755e5f8e9348854516c9513236b846adb15e9a21490fd9dc391537946af27457.json
filesystem is case-sensitive
proposed PythonInfo(spec=CPython3.15.2.final.0-64-x86_64, system=/usr/local/bin/python3.15, exe=/opt/venv/bin/python3, platform=linux, version='3.15.2 (main, Oct 28 2026, 04:34:17) [GCC 12.2.0]', encoding_fs_io=utf-8-utf-8)
discover PATH[0]=/tmp/tmpfvw5s9vi/pypy3.11-bin
get interpreter info via cmd: /tmp/tmpfvw5s9vi/pypy3.11-bin/pypy3.11 /src/src/python_discovery/_py_info_collect.py 9fcfe3d845c385441bc6d1b380883f13 bfa1536c692e6874a43ac6c520ae0043
wrote python info at /tmp/tmpfvw5s9vi/pypy3.11-cache/py_info/4/88774105092c9778ffe9cc8db2f726a53d66e92f7abd57c1f26faf142e5f3065.json
get interpreter info via cmd: /usr/bin/python3.11 /src/src/python_discovery/_py_info_collect.py 5d4342aec3bdafcde1b509d64dbf88c9 5c1bafa4314d474bb2b870f574689bc0
wrote python info at /tmp/tmpfvw5s9vi/pypy3.11-cache/py_info/4/ca3ed784184f1b3bb7c3539bfb45e71710cd27667424f92c2d5bb4df9c107c23.json
proposed PathPythonInfo(spec=CPython3.11.2.final.0-64-x86_64, system=/usr/bin/python3.11, exe=/tmp/tmpfvw5s9vi/pypy3.11-bin/pypy3.11, platform=linux, version='3.11.2 (main, Oct 7 2026, 12:35:07) [GCC 12.2.0]', encoding_fs_io=utf-8-utf-8)
accepted PathPythonInfo(spec=CPython3.11.2.final.0-64-x86_64, system=/usr/bin/python3.11, exe=/tmp/tmpfvw5s9vi/pypy3.11-bin/pypy3.11, platform=linux, version='3.11.2 (main, Oct 7 2026, 12:35:07) [GCC 12.2.0]', encoding_fs_io=utf-8-utf-8)
PATH=/tmp/tmpfvw5s9vi/Python3.11-bin (Python3.11 -> /usr/bin/python3.11) get_interpreter('pypy3.11') log:
find interpreter for spec PythonSpec(implementation=pypy, major=3, minor=11, free_threaded=False)
get interpreter info via cmd: /usr/local/bin/python3.15 /src/src/python_discovery/_py_info_collect.py 582d82a615af9f8e71569468bcd0ab3d a58736cb88b725298c9e0a0892ba10c8
wrote python info at /tmp/tmpfvw5s9vi/Python3.11-cache/py_info/4/755e5f8e9348854516c9513236b846adb15e9a21490fd9dc391537946af27457.json
filesystem is case-sensitive
proposed PythonInfo(spec=CPython3.15.2.final.0-64-x86_64, system=/usr/local/bin/python3.15, exe=/opt/venv/bin/python3, platform=linux, version='3.15.2 (main, Oct 28 2026, 04:34:17) [GCC 12.2.0]', encoding_fs_io=utf-8-utf-8)
discover PATH[0]=/tmp/tmpfvw5s9vi/Python3.11-bin
get interpreter info via cmd: /tmp/tmpfvw5s9vi/Python3.11-bin/Python3.11 /src/src/python_discovery/_py_info_collect.py afd7b421f8a64a84651f61ef58d3167c 08a5ba309be2b3fe3319181896831406
wrote python info at /tmp/tmpfvw5s9vi/Python3.11-cache/py_info/4/a64e97bd7b6718c8c60e29fd10f4fefe0364968f0468a157dabcaf90244b7521.json
get interpreter info via cmd: /usr/bin/python3.11 /src/src/python_discovery/_py_info_collect.py d13b2277dfd877d7e2be17f89615f328 44502ea8ca495d93333c6d17b5ae4915
wrote python info at /tmp/tmpfvw5s9vi/Python3.11-cache/py_info/4/ca3ed784184f1b3bb7c3539bfb45e71710cd27667424f92c2d5bb4df9c107c23.json
proposed PathPythonInfo(spec=CPython3.11.2.final.0-64-x86_64, system=/usr/bin/python3.11, exe=/tmp/tmpfvw5s9vi/Python3.11-bin/Python3.11, platform=linux, version='3.11.2 (main, Oct 7 2026, 12:35:07) [GCC 12.2.0]', encoding_fs_io=utf-8-utf-8)
accepted PathPythonInfo(spec=CPython3.11.2.final.0-64-x86_64, system=/usr/bin/python3.11, exe=/tmp/tmpfvw5s9vi/Python3.11-bin/Python3.11, platform=linux, version='3.11.2 (main, Oct 7 2026, 12:35:07) [GCC 12.2.0]', encoding_fs_io=utf-8-utf-8)
python3.11 on PATH -> None
pypy3.11 on PATH -> {'executable': '/tmp/tmpfvw5s9vi/pypy3.11-bin/pypy3.11', 'system_executable': '/usr/bin/python3.11', 'implementation': 'CPython', 'spec': 'CPython3.11.2.final.0-64-x86_64'}
Python3.11 on PATH -> {'executable': '/tmp/tmpfvw5s9vi/Python3.11-bin/Python3.11', 'system_executable': '/usr/bin/python3.11', 'implementation': 'CPython', 'spec': 'CPython3.11.2.final.0-64-x86_64'}
BUG
Expected: A PyPy spec should reject an interpreter that reports CPython.
Impact
Reproducibility is moderate because the setup requires a specific candidate or configuration. Exposure is niche: a CPython executable must sit on PATH under a PyPy-like filename. The reproduced effect stays within the caller's configured cache, search path, or interpreter.
Credits
Reported by Anthropic's OSS Scanner: ANT-2026-Y51C67N2.
Activity