Why does Python raise ModuleNotFoundError when modifying Django’s INSTALLED_APPS?
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.
Read my book Boost Your Git DX to Git better.
One summary email a week, no spam, I pinky promise.
Related posts: