Jinja genera HTML y otros formatos de texto a partir de plantillas. La configuración central debe controlar carga, valores indefinidos y escape; las reglas de negocio permanecen en Python.
from jinja2 import Environment, FileSystemLoader, StrictUndefined, select_autoescape
env = Environment(
loader=FileSystemLoader("templates"),
autoescape=select_autoescape(["html", "xml"]),
undefined=StrictUndefined,
)
template = env.get_template("producto.html")
html = template.render(producto={"nombre": "Curso Python"})
StrictUndefined revela variables ausentes. autoescape reduce XSS en HTML, pero no protege cualquier contexto, como JavaScript o URL. No marques contenido de usuarios como seguro para saltar el escape.
Usa herencia para layouts, include para fragmentos y macros para presentación repetida. Mantén consultas, autorización y transformaciones complejas fuera. La guía de Flask aporta contexto web.
La documentación oficial de Jinja, consultada el 22 de julio de 2026, cubre entornos, loaders, escape y herencia. Prueba la salida con caracteres especiales.
Instalación y estructura
Instala la biblioteca en un entorno virtual con python -m pip install Jinja2. La distribución se llama Jinja2, mientras que el módulo importado es jinja2. Una aplicación pequeña puede separar templates/, static/ y el código Python. El loader resuelve nombres dentro de su raíz configurada, por lo que conviene usar env.get_template("correos/bienvenida.html") y no construir una ruta recibida del usuario.
Un Environment centraliza las políticas y reutiliza la caché de plantillas. Crea un entorno por configuración. Las páginas HTML con escape automático y los archivos de texto sin escape tienen reglas distintas y no deberían compartirlas accidentalmente.
from pathlib import Path
from jinja2 import Environment, FileSystemLoader, StrictUndefined, select_autoescape
BASE_DIR = Path(__file__).resolve().parent
env = Environment(
loader=FileSystemLoader(BASE_DIR / "templates"),
autoescape=select_autoescape(
enabled_extensions=("html", "htm", "xml"),
default_for_string=True,
),
undefined=StrictUndefined,
trim_blocks=True,
lstrip_blocks=True,
)
Las opciones de espacios reducen líneas vacías alrededor de bloques. default_for_string=True protege plantillas creadas con env.from_string(). Aun así, ese método debe recibir texto controlado por la aplicación, nunca expresiones enviadas por visitantes.
Variables, filtros y pruebas
Las expresiones {{ ... }} imprimen valores, los bloques {% ... %} controlan el flujo y los comentarios {# ... #} desaparecen del resultado. Pasa datos preparados en lugar de objetos de dominio con demasiadas capacidades. Un diccionario pequeño hace explícito el contrato entre negocio y presentación.
<h2>{{ pagina.titulo }}</h2>
{% if productos %}
<ul>
{% for producto in productos %}
<li>
{{ producto.nombre }}
<span>{{ producto.precio_centavos | moneda }}</span>
</li>
{% endfor %}
</ul>
{% else %}
<p>No hay productos disponibles.</p>
{% endif %}
Los filtros deben hacer transformaciones de presentación pequeñas y deterministas. Un filtro de moneda puede formatear centavos, pero no debería consultar un servicio de cambio. Las pruebas usadas con is responden preguntas como valor is defined o numero is odd.
def moneda(centavos: int) -> str:
return f"EUR {centavos / 100:,.2f}"
env.filters["moneda"] = moneda
El filtro default ofrece una alternativa intencional, pero no debe ocultar campos obligatorios. StrictUndefined convierte un nombre mal escrito en error, en vez de producir una página incompleta. Captura la excepción en el límite de la aplicación, registra el nombre de la plantilla y muestra una respuesta genérica sin revelar rutas internas.
Herencia y bloques
La herencia evita copiar cabecera, navegación y pie. Una plantilla hija extiende la base y completa bloques con nombres claros.
{# templates/base.html #}
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>{% block title %}Mi aplicación{% endblock %}</title>
</head>
<body>
<main>{% block content required %}{% endblock %}</main>
</body>
</html>
{# templates/productos/detalle.html #}
{% extends "base.html" %}
{% block title %}{{ producto.nombre }} | Mi aplicación{% endblock %}
{% block content %}
<h2>{{ producto.nombre }}</h2>
<p>{{ producto.descripcion }}</p>
{% endblock %}
El modificador required detecta descendientes que omiten un bloque esencial. super() incluye contenido del padre cuando la extensión debe añadir y no reemplazar. Usa include para fragmentos que comparten el contexto e import para macros. Si un componente depende de muchas variables implícitas, será difícil reutilizarlo; pasa argumentos claros.
Macros reutilizables
Las macros actúan como funciones de presentación. Son apropiadas para botones, campos y tarjetas cuyo marcado debe ser consistente. No conviertas cada línea en macro, pues demasiada indirección oculta la estructura.
{# templates/componentes.html #}
{% macro enlace_accion(texto, url, variante="primaria") -%}
<a class="boton boton--{{ variante | e }}" href="{{ url }}">{{ texto }}</a>
{%- endmacro %}
{% from "componentes.html" import enlace_accion %}
{{ enlace_accion("Ver detalles", url_producto) }}
Escape no significa validación. Una URL javascript: puede seguir siendo peligrosa aunque sus caracteres HTML estén escapados. Genera enlaces con el router o valida esquema y destino antes de pasarlos a la plantilla. También limita variante a nombres de clase conocidos desde Python.
Autoescape, Markup y contextos
Con autoescape activo, Jinja reemplaza <, > y & por entidades HTML. Esto permite insertar contenido en nodos de texto y atributos entre comillas. Pon siempre comillas en atributos. Para datos JavaScript, serializa JSON con tojson en lugar de interpolar cadenas.
<script>
const configuracion = {{ configuracion_publica | tojson }};
</script>
El filtro safe y los objetos Markup declaran que un fragmento ya es confiable. Esa declaración traslada la responsabilidad al productor. Resérvala para HTML estático creado y revisado por la aplicación. Si los usuarios escriben Markdown, conviértelo y aplica después un sanitizador HTML con una lista permitida; Jinja no es un sanitizador.
Las plantillas de terceros pueden inspeccionar atributos y ejecutar operaciones admitidas por el lenguaje. SandboxedEnvironment restringe parte del comportamiento, pero no es una frontera completa contra el consumo de recursos. Plantillas realmente no confiables requieren aislamiento de proceso y límites de tiempo, memoria y salida. A menudo es más seguro ofrecer campos configurables que aceptar código Jinja.
Renderizado, streaming y otros formatos
render() devuelve una cadena completa. Para salidas grandes, generate() produce fragmentos y stream() controla el buffering. El streaming puede reducir memoria, pero un error tardío puede aparecer después de enviar parte de una respuesta HTTP. Valida primero los datos obligatorios y úsalo solo cuando aporte un beneficio medible.
Jinja también genera correos, SQL y configuraciones, pero el escape debe corresponder al destino. El escape HTML no protege SQL, shell, CSV ni YAML. Nunca construyas SQL con valores externos mediante una plantilla; usa parámetros del driver. Crea otro entorno para texto plano y aplica las reglas específicas de ese formato.
def renderizar_confirmacion(nombre: str, enlace: str) -> str:
template = env.get_template("correos/confirmacion.html")
return template.render(nombre=nombre, enlace=enlace)
Pruebas y diagnóstico
Prueba las plantillas con datos mínimos, completos y cadenas con caracteres especiales. Las aserciones sobre elementos relevantes suelen ser más estables que comparar todo el documento. Carga las plantillas principales durante los tests para encontrar errores de sintaxis, archivos ausentes y bloques obligatorios no implementados.
from markupsafe import escape
def test_nombre_se_escapa():
template = env.from_string("<p>{{ nombre }}</p>")
salida = template.render(nombre="<script>alert(1)</script>")
assert str(escape("<script>alert(1)</script>")) in salida
assert "<script>" not in salida
En producción, registra contexto técnico sin volcar todo el diccionario de renderizado. El contexto puede contener tokens, direcciones o datos personales. La página de error pública debe ser genérica.
Lista de comprobación
Antes de publicar, confirma que las plantillas HTML tienen autoescape, las variables obligatorias fallan pronto y los nombres de plantilla vienen de código controlado. Revisa atributos entre comillas, construcción confiable de URL, ausencia de safe sobre entradas externas y ninguna consulta de red o base de datos escondida en filtros.
Mantén convenciones para directorios, bloques y componentes. Valida el HTML, la navegación por teclado y el idioma. Jinja compone texto; la semántica, accesibilidad, seguridad contextual y gestión de errores siguen siendo responsabilidad de la aplicación. Esa frontera produce plantillas más pequeñas, predecibles y fáciles de mantener.
Para internacionalización, pasa mensajes ya seleccionados o expón una función de traducción muy limitada. No repartas reglas de selección de idioma por las plantillas. Fechas, números, plurales y monedas necesitan formato sensible al locale, no sustituciones de texto. Define el idioma del documento desde un estado confiable y prueba escritura de derecha a izquierda si el producto la admite.
Las dependencias entre plantillas también merecen pruebas. Renombrar un archivo puede romper un extends, include o import que los tests normales nunca renderizan. Un smoke test puede recorrer cada página pública con fixtures representativas. Acompaña esa verificación con actualizaciones de dependencias y revisión de notas de versión.