Adam Johnson

Home | Blog | Training | Projects | Colophon | Contact

Why does Python raise ModuleNotFoundError when modifying Django's INSTALLED_APPS?

2020-06-29 Pegasus, chained

Imagine we are installing the third party package django-cors-headers, which I maintain. Step one in its installation process is to install the package, so we run the command:

python -m pip install django-cors-headers

Step two is to add the module to our settings file’s INSTALLED_APPS. We might add it between the the Django contrib apps and our project’s own core app:

INSTALLED_APPS = [
    # Django contrib apps
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "django.contrib.sites",
    "django.contrib.humanize",
    # Third-party apps
    "corsheaders"
    # Project apps
    "example.core"
]

Unfortunately with the above INSTALLED_APPS has a bug. Can you spot it?

It causes Python to raise the following ModuleNotFoundError when trying to run the server:

$ python manage.py runserver
Exception in thread django-main-thread:
Traceback (most recent call last):
  File "/.../python3.8/threading.py", line 932, in _bootstrap_inner
    self.run()
  ...
  File "/.../site-packages/django/__init__.py", line 24, in setup
    apps.populate(settings.INSTALLED_APPS)
  File "/.../site-packages/django/apps/registry.py", line 91, in populate
    app_config = AppConfig.create(entry)
  File "/.../site-packages/django/apps/config.py", line 116, in create
    mod = import_module(mod_path)
  File "/.../python3.8/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1014, in _gcd_import
  File "<frozen importlib._bootstrap>", line 991, in _find_and_load
  File "<frozen importlib._bootstrap>", line 961, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "<frozen importlib._bootstrap>", line 1014, in _gcd_import
  File "<frozen importlib._bootstrap>", line 991, in _find_and_load
  File "<frozen importlib._bootstrap>", line 961, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "<frozen importlib._bootstrap>", line 1014, in _gcd_import
  File "<frozen importlib._bootstrap>", line 991, in _find_and_load
  File "<frozen importlib._bootstrap>", line 973, in _find_and_load_unlocked
ModuleNotFoundError: No module named 'corsheadersexample'

The mistake is that a comma is missing at the end of "corsheaders". This causes Python to glue the two app module strings together - "corsheaders" plus "example.core", leading to "corsheadersexample.core".

(This is a feature known as implicit string concatenation - more on which below.)

Django sees only "corsheadersexample.core" in the INSTALLED_APPS setting. Thus when it starts up, it tries to import a module by that name. Python resolves that by first trying to import "corsheadersexample", which does not exist. All the frames in our traceback that appear in "<frozen importlib._bootstrap>" are Python’s built-in import machinery.

The solution is a small change - to add a comma after "corsheaders". We can also add a comma after "example.core", to protect against this mistake happening again if we add an app at the end:

INSTALLED_APPS = [
    # Django contrib apps
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "django.contrib.sites",
    "django.contrib.humanize",
    # Third-party apps
    "corsheaders",
    # Project apps
    "example.core",
]

Then Django should be able to import both "corsheaders" and "example.core" and start. And we can continue following the rest of the installation steps for django-cors-headers.

Implicit String Concatenation

This automatic concatenation of strings, even on separate lines or with separate quoting styles, is a Python feature. It’s documented in String Literal Concatenation in the “Lexical analysis” section of the language reference.

The feature is controversial. It makes writing long strings easier, as the + operator isn’t needed between the pieces. However it leads to bugs like this, where two strings were meant instead of one.

PEP 3126 suggested removing this feature from Python during the transition from Python 2 to 3, however it was rejected.

More recently, Guido van Rossum, the creator of Python, suggested its removal on the python-ideas mailing list. Whilst a mixed conversation, it seems the removal of this feature would be generally accepted, but it’s hard to do so because of backwards compatibility concerns. Therefore, it’s unlikely to be removed any time soon.

I personally tend to use explicit concatenation, writing out the plus signs, whenever I split a string. I also try to always end lines in multi-line function calls and data structures with a comma. Then I know any implicit string concatenation I spot is my mistake.

Fin

Hope this helps,

—Adam


Are your Django project's tests slow? Read Speed Up Your Django Tests now!


Subscribe via RSS, Twitter, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: django, python