PYTHON / DJANGO
How Django structures a web project
Trace how Django boots a project: one settings dotted path, django.setup(), the app registry, and the lazily resolved strings that wire it together.
What you will learn
- Name the three boot phases: settings loaded, apps populated, URLconf resolved on demand
- Inspect the app registry with django.apps.apps to see labels, names, and models
- Predict AppRegistryNotReady from where a model import happens relative to setup()
- Diagnose duplicate app labels and know that labels, not paths, are the registry keys
Understanding How Django structures a web project
A Django project is not a directory layout that the framework scans; it is a bootstrap sequence that happens to be spread over a few files. manage.py, wsgi.py, asgi.py and the test runner all do the same two things: set the environment variable DJANGO_SETTINGS_MODULE to a dotted import path such as myproject.settings, then call django.setup(). Nothing about the folder names is special to Django, only that the path is importable from sys.path, which is why moving the project package one level down breaks everything until the dotted path is updated.
django.setup() does the real structural work: it loads the settings object, configures logging, and populates the application registry. For each string in INSTALLED_APPS the registry builds an AppConfig, derives a label from the last segment of the path, imports that package's models module, and files every model class it finds under that label. This is the mechanism behind rules that otherwise look arbitrary: a model in a package missing from INSTALLED_APPS is invisible to makemigrations, and two apps whose paths end in the same segment cannot coexist because they would claim the same registry key.
Almost every other structural link in settings is a string, not an object: ROOT_URLCONF, MIDDLEWARE entries, AUTH_USER_MODEL, WSGI_APPLICATION, template backends. Django imports these on first use rather than at startup, so a misspelled view path surfaces as an ImportError on the first matching request, not when the server boots. Holding the order in mind — settings, then apps, then URLconf on demand — explains the common failure mode where importing a model at the top of a standalone script raises AppRegistryNotReady, because the model class is trying to register itself with a registry that does not exist yet.
import django
from django.conf import settings
settings.configure(
USE_TZ=True,
INSTALLED_APPS=[
"django.contrib.contenttypes",
"django.contrib.auth",
],
)
django.setup()
from django.apps import apps
for config in apps.get_app_configs():
print(config.label, "->", config.name)
print("auth models:", [m.__name__ for m in apps.get_app_config("auth").get_models()])
print("registry ready:", apps.ready)A Django project's structure is a startup sequence that resolves dotted-path strings into a populated app registry, not a folder layout the framework inspects.
Worked examples
The URLconf is a string resolved later
Shows that ROOT_URLCONF is only a dotted path, imported the first time a URL is reversed or resolved.
import sys
import types
import django
from django.conf import settings
from django.http import HttpResponse
from django.urls import path
def home(request):
return HttpResponse("ok")
urlconf = types.ModuleType("myproject.urls")
urlconf.urlpatterns = [path("", home, name="home")]
sys.modules["myproject.urls"] = urlconf
settings.configure(USE_TZ=True, ROOT_URLCONF="myproject.urls")
django.setup()
from django.urls import resolve, reverse
print(settings.ROOT_URLCONF)
print(reverse("home"))
match = resolve("/")
print(match.func.__name__, match.url_name)Example explained
Line 1sys.modules["myproject.urls"] = urlconf fakes an importable module, proving Django only needs the name to be resolvable.
Line 2settings.ROOT_URLCONF stays a plain string; no import happens while it is only stored.
Line 3reverse("home") triggers the first import of the URLconf and builds the resolver from urlpatterns.
Line 4resolve("/") returns a match whose func is the original view object, so the string was turned into real callables.
Labels are the registry keys
Demonstrates that the app registry is keyed by app label, so a repeated or colliding final path segment aborts startup.
import django
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
settings.configure(
USE_TZ=True,
INSTALLED_APPS=[
"django.contrib.contenttypes",
"django.contrib.auth",
"django.contrib.auth",
],
)
try:
django.setup()
except ImproperlyConfigured as exc:
print(type(exc).__name__)
print(exc)Example explained
Line 1The registry stores app configs in a dict keyed by label, and "django.contrib.auth" yields the label auth.
Line 2The second occurrence finds that key taken, so populate() raises before any models are imported.
Line 3The same error appears for two genuinely different apps such as shop.core and blog.core; setting label on their AppConfig classes fixes it.
Important notes
django.setup() returns immediately if the registry is already ready, but a setup() that raised leaves the registry half-populated; fix the cause and start a fresh process rather than retrying in place.
settings.configure() is only for standalone scripts like the examples here; in a real project the entry points set DJANGO_SETTINGS_MODULE, and calling configure() once settings are loaded raises RuntimeError.
Common mistakes
Creating an app package with models.py but never adding it to INSTALLED_APPS: the registry never imports the module, so makemigrations prints "No changes detected" and no table is ever created.
Importing a model at the top of a standalone script or above get_wsgi_application() in wsgi.py: the class tries to register before django.setup() runs, raising AppRegistryNotReady.
Renaming or moving the project package without updating DJANGO_SETTINGS_MODULE in manage.py, wsgi.py and asgi.py: manage.py may still work from one directory while the deployed server fails with ModuleNotFoundError on the settings module.
Try it yourself
Change, predict, then run
Write a script that calls settings.configure() with only "django.contrib.contenttypes" in INSTALLED_APPS, runs django.setup(), and then calls apps.get_model("auth", "User") inside a try block, printing the exception type and message. Then add "django.contrib.auth" to the list, rerun in a fresh process, and print the returned class instead.
Open the Python workspaceCheck your understanding
A standalone script has `from shop.models import Order` at the top and `django.setup()` a few lines below it. Why does running it raise AppRegistryNotReady?
- Importing the module defines the model class, and model classes register themselves with an app registry that setup() has not populated yet
- Models may only be imported inside views, management commands, or the shell
- The database connection has not been opened, so Django cannot verify the model's table
- Scripts must be launched through manage.py, which is the only place setup() is allowed to run
Show answer
Class creation is what triggers registration: ModelBase asks the registry which app the class belongs to, and that lookup fails before populate() has run, so the import must come after setup(). The database answer is tempting but wrong, since importing a model never touches the database; Django opens a connection lazily on the first query, and this script fails before any query exists.