Validación de formularios con WTForms

Intermedio
Flask
Flask
Actualizado: 20/06/2025

¡Desbloquea el curso de Flask completo!

IA
Ejercicios
Certificado
Entrar

Mira la lección en vídeo

Accede al vídeo completo de esta lección y a más contenido exclusivo con el Plan Plus.

Desbloquear Plan Plus

Creación de formularios WTForms

WTForms es una biblioteca de Python que simplifica la creación y validación de formularios web. A diferencia de trabajar directamente con formularios HTML, WTForms permite definir la estructura del formulario mediante clases de Python, proporcionando una interfaz orientada a objetos para gestionar campos, tipos de datos y reglas de validación.

La principal ventaja de utilizar WTForms radica en su capacidad para centralizar la lógica del formulario en el código Python, manteniendo separada la presentación (HTML) de la funcionalidad. Esto facilita el mantenimiento del código y permite reutilizar formularios en diferentes contextos.

Instalación y configuración inicial

Para comenzar a trabajar con WTForms en Flask, necesitamos instalar la extensión Flask-WTF, que integra WTForms con Flask de manera nativa:

pip install Flask-WTF

Una vez instalada, debemos configurar una clave secreta en nuestra aplicación Flask. Esta clave es necesaria para la generación de tokens de seguridad:

from flask import Flask

app = Flask(__name__)
app.config['SECRET_KEY'] = 'tu-clave-secreta-aqui'

Definición de formularios con clases

Los formularios en WTForms se definen como clases de Python que heredan de FlaskForm. Cada campo del formulario se representa mediante un atributo de clase con un tipo específico:

from flask_wtf import FlaskForm
from wtforms import StringField, EmailField, TextAreaField, SubmitField
from wtforms.validators import DataRequired, Email, Length

class ContactoForm(FlaskForm):
    nombre = StringField('Nombre', validators=[DataRequired(), Length(min=2, max=50)])
    email = EmailField('Email', validators=[DataRequired(), Email()])
    mensaje = TextAreaField('Mensaje', validators=[DataRequired(), Length(min=10, max=500)])
    enviar = SubmitField('Enviar mensaje')

En este ejemplo, hemos creado un formulario de contacto con tres campos de entrada y un botón de envío. Cada campo incluye validadores que definen las reglas que deben cumplir los datos introducidos.

Tipos de campos disponibles

WTForms ofrece una amplia variedad de tipos de campos para diferentes necesidades:

Campos de texto básicos:

from wtforms import StringField, TextAreaField, PasswordField

nombre = StringField('Nombre completo')
descripcion = TextAreaField('Descripción')
password = PasswordField('Contraseña')

Campos numéricos y de selección:

from wtforms import IntegerField, SelectField, RadioField, BooleanField

edad = IntegerField('Edad')
categoria = SelectField('Categoría', choices=[('tech', 'Tecnología'), ('design', 'Diseño')])
genero = RadioField('Género', choices=[('m', 'Masculino'), ('f', 'Femenino')])
acepta_terminos = BooleanField('Acepto los términos y condiciones')

Campos especializados:

from wtforms import EmailField, URLField, DateField
from wtforms.widgets import DateInput

email = EmailField('Correo electrónico')
sitio_web = URLField('Sitio web')
fecha_nacimiento = DateField('Fecha de nacimiento', widget=DateInput())

Integración con controladores Flask

Para utilizar un formulario WTForms en un controlador Flask, creamos una instancia del formulario y la pasamos a la plantilla:

from flask import render_template, request, redirect, url_for, flash

@app.route('/contacto', methods=['GET', 'POST'])
def contacto():
    form = ContactoForm()
    
    if form.validate_on_submit():
        # Procesar los datos del formulario
        nombre = form.nombre.data
        email = form.email.data
        mensaje = form.mensaje.data
        
        # Aquí iría la lógica para guardar o enviar los datos
        flash('Mensaje enviado correctamente', 'success')
        return redirect(url_for('contacto'))
    
    return render_template('contacto.html', form=form)

El método validate_on_submit() verifica si el formulario ha sido enviado mediante POST y si todos los validadores han pasado correctamente.

Renderizado en plantillas Jinja

En la plantilla HTML, utilizamos los métodos del objeto formulario para generar los campos:

<form method="POST">
    {{ form.hidden_tag() }}
    
    <div class="form-group">
        {{ form.nombre.label(class="form-label") }}
        {{ form.nombre(class="form-control") }}
        {% if form.nombre.errors %}
            <div class="text-danger">
                {% for error in form.nombre.errors %}
                    <small>{{ error }}</small>
                {% endfor %}
            </div>
        {% endif %}
    </div>
    
    <div class="form-group">
        {{ form.email.label(class="form-label") }}
        {{ form.email(class="form-control") }}
        {% if form.email.errors %}
            <div class="text-danger">
                {% for error in form.email.errors %}
                    <small>{{ error }}</small>
                {% endfor %}
            </div>
        {% endif %}
    </div>
    
    <div class="form-group">
        {{ form.mensaje.label(class="form-label") }}
        {{ form.mensaje(class="form-control", rows="4") }}
        {% if form.mensaje.errors %}
            <div class="text-danger">
                {% for error in form.mensaje.errors %}
                    <small>{{ error }}</small>
                {% endfor %}
            </div>
        {% endif %}
    </div>
    
    {{ form.enviar(class="btn btn-primary") }}
