ModuleNotFoundError vs ImportError (tested)
ModuleNotFoundError and ImportError look similar and get confused constantly. ModuleNotFoundError is a subclass of ImportError, added in Python 3.6, raised specifically when the module itself cannot be located. ImportError is the broader parent class, raised when the module is found but something about the import still fails. We reproduced four real failure cases on this box and captured the exact text.
Catching ImportError catches both. Catching only ModuleNotFoundError misses the other failure modes below.
1. The class relationship
We checked it directly:
$ python3 -c "print(issubclass(ModuleNotFoundError, ImportError))"
True
ModuleNotFoundError was added in Python 3.6 (per the Python docs, not independently measured here). Before 3.6, a missing module also raised plain ImportError. Because of the subclass relationship, except ImportError: still catches a missing-module error today; except ModuleNotFoundError: does not catch the other ImportError cases in this guide.
2. Case A: the module genuinely does not exist
We ran this against Python 3.13.5:
$ python3 -c "import totally_fake_module_xyz"
Traceback (most recent call last):
File "<string>", line 1, in <module>
import totally_fake_module_xyz
ModuleNotFoundError: No module named 'totally_fake_module_xyz'
This is the clean case: Python searched sys.path, found nothing named totally_fake_module_xyz, and raised ModuleNotFoundError. The exception carries the missing name in e.name; we checked and e.path is None here because no file was ever located.
2b. Case B: the module exists, but the name inside it does not
os is a real, importable module, but it has no fake_function_xyz:
$ python3 -c "from os import fake_function_xyz"
Traceback (most recent call last):
File "<string>", line 1, in <module>
from os import fake_function_xyz
ImportError: cannot import name 'fake_function_xyz' from 'os' (/usr/lib/python3.13/os.py)
This is plain ImportError, not ModuleNotFoundError. Python found and loaded os fine (the message even gives the exact file it loaded, /usr/lib/python3.13/os.py); the failure is that the name you asked for isn't an attribute of that module. We confirmed the class split directly:
$ python3 -c "
try:
from os import fake_function_xyz
except ModuleNotFoundError as e:
print('caught as ModuleNotFoundError')
except ImportError as e:
print('caught as ImportError:', e)
"
caught as ImportError: cannot import name 'fake_function_xyz' from 'os' (/usr/lib/python3.13/os.py)
An except ModuleNotFoundError: clause silently falls through to the next handler here — it never fires, because this error is ImportError but not the subclass.
3. Case C: circular imports
We created two files that import each other:
# file_a.py
print("file_a: starting")
import file_b
print("file_a: got file_b")
def a_func():
return "a_func result"
# file_b.py
print("file_b: starting")
from file_a import a_func
print("file_b: got a_func")
# main_circular.py
import file_a
Running python3 main_circular.py gave this exact output on Python 3.13.5:
file_a: starting
file_b: starting
file_a: starting
file_b: starting
Traceback (most recent call last):
File "/tmp/py-import-guide/main_circular.py", line 1, in <module>
import file_a
File "/tmp/py-import-guide/file_a.py", line 2, in <module>
import file_b
File "/tmp/py-import-guide/file_b.py", line 2, in <module>
from file_a import a_func
ImportError: cannot import name 'a_func' from 'file_a' (consider renaming '/tmp/py-import-guide/file_a.py' if it has the same name as a library you intended to import)
Two things surprised us. First, both files' top-level print lines ran twice before the final error — Python 3.13's import machinery makes a second diagnostic pass to build a better message after the first failed name lookup, which re-executes the partially-imported modules' module-level code. Second, the message on this version is not the older, more commonly quoted "cannot import name 'a_func' from partially initialized module 'file_a' (most likely due to a circular import)" text; instead it printed the shadowing-style hint shown above, because file_a was still mid-import (not fully in sys.modules with a_func defined) when file_b asked for it. Either phrasing means the same root cause: file_a imports file_b, which tries to import something from file_a before file_a has finished running top to bottom.
The fix is to break the cycle: move the cross-import inside a function (so it runs after both modules are fully loaded), or import the whole module (import file_a then use file_a.a_func) instead of pulling a name out at import time, or merge shared code into a third module both sides import from.
4. Case D: a local file shadows a stdlib module name
We made a file named json.py in the same directory as the script, alongside a script that does import json:
# json.py
print("my local json.py loaded")
# use_json.py
import json
print("about to call json.dumps")
print(json.dumps({"a": 1}))
Running python3 use_json.py from that directory gave:
my local json.py loaded
about to call json.dumps
Traceback (most recent call last):
File "/tmp/py-import-guide/shadow_test/use_json.py", line 3, in <module>
print(json.dumps({"a": 1}))
^^^^^^^^^^
AttributeError: module 'json' has no attribute 'dumps' (consider renaming '/tmp/py-import-guide/shadow_test/json.py' since it has the same name as the standard library module named 'json' and prevents importing that standard library module)
This is the sharpest finding of the four: shadowing does not raise ImportError or ModuleNotFoundError at all. The import json line succeeds — it happily imports your local, empty-looking json.py instead of the real standard-library module, because the script's own directory is first on sys.path. The failure only shows up later, as AttributeError, the moment you call something the real stdlib module would have had. Python 3.13 adds a helpful parenthetical naming the exact shadowing file, which made this easy to diagnose; older Python versions give a bare AttributeError with no such hint.
5. Which exception to catch, and when
| Situation | Exception raised | Catch with |
|---|---|---|
| Module not found anywhere on sys.path | ModuleNotFoundError | ImportError or ModuleNotFoundError |
| Module found, but requested name missing from it | ImportError | ImportError only |
| Circular import mid-initialization | ImportError | ImportError only |
| Local file shadows a stdlib/package name | AttributeError (usually, not at import time) | Neither — fix the filename |
Practical rule: if you only want to handle "this dependency isn't installed" (for example to fall back to a different library), catch ModuleNotFoundError specifically and check e.name against the package you expected:
try:
import orjson as json_impl
except ModuleNotFoundError as e:
if e.name != "orjson":
raise
import json as json_impl
That re-raise matters: a bare except ModuleNotFoundError: with no name check will also swallow an unrelated missing import that happens to occur inside orjson's own code, hiding a real bug.
6. Quick checklist
- "No module named X" → ModuleNotFoundError → the package isn't installed, isn't on
sys.path, or you misspelled it. Check withpython3 -c "import X"directly, matching the interpreter and virtualenv you actually run. - "cannot import name X from Y" where Y loaded fine → plain ImportError → the name doesn't exist in that module, or it exists but isn't exported the way you think. Check with
python3 -c "import Y; print(dir(Y))". - "cannot import name X from Y" with a circular-import or "partially initialized module" hint → two of your own files import each other at module level. Move the import inside a function or restructure.
- A working third-party import suddenly breaks only in one directory → check for a local file with the same name as the package (our Python requests ConnectionError guide hits a similar "it worked yesterday" pattern from a different angle).
Frequently asked questions
Is ModuleNotFoundError the same as ImportError?
No, but it is related: ModuleNotFoundError is a subclass of ImportError, added in Python 3.6. We confirmed issubclass(ModuleNotFoundError, ImportError) returns True on Python 3.13.5. Every ModuleNotFoundError is an ImportError, but not every ImportError is a ModuleNotFoundError.
Why did I get ImportError instead of ModuleNotFoundError?
ModuleNotFoundError only fires when Python cannot find the module file at all. If the module loads successfully but the specific name you imported from it doesn't exist, you get plain ImportError instead. We measured this directly: from os import fake_function_xyz raised ImportError: cannot import name 'fake_function_xyz' from 'os', not ModuleNotFoundError, because os itself was found and loaded fine.
Why does my circular import error mention a module being partially initialized?
Because Python imports modules top to bottom and caches each module in sys.modules the moment it starts, not when it finishes. If module A imports module B, and B tries to import a name from A before A has finished running, A exists in sys.modules but doesn't have that name defined yet. In our test, Python 3.13.5 reported this as an ImportError naming the file, after first re-running both modules' top-level code for diagnostics.
Can a local file break a standard library import without raising ImportError?
Yes. We named a local file json.py next to a script that did import json. The import itself succeeded silently, because Python found our local file on sys.path before the real standard library json module. The failure only appeared later as AttributeError: module 'json' has no attribute 'dumps' when we tried to use a real json function. Python 3.13 added a hint identifying the shadowing file; this import-time silence is the main reason this bug is confusing.
Should I catch ModuleNotFoundError or ImportError in my code?
Catch ModuleNotFoundError only when you specifically want to react to a missing optional dependency, and check exception.name to make sure it's the package you expected before swallowing it. Catch the broader ImportError when you want to handle any import failure the same way, including name-mismatch and circular-import errors. We verified except ImportError catches a ModuleNotFoundError, but except ModuleNotFoundError does not catch a plain ImportError raised by a bad from-import.
Written with AI assistance. Measured on 2026-10-02 with Python 3.13.5 (GCC 14.2.0) on Linux, using plain python3 -c one-liners and small scripts under /tmp/py-import-guide/. Every traceback below is copy-pasted output, not paraphrased. PlainHeaders is our extension; Python is free.