Django's template engine uses HTML files with special syntax to separate presentation from logic. Variables are output using double curly braces like {{ variable }}, template tags use curly-brace percent syntax like {% tag %} for control flow, and filters transform values using the pipe syntax like {{ value|filter }}. Common built-in filters include lower, truncatewords, default, safe for marking unescaped HTML, and date for date formatting. Filters can be chained, allowing expressions like {{ value|lower|truncatechars:30 }}. Templates are stored in the app's templates/ directory and loaded with the {% load %} tag, such as {% load static %} for static file references or {% load myfilters %} for custom template filters. Custom template filters are registered through a templatetags/ module using the @register.filter decorator.
Template inheritance is a powerful feature for promoting the DRY (Don't Repeat Yourself) principle. A base template defines {% block %} placeholders that child templates override, and child templates start with {% extends "base.html" %} followed by their own block definitions. The {% include %} tag renders another template inline with optional context, ideal for reusable snippets like partials. Context is passed to a template from a view as a dictionary via render(request, 'template.html', context), making each variable accessible by its key. Inside {% for %} loops, the forloop object exposes useful properties: forloop.counter for 1-indexed counting, forloop.first and forloop.last for boundary checks, and forloop.parentloop for accessing the outer loop context. The {% empty %} clause inside a {% for %} block renders a fallback when the iterable is empty.
Context processors are functions that automatically inject variables into every template context. They are configured in the TEMPLATES setting under OPTIONS.context_processors. Built-in processors include django.template.context_processors.request, which adds the request object, auth which adds user and perms, and messages for the messages framework. Custom context processors are simple Python functions that take a request and return a dictionary. The {% csrf_token %} tag inserts a hidden input containing the CSRF token bound to the user's session, required inside every POST form. The messages framework provides a one-time notification system, where views call messages.success(request, 'Saved!') and templates iterate over {% if messages %} blocks to display them; messages are stored in the session or cookie and shown only once, with MESSAGE_STORAGE defaulting to SessionStorage.