On a Django news site, a TemplateView's extra_context shows the same headlines and copyright year until the server restarts; why, and how do you fix it?
answer
- evaluated when the module loads
- one dict for every request
- a QuerySet caches its rows
- workers started at different times
- move work into get_context_data
basics
~20 sextra_context is evaluated once, when the class body or URLconf loads, and the same dict is merged into every request's context. A computed year freezes, and a QuerySet stored there caches its rows on first render. Compute such values in get_context_data().
solid answer
~40 s`extra_context={"year": date.today().year, "headlines": Story.objects.order_by("-published")[:5]}` is a Python expression evaluated once per process: when the class body runs or when the URLconf calls `as_view()`. `ContextMixin.get_context_data()` then merges that same dict into every request's context. The year is a frozen integer. The QuerySet is lazy, so the first render runs the query, and the QuerySet object caches its rows; because every request gets the same object, later requests reuse those rows and never query again. Each worker process freezes its own copy at its own start time, so after a deploy different users can see different headlines. Fix it by overriding `get_context_data()` and building the QuerySet and date there, or use the `{% now "Y" %}` template tag for the year. Keep `extra_context` for true constants.
code
python · 11 linesfrom django.test import TestCase
from news.models import Story
class HomePageFreshnessTests(TestCase):
def test_new_story_appears_without_restart(self):
self.client.get("/")
Story.objects.create(title="Breaking", slug="breaking")
response = self.client.get("/")
self.assertContains(response, "Breaking")go deeper
Recall that anything you write inside extra_context is computed once, so dates, queries and function calls there do not update per request.
Explain that the same dict and the same QuerySet object are merged into every request's context, so the QuerySet's cached rows are reused.
Diagnose from the symptoms: fixed until restart, different per worker. Move the work into get_context_data() and add a two-request regression test.
Turn it into a review rule for all class-level view configuration: constants only, and caching added deliberately with explicit invalidation.
## The symptom The home page of a news site is a `TemplateView` configured in the URLconf: ```python from datetime import date from django.urls import path from django.views.generic import TemplateView from news.models import Story urlpatterns = [ path( "", TemplateView.as_view( template_name="home.html", extra_context={ "year": date.today().year, "headlines": Story.objects.order_by("-published")[:5], }, ), ), ] ``` Editors publish a breaking story and it never appears on the home page. On 1 January the footer still says last year. A restart fixes both, until the next story. Users on different workers sometimes see different headlines. ## Why: extra_context is built once **`extra_context`** is an ordinary Python dict. The expressions inside it run when the dict literal is evaluated: - In a URLconf, that is when `urls.py` is imported and `as_view()` is called — once per worker process. - As a class attribute (`extra_context = {...}` in the class body), it is when the module defining the view is imported — also once per process. `ContextMixin.get_context_data()` does `kwargs.update(self.extra_context)` on every request, but it merges the **same dict**, holding the **same objects**, every time. ## Why the year freezes `date.today().year` ran once and produced an integer. Every request copies that integer into its context. Nothing re-runs the expression. ## Why the headlines freeze The QuerySet case is subtler, because QuerySets are lazy: 1. At import, `Story.objects.order_by("-published")[:5]` builds a QuerySet but runs **no** query. 2. The first request's template iterates it, which runs the SQL and stores the rows in the QuerySet's **result cache**. 3. Every later request receives the **same QuerySet object** from `extra_context`. Iterating it again reads the cache. No new query is run, so new stories never appear. (The general rules for when a QuerySet is evaluated and cached belong to the QuerySet API; the Django-specific trap here is that `extra_context` shares one QuerySet object across requests.) ## Why workers disagree | Worker | Started | Cached headlines | |---|---|---| | Process A | Before the breaking story | Old five stories | | Process B | Restarted after it | Includes the breaking story | Each process has its own copy of the URLconf, its own `extra_context` dict and its own cached QuerySet. Which version a reader sees depends on which process serves the request. ## The fix Build anything time-dependent inside **`get_context_data()`**, which runs per request: ```python from django.views.generic import TemplateView from news.models import Story class HomeView(TemplateView): template_name = "home.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["headlines"] = Story.objects.order_by("-published")[:5] return context ``` - A new QuerySet is created on every call, so every request queries fresh rows. - For the year, the template can use **`{% now "Y" %}`**, which is evaluated at render time. - Keep **`extra_context`** for true constants: a section name, a page heading, a feature label. - If fresh-per-request is too expensive, add caching deliberately, with a timeout and invalidation, rather than getting it by accident from a frozen object. ## Other values that freeze the same way The home-page case is the common one, but any expression inside `extra_context`, or in any class attribute of a view, has the same lifetime: - **Settings-derived values that are later changed at runtime**, for example a feature flag read from the database once at import. - **Translated strings** built with `gettext()` rather than `gettext_lazy()`: the language active at import time is frozen for every visitor. - **Model instances**, such as `Section.objects.get(slug="home")`, which run a query at import time. That can even break `migrate` on a fresh database, because the table does not exist yet when the URLconf is imported. - **Counts and aggregates**, such as a subscriber total, which never move. The rule is the same for all of them: `extra_context` and class attributes are **configuration**, fixed per process; anything that describes the current state of the world belongs in a method that runs per request. ## How to catch it - **Test with two requests**: render the page, create a new `Story`, render again, and assert it appears. A frozen QuerySet fails this within one test process. - **Review rule**: no function calls, QuerySets or `now()`-style expressions inside `extra_context` literals.
- Would storing Story.objects.all() without a slice in extra_context avoid the stale rows?No. Slicing is irrelevant; the problem is that one QuerySet object is shared. Once any request iterates it, its result cache is filled and every later request reads those cached rows. Only building a new QuerySet per request, in `get_context_data()` or a handler, gets fresh data.
- Why does the stale page differ between users after a deploy?Every worker process imports the URLconf itself and freezes its own `extra_context` values at its own start time. Processes started, or first rendering the page, at different moments hold different cached rows. Which headlines a reader sees depends on which process handles the request.
saying these in an interview costs you the question
- Believing extra_context is re-evaluated on every request
- Assuming a QuerySet in extra_context runs a fresh query each time it is rendered
- Blaming the database or a cache backend before checking where the QuerySet is built
- Fixing staleness by scheduling periodic server restarts
- Thinking removing the slice from the QuerySet makes it refresh