Django / Request and data foundations / Lesson 4
Templates, context, and escaping
Render meaningful HTML with a narrow context and preserve the difference between text escaping and trust in other output contexts.
On this page
Lesson goals
- Templates describe presentation
- Keep untrusted strings as text
- Review output as a complete accessible page
Templates describe presentation
A view chooses data and an outcome; a template describes how approved data appears in a document. Keep expensive queries, permission decisions, and state changes outside presentation. Djangoโs template language intentionally offers a constrained expression system rather than arbitrary Python execution. A context should contain the values this view needs, not every request attribute or model field. Named templates and app-level directories help organization, while inheritance can share a document shell. That reuse does not automatically provide valid heading hierarchy or accessible labels; the resulting HTML still needs review as one complete document.
Find templates through explicit configuration
Template engines and loaders determine where a template name is searched. A project can use configured directories and app template directories under the selected backend. Namespace app templates with a subdirectory to avoid generic names colliding. TemplateDoesNotExist can mean the wrong name, directory, installed app, or engine configuration rather than a missing Python import. Inspect the attempted path and loader evidence in a controlled environment without exposing private filesystem details publicly. The complete examples use an in-memory Engine, so no project file lookup is claimed; later application fragments must supply their own real template files and backend setup.
Keep untrusted strings as text
The first example inserts markup-looking text into a paragraph under normal autoescaping. The result contains escaped angle brackets, not a new emphasis element from the string. This supports safe ordinary HTML text output, but it is not a universal sanitizer for JavaScript, CSS, URLs, or arbitrary attribute construction. Do not use safe or disable autoescape merely because content came from your own database; it may still be user-authored. Prefer structured template markup around data and use a reviewed rich-content boundary only when the product genuinely needs it. Escaping and authorization remain independent responsibilities.
from django.conf import settings
settings.configure(
SECRET_KEY="synthetic-course-key",
USE_I18N=False, USE_TZ=True,
INSTALLED_APPS=[],
DATABASES={"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}},
DEFAULT_AUTO_FIELD="django.db.models.AutoField",
ALLOWED_HOSTS=["testserver"],
)
import django
django.setup()
from django.template import Engine, Context
template = Engine().from_string("<p>{{ label }}</p>")
print(template.render(Context({"label": "<em>Draft</em>"})))
<p><em>Draft</em></p>
Normal autoescaping preserves the string as text inside the known paragraph markup. No raw HTML escape hatch is used.
Render collections with explicit empty behavior
The second example renders a synthetic list and an intentional empty alternative. A list structure communicates grouping to browsers and assistive technology, while the empty message distinguishes a successful no-results state from a hidden error. Do not catch a database failure and pass an empty collection as though the query succeeded. Templates can trigger lazy attribute access, so prepare relationships and bounded data before rendering to avoid a hidden query per row. The in-memory fixture only contains dictionaries and proves the template branch behavior; it does not measure an application query plan or runtime database cost.
from django.conf import settings
settings.configure(
SECRET_KEY="synthetic-course-key",
USE_I18N=False, USE_TZ=True,
INSTALLED_APPS=[],
DATABASES={"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}},
DEFAULT_AUTO_FIELD="django.db.models.AutoField",
ALLOWED_HOSTS=["testserver"],
)
import django
django.setup()
from django.template import Engine, Context
template = Engine().from_string("<ul>{% for item in items %}<li>{{ item.title }}</li>{% empty %}<li>No courses</li>{% endfor %}</ul>")
print(template.render(Context({"items": [{"title": "Django"}, {"title": "Python"}]})))
print(template.render(Context({"items": []})))
<ul><li>Django</li><li>Python</li></ul>
<ul><li>No courses</li></ul>
The same template has distinct ordinary and empty-success outputs. Inputs are local dictionaries, not a database query.
Review output as a complete accessible page
Inspect the final HTML, not just the template source. Verify unique ids, meaningful link text, headings, form labels, and focusable controls after inheritance and includes are composed. Keep localized text and direction metadata deliberate so Arabic content does not rely on reversing strings or physical-side hacks. Browser validation, keyboard review, and appropriate accessibility testing complement template tests. A template assertion that a phrase exists cannot prove the phrase is visible or programmatically associated with the right field. Treat cached fragments and conditional visibility carefully when content depends on user permissions; a presentation cache must not mix private responses across callers.
Common errors and fixes
A stored string becomes unintended markup
- Why it happens
- Autoescaping was bypassed without a trust contract.
- How to fix it
- Use normal text rendering or a reviewed rich-content boundary.
- Prevent it next time
- Keep safe and autoescape changes exceptional and tested.
A template cannot be found after moving an app
- Why it happens
- Loader paths or installed-app configuration no longer match.
- How to fix it
- Inspect the name and configured search locations.
- Prevent it next time
- Namespace templates and test resolution.
Rendering a list issues many extra queries
- Why it happens
- Lazy relationships are dereferenced in the template.
- How to fix it
- Prepare the required data graph before rendering.
- Prevent it next time
- Measure representative multi-row requests.
Check your understanding
1. Why can safe be dangerous for text retrieved from the application database?
Show worked solution
Storage does not establish a trusted author or safe markup. A user-controlled value can remain dangerous after persistence. Keep normal escaping, or pass deliberately sanitized supported rich content through a narrow reviewed boundary. Do not transfer an ordinary database-read guarantee into an HTML execution guarantee.
2. What should a template test add beyond asserting the text โEmailโ exists?
Show worked solution
Verify that the visible label is associated with the intended input, ids are unique, help and errors are connected where required, and keyboard interaction works in the rendered page. The word could exist in an unrelated paragraph while the actual control remains unnamed.
Key takeaways
- Keep presentation context intentional and bounded.
- Autoescaping protects a specific output context.
- Inspect the composed HTML and its behavior.