skip to content

In Django, what does a form or widget's inner Media class declare, and how does {{ form.media }} combine and order those assets?

level: middleimportance: should knowfreq 30%

answer

  1. CSS and JavaScript a widget needs
  2. the form sums its widgets
  3. rendered separately from the form
  4. duplicates merged, order kept
  5. static() resolves relative paths

basics

~20 s

An inner Media class lists the CSS, by medium, and JavaScript a widget or form needs. form.media merges all widgets' assets plus the form's own, dropping duplicates but keeping order; {{ form.media }} renders the tags.

solid answer

~50 s

A widget or form can declare `class Media` with `css = {"all": [...]}` and `js = [...]`. Every form has a `media` property that adds up its widgets' `Media` and then its own declaration. Combining keeps each file once and preserves the relative order each list declared, so a shared library listed first by two widgets comes out once, before both; lists that demand opposite orders trigger a `MediaOrderConflictWarning`. `{{ form }}` does **not** include these tags — the page renders `{{ form.media }}`, or `{{ form.media.css }}` in `<head>` and `{{ form.media.js }}` before `</body>`. Relative paths are passed through `static()`, so `STATIC_URL` and the static storage apply; absolute URLs are left alone. `Script` (5.2) and `Stylesheet` (6.1) objects add HTML attributes such as `defer` or `integrity`. Subclasses inherit their parent's media unless `extend = False`.

go deeper

for a junior

Know that widgets and forms can declare CSS and JavaScript in class Media and that the page must render {{ form.media }} itself.

for a middle

Explain how form.media sums widget media plus the form's own, how merging removes duplicates while keeping order, and how static() turns relative paths into URLs.

for a senior

Combine media across forms before rendering, keep asset attributes consistent, and treat a MediaOrderConflictWarning as a real ordering bug in shared widgets.

for a principal

Decide whether widget assets stay declared in Python Media or move to the front-end build, weighing admin compatibility against one bundling pipeline.

## What Media declares Some widgets need front-end assets: a date picker's JavaScript, a colour picker's stylesheet, a donation amount slider. Django lets the Python class that renders the widget also **declare** those assets, through an inner `Media` class: ```python from django import forms class AmountSliderWidget(forms.NumberInput): class Media: css = {"all": ["donations/slider.css"]} js = ["donations/vendor/range.js", "donations/slider.js"] ``` - `css` is a dictionary from **media type** (`"all"`, `"screen"`, `"print"`, or a comma-separated list) to a list of stylesheet paths. - `js` is a list or tuple of script paths. - `extend` controls inheritance: by default a subclass's media is its parent's plus its own; `extend = False` drops the parent's, and a list such as `extend = ["css"]` keeps only those types. A `Form` can declare `Media` too, for assets that belong to the form rather than a widget, such as layout CSS. A dynamic `media` property can replace the static declaration when the assets depend on configuration. ## How a form combines its widgets' assets Every form has a `media` property whether or not it declares anything. It: 1. starts with an empty `Media` object; 2. adds `field.widget.media` for every field, in field order; 3. adds the form's own `Media` declaration; if that declaration sets `extend = False`, the widgets' assets are dropped and only the form's own remain. Adding `Media` objects **merges** their lists rather than concatenating them: - a path that appears in several lists is output once; - the relative order in which each list declared its files is preserved, so a library that two widgets both list before their own script is emitted before both scripts; - if two lists demand opposite orders — one says `a.js` then `b.js`, the other `b.js` then `a.js` — Django emits a **`MediaOrderConflictWarning`** and falls back to a simple de-duplicated order. ## Rendering the tags `{{ form }}` renders fields only; the page template must ask for the assets: ```django <head> {{ form.media.css }} </head> <body> <form method="post">{% csrf_token %}{{ form }}<button>Donate</button></form> {{ form.media.js }} </body> ``` | expression | output | |---|---| | `{{ form.media }}` | every `<link rel="stylesheet">` then every `<script>` | | `{{ form.media.css }}` | only the stylesheet links | | `{{ form.media.js }}` | only the script tags | A formset's `media` is the media of its first form (or of `empty_form` when there are none), and the admin renders `Media` for every widget it uses, which is why custom admin widgets declare it. ## How paths become URLs - A path starting with `http://`, `https://` or `/` is used as written. - Any other path is passed through Django's `static()` helper, so it gains `STATIC_URL` and, with a hashing storage, the hashed file name. Collecting and serving those files is the static files layer's job. ## Attributes on the tags Plain strings render bare `<script src>` and `<link href>` tags. To add attributes, use asset objects: - `forms.Script("donations/slider.js", defer=True)` — Django 5.2+; - `forms.Stylesheet("donations/print.css", media="print")` — Django 6.1+. ```python from django import forms class AmountSliderWidget(forms.NumberInput): class Media: js = [forms.Script("donations/slider.js", defer=True)] ``` Two assets with the same path but different attributes are treated as different entries when merging, so keep attributes consistent across widgets that share a file. ## Dynamic media When the assets depend on configuration, define a `media` property instead of an inner class; it must return a `forms.Media` object: ```python from django import forms from django.conf import settings class AmountSliderWidget(forms.NumberInput): @property def media(self): theme = getattr(settings, "DONATION_THEME", "light") return forms.Media(css={"all": [f"donations/slider-{theme}.css"]}, js=["donations/slider.js"]) ``` `DONATION_THEME` here is a project setting of your own, not a Django one. A property replaces the declarative merge with the parent class, so add `super().media` yourself if the parent's assets are still needed. ## Common mistakes - Expecting `{{ form }}` to include the assets, then wondering why a widget is unstyled. - Rendering `{{ form.media }}` once per form on a page with several forms, loading shared libraries twice — add the `Media` objects (`form_a.media + form_b.media`) and render the sum once. - Hard-coding `/static/...` in `Media`, which bypasses `static()` and breaks hashed file names.

  • How do you avoid loading a shared library twice when a page shows two forms?
    Add the forms' `Media` objects — `combined = form_a.media + form_b.media` — pass the sum to the template and render it once. Addition merges the lists, keeping each file once in a consistent order, whereas rendering `{{ form_a.media }}` and `{{ form_b.media }}` separately repeats shared files.
  • What does extend = False on a subclass's Media do?
    It stops the subclass from inheriting its parent's assets, so only the subclass's own `css` and `js` are used. The default, `extend = True`, adds the parent's media first; a list such as `extend = ["css"]` inherits only the named media types.

Combining Media is like merging two recipes' shopping lists: an ingredient both need is bought once, and each recipe's 'this before that' order is respected. If one recipe needs eggs before flour and the other flour before eggs, no single list satisfies both, so Django warns you.

saying these in an interview costs you the question

  • {{ form }} automatically outputs the widgets' script and link tags
  • form media repeats a file once for every widget that lists it
  • relative Media paths are resolved against MEDIA_URL
  • Media order is alphabetical, so declaration order does not matter
  • only widgets, not forms, can declare a Media class