Saltar al contenido
🐍 Curso de Python
/

Módulo 4 · Lección 17 de 45

El lenguaje de plantillas

Un lenguaje a propósito pequeño, el punto que sirve para cuatro cosas, y el silencio con el que se traga tus erratas.

⏱ 16 min de lectura

Una plantilla de Django es un fichero de texto con huecos. No tiene por qué ser HTML: sirve igual para un correo, un CSV o un fichero de configuración. Lo que la distingue de una f-string es que está pensada para que la escriba alguien que no es quien programa, y de esa decisión salen todas sus rarezas.

Porque el lenguaje es pequeño a propósito. No se puede sumar, no se pueden llamar funciones con argumentos, no se puede importar nada y no hay variables sueltas. Eso frustra al principio y tiene un motivo que conviene aceptar antes de pelearse con él: si en la plantilla se puede hacer poco, la lógica no acaba en la plantilla.

Tres sintaxis y nada más#

html
{{ variable }}              Escribe algo
{% etiqueta %}              Hace algo
{# comentario #}            No sale en la salida

{{ }} evalúa y escribe. {% %} ejecuta lógica: bucles, condicionales, herencia. Y {# #} es un comentario que desaparece de la salida, al contrario que <!-- -->, que llega al navegador y cualquiera puede leerlo con "ver código fuente".

El punto hace cuatro cosas#

html
{{ contacto.nombre }}       un atributo
{{ datos.clave }}           una clave de diccionario
{{ lista.0 }}               un índice
{{ contacto.iniciales }}    un método o propiedad, sin paréntesis

Los cuatro con el mismo punto. Django los prueba en este orden: primero clave de diccionario, después atributo o método, y por último índice numérico. El primero que funciona gana.

Salida en pantalla
{{ c.iniciales }} / {{ c.nombre|upper }}   ->  AR / ANA RUIZ
{{ d.clave }}                              ->  valor
{{ l.0 }}{{ l.1 }}                         ->  ab

Fíjate en que iniciales y nombre se escriben igual aunque uno sea una propiedad y el otro un campo. Y en que un método se llama sin paréntesis: {{ contacto.get_grupo_display }} ejecuta el método. Si pusieras los paréntesis, sería un error de sintaxis.

Ese orden de búsqueda tiene una consecuencia práctica: un diccionario con una clave llamada items, values o keys da problemas, porque Django encontrará antes el método del diccionario que tu clave. Es raro y cuando pasa cuesta verlo.

Lo que no se puede hacer#

Esta es la lista, y conviene leerla entera una vez para no perder el tiempo intentándolo:

html
{{ 2 + 2 }}                       TemplateSyntaxError
{{ contacto.busca("ana") }}       No se pueden pasar argumentos
{% x = 5 %}                       No hay asignación
{% import datetime %}             No se importa nada
{{ contactos[0] }}                No hay corchetes; es contactos.0
Salida en pantalla
TemplateSyntaxError: Could not parse the remainder: ' + 2' from '2 + 2'

Para lo poco que hace falta hay filtros:

html
{{ 2|add:2 }}         ->  4

Y para todo lo demás, la respuesta correcta no es buscar el truco, es calcularlo en la vista o en el modelo y pasarlo hecho. Cuando te veas peleando con la plantilla para conseguir un número, casi siempre es la señal de que ese número es una propiedad del modelo.

Filtros#

Un filtro transforma un valor, se aplica con | y se puede encadenar:

html
{{ s|lower|title|truncatewords:2 }}   ->  Hola Que …

Los argumentos van detrás de dos puntos, sin espacios. Los que se usan de verdad:

FiltroQué hace
default:"x"Usa x si el valor es falso: vacío, cero, None
default_if_none:"x"Usa x solo si es None
lengthCuántos elementos tiene
upper, lower, title, capfirstMayúsculas y minúsculas
truncatewords:n, truncatechars:nCortar
date:"formato"Dar formato a una fecha
join:", "Unir una lista
pluralizeLa s del plural
yesno:"sí,no"Convertir un booleano en texto
linebreaks, linebreaksbrSaltos de línea en <p> o en <br>
safeNo escapar este valor
floatformat:2Decimales

La diferencia entre default y default_if_none se ve mejor con las dos juntas:

Salida en pantalla
[{{ n|default_if_none:"nada" }}][{{ v|default:"vacio" }}]   ->  [nada][vacio]

default salta con cualquier cosa falsa, y eso incluye el cero. Para un contador, {{ total|default:"ninguno" }} enseña ninguno cuando el total es cero, que casi nunca es lo que se quería. Con números, default_if_none.

pluralize es más útil de lo que parece en español:

html
{{ n }} contacto{{ n|pluralize }}              ->  2 contactos
{{ n }} direcci{{ n|pluralize:"ón,ones" }}     ->  2 direcciones

Sin argumento añade una s; con dos valores separados por coma, elige entre singular y plural. La segunda forma es la que resuelve el español.

Fechas#

date usa un formato propio que no es el de strftime:

html
{{ c.creado|date:"d/m/Y H:i" }}                ->  22/09/2026 18:29
{{ c.creado|date:"j \d\e F \d\e Y" }}          ->  22 de septiembre de 2026

El segundo sale en español por el LANGUAGE_CODE de la lección 5, sin instalar nada. Las letras sueltas hay que escaparlas con barras, porque casi todas significan algo.

Hay tres formatos con nombre que respetan el idioma sin tener que escribir nada: {{ fecha|date:"SHORT_DATE_FORMAT" }}, "DATE_FORMAT" y "DATETIME_FORMAT". Son los que conviene usar cuando el formato exacto da igual.

Etiquetas#

for#

html
{% for contacto in contactos %}
  <li>{{ contacto.nombre }}</li>
{% empty %}
  <li>No hay contactos.</li>
{% endfor %}

{% empty %} es el bloque que sale cuando la lista está vacía, y ahorra el {% if %} de alrededor.

Dentro del bucle hay una variable forloop:

forloop.*Qué es
counterEl número, empezando en 1
counter0El número, empezando en 0
first, lastSi es el primero o el último
revcounterLos que quedan
parentloopEl forloop del bucle de fuera
Salida en pantalla
{% for x in l %}{{ forloop.counter }}:{{ x }}{% if not forloop.last %}, {% endif %}{% endfor %}
1:a, 2:b, 3:c

Para recorrer un diccionario se usa items, como en Python pero sin paréntesis:

html
{% for clave, valor in datos.items %}{{ clave }}={{ valor }}{% endfor %}

if#

html
{% if contacto.telefono %}
  <a href="tel:{{ contacto.telefono }}">Llamar</a>
{% elif contacto.correo %}
  <a href="mailto:{{ contacto.correo }}">Escribir</a>
{% else %}
  Sin forma de contacto
{% endif %}

Admite ==, !=, <, >, <=, >=, in, not in, is, and, or y not. Es casi Python, con la diferencia de que no hay paréntesis para agrupar: cuando la condición necesita paréntesis, es que le toca a la vista.

with#

Guarda un valor calculado para no repetirlo:

html
{% with total=contactos.count %}
  Hay {{ total }} contacto{{ total|pluralize }}.
{% endwith %}

Esto no es solo comodidad. contactos.count es una consulta a la base de datos, y escribirlo tres veces en la plantilla son tres consultas. with la hace una vez.

El escapado#

Ya se vio en la lección anterior: todo lo que se inserta se escapa.

Salida en pantalla
{{ h }} || {{ h|safe }}     ->  &lt;b&gt;x&lt;/b&gt; || <b>x</b>

safe desactiva el escapado de ese valor, y {% autoescape off %} lo desactiva para un bloque entero.