Describe the bug
mssql_python/ddbc_bindings.py picks which compiled extension to load using platform.machine():
https://github.com/microsoft/mssql-python/blob/main/mssql_python/ddbc_bindings.py#L79
On Windows, modern CPython resolves platform.machine() through platform.uname(), which reports the native/host CPU. It does not report the architecture the running interpreter was built for. Those differ whenever x64 Python runs on a Windows ARM64 machine, which is the default experience today: python.org's "Windows installer (64-bit)" is x64, and pip correctly installs the win_amd64 wheel for it.
The result is that the loader looks for a binary that was never in the wheel:
platform.machine() → ARM64 → expected ddbc_bindings.cp314-arm64.pyd
- installed wheel is
win_amd64, which contains ddbc_bindings.cp314-amd64.pyd
The lookup misses and control falls into the fallback branch, which prints on stdout and then loads whatever happens to be first in a directory listing:
https://github.com/microsoft/mssql-python/blob/main/mssql_python/ddbc_bindings.py#L122-L123
Two problems with that:
print() to stdout on every import. Not warnings.warn, not stderr. Any tool that parses a script's stdout gets a stray Warning: ... line prepended to its data. That is a correctness issue for anyone piping output.
module_files[0] is arbitrary. It works today only because Windows wheels ship exactly one .pyd. It is an ordering-dependent pick, not a match, so it is one packaging change away from silently loading the wrong binary rather than raising.
Native x64 and native ARM64 hosts are unaffected — platform.machine() agrees with the interpreter there, so the primary path resolves. The bug is specific to the mismatched (emulated) combination, and that combination is common on ARM64 laptops.
To reproduce
On a Windows ARM64 machine, using an x64 Python interpreter:
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install mssql-python==1.13.0
.\.venv\Scripts\python.exe -c "import mssql_python"
Output:
Warning: Using fallback module file ddbc_bindings.cp314-amd64.pyd instead of ddbc_bindings.cp314-arm64.pyd
The mismatch between the two sources of truth:
platform.machine() = ARM64 <-- host CPU, used by the loader
sysconfig.get_platform() = win-amd64 <-- interpreter, matches the installed wheel
sys.version = 3.14.0 ... MSC v.1944 64 bit (AMD64)
PROCESSOR_ARCHITECTURE = AMD64
PROCESSOR_ARCHITEW6432 = None
Note that even PROCESSOR_ARCHITECTURE reports AMD64 correctly here; platform.machine() is the outlier because platform.uname() on Windows queries the native system rather than the process.
Expected behavior
The loader resolves ddbc_bindings.cp314-amd64.pyd on the primary path, with no warning printed, because that is the binary in the wheel that was installed for this interpreter.
Suggested fix
Derive the architecture from the interpreter rather than the host. sysconfig.get_platform() already returns exactly what the wheel tag was built from:
| Interpreter |
sysconfig.get_platform() |
wanted suffix |
| Windows x64 |
win-amd64 |
amd64 |
| Windows ARM64 |
win-arm64 |
arm64 |
| Windows x86 |
win32 |
x86 |
platform.machine() remains correct for Linux, where the emulated-interpreter case is rare, though switching both to one source of truth would be more consistent.
Separately, the fallback branch is worth tightening regardless of how the architecture is derived: warnings.warn(...) (or stderr) instead of print(), and raising rather than guessing when no filename actually matches.
Also affected
tests/test_000_dependencies.py builds its expected extension filename from platform.machine().lower() at line 23, so test_python_extension_exists asserts on the same wrong name and fails for the same reason on this configuration.
That is the assertion failure reported in #171 (Python extension module not found: ...ddbc_bindings.cp313-arm64.pyd). #171 was closed after the ARM64 build toolchain issue was addressed, but the architecture-detection half of it looks like it is still present on main.
Further technical details
- mssql-python version: 1.13.0 (also present on
main)
- Python version: 3.14.0, x64 build
- Operating system: Windows 11, ARM64 host
- SQL Server version: 2022 CU18 (not relevant to this issue; the failure is at import)
Found while building an Arrow/DuckDB demo. The driver works fine via the fallback, so this is not blocking anything, but the stdout warning is user-visible on every import and the fallback selection is fragile.
Describe the bug
mssql_python/ddbc_bindings.pypicks which compiled extension to load usingplatform.machine():https://github.com/microsoft/mssql-python/blob/main/mssql_python/ddbc_bindings.py#L79
On Windows, modern CPython resolves
platform.machine()throughplatform.uname(), which reports the native/host CPU. It does not report the architecture the running interpreter was built for. Those differ whenever x64 Python runs on a Windows ARM64 machine, which is the default experience today: python.org's "Windows installer (64-bit)" is x64, and pip correctly installs thewin_amd64wheel for it.The result is that the loader looks for a binary that was never in the wheel:
platform.machine()→ARM64→ expectedddbc_bindings.cp314-arm64.pydwin_amd64, which containsddbc_bindings.cp314-amd64.pydThe lookup misses and control falls into the fallback branch, which prints on stdout and then loads whatever happens to be first in a directory listing:
https://github.com/microsoft/mssql-python/blob/main/mssql_python/ddbc_bindings.py#L122-L123
Two problems with that:
print()to stdout on every import. Notwarnings.warn, not stderr. Any tool that parses a script's stdout gets a strayWarning: ...line prepended to its data. That is a correctness issue for anyone piping output.module_files[0]is arbitrary. It works today only because Windows wheels ship exactly one.pyd. It is an ordering-dependent pick, not a match, so it is one packaging change away from silently loading the wrong binary rather than raising.Native x64 and native ARM64 hosts are unaffected —
platform.machine()agrees with the interpreter there, so the primary path resolves. The bug is specific to the mismatched (emulated) combination, and that combination is common on ARM64 laptops.To reproduce
On a Windows ARM64 machine, using an x64 Python interpreter:
Output:
The mismatch between the two sources of truth:
Note that even
PROCESSOR_ARCHITECTUREreportsAMD64correctly here;platform.machine()is the outlier becauseplatform.uname()on Windows queries the native system rather than the process.Expected behavior
The loader resolves
ddbc_bindings.cp314-amd64.pydon the primary path, with no warning printed, because that is the binary in the wheel that was installed for this interpreter.Suggested fix
Derive the architecture from the interpreter rather than the host.
sysconfig.get_platform()already returns exactly what the wheel tag was built from:sysconfig.get_platform()win-amd64amd64win-arm64arm64win32x86platform.machine()remains correct for Linux, where the emulated-interpreter case is rare, though switching both to one source of truth would be more consistent.Separately, the fallback branch is worth tightening regardless of how the architecture is derived:
warnings.warn(...)(or stderr) instead ofprint(), and raising rather than guessing when no filename actually matches.Also affected
tests/test_000_dependencies.pybuilds its expected extension filename fromplatform.machine().lower()at line 23, sotest_python_extension_existsasserts on the same wrong name and fails for the same reason on this configuration.That is the assertion failure reported in #171 (
Python extension module not found: ...ddbc_bindings.cp313-arm64.pyd). #171 was closed after the ARM64 build toolchain issue was addressed, but the architecture-detection half of it looks like it is still present onmain.Further technical details
main)Found while building an Arrow/DuckDB demo. The driver works fine via the fallback, so this is not blocking anything, but the stdout warning is user-visible on every import and the fallback selection is fragile.