In Django, why does success_url = reverse('job-list') on a class-based view break at import time, and what does reverse_lazy() change?
answer
- when a class body runs
- who imports views.py
- a half-loaded URLconf
- a string computed on use
basics
~20 sClass attributes run when views.py is imported, usually while the URLconf that imports it is still loading, so reverse() finds no patterns and raises ImproperlyConfigured. reverse_lazy() returns a lazy string that reverses only when converted to text, after loading.
solid answer
~40 sThe root URLconf imports `views.py`; executing the class body calls `reverse('job-list')`, which asks the resolver to load the root URLconf. That module is only half-imported and has no `urlpatterns` yet, so Django raises `ImproperlyConfigured`: the included URLconf does not appear to have any patterns in it, probably because of a circular import. `reverse_lazy()` is `reverse()` wrapped in Django's `lazy()`: it returns a proxy that calls `reverse()` only when the value is converted to a string, and generic views do that inside `get_success_url()` during a request. Use it for class attributes, decorator arguments such as `login_url`, default parameter values and module-level constants. Inside a function or method that runs per request, plain `reverse()` is fine, and overriding `get_success_url()` is often clearer.
code
python · 7 linesfrom django.contrib.auth.decorators import permission_required
from django.urls import reverse_lazy
@permission_required('jobs.add_job', login_url=reverse_lazy('login'))
def post_job(request):
...go deeper
Recall that class attributes like success_url need reverse_lazy(), while code inside a view can call reverse() directly.
Explain the import chain that makes reverse() see a half-loaded URLconf, and that reverse_lazy() defers the lookup until the value is converted to a string.
Prefer get_success_url() when the target depends on the object, recognise the circular-import error message instantly, and test success paths so lazy typos surface.
Set a codebase convention that nothing at import time performs URL lookups, so apps stay importable in any order and tooling can load them safely.
## The failing code A job board has a view for posting jobs: ```python # jobs/views.py from django.urls import reverse from django.views.generic.edit import CreateView from .models import Job class JobCreateView(CreateView): model = Job fields = ['title', 'description'] success_url = reverse('job-list') # breaks at import time ``` The project fails to start, or the first request fails, with `ImproperlyConfigured`: *The included URLconf 'mysite.urls' does not appear to have any patterns in it. If you see the 'urlpatterns' variable with valid patterns in the file then the issue is probably caused by a circular import.* ## Why it happens Python executes a **class body** once, when the module defining it is imported. The import chain is the problem: 1. On the first request, Django's resolver imports the root URLconf, `mysite/urls.py`. 2. That module imports `jobs.urls`, which imports `jobs.views`. 3. Executing `JobCreateView`'s body calls `reverse('job-list')`. 4. `reverse()` asks the resolver for the root URLconf's patterns. `mysite.urls` is in the middle of being imported, so its `urlpatterns` variable does not exist yet. 5. The resolver finds no patterns and raises the circular-import error. The same failure hits any module-level call to `reverse()` in code the URLconf imports: constants, default arguments, decorator arguments. ## What reverse_lazy() changes `django.urls.reverse_lazy` is defined as `lazy(reverse, str)`. Calling it does **not** reverse anything; it records the arguments and returns a **lazy proxy** that behaves like a string. `reverse()` runs when something converts the proxy to text: - `CreateView.get_success_url()` formats `success_url` with the saved object's fields after a valid form is saved; that string operation converts the proxy during a request, long after the URLconf has loaded. - `redirect()` expands a lazy value before resolving it. - String formatting, concatenation or `str()` in your own code trigger it the same way. ```python from django.urls import reverse_lazy class JobCreateView(CreateView): model = Job fields = ['title', 'description'] success_url = reverse_lazy('job-list') ``` `reverse_lazy()` accepts the same arguments as `reverse()`, including `kwargs`, `current_app` and, since Django 5.2, `query` and `fragment`. ## Where each one belongs | Place in the code | Runs at | Use | |---|---|---| | class attribute such as `success_url` | import | `reverse_lazy()` | | decorator argument such as `login_url=` | import | `reverse_lazy()` | | default value in a function signature | import | `reverse_lazy()` | | module-level constant | import | `reverse_lazy()` | | body of a view or method | each call | `reverse()` | | `get_success_url()` override | each request | `reverse()` | ## Better alternatives in common cases - **Per-object redirects need a method anyway.** After saving a job application, the success URL depends on the new object, so override `get_success_url()` and call `reverse('application-detail', kwargs={'pk': self.object.pk})`; a lazy class attribute cannot see `self.object`. - **Some settings take names directly.** `LOGIN_URL` accepts a named URL pattern, so `LOGIN_URL = 'login'` needs no reversing in `settings.py`. - **`redirect()` takes names.** In a function view, `redirect('job-list')` reverses at call time without any lazy wrapper. ## The same pattern elsewhere in Django `reverse_lazy()` is one instance of a general Django tool: `django.utils.functional.lazy()` wraps a function so that calling it returns a proxy and the real work happens on first use as the declared result type. The best-known sibling is `gettext_lazy()`, used for model field labels and form labels, which must not be translated at import time because the active language is only known per request. Seeing both as the same mechanism explains the shared rules: - evaluate at the point of use, not at import; - expect a proxy object, not a plain `str`, until something converts it; - keep eager versions (`reverse()`, `gettext()`) for code that already runs per request. ## Pitfalls with the lazy proxy - The proxy is not a `str` instance, so `isinstance(url, str)` is `False` until it is converted, and a library that type-checks strictly may reject it; wrap it in `str()` at the point of use. - A wrong name is not caught at import: `reverse_lazy('job-lsit')` only raises `NoReverseMatch` when the proxy is first converted, typically after a user submits the form. A test that exercises the success path catches it. - Because conversion happens at use time, a lazy URL reflects the URLconf and script prefix in effect at that moment, which is usually what you want.
- Why doesn't reverse() inside a Django view function need to be lazy?A view body runs only when a request is handled, and by then the resolver has fully imported the root URLconf. Only code executed during import, such as class bodies, decorator arguments, default parameter values and module constants, runs before the patterns exist.
- When would a typo in reverse_lazy('job-lsit') be discovered in a Django project?Only when the proxy is converted to a string, for example when `get_success_url()` runs after a successful form submission. Import succeeds, so the error surfaces as `NoReverseMatch` at request time. Tests that post valid data to each form view catch it before users do.
reverse_lazy() is like sealing an envelope that says 'look up the address on delivery' while the address book is still being printed; reverse() at import time is trying to read the unfinished book.
saying these in an interview costs you the question
- reverse_lazy() resolves the URL immediately and caches it for later.
- Django loads every URLconf before importing any views module.
- Plain reverse() inside a view function must also be replaced by reverse_lazy().
- The circular-import error means urls.py genuinely has no urlpatterns variable.
- A reverse_lazy() typo is reported as soon as the module is imported.