</form>

La función hidden_tag() genera campos ocultos necesarios para la seguridad del formulario. Cada campo se puede renderizar con atributos HTML adicionales pasados como argumentos.

Formularios con datos predefinidos

WTForms permite precargar formularios con datos existentes, útil para formularios de edición:

@app.route('/editar-perfil', methods=['GET', 'POST'])
def editar_perfil():
    form = PerfilForm()
    
    if request.method == 'GET':
        # Cargar datos existentes del usuario
        form.nombre.data = "Juan Pérez"
        form.email.data = "juan@ejemplo.com"
        form.biografia.data = "Desarrollador Python"
    
    if form.validate_on_submit():
        # Actualizar datos del usuario
        pass
    
    return render_template('editar_perfil.html', form=form)

Esta aproximación permite que el formulario muestre los valores actuales cuando se carga por primera vez, facilitando la experiencia de edición para el usuario.

Validación automática

Guarda tu progreso

Inicia sesión para no perder tu progreso y accede a miles de tutoriales, ejercicios prácticos y nuestro asistente de IA.

Progreso guardado
Asistente IA
Ejercicios
Iniciar sesión gratis

Más de 25.000 desarrolladores ya confían en CertiDevs

La validación automática en WTForms se ejecuta cuando el formulario es procesado, aplicando todas las reglas definidas en los validadores de cada campo. Este mecanismo permite verificar la integridad de los datos antes de procesarlos, garantizando que cumplan con los criterios establecidos.

Funcionamiento del sistema de validación

Cuando se invoca el método validate_on_submit(), WTForms ejecuta automáticamente todos los validadores asociados a cada campo del formulario. El proceso sigue esta secuencia:

@app.route('/registro', methods=['GET', 'POST'])
def registro():
    form = RegistroForm()
    
    if form.validate_on_submit():
        # Solo se ejecuta si TODOS los validadores pasan
        usuario = form.username.data
        email = form.email.data
        return redirect(url_for('exito'))
    
    # Si hay errores, se renderiza el formulario con mensajes
    return render_template('registro.html', form=form)

El método validate_on_submit() devuelve True únicamente cuando todos los validadores de todos los campos han pasado correctamente. Si algún validador falla, devuelve False y los errores quedan disponibles en el atributo errors de cada campo.

Validadores incorporados

WTForms incluye una amplia gama de validadores predefinidos que cubren las necesidades más comunes:

Validadores de presencia y longitud:

from wtforms.validators import DataRequired, Length, Optional

class PerfilForm(FlaskForm):
    nombre = StringField('Nombre', validators=[
        DataRequired(message='El nombre es obligatorio'),
        Length(min=2, max=50, message='El nombre debe tener entre 2 y 50 caracteres')
    ])
    
    biografia = TextAreaField('Biografía', validators=[
        Optional(),  # Campo opcional
        Length(max=200, message='La biografía no puede exceder 200 caracteres')
    ])

Validadores de formato:

from wtforms.validators import Email, URL, Regexp

email = EmailField('Email', validators=[
    DataRequired(),
    Email(message='Introduce un email válido')
])

sitio_web = URLField('Sitio web', validators=[
    Optional(),
    URL(message='Introduce una URL válida')
])

telefono = StringField('Teléfono', validators=[
    Optional(),
    Regexp(r'^\d{9}$', message='El teléfono debe tener 9 dígitos')
])

Validadores numéricos:

from wtforms.validators import NumberRange

edad = IntegerField('Edad', validators=[
    DataRequired(),
    NumberRange(min=18, max=120, message='La edad debe estar entre 18 y 120 años')
])

precio = DecimalField('Precio', validators=[
    DataRequired(),
    NumberRange(min=0.01, message='El precio debe ser mayor que 0')
])

Validadores de comparación

Los validadores de comparación permiten establecer relaciones entre diferentes campos del formulario:

from wtforms.validators import EqualTo

class CambiarPasswordForm(FlaskForm):
    password_actual = PasswordField('Contraseña actual', validators=[
        DataRequired(message='Introduce tu contraseña actual')
    ])
    
    password_nueva = PasswordField('Nueva contraseña', validators=[
        DataRequired(),
        Length(min=8, message='La contraseña debe tener al menos 8 caracteres')
    ])
    
    confirmar_password = PasswordField('Confirmar contraseña', validators=[
        DataRequired(),
        EqualTo('password_nueva', message='Las contraseñas no coinciden')
    ])

