Skip to content

Extension loader selects architecture from host CPU instead of the interpreter, so x64 Python on Windows ARM64 always takes the fallback path #726

Description

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:

  1. 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.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: packaging-platformWheels, version support, OS/arch coverage (Linux/macOS/Windows), install or import failures.bugSomething isn't workingtriage neededFor new issues, not triaged yet.

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions