Bug description:
On a Windows CPython built with clang-cl (build.bat "/p:PlatformToolset=ClangCL", documented in PCbuild/readme.txt; gh-77532), ctypes.util.find_msvcrt() returns 'msvcrt.dll' instead of None. The cause is sys.version truncation: Python/getversion.c caps the compiler segment at 80 chars (%.80s), which cuts off the "MSC v." marker that ctypes.util._get_build_version() sniffs for, so it falls back to "assume MSVC 6". The practical impact: CDLL(find_library("c"), use_errno=True) loads the OS-private legacy msvcrt.dll, whose errno is a different variable from the ucrt errno that _ctypes saves/restores around foreign calls — ctypes.get_errno() returns stale values (typically 0) with no exception raised.
Observable symptom: test.test_ctypes.test_errno.Test.test_open fails on clang-cl builds, while on MSVC builds find_msvcrt() returns None and the same test silently skips (skipTest("Unable to find C library")), which is why this has gone unnoticed.
import sys, errno from ctypes import CDLL, c_char_p, c_int, get_errno from ctypes.util import find_library, find_msvcrt print(sys.version) print(find_msvcrt()) # 'msvcrt.dll' — expected None, as on MSVC builds libc = CDLL(find_library("c"), use_errno=True) libc._open.argtypes = c_char_p, c_int print(libc._open(b"", 0)) # -1 (call fails, as intended) print(get_errno()) # 0 — expected errno.ENOENT == 2
Observed on a clang-cl build of main:
3.16.0a0 free-threading build (heads/ft-opt-dirty:2f032556f42, Jul 16 2026, 11:20:12) [Clang 22.1.8 (https://github.com/llvm/llvm-project ca7933e47d3a3451d81e72ac174d
msvcrt.dll
-1
0
$ python -m unittest -v test.test_ctypes.test_errno
...
FAIL: test_open (test.test_ctypes.test_errno.Test.test_open)
----------------------------------------------------------------------
Traceback (most recent call last):
File "...\Lib\test\test_ctypes\test_errno.py", line 25, in test_open
self.assertEqual(get_errno(), errno.ENOENT)
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: 0 != 2
The same build additionally shows both CRTs loaded after the call (ctypes.util.dllist() lists ucrtbase.dll and msvcrt.dll), which is the divergence in one line.
Root cause chain (permalinks sha-pinned to main @ 16d0c89 , 2026-07-18; _get_build_version/find_msvcrt/find_library are textually unchanged on today's main, merely shifted a few lines by later unrelated ctypes commits):
PC/pyconfig.h#L161builds the clang banner as"[Clang " __clang_version__ "] 64 bit (AMD64) with MSC v." _Py_STRINGIZE(_MSC_VER) " CRT]"(added in gh-77532: Minor tweaks to allow compiling with PlatformToolset=ClangCL on Windows #101352) — so theMSC v.marker is intended to be present.- But
Python/getversion.c#L18-L20composessys.versionwith"%.80s (%.80s) %.80s"(free-threaded builds use the"%.80s free-threading build (%.80s) %.80s"variant — same%.80scap), truncating the compiler segment at 80 chars. Official LLVM Windows binaries have a long__clang_version__(22.1.8 (https://github.com/llvm/llvm-project <40-hex-sha>)— 86 chars by itself, 93 with the[Clangprefix), so the tail carrying both markers is always truncated away. The fullPy_GetCompiler()string on my build is 130 chars:while the[Clang 22.1.8 (https://github.com/llvm/llvm-project ca7933e47d3a3451d81e72ac174dcb5aa28b59d1)] 64 bit (AMD64) with MSC v.1951 CRT]sys.versioncompiler segment ends at...ca7933e47d3a3451d81e72ac174d— 28 characters into the commit hash, withoutMSC v.,(AMD64), or the closing bracket. (An MSVC banner is ~27 chars and is never truncated, which is why this is clang-specific. The same truncation already brokesysconfig.get_platform()on clang-cl builds — sysconfig.get_platform() could return wrong value because sys.version is truncated #145410, fixed in March 2026 by moving the check into C: gh-145410: Add _sysconfig.get_platform() function #146145.) Lib/ctypes/util.py#L10-L33:_get_build_version()sniffssys.versionfor"MSC v."and, when absent, returns 6 ("assume the compiler is MSVC 6", a distutils-era assumption).Lib/ctypes/util.py#L35-L54:find_msvcrt()mapsversion <= 6to'msvcrt.dll', andfind_library("c")returns that.msvcrt.dllmaintains its own CRT state; the interpreter and_ctypesuse the ucrt. Theerrnoset bymsvcrt.dll's_openis never seen by theuse_errnomachinery →get_errno() == 0.
Note that the 'msvcrt.dll' return is reachable only through this banner-sniff failure: when the marker survives (e.g. MSVC's [MSC v.1943 64 bit (AMD64)]), _get_build_version() yields 14.x and find_msvcrt() returns None — the code comment there cites issue23606, "CRT is no longer directly loadable" (gh-67794/bpo-23606). A clang-cl build links the same ucrt as the MSVC build, so 'msvcrt.dll' is simply the wrong answer for it. The MSVC-6 fallback would still be meaningful for a toolchain that genuinely links msvcrt.dll (classic MinGW), which is worth weighing when choosing between the options below.
Suggested fix directions (deferring to the maintainers):
- Minimal: make
_get_build_version()returnNoneinstead of6when"MSC v."is absent, sofind_msvcrt()/find_library("c")behave exactly as on modern MSVC builds andtest_errnoskips consistently. This trades the VC6-era fallback away; if the MinGW case above is considered worth preserving, the next option handles both. - Alternative: make
find_msvcrt()report the CRT the interpreter is actually linked against (ucrtbase.dll/ theapi-ms-win-crt-*stable ABI) independent of the compiler banner — correct for every toolchain, but it would reverse the bpo-23606 decision that modern MSVC returnsNone. - Longer-term: stop deriving build facts from
sys.versionstring-sniffing altogether; the truncation shows the banner is not a reliable data channel (sysconfig.get_platform() could return wrong value because sys.version is truncated #145410 already movedsysconfigoff it).
I'm happy to submit a PR for whichever direction a maintainer prefers, with a regression test that exercises _get_build_version() against a truncated clang-style banner.
Side observations I can file separately if useful: the %.80s cap leaves sys.version ending mid-commit-hash without its closing bracket on official-LLVM clang builds, and the clang _Py_COMPILER string literal has unbalanced brackets (one [, two ]).
Environment / build: CPython main @ f62050d (2026-07-16), built per PCbuild with build.bat -p x64 --disable-gil --pgo --tail-call-interp "/p:PlatformToolset=ClangCL", LLVM 22.1.8 official Windows release, Windows 11 x64. Also reproduced identically on an earlier (June 2026) clang-cl build, and I confirmed the contrasting skip behaviour on an MSVC build of the same source. My working branch carries only build-system tuning (PCbuild props/vcxproj plus a zlib-ng header) and build-regenerated files (hence the -dirty banner); the files involved here — Lib/ctypes/util.py, Lib/test/test_ctypes/test_errno.py, Python/getversion.c, PC/pyconfig.h — are unmodified from upstream. The failure is not related to free-threading — the mechanism only involves the compiler banner segment.
I'm aware clang-cl is PY_SUPPORT_TIER 0; filing since PCbuild documents and actively maintains the toolset (gh-77532, and it is still being kept working — e.g. gh-146210 in 2026, gh-131278/gh-130090/gh-131035 in 2025), the defect is in a stdlib banner-sniff rather than the toolchain, and the same latent defect affects any build whose banner lacks the marker.
Related: gh-77532 / GH-101352 (clang banner introduction), gh-67794 (bpo-23606, find_msvcrt → None for modern MSVC), gh-70914 (user report that find_msvcrt() returns None on post-VC6 builds; closed as a duplicate of bpo-23606), gh-145916 (soft-deprecation of ctypes.util.find_library — its linked PR touches only the docs, so it removes neither this defect nor the test failure), gh-145410 / GH-146145 (same truncation, sysconfig side). The same truncated banner also breaks the vendored get_platform() in Lib/test/test_importlib/test_windows.py; filed separately as gh-154200.
CPython versions tested on:
CPython main branch
Operating systems tested on:
Windows