Type-Safe Template Validation
Python's dynamic typing makes template-variable bugs the #1 source of "it rendered blank and I don't know why" issues. manage.py djust_typecheck reads every LiveView template, extracts every variable reference, and flags names that the view can't possibly provide.
No TypeScript. No type stubs. No build step. Just a management command you run locally or in CI.
Running the check
python manage.py djust_typecheck
Example output:
djust_typecheck: 2 view(s) with unresolved names
myapp.views.CheckoutView (checkout.html)
line 14: cartt
line 22: total_amount
myapp.views.ProfileView (profile.html)
line 7: usre
The command walks every LiveView subclass in the project, resolves each view's template_name via Django's template loaders, and looks for any variable or tag reference that isn't covered by the view's declared context.
Options
python manage.py djust_typecheck --json # machine-readable JSON
python manage.py djust_typecheck --app myapp # one Django app
python manage.py djust_typecheck --view CheckoutView # one view
python manage.py djust_typecheck --strict # exit non-zero on findings
Add it to CI with --strict:
# .github/workflows/ci.yml
- name: Template typecheck
run: python manage.py djust_typecheck --strict
What counts as "declared"
A reference resolves against the union of:
- Public class attributes — anything set on the class body (or inherited from user mixins) with a non-underscore name.
self.foo = ...assignments — anywhere in the class (mount, event handlers, helpers). AST-extracted without running the code.@propertydecorated methods — treated as attributes.- Literal
return {...}inget_context_data— keys from areturn {"foo": ...}orreturn {**super, "foo": ...}literal. - Template-local names —
{% for x in ... %},{% with x=... %}, and{% inputs_for fs as form %}all bind identifiers the rest of the block may use. - Framework built-ins —
user,request,perms,csrf_token,messages,forloop,djust,is_dirty,changed_fields,async_pending, etc. - Project-wide globals — anything listed in
settings.DJUST_TEMPLATE_GLOBALS.
Example:
# settings.py
DJUST_TEMPLATE_GLOBALS = ["site_title", "current_year", "feature_flags"]
Context processors that inject values into every template should be listed here — otherwise the checker will flag every reference to them.
Silencing false positives
The checker is intentionally conservative — anything it can't statically resolve gets flagged. Three ways to silence:
Per-template pragma
{# djust_typecheck: noqa #}
Silences the entire template. Use noqa name1, name2 to silence specific names only:
{# djust_typecheck: noqa dynamic_section, late_bound_flag #}
Per-view opt-in to strict mode
class CheckoutView(LiveView):
strict_context = True
template_name = "checkout.html"
strict_context = True makes that view's findings count as errors regardless of the global --strict flag. Good for "this view is fully typed; regress at your peril."
Global globals list
# settings.py
DJUST_TEMPLATE_GLOBALS = ["navbar", "breadcrumbs"]
For names injected by base templates, context processors, or framework plumbing outside djust.
Tag coverage
Beyond the obvious {{ var }} / {% if %} / {% for %} references,
the extractor follows positional context-variable arguments inside
several Django built-in tags so the checker reports neither
false-positives on tag-local names nor false-negatives on positional
references. Coverage as of v0.5.2:
| Tag | What's extracted | What's ignored |
|---|---|---|
{% firstof a b "literal" %} | a, b (positional refs) | string literals |
{% cycle a b c as label %} | a, b, c (positional refs) | as <label> suffix |
{% blocktrans with x=expr count y=qty %} | expr, qty (the references) | x, y (template-local bindings, scoped to the block) |
{% blocktranslate %} | same as blocktrans | same |
{% with name=expr %} | expr | name (block-local) |
{% include "path" with k=v only %} | v | k, only |
{% url "name" arg1 arg2 %} | arg1, arg2 | the URL name string |
Adding tag coverage is mostly a question of teaching
python/djust/management/commands/djust_typecheck.py what each tag's
arguments mean — the extractor is regex-based and incremental. File
an issue if you hit a built-in or third-party tag whose arguments
should be tracked.
Known limitations
- Dynamic dict returns —
get_context_datathat builds its dict in a loop, viadict(...), or withupdate()calls is not followed. Declare the keys onselfinstead, or list them inDJUST_TEMPLATE_GLOBALS. {% include %}scope — the included template's variables aren't cross-checked against the including view's context. Include templates should be checked independently if they also belong to a LiveView.- Filter argument evaluation —
{{ foo|default:bar }}is treated as a reference tofooonly (thebarargument is not statically extracted yet). - Component slots —
LiveComponenttemplates that reference slot content are not traced back to the parent's context.
The checker errs on the side of false positives over false negatives — a flagged name is either a real bug or a name you should tell the checker about (via noqa, DJUST_TEMPLATE_GLOBALS, or a class attribute).
Why this is a differentiator
Neither Phoenix nor React catches template-variable typos statically without an external type system (TypeScript, Flow). djust is the first Python-side LiveView-style framework with a first-party static template check. The tradeoff: accepting some false positives in exchange for catching real bugs before they reach users — most projects find the first run worth it.
Related
- Developer Tools —
djust_doctorconfiguration check, latency simulator - Error Overlay (Dev Mode) — catches the other kind of template bug: runtime rendering errors