Error message improvements when `import_module` fails

A constant papercut for me is when I mistype some class string, and end up with an ImportError. Very rarely is the stack trace good at pointing out the sourcing of the string it just tried to import.

This patch shows some ad-hoc fixes for this, but I’m wondering if there’s a smarter strategy here.

I’ve generally found that “configuration-loading”-related errors for Django have always been a bit miserable to debug because so much of it is lazy loading. A URL Conf misconfiguration will show up inside of the custom error handlers check! The stack trace is never pointing at the configuration, and it’s not clear to me if there’s something doable there.

the stacktrace when I have a mistaken ROOT_URLCONF:

Traceback (most recent call last):
  File "/Users/rtpg/proj/django/django/core/checks/urls.py", line 136, in check_custom_error_handlers
    handler = resolver.resolve_error_handler(status_code)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/rtpg/proj/django/django/urls/resolvers.py", line 743, in resolve_error_handler
    callback = getattr(self.urlconf_module, "handler%s" % view_type, None)
                       ^^^^^^^^^^^^^^^^^^^
  File "/Users/rtpg/proj/django/django/utils/functional.py", line 47, in __get__
    res = instance.__dict__[self.name] = self.func(instance)
                                         ^^^^^^^^^^^^^^^^^^^
  File "/Users/rtpg/proj/django/django/urls/resolvers.py", line 711, in urlconf_module
    return import_module(self.urlconf_name)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/rtpg/.local/share/uv/python/cpython-3.12.6-macos-aarch64-none/lib/python3.12/importlib/__init__.py", line 90, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1310, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
  File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1324, in _find_and_load_unlocked
ModuleNotFoundError: No module named 'foo'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/rtpg/proj/saasfarm/backend/manage.py", line 22, in <module>
    main()
  File "/Users/rtpg/proj/saasfarm/backend/manage.py", line 18, in main
    execute_from_command_line(sys.argv)
  File "/Users/rtpg/proj/django/django/core/management/__init__.py", line 442, in execute_from_command_line
    utility.execute()
  File "/Users/rtpg/proj/django/django/core/management/__init__.py", line 436, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/Users/rtpg/proj/django/django/core/management/base.py", line 413, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/Users/rtpg/proj/django/django/core/management/base.py", line 454, in execute
    self.check()
  File "/Users/rtpg/proj/django/django/core/management/base.py", line 486, in check
    all_issues = checks.run_checks(
                 ^^^^^^^^^^^^^^^^^^
  File "/Users/rtpg/proj/django/django/core/checks/registry.py", line 89, in run_checks
    new_errors = check(app_configs=app_configs, databases=databases)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/rtpg/proj/django/django/core/checks/urls.py", line 138, in check_custom_error_handlers
    path = getattr(resolver.urlconf_module, "handler%s" % status_code)
                   ^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/rtpg/proj/django/django/utils/functional.py", line 47, in __get__
    res = instance.__dict__[self.name] = self.func(instance)
                                         ^^^^^^^^^^^^^^^^^^^
  File "/Users/rtpg/proj/django/django/urls/resolvers.py", line 711, in urlconf_module
    return import_module(self.urlconf_name)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/rtpg/.local/share/uv/python/cpython-3.12.6-macos-aarch64-none/lib/python3.12/importlib/__init__.py", line 90, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1310, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
  File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1324, in _find_and_load_unlocked
ModuleNotFoundError: No module named 'foo'

The “infinite CPU cycles” solution would involve adding provenance annotations to settings attributes through some proxy object… but that’s a lot of CPU churn for when I mistype the settings.

Would it make sense to just add an explicit check that tries to do all the relevant imports for common settings so we can provide a more straightforward error message? Or is this a non-issue in people’s eyes?

I agree that it’s an issue that should be addressed. You can produce synthetic stack frames to improve errors. We do that in iommi: iommi/iommi/synthetic_traceback.py at master · iommirocks/iommi · GitHub

Getting this into Django itself might be a long road, but I would be very interested in adding this to django-fastdev at least as a first step in trying this out before proposing adding it to Django itself.