Skip to content

Add import suggestions for ModuleNotFoundError #134872

Closed

Description

@Locked-chess-official

Feature or enhancement

Proposal:

#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.

>>>import multprocessing.dummy.connection #wrong in top module
Traceback(most recent call last):
    File "<python-input-0>" line 1, in <module>
        import multprocessing.dummy.connection #wrong in top module
               ^^^^^^^^^^^^^^
ModuleNotFoundError: no module named 'multprocessing'. Did you mean 'multiprocessing'?
>>>import multiprocessing.dumy.connection #wrong in first child module
Traceback(most recent call last):
    File "<python-input-1>" line 1, in <module>
        import multiprocessing.dumy.connection #wrong in first child module
               ~~~~~~~~~~~~~~~~^^^^
ModuleNotFoundError: module 'multiprocessing' has no child module 'dumy'. Did you mean 'dummy'?
>>>import multiprocessing.dummy.connections #wrong in second child module
Traceback(most recent call last):
    File "<python-input-2>" line 1, in <module>
        import multiprocessing.dummy.connections #wrong in second child module
               ~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^
ModuleNotFoundError: module 'multiprocessing.dummy' has no child module 'connections'. Did you mean 'connection'?

The user can see clearly which module is wrong first in import.

Has this already been discussed elsewhere?

No response given

Links to previous discussion of this feature:

No response

Linked PRs

Activity

  1. picnixz commented on Aug 9, 2025

    @picnixz
    Member

    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)

  2. Locked-chess-official commented on Aug 9, 2025

    @Locked-chess-official
    ContributorAuthor
  3. picnixz commented on Aug 9, 2025

    @picnixz
    Member

    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).

  4. Locked-chess-official commented on Aug 9, 2025

    @Locked-chess-official
    ContributorAuthor

    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.

  5. Locked-chess-official commented on Aug 9, 2025

    @Locked-chess-official
    ContributorAuthor

    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;
    }
    
    
  6. Locked-chess-official commented on Aug 9, 2025

    @Locked-chess-official
    ContributorAuthor

    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'?
    
  7. picnixz commented on Aug 9, 2025

    @picnixz
    Member

    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).

  8. Locked-chess-official commented on Aug 9, 2025

    @Locked-chess-official
    ContributorAuthor

    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 ... 来更改:

    #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;
    }
    

    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.

  9. Locked-chess-official commented on Sep 9, 2025

    @Locked-chess-official
    ContributorAuthor

    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.

    Now the third-party library friendly_module_not_found_error is the monkey patch about it in all of the versions.

  10. added 2 commits that reference this issue on Oct 10, 2025
  11. FFY00 commented on Oct 10, 2025

    @FFY00
    Member

    I'm revisiting this with a new API proposal in GH-139899.

    Here's a working implementation on top of GH-139900.

  12. reopened this on Oct 10, 2025
  13. changed the title [-]Suggest for child module in ModuleNotFoundError[/-] [+]Add `import` suggestions for `ModuleNotFoundError`[/+] on Oct 10, 2025
  14. added 2 commits that reference this issue on Dec 10, 2025
  15. added 3 commits that reference this issue on Feb 19, 2026
  16. FFY00 commented on Feb 19, 2026

    @FFY00
    Member

    Implemented in GH-142512

  17. added a commit that references this issue on Feb 28, 2026
  18. added a commit that references this issue on Apr 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    interpreter-core(Objects, Python, Grammar, and Parser dirs)type-featureA feature request or enhancement

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions