skip to content

In Django, why must a model field's verbose_name or a form field's label use gettext_lazy() instead of gettext()?

level: middleimportance: must knowfreq 66%

answer

  1. class bodies run once
  2. no request, no active language
  3. a proxy, not a string
  4. frozen in the import-time language

basics

~20 s

Model and form fields are class attributes, evaluated once when the module is imported, before any request activates a language. gettext() would translate at that moment. gettext_lazy() returns a proxy that translates whenever it is rendered, in the language active for that request.

solid answer

~40 s

Everything in a class body runs once, at import: `verbose_name`, `help_text`, `choices` labels, `Meta.verbose_name`, form `label`s and `error_messages`, `@admin.display(description=...)`. At that moment no request has activated the shopper's language. Plain `gettext()` there would bake in whatever language was active, typically `LANGUAGE_CODE`. Called even earlier, before the app registry has loaded (at module level in an app's `__init__.py` or `apps.py`), it fails with `AppRegistryNotReady`, whose message says to check for non-lazy gettext calls at import time. `gettext_lazy()` returns a lazy proxy that stores the msgid and calls `gettext()` each time it is turned into a `str`, for example when a template or the admin renders it. So a French shopper gets French and a German one German. Inside a view or other function body, plain `gettext()` is right, because a language is already active.

code

python · 23 lines
python
from django import forms
from django.db import models
from django.utils.translation import gettext_lazy as _


class Product(models.Model):
    class Shipping(models.TextChoices):
        STANDARD = "std", _("Standard shipping")
        EXPRESS = "exp", _("Express shipping")

    name = models.CharField(_("name"), max_length=200)
    shipping = models.CharField(_("shipping"), max_length=3, choices=Shipping)

    class Meta:
        verbose_name = _("product")
        verbose_name_plural = _("products")


class CheckoutForm(forms.Form):
    postcode = forms.CharField(
        label=_("Postcode"),
        error_messages={"required": _("Please enter your postcode.")},
    )

go deeper

for a junior

Recall that model and form field texts use gettext_lazy, and view code uses gettext.

for a middle

Explain import time versus request time, the Promise proxy, and why an eager call in models.py fails silently rather than loudly.

for a senior

Show how an eager call silently freezes a language in production and how you keep proxies lazy through serializers and APIs.

for a principal

Set the module-scope-lazy, function-scope-eager convention and enforce it in review or with a lint rule.

## Import time versus request time A Django process imports your modules once, at startup or on first use, and then serves many requests, each in a possibly different language. Anything written directly in a **class body** or at **module level** runs during that import: ```python class Product(models.Model): name = models.CharField(_("name"), max_length=200) # evaluated at import ``` The **active language** is per-thread (and per-async-context) state, normally set for each request by the locale middleware. During import no request exists, so the active language is whatever happens to be active then: usually the default from `LANGUAGE_CODE`, sometimes the language of the request that triggered a lazy import. ## What goes wrong with eager `gettext()` | Where the call runs | What happens | |---|---| | module level of an app's `__init__.py` or `apps.py`, before the app registry has loaded | `AppRegistryNotReady`: "The translation infrastructure cannot be initialized before the apps registry is ready. Check that you don't make non-lazy gettext calls at import time." | | `models.py`, `forms.py`, `admin.py` (imported once the app configs exist) | no error, but the label is translated **once** and stays in that language for the life of the process | | a view function body | correct: runs per request with the shopper's language active | The second row is the dangerous one, because nothing fails. Everything seems to work in development, where everyone uses one language. Then the French storefront shows German model and form labels, because the process imported `models.py` and `forms.py` while German was active. ## What the lazy proxy is `gettext_lazy` is defined as `lazy(gettext, str)`. Calling it returns a **proxy object** (an instance of a subclass of `django.utils.functional.Promise`). The proxy remembers the function and its arguments, and runs `gettext()` again **every time** it is converted to a string: - `str(proxy)`, `format()`, f-strings, `%` and `+` all force a translation, in the language active **at that moment**; - Django's own code (templates, forms, the admin, `ValidationError`) accepts proxies and converts them as late as possible; - `DjangoJSONEncoder` knows how to serialize `Promise` objects. The standard `json` module and third-party libraries do not, so pass them `str(proxy)` at the point of use. The same pattern applies to the other lazy variants: `ngettext_lazy`, `pgettext_lazy` and `npgettext_lazy`. ## Where lazy is required Use the lazy form for anything evaluated at import: - model field `verbose_name` (the first positional argument) and `help_text`, plus relation fields' `verbose_name`; - `Meta.verbose_name` and `Meta.verbose_name_plural`; - `choices` labels, including `models.TextChoices` members such as `STANDARD = "std", _("Standard shipping")`; - form field `label`, `help_text` and `error_messages` declared on the class, and class-level validator messages; - `@admin.display(description=_("In stock?"))` and other decorator arguments; - a custom `LANGUAGES` setting, whose names Django's docs mark with `gettext_lazy`. ## Where plain `gettext()` is right Inside any function or method body that runs during a request, such as a view, `Form.clean()`, a signal receiver or a template tag, the correct language is already active. The eager function is simpler and returns a real `str`. The same goes for code that switches the language explicitly before building text, like rendering an email in the recipient's language. A useful rule: **module and class scope → lazy; function scope → eager**. ## Eager and lazy functions at a glance | Purpose | Eager (function scope) | Lazy (module or class scope) | |---|---|---| | plain string | `gettext` | `gettext_lazy` | | plural | `ngettext` | `ngettext_lazy` | | with context | `pgettext` | `pgettext_lazy` | | context and plural | `npgettext` | `npgettext_lazy` | | composing strings | `str.format()` or `%` | `format_lazy()` | A quick way to check which one a module needs: ask when the line runs. If it runs when Python imports the file, as class attributes, default arguments and module constants do, it needs the lazy column. Otherwise use the eager one. ## Keeping it lazy downstream Laziness is lost the moment something forces the proxy into a `str` too early. Module-level concatenation, f-strings and `.format()` all do that. For composed strings use `django.utils.text.format_lazy("{}: {}", _("Price"), _("incl. VAT"))`, which stays lazy until rendered. For any other function, wrap it with `django.utils.functional.lazy(func, str)`. Finding where laziness leaks is a diagnosis skill of its own.

  • What happens if you call plain gettext() for a verbose_name in an installed app's models.py?
    Nothing fails. Models are imported after the app configs exist, so the catalogs can load and `gettext()` runs once, in whichever language is active at import, usually `LANGUAGE_CODE`. The label then stays in that language for every shopper. `AppRegistryNotReady` only appears for eager calls made even earlier, such as at module level in an app's `__init__.py` or `apps.py`.
  • Your API passes a gettext_lazy() value to json.dumps() and gets a TypeError. What are the options?
    The standard `json` encoder doesn't know Django's lazy `Promise` proxy. Convert it with `str(value)` at the point of use, when the right language is active. Alternatively, serialize with `django.core.serializers.json.DjangoJSONEncoder`, which handles `Promise` objects, or use `JsonResponse`, which uses that encoder by default.
  • Why not simply use gettext_lazy() everywhere, including in views?
    It works, but it hides when translation happens and returns a proxy instead of a `str`. Proxies fail in code that expects real strings (`json.dumps`, third-party libraries, `isinstance(x, str)` checks). Inside a request the language is already active, so eager `gettext()` gives the same text with fewer surprises.

gettext() is a printed price tag in one language, stuck on at the factory. gettext_lazy() is a tag with a code that the till translates each time a customer scans it, in whatever language that customer chose.

saying these in an interview costs you the question

  • gettext() in a class body is re-evaluated for every request.
  • gettext_lazy() translates once at startup and caches the result.
  • A lazy proxy is a normal str, so any library can serialize it.
  • Plain gettext() is wrong inside view functions and should always be lazy.
  • verbose_name only needs translating in the admin, so eager gettext is fine.