https://github.com/python/cpython/commit/1ac9d138ae0563f2829ba91efe7989af507f47e0
commit: 1ac9d138ae0563f2829ba91efe7989af507f47e0
branch: main
author: Filipe Laíns <[email protected]>
committer: FFY00 <[email protected]>
date: 2026-02-25T00:53:01Z
summary:

GH-145006: add ModuleNotFoundError hints when a module for a differen… (#145007)

* GH-145006: add ModuleNotFoundError hints when a module for a different ABI 
exists

Signed-off-by: Filipe Laíns <[email protected]>

* Fix deprecation warnings

Signed-off-by: Filipe Laíns <[email protected]>

* Use SHLIB_SUFFIX in test_find_incompatible_extension_modules when available

Signed-off-by: Filipe Laíns <[email protected]>

* Add test_incompatible_extension_modules_hint

Signed-off-by: Filipe Laíns <[email protected]>

* Fix Windows

Signed-off-by: Filipe Laíns <[email protected]>

* Show the whole extension module file name in hint

Signed-off-by: Filipe Laíns <[email protected]>

---------

Signed-off-by: Filipe Laíns <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-02-19-17-50-47.gh-issue-145006.9gqA0Q.rst
M Lib/test/test_traceback.py
M Lib/traceback.py

diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py
index 99ac7fd83d91cb..3896f34a34c8d6 100644
--- a/Lib/test/test_traceback.py
+++ b/Lib/test/test_traceback.py
@@ -9,15 +9,18 @@
 import builtins
 import unittest
 import unittest.mock
+import os
 import re
 import tempfile
 import random
 import string
+import importlib.machinery
+import sysconfig
 from test import support
 import shutil
 from test.support import (Error, captured_output, cpython_only, ALWAYS_EQ,
                           requires_debug_ranges, has_no_debug_ranges,
-                          requires_subprocess)
+                          requires_subprocess, os_helper)
 from test.support.os_helper import TESTFN, temp_dir, unlink
 from test.support.script_helper import assert_python_ok, 
assert_python_failure, make_script
 from test.support.import_helper import forget
@@ -5194,6 +5197,56 @@ def test_windows_only_module_error(self):
         else:
             self.fail("ModuleNotFoundError was not raised")
 
+    @unittest.skipIf(not importlib.machinery.EXTENSION_SUFFIXES, 'Platform 
does not support extension modules')
+    def test_find_incompatible_extension_modules(self):
+        """_find_incompatible_extension_modules assumes the last extension in
+        importlib.machinery.EXTENSION_SUFFIXES (defined in Python/dynload_*.c)
+        is untagged (eg. .so, .pyd).
+
+        This test exists to make sure that assumption is correct.
+        """
+        last_extension_suffix = importlib.machinery.EXTENSION_SUFFIXES[-1]
+        if shlib_suffix := sysconfig.get_config_var('SHLIB_SUFFIX'):
+            self.assertEqual(last_extension_suffix, shlib_suffix)
+        else:
+            before_dot, *extensions = last_extension_suffix.split('.')
+            expected_prefixes = ['']
+            if os.name == 'nt':
+                # Windows puts the debug tag in the module file stem (eg. 
foo_d.pyd)
+                expected_prefixes.append('_d')
+            self.assertIn(before_dot, expected_prefixes, msg=(
+                f'Unexpected prefix {before_dot!r} in extension module '
+                f'suffix {last_extension_suffix!r}. '
+                'traceback._find_incompatible_extension_module needs to be '
+                'updated to take this into account!'
+            ))
+            # if SHLIB_SUFFIX is not define, we assume the native
+            # shared library suffix only contains one extension
+            # (eg. '.so', bad eg. '.cpython-315-x86_64-linux-gnu.so')
+            self.assertEqual(len(extensions), 1, msg=(
+                'The last suffix in importlib.machinery.EXTENSION_SUFFIXES '
+                'contains more than one extension, so it is probably different 
'
+                'than SHLIB_SUFFIX. It probably contains an ABI tag! '
+                'If this is a false positive, define SHLIB_SUFFIX in 
sysconfig.'
+            ))
+
+    @unittest.skipIf(not importlib.machinery.EXTENSION_SUFFIXES, 'Platform 
does not support extension modules')
+    def test_incompatible_extension_modules_hint(self):
+        untagged_suffix = importlib.machinery.EXTENSION_SUFFIXES[-1]
+        with os_helper.temp_dir() as tmp:
+            # create a module with a incompatible ABI tag
+            incompatible_module = f'foo.some-abi{untagged_suffix}'
+            open(os.path.join(tmp, incompatible_module), "wb").close()
+            # try importing it
+            code = f'''
+                import sys
+                sys.path.insert(0, {tmp!r})
+                import foo
+            '''
+            _, _, stderr = assert_python_failure('-c', code, __cwd=tmp)
+        hint = f'Although a module with this name was found for a different 
Python version ({incompatible_module}).'
+        self.assertIn(hint, stderr.decode())
+
 
 class TestColorizedTraceback(unittest.TestCase):
     maxDiff = None
diff --git a/Lib/traceback.py b/Lib/traceback.py
index b16cd8646e43f1..4e809acb7a01bb 100644
--- a/Lib/traceback.py
+++ b/Lib/traceback.py
@@ -3,6 +3,7 @@
 import collections.abc
 import itertools
 import linecache
+import os
 import sys
 import textwrap
 import types
@@ -12,6 +13,7 @@
 import tokenize
 import io
 import importlib.util
+import pathlib
 import _colorize
 
 from contextlib import suppress
@@ -1129,6 +1131,11 @@ def __init__(self, exc_type, exc_value, exc_traceback, 
*, limit=None,
                 self._str += (". Site initialization is disabled, did you 
forget to "
                     + "add the site-packages directory to sys.path "
                     + "or to enable your virtual environment?")
+            elif abi_tag := _find_incompatible_extension_module(module_name):
+                self._str += (
+                    ". Although a module with this name was found for a "
+                    f"different Python version ({abi_tag})."
+                )
             else:
                 suggestion = _compute_suggestion_error(exc_value, 
exc_traceback, module_name)
                 if suggestion:
@@ -1880,3 +1887,32 @@ def _levenshtein_distance(a, b, max_cost):
             # Everything in this row is too big, so bail early.
             return max_cost + 1
     return result
+
+
+def _find_incompatible_extension_module(module_name):
+    import importlib.machinery
+    import importlib.resources.readers
+
+    if not module_name or not importlib.machinery.EXTENSION_SUFFIXES:
+        return
+
+    # We assume the last extension is untagged (eg. .so, .pyd)!
+    # tests.test_traceback.MiscTest.test_find_incompatible_extension_modules
+    # tests that assumption.
+    untagged_suffix = importlib.machinery.EXTENSION_SUFFIXES[-1]
+    # On Windows the debug tag is part of the module file stem, instead of the
+    # extension (eg. foo_d.pyd), so let's remove it and just look for .pyd.
+    if os.name == 'nt':
+        untagged_suffix = untagged_suffix.removeprefix('_d')
+
+    parent, _, child = module_name.rpartition('.')
+    if parent:
+        traversable = importlib.resources.files(parent)
+    else:
+        traversable = importlib.resources.readers.MultiplexedPath(
+            *map(pathlib.Path, filter(os.path.isdir, sys.path))
+        )
+
+    for entry in traversable.iterdir():
+        if entry.name.startswith(child + '.') and 
entry.name.endswith(untagged_suffix):
+            return entry.name
diff --git 
a/Misc/NEWS.d/next/Library/2026-02-19-17-50-47.gh-issue-145006.9gqA0Q.rst 
b/Misc/NEWS.d/next/Library/2026-02-19-17-50-47.gh-issue-145006.9gqA0Q.rst
new file mode 100644
index 00000000000000..69052c7ca92c8a
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-02-19-17-50-47.gh-issue-145006.9gqA0Q.rst
@@ -0,0 +1,2 @@
+Add :exc:`ModuleNotFoundError` hints when a module for a different ABI
+exists.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to