Filter concepts by levelShowing all levels.

Django · Section 3

Django App Registry

Level
intermediate
Read
14 min
Concepts
2

How Django's three startup stages run as full passes across every installed app rather than app-by-app, and the import-time side effects — database queries, apps.get_model(), eager translation — that can run before the registry or database is ready.

This section

What is true here

  1. All apps finish stage 1 (import configs) before any app starts stage 2 (import models), and likewise before stage 3 (ready()).
  2. A string model reference ("app.Model") sidesteps import order — Django resolves it lazily once the registry is fully populated.
  3. A database query or apps.get_model() call at import time can run before the registry — or a database connection — is ready.
  4. AppRegistryNotReady means code depending on the registry ran before it finished populating.
  5. gettext_lazy(), not gettext(), for any string evaluated at import time, like a model field's verbose_name.

What you will be able to do

  • Explain why the three startup stages run as full passes, not one app at a time
  • Use a string model reference to avoid an import-order dependency between two apps
  • Recognize an import-time database query or apps.get_model() call as unsafe
  • Fix an AppRegistryNotReady error by moving code into ready() or a function

Startup order and import-time safety

How the registry populates across every app, and what can go wrong running code too early in that process.

App loading order

standardintermediate

Django runs each of the three startup stages — import configs, import models, call ready() — as a full pass across every app in INSTALLED_APPS order, not app-by-app. Every app finishes stage 1 before any app starts stage 2.

Think of it as

Think of three separate roll calls, not three relay racers. Stage 1 calls every app's name in INSTALLED_APPS order and imports its config; only once every app has answered does stage 2 begin, calling the roll again to import every app's models; only then does stage 3 call ready() on each, in the same order. An app late in the list still has its config imported before an earlier app's models are — because that's stage 1 finishing everywhere before stage 2 starts anywhere.

python
INSTALLED_APPS = [
    "django.contrib.admin",
    "billing",
    "orders",
]
# billing's config imports before orders' config (stage 1),
# but billing's MODELS import before orders' models too (stage 2) —
# same list order, each stage run in full across every app

What we're doing: Reference another app's model safely regardless of INSTALLED_APPS order, by using a string reference instead of a direct import.

billing/models.pypython
from django.db import models

class Invoice(models.Model):
    amount = models.DecimalField(max_digits=10, decimal_places=2)
    order = models.ForeignKey("orders.Order", on_delete=models.CASCADE)
5
"orders.Order" is a string — Django resolves it lazily, once the whole registry has finished stage 2, so it works no matter which app is listed first in INSTALLED_APPS.

Why this works: A string reference defers resolution until the full registry is populated, so it never depends on billing's models.py happening to import after orders' models.py — the two apps' relative position in INSTALLED_APPS stops mattering for this one relationship.

Importing another app's model class directly inside models.py

Wrong

python
# billing/models.py
from orders.models import Order   # direct import at stage 2

class Invoice(models.Model):
    order = models.ForeignKey(Order, on_delete=models.CASCADE)

Better

python
# billing/models.py
class Invoice(models.Model):
    order = models.ForeignKey("orders.Order", on_delete=models.CASCADE)

What you see: ImportError or circular-import failure that depends on whether "billing" or "orders" comes first in INSTALLED_APPS — works today, breaks after someone reorders the list.

Why: A direct import forces Python's own import machinery to resolve orders.models while billing.models is still mid-import (stage 2) — if orders also imports something from billing, the two imports can deadlock each other. A string reference avoids the problem entirely by not importing the other app's module at all.

Three full passes, not one pass per app
all appsdoneall appsdone

Stage 1

import every apps.py

Stage 2

import every models.py

Stage 3

call every ready()

  • Stage 1 — import every apps.py
    • leads to Stage 2 (all apps done)
  • Stage 2 — import every models.py
    • leads to Stage 3 (all apps done)
  • Stage 3 — call every ready()

What is safe to do in each stage

What is safe to do in each stage
StageWhat runsSafe to do
1import every app's config (apps.py)read INSTALLED_APPS, define AppConfig
2import every app's models moduledefine models, reference other apps' models by string
3call ready() on every AppConfig, in orderimport any model directly, connect signals

