#134175 It is a suggest for the top module. But we can use it in this issue.
In the feature in this issue, the child module will raise like this.
>>>importmultprocessing.dummy.connection#wrong in top moduleTraceback(mostrecentcalllast):
File"<python-input-0>"line1, in<module>importmultprocessing.dummy.connection#wrong in top module^^^^^^^^^^^^^^ModuleNotFoundError: nomodulenamed'multprocessing'. Didyoumean'multiprocessing'?
>>>importmultiprocessing.dumy.connection#wrong in first child moduleTraceback(mostrecentcalllast):
File"<python-input-1>"line1, in<module>importmultiprocessing.dumy.connection#wrong in first child module~~~~~~~~~~~~~~~~^^^^ModuleNotFoundError: module'multiprocessing'hasnochildmodule'dumy'. Didyoumean'dummy'?
>>>importmultiprocessing.dummy.connections#wrong in second child moduleTraceback(mostrecentcalllast):
File"<python-input-2>"line1, in<module>importmultiprocessing.dummy.connections#wrong in second child module~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^ModuleNotFoundError: module'multiprocessing.dummy'hasnochildmodule'connections'. Didyoumean'connection'?
The user can see clearly which module is wrong first in import.
I'm still not convinced by this. This would require discovering modules that are deeply nested. What if we have a.b.c and a.c.b and someone writes a.b.b. Should it be a.b.c or a.c.b? I'm going to close this one for now. Actually, my previous idea was wrong because the cache is built at the REPL level, but suggestions are raised at the interpreter's level so no we shouldn't only have suggestions in the REPL but no in the interpreter (that would make the treatment of exceptions way harder)
What if we have a.b.c and a.c.b and someone writes a.b.b. Should it be a.b.c or a.c.b
I don't know what it says. If it is a.b.b, the module 'a.b' will be successfully import first, so 'a.c.b' is impossible.
And anyway, there are two messages first:"no module named 'a.b.b'" and "module 'a.b' has no child module 'b'", which is more clear?
---- Replied Message ----
| From | Bénédikt ***@***.***> |
| Date | 08/09/2025 18:36 |
| To | ***@***.***> |
| Cc | ***@***.***>***@***.***> |
| Subject | Re: [python/cpython] Suggest for child module in ModuleNotFoundError (Issue #134872) |
picnixz left a comment (python/cpython#134872)
I'm still not convinced by this. This would require discovering modules that are deeply nested. What if we have a.b.c and a.c.b and someone writes a.b.b. Should it be a.b.c or a.c.b? I'm going to close this one for now. Actually, my previous idea was wrong because the cache is built at the REPL level, but suggestions are raised at the interpreter's level so no we shouldn't only have suggestions in the REPL but no in the interpreter (that would make the treatment of exceptions way harder)
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you authored the thread.Message ID: ***@***.***>
So you would suggest that we only present children modules if any. Ok I misunderstood that part. However, the problem with the cache remains and changing this at the interpreter level (actually it would be in importlib/_bootstrap.py) would be too complex IMO (the cache may grow indefinitely and fast depending on the number of modules inside site-packages).
Before accepting such feature, I want first a PoC that doesn't affect import performance nor the runtime memory. Also, I don't think we should keep two issues open (though I closed both) for the same feature. If we can reuse the module completer from the REPL during bootstrapping (but I'm not sure we can rely on pkgutil because of that), maybe we could do something but I'm afraid it will be too complex.
Also, more generally, if we have two packages with close names, suggesting the correct one may be hard and still misleading (if we want to import say mod1 but we have mod2 and mod3, I don't really like suggesting mod2over mod3 or vice-versa; for delattr/getattr, it's usually fine because we don't have thousands of possible names, but for modules, collisions are more likely to happen IMO).
Also, more generally, if we have two packages with close names, suggesting the correct one may be hard and still misleading (if we want to import say mod1 but we have mod2 and mod3, I don't really like suggesting mod2over mod3 or vice-versa; for delattr/getattr, it's usually fine because we don't have thousands of possible names, but for modules, collisions are more likely to happen IMO).
I test the suggestion when there are many names in locals():
for i in range(1000000): exec(f"a{i}=1", globals())
a_1000
Then it doesn't suggest "did you mean 'a1000'?" So I think that if there are too many similar name it is nature that it won't suggest.
And I wrote a code to return all of the module name. However, I don't know whether the sys.builtin_module_names different, so it may be changed by #ifdef ...:
>>>a_1002=1
>>>a_1004=1
>>>a_1003
Traceback (most recent call last):
File "<python-input-6>", line 1, in <module>
a_1003
NameError: name 'a_1003' is not defined. Did you mean: 'a_1002'?
Yeah the maximum number of possible suggestions is 750 (harcoded in MAX_CANDIDATE_ITEMS) and we don't suggest for names longer than 40 characters.
As for iterating over the modules, we should be careful with whether the file is a module or not. pkgutil.iter_modules() iterates over the modules of a package but it still imports the packages. The problem we face however is that the cache needs to be built during import bootstrapping and maintained. However I'm not sure we can re-use import_module from importlib for that as it could then have a recursive issue (if we try to import a module that doesn't exist, and then try to check which are those that exist, then we may need to import a package, and for that we need to be able to use the import functions correctly).
Anyway, I would first suggest having a strong PoC that shows how to integrate this component in the bootstrapping. Checking for suggestions would be fine if we just export the functions that calculate the suggestions (we need them in C otherwise it'll be too slow I think).
And I wrote a code to return all of the module name. However, I don't know whether the sys.builtin_module_names different, so it may be changed by #ifdef ...:我写了一段代码来返回所有模块的名称。但是,我不知道 sys.builtin_module_names 是否不同,所以可以通过 #ifdef ... 来更改:
Now the question is :
Whether the function scan_dir() right in all of the system and hou to get the initialize the builtin_modules. Now I test it on windows and no problems.
As for iterating over the modules, we should be careful with whether the file is a module or not. pkgutil.iter_modules() iterates over the modules of a package but it still imports the packages. The problem we face however is that the cache needs to be built during import bootstrapping and maintained. However I'm not sure we can re-use import_module from importlib for that as it could then have a recursive issue (if we try to import a module that doesn't exist, and then try to check which are those that exist, then we may need to import a package, and for that we need to be able to use the import functions correctly).
Anyway, I would first suggest having a strong PoC that shows how to integrate this component in the bootstrapping. Checking for suggestions would be fine if we just export the functions that calculate the suggestions (we need them in C otherwise it'll be too slow I think).
Now it seems not a problem. See here. It is a better implement. The suggestions are given in traceback.py instead of the interpreter itself so that we can re-use the cache just in that file without any worries for recursive issue, though I didn't see any benifit for the cache used in here (In normal condition, I assumed that the maximun quantity of third-party libraries in one environment is 300, and the function scan_dir took 5ms to find the librarys in "site-packages").
Now the only problem is only the "__find__" method to help finding all of the modules from the custom import hook. It may be need a discussion.
Activity
picnixz commented on Aug 9, 2025
I'm still not convinced by this. This would require discovering modules that are deeply nested. What if we have a.b.c and a.c.b and someone writes a.b.b. Should it be a.b.c or a.c.b? I'm going to close this one for now. Actually, my previous idea was wrong because the cache is built at the REPL level, but suggestions are raised at the interpreter's level so no we shouldn't only have suggestions in the REPL but no in the interpreter (that would make the treatment of exceptions way harder)
Locked-chess-official commented on Aug 9, 2025
picnixz commented on Aug 9, 2025
So you would suggest that we only present children modules if any. Ok I misunderstood that part. However, the problem with the cache remains and changing this at the interpreter level (actually it would be in importlib/_bootstrap.py) would be too complex IMO (the cache may grow indefinitely and fast depending on the number of modules inside site-packages).
Before accepting such feature, I want first a PoC that doesn't affect import performance nor the runtime memory. Also, I don't think we should keep two issues open (though I closed both) for the same feature. If we can reuse the module completer from the REPL during bootstrapping (but I'm not sure we can rely on pkgutil because of that), maybe we could do something but I'm afraid it will be too complex.
Also, more generally, if we have two packages with close names, suggesting the correct one may be hard and still misleading (if we want to import say mod1 but we have mod2 and mod3, I don't really like suggesting mod2over mod3 or vice-versa; for delattr/getattr, it's usually fine because we don't have thousands of possible names, but for modules, collisions are more likely to happen IMO).
Locked-chess-official commented on Aug 9, 2025
I test the suggestion when there are many names in locals():
for i in range(1000000): exec(f"a{i}=1", globals()) a_1000Then it doesn't suggest "did you mean 'a1000'?" So I think that if there are too many similar name it is nature that it won't suggest.
Locked-chess-official commented on Aug 9, 2025
And I wrote a code to return all of the module name. However, I don't know whether the sys.builtin_module_names different, so it may be changed by #ifdef ...:
#include <stdio.h> #include <stdlib.h> #include <string.h> #ifdef _WIN32 #include <windows.h> #else #include <dirent.h> #endif #include <sys/stat.h> // gcc list_modules.c -DPYTHON_STD_PATH=\"/usr/lib/python3.11\" -o list_modules // gcc list_modules.c -DPY_EXT_PREFIX=\"cpython-311-x86_64-linux-gnu\" -o list_modules #define PY_EXT_PY ".py" #ifdef _WIN32 #define PY_EXT_PYD ".pyd" #else #define PY_EXT_SO ".so" #endif const char* builtin_modules[] = { "_abc", "_ast", "_bisect", "_blake2", "_codecs", "_codecs_cn", "_codecs_hk", "_codecs_iso2022", "_codecs_jp", "_codecs_kr", "_codecs_tw", "_collections", "_contextvars", "_csv", "_datetime", "_functools", "_heapq", "_imp", "_interpchannels", "_interpqueues", "_interpreters", "_io", "_json", "_locale", "_lsprof", "_md5", "_multibytecodec", "_opcode", "_operator", "_pickle", "_random", "_sha1", "_sha2", "_sha3", "_signal", "_sre", "_stat", "_statistics", "_string", "_struct", "_suggestions", "_symtable", "_sysconfig", "_thread", "_tokenize", "_tracemalloc", "_typing", "_warnings", "_weakref", "_winapi", "array", "atexit", "binascii", "builtins", "cmath", "errno", "faulthandler", "gc", "itertools", "marshal", "math", "mmap", "msvcrt", "nt", "sys", "time", "winreg", "xxsubtype", "zlib", NULL }; int has_suffix(const char *name, const char *suffix) { size_t len1 = strlen(name); size_t len2 = strlen(suffix); return len1 > len2 && strcmp(name + len1 - len2, suffix) == 0; } #ifdef PY_EXT_PREFIX int match_ext_prefix(const char* filename) { return strstr(filename, PY_EXT_PREFIX) != NULL; } #endif void add_module_name(const char* filename) { printf("%s\n", filename); } void scan_dir(const char* path) { #ifdef _WIN32 WIN32_FIND_DATA findFileData; HANDLE hFind; char searchPath[MAX_PATH]; snprintf(searchPath, sizeof(searchPath), "%s\\*", path); hFind = FindFirstFile(searchPath, &findFileData); if (hFind == INVALID_HANDLE_VALUE) return; do { if (strcmp(findFileData.cFileName, ".") == 0 || strcmp(findFileData.cFileName, "..") == 0) continue; char fullpath[MAX_PATH]; snprintf(fullpath, sizeof(fullpath), "%s\\%s", path, findFileData.cFileName); if (findFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { char initfile[MAX_PATH]; snprintf(initfile, sizeof(initfile), "%s\\__init__.py", fullpath); WIN32_FILE_ATTRIBUTE_DATA fileData; if (GetFileAttributesEx(initfile, GetFileExInfoStandard, &fileData)) { add_module_name(findFileData.cFileName); } } else { if (has_suffix(findFileData.cFileName, PY_EXT_PY)) { size_t len = strlen(findFileData.cFileName) - strlen(PY_EXT_PY); char modname[256]; strncpy(modname, findFileData.cFileName, len); modname[len] = '\0'; add_module_name(modname); } #ifdef PY_EXT_PREFIX else if (has_suffix(findFileData.cFileName, PY_EXT_PYD) && match_ext_prefix(findFileData.cFileName)) { size_t len = strlen(findFileData.cFileName) - strlen(PY_EXT_PYD); char modname[256]; strncpy(modname, findFileData.cFileName, len); modname[len] = '\0'; add_module_name(modname); } #endif } } while (FindNextFile(hFind, &findFileData)); FindClose(hFind); #else DIR *dir = opendir(path); if (!dir) return; struct dirent *entry; while ((entry = readdir(dir)) != NULL) { if (entry->d_name[0] == '.') continue; char fullpath[1024]; snprintf(fullpath, sizeof(fullpath), "%s/%s", path, entry->d_name); struct stat st; if (stat(fullpath, &st) == -1) continue; if (S_ISREG(st.st_mode)) { // .py if (has_suffix(entry->d_name, PY_EXT_PY)) { size_t len = strlen(entry->d_name) - strlen(PY_EXT_PY); char modname[256]; strncpy(modname, entry->d_name, len); modname[len] = '\0'; add_module_name(modname); } #ifdef PY_EXT_PREFIX #ifdef _WIN32 else if (has_suffix(entry->d_name, PY_EXT_PYD) && match_ext_prefix(entry->d_name)) { size_t len = strlen(entry->d_name) - strlen(PY_EXT_PYD); char modname[256]; strncpy(modname, entry->d_name, len); modname[len] = '\0'; add_module_name(modname); } #else else if (has_suffix(entry->d_name, PY_EXT_SO) && match_ext_prefix(entry->d_name)) { const char* dot = strchr(entry->d_name, '.'); if (dot) { size_t len = dot - entry->d_name; char modname[256]; strncpy(modname, entry->d_name, len); modname[len] = '\0'; add_module_name(modname); } } #endif #endif } else if (S_ISDIR(st.st_mode)) { char initfile[1024]; snprintf(initfile, sizeof(initfile), "%s/__init__.py", fullpath); if (stat(initfile, &st) == 0) { add_module_name(entry->d_name); } } } closedir(dir); #endif } int main() { #ifdef PYTHON_STD_PATH scan_dir(PYTHON_STD_PATH); #endif #ifdef PYTHON_SITE_PATH scan_dir(PYTHON_SITE_PATH); #endif for (int i = 0; builtin_modules[i] != NULL; i++) { printf("%s\n", builtin_modules[i]); } return 0; }Locked-chess-official commented on Aug 9, 2025
For more test, the suggestion only get one name:
>>>a_1002=1 >>>a_1004=1 >>>a_1003 Traceback (most recent call last): File "<python-input-6>", line 1, in <module> a_1003 NameError: name 'a_1003' is not defined. Did you mean: 'a_1002'?picnixz commented on Aug 9, 2025
Yeah the maximum number of possible suggestions is 750 (harcoded in MAX_CANDIDATE_ITEMS) and we don't suggest for names longer than 40 characters.
As for iterating over the modules, we should be careful with whether the file is a module or not. pkgutil.iter_modules() iterates over the modules of a package but it still imports the packages. The problem we face however is that the cache needs to be built during import bootstrapping and maintained. However I'm not sure we can re-use import_module from importlib for that as it could then have a recursive issue (if we try to import a module that doesn't exist, and then try to check which are those that exist, then we may need to import a package, and for that we need to be able to use the import functions correctly).
Anyway, I would first suggest having a strong PoC that shows how to integrate this component in the bootstrapping. Checking for suggestions would be fine if we just export the functions that calculate the suggestions (we need them in C otherwise it'll be too slow I think).
Locked-chess-official commented on Aug 9, 2025
Now the question is :
Whether the function scan_dir() right in all of the system and hou to get the initialize the builtin_modules. Now I test it on windows and no problems.
Locked-chess-official commented on Sep 9, 2025
Now it seems not a problem. See here. It is a better implement. The suggestions are given in traceback.py instead of the interpreter itself so that we can re-use the cache just in that file without any worries for recursive issue, though I didn't see any benifit for the cache used in here (In normal condition, I assumed that the maximun quantity of third-party libraries in one environment is 300, and the function scan_dir took 5ms to find the librarys in "site-packages").
Now the only problem is only the "__find__" method to help finding all of the modules from the custom import hook. It may be need a discussion.
Now the third-party library friendly_module_not_found_error is the monkey patch about it in all of the versions.
pythongh-134872: Add traceback suggestions for ModuleNotFoundError
pythongh-134872: Add traceback suggestions for ModuleNotFoundError
FFY00 commented on Oct 10, 2025
I'm revisiting this with a new API proposal in GH-139899.
Here's a working implementation on top of GH-139900.
pythongh-134872: Add traceback suggestions for ModuleNotFoundError
pythongh-134872: Add traceback suggestions for ModuleNotFoundError
pythongh-134872: Add traceback suggestions for ModuleNotFoundError
pythongh-134872: Add traceback suggestions for ModuleNotFoundError
GH-134872: add ModuleNotFoundError suggestions (#142512)
FFY00 commented on Feb 19, 2026
Implemented in GH-142512
pythonGH-134872: add ModuleNotFoundError suggestions (python#142512)
pythonGH-134872: add ModuleNotFoundError suggestions (python#142512)