Validación condicional

WTForms permite implementar validación condicional mediante validadores personalizados que se ejecutan según el estado de otros campos:

from wtforms.validators import ValidationError

class EventoForm(FlaskForm):
    tipo_evento = SelectField('Tipo', choices=[
        ('presencial', 'Presencial'),
        ('online', 'Online')
    ])
    
    direccion = StringField('Dirección')
    enlace_streaming = URLField('Enlace de streaming')
    
    def validate_direccion(self, field):
        if self.tipo_evento.data == 'presencial' and not field.data:
            raise ValidationError('La dirección es obligatoria para eventos presenciales')
    
    def validate_enlace_streaming(self, field):
        if self.tipo_evento.data == 'online' and not field.data:
            raise ValidationError('El enlace es obligatorio para eventos online')

Los métodos que siguen el patrón validate_<nombre_campo> se ejecutan automáticamente como validadores adicionales para ese campo específico.

Manejo de errores de validación

Los errores de validación se almacenan automáticamente y pueden ser accedidos tanto desde el controlador como desde la plantilla:

@app.route('/producto', methods=['GET', 'POST'])
def crear_producto():
    form = ProductoForm()
    
    if form.validate_on_submit():
        # Procesar datos válidos
        pass
    else:
        # Mostrar errores específicos en el log
        for field, errors in form.errors.items():
            for error in errors:
                print(f"Error en {field}: {error}")
    
    return render_template('producto.html', form=form)

En la plantilla, los errores se muestran automáticamente utilizando la estructura de errores del campo:

<div class="form-group">
    {{ form.precio.label(class="form-label") }}
    {{ form.precio(class="form-control" + (" is-invalid" if form.precio.errors else "")) }}
    
    {% if form.precio.errors %}
        <div class="invalid-feedback">
            {% for error in form.precio.errors %}
                {{ error }}
            {% endfor %}
        </div>
    {% endif %}
</div>

Validación de archivos

Para formularios que incluyen subida de archivos, WTForms proporciona validadores específicos:

from flask_wtf.file import FileField, FileRequired, FileAllowed
from werkzeug.utils import secure_filename

class DocumentoForm(FlaskForm):
    archivo = FileField('Documento', validators=[
        FileRequired(message='Selecciona un archivo'),
        FileAllowed(['pdf', 'doc', 'docx'], message='Solo se permiten archivos PDF y Word')
    ])
    
    imagen = FileField('Imagen', validators=[
        Optional(),
        FileAllowed(['jpg', 'jpeg', 'png', 'gif'], message='Solo imágenes JPG, PNG o GIF')
    ])

Personalización de mensajes de error

Los mensajes de error pueden personalizarse tanto a nivel de validador como globalmente:

class UsuarioForm(FlaskForm):
    username = StringField('Usuario', validators=[
        DataRequired(message='El nombre de usuario es obligatorio'),
        Length(min=3, max=20, message='Entre %(min)d y %(max)d caracteres'),
        Regexp(r'^[a-zA-Z0-9_]+$', message='Solo letras, números y guiones bajos')
    ])
    
    email = EmailField('Email', validators=[
        DataRequired(message='El email es obligatorio'),
        Email(message='Formato de email inválido')
    ])

Los mensajes pueden incluir variables de plantilla como %(min)d y %(max)d que se sustituyen automáticamente con los valores del validador.

Validación manual

Además de la validación automática, es posible ejecutar la validación de forma manual para casos específicos:

@app.route('/api/validar', methods=['POST'])
def validar_datos():
    form = ContactoForm(data=request.json)
    
    if form.validate():
        return {'status': 'success', 'message': 'Datos válidos'}
    else:
        return {
            'status': 'error',
            'errors': form.errors
        }, 400

El método validate() ejecuta todos los validadores sin requerir que el formulario haya sido enviado mediante POST, útil para validación en tiempo real o APIs.

Aprendizajes de esta lección de Flask

  • Comprender la integración de WTForms con Flask mediante Flask-WTF.
  • Definir formularios como clases de Python con campos y validadores.
  • Implementar validación automática y personalizada de datos de formulario.
  • Gestionar la renderización de formularios y errores en plantillas Jinja.
  • Aplicar validación condicional y manejo de archivos en formularios web.

Completa este curso de Flask y certifícate

Únete a nuestra plataforma de cursos de programación y accede a miles de tutoriales, ejercicios prácticos, proyectos reales y nuestro asistente de IA personalizado para acelerar tu aprendizaje.

Asistente IA

Resuelve dudas al instante

Ejercicios

Practica con proyectos reales

Certificados

Valida tus conocimientos

Más de 25.000 desarrolladores ya se han certificado con CertiDevs

⭐⭐⭐⭐⭐
4.9/5 valoración