Together

python
# billing/models.py — stage 2, safe even if "orders" loads later
from django.db import models

class Invoice(models.Model):
    order = models.ForeignKey("orders.Order", on_delete=models.CASCADE)

Remember: Each of the three startup stages runs across every app, in INSTALLED_APPS order, before the next stage begins anywhere — not stage 1-2-3 per app.

See also: app registry · import time side effects · app apps py

Import-time side effects

coreintermediate

Code that runs a database query, calls apps.get_model(), or eagerly translates a string at module import time can run before the registry is ready — raising AppRegistryNotReady or querying a database connection that may not exist yet.

Think of it as

Import time is the building still being constructed — walls going up in a fixed order. A query to the database, or a call to apps.get_model(), is someone trying to walk into a room that hasn't been built yet. It might work by luck (if that room happens to already exist), but it depends entirely on construction order, which is exactly the kind of hidden dependency the three-stage startup process exists to avoid.

python
from django.utils.translation import gettext_lazy as _

class Article(models.Model):
    title = models.CharField(max_length=200, verbose_name=_("title"))

What we're doing: Fix a ChoiceField built from a live database query at import time, which runs before the registry — and often before a database connection — is ready.

billing/forms.pypython
from django import forms
from .models import Country

class ShippingForm(forms.Form):
    # BEFORE: evaluated once, at import time
    # country = forms.ChoiceField(
    #     choices=[(c.id, c.name) for c in Country.objects.all()]
    # )

    country = forms.ModelChoiceField(queryset=Country.objects.all())
8
ModelChoiceField stores the queryset, unevaluated, and only runs it when the form is actually rendered or validated — never at import time.

Why this works: A list comprehension over Country.objects.all() at class-body level runs the instant forms.py is imported — during Django's startup, or even during a `manage.py migrate` invoked before that table exists. ModelChoiceField defers evaluation to request time, when the registry is fully populated and the table is guaranteed to exist.

Calling apps.get_model() at module level instead of inside ready()

Wrong

python
# billing/apps.py
from django.apps import AppConfig, apps

Invoice = apps.get_model("billing", "Invoice")  # module level — too early

class BillingConfig(AppConfig):
    name = "billing"

Better

python
# billing/apps.py
from django.apps import AppConfig

class BillingConfig(AppConfig):
    name = "billing"

    def ready(self):
        from django.apps import apps
        Invoice = apps.get_model("billing", "Invoice")  # safe — stage 3

What you see: django.core.exceptions.AppRegistryNotReady: Apps aren't loaded yet.

Why: apps.py is imported during stage 1, before any app's models module is guaranteed to exist in the registry yet — apps.get_model() needs stage 2 to have completed. Moving the same call inside ready() (stage 3) runs it only after every app's models are fully loaded.

A module-level DB query runs before the registry is ready
Python import
forms.py (module body)
App registry
  1. 1. import forms
  2. 2. Country.objects.all()
  3. 3. AppRegistryNotReady
  1. Python import → forms.py (module body): import forms
  2. forms.py (module body) → App registry: Country.objects.all()
  3. App registry → forms.py (module body): AppRegistryNotReady

Import-time hazards and their fix

Import-time hazards and their fix
HazardSymptomFix
DB query at module levelfails or queries a stale/unmigrated schemamove inside a view/method, or a queryset default
apps.get_model() at import timeAppRegistryNotReadycall inside ready(), or after django.setup()
gettext() for a field labelwrong or frozen translation, evaluated once at importuse gettext_lazy() instead
Standalone script with no django.setup()AppRegistryNotReady on first ORM accesscall django.setup() before touching models

Together

python
# standalone script outside manage.py
import django
django.setup()  # populate the registry before any model import

from blog.models import Post
print(Post.objects.count())

Remember: Never query the database or call apps.get_model() at import time — defer to a function, ready(), or a lazy field like gettext_lazy/ModelChoiceField.

See also: app registry · app loading order · signals

Advertisement