Daily Tech Brief

Top startup stories in your inbox

Subscribe Free

© 2026 rakrisi Daily

Forms - User Input and Validation

Forms: User Input and Validation

Welcome to Forms! Think of forms as conversations with your users - they ask for information, validate it, and process it safely. Forms are how users interact with your web applications.

HTML Forms Basics

Let’s start with a simple HTML form:

<!-- templates/contact.html -->
<!DOCTYPE html>
<html>
<head>
    <title>Contact Us</title>
</head>
<body>
    <h1>Contact Us</h1>
    <form action="/contact" method="POST">
        <label for="name">Name:</label>
        <input type="text" id="name" name="name" required><br>
        
        <label for="email">Email:</label>
        <input type="email" id="email" name="email" required><br>
        
        <label for="message">Message:</label>
        <textarea id="message" name="message" required></textarea><br>
        
        <input type="submit" value="Send Message">
    </form>
</body>
</html>
from flask import Flask, request, render_template

app = Flask(__name__)

@app.route('/contact', methods=['GET', 'POST'])
def contact():
    if request.method == 'POST':
        name = request.form.get('name')
        email = request.form.get('email')
        message = request.form.get('message')
        
        # Process the form data
        print(f"Message from {name} ({email}): {message}")
        return "Thank you for your message!"
    
    return render_template('contact.html')

if __name__ == '__main__':
    app.run(debug=True)

Flask-WTF for Better Forms

Flask-WTF makes forms much easier and more secure:

pip install flask-wtf
from flask import Flask, render_template, flash, redirect, url_for
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SubmitField
from wtforms.validators import DataRequired, Email, Length

app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key-here'

class ContactForm(FlaskForm):
    name = StringField('Name', validators=[DataRequired(), Length(min=2, max=50)])
    email = StringField('Email', validators=[DataRequired(), Email()])
    message = TextAreaField('Message', validators=[DataRequired(), Length(min=10, max=500)])
    submit = SubmitField('Send Message')

@app.route('/contact', methods=['GET', 'POST'])
def contact():
    form = ContactForm()
    
    if form.validate_on_submit():
        # Form is valid, process the data
        name = form.name.data
        email = form.email.data
        message = form.message.data
        
        # Here you would typically save to database or send email
        print(f"Message from {name} ({email}): {message}")
        
        flash('Your message has been sent!', 'success')
        return redirect(url_for('contact'))
    
    return render_template('contact.html', form=form)

if __name__ == '__main__':
    app.run(debug=True)
<!-- templates/contact.html -->
{% extends "base.html" %}

{% block content %}
<h1>Contact Us</h1>

{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}

<form method="POST">
    {{ form.hidden_tag() }}
    
    <div class="form-group">
        {{ form.name.label }}
        {{ form.name(class="form-control") }}
        {% if form.name.errors %}
        <div class="error">
            {% for error in form.name.errors %}
            <span>{{ error }}</span>
            {% endfor %}
        </div>
        {% endif %}
    </div>
    
    <div class="form-group">
        {{ form.email.label }}
        {{ form.email(class="form-control") }}
        {% if form.email.errors %}
        <div class="error">
            {% for error in form.email.errors %}
            <span>{{ error }}</span>
            {% endfor %}
        </div>
        {% endif %}
    </div>
    
    <div class="form-group">
        {{ form.message.label }}
        {{ form.message(class="form-control") }}
        {% if form.message.errors %}
        <div class="error">
            {% for error in form.message.errors %}
            <span>{{ error }}</span>
            {% endfor %}
        </div>
        {% endif %}
    </div>
    
    {{ form.submit(class="btn btn-primary") }}
</form>
{% endblock %}

Form Fields and Validators

Common Field Types

from wtforms import (
    StringField, TextAreaField, PasswordField, 
    IntegerField, FloatField, BooleanField,
    SelectField, SelectMultipleField, RadioField,
    FileField, DateField, DateTimeField
)
from wtforms.validators import (
    DataRequired, Email, EqualTo, Length, 
    NumberRange, URL, Regexp, Optional
)

class UserForm(FlaskForm):
    # Text fields
    username = StringField('Username', validators=[DataRequired(), Length(min=3, max=20)])
    email = StringField('Email', validators=[DataRequired(), Email()])
    password = PasswordField('Password', validators=[DataRequired(), Length(min=6)])
    confirm_password = PasswordField('Confirm Password', 
                                   validators=[DataRequired(), EqualTo('password')])
    
    # Text area
    bio = TextAreaField('Biography', validators=[Length(max=500)])
    
    # Numbers
    age = IntegerField('Age', validators=[NumberRange(min=13, max=120)])
    height = FloatField('Height (cm)', validators=[NumberRange(min=50, max=250)])
    
    # Choices
    gender = SelectField('Gender', choices=[
        ('', 'Select gender'),
        ('male', 'Male'),
        ('female', 'Female'),
        ('other', 'Other')
    ])
    
    interests = SelectMultipleField('Interests', choices=[
        ('sports', 'Sports'),
        ('music', 'Music'),
        ('reading', 'Reading'),
        ('travel', 'Travel')
    ])
    
    # Boolean
    newsletter = BooleanField('Subscribe to newsletter')
    
    # Date
    birth_date = DateField('Birth Date', validators=[DataRequired()])
    
    submit = SubmitField('Register')

Custom Validators

from wtforms.validators import ValidationError
import re

def phone_number_validator(form, field):
    """Validate phone number format."""
    pattern = re.compile(r'^\+?1?[-.\s]?\(?([0-9]{3})\)?[-.\s]?([0-9]{3})[-.\s]?([0-9]{4})$')
    if not pattern.match(field.data):
        raise ValidationError('Invalid phone number format.')

def strong_password_validator(form, field):
    """Validate strong password requirements."""
    password = field.data
    if len(password) < 8:
        raise ValidationError('Password must be at least 8 characters long.')
    if not re.search(r'[A-Z]', password):
        raise ValidationError('Password must contain at least one uppercase letter.')
    if not re.search(r'[a-z]', password):
        raise ValidationError('Password must contain at least one lowercase letter.')
    if not re.search(r'[0-9]', password):
        raise ValidationError('Password must contain at least one number.')

class RegistrationForm(FlaskForm):
    phone = StringField('Phone', validators=[DataRequired(), phone_number_validator])
    password = PasswordField('Password', validators=[DataRequired(), strong_password_validator])
    submit = SubmitField('Register')

File Uploads

Handle file uploads securely:

import os
from werkzeug.utils import secure_filename
from flask import current_app

class UploadForm(FlaskForm):
    file = FileField('File', validators=[DataRequired()])
    submit = SubmitField('Upload')

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in {'png', 'jpg', 'jpeg', 'gif'}

@app.route('/upload', methods=['GET', 'POST'])
def upload():
    form = UploadForm()
    
    if form.validate_on_submit():
        file = form.file.data
        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)
            file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], filename)
            file.save(file_path)
            flash('File uploaded successfully!', 'success')
            return redirect(url_for('upload'))
        else:
            flash('Invalid file type.', 'error')
    
    return render_template('upload.html', form=form)

Form Processing Patterns

Create, Read, Update, Delete (CRUD)

# Mock database
users = []

class UserForm(FlaskForm):
    name = StringField('Name', validators=[DataRequired()])
    email = StringField('Email', validators=[DataRequired(), Email()])
    submit = SubmitField()

@app.route('/users')
def list_users():
    return render_template('users/list.html', users=users)

@app.route('/users/create', methods=['GET', 'POST'])
def create_user():
    form = UserForm()
    
    if form.validate_on_submit():
        user = {
            'id': len(users) + 1,
            'name': form.name.data,
            'email': form.email.data
        }
        users.append(user)
        flash('User created successfully!', 'success')
        return redirect(url_for('list_users'))
    
    return render_template('users/create.html', form=form)

@app.route('/users/<int:user_id>/edit', methods=['GET', 'POST'])
def edit_user(user_id):
    user = next((u for u in users if u['id'] == user_id), None)
    if not user:
        abort(404)
    
    form = UserForm(obj=user)  # Pre-populate form
    
    if form.validate_on_submit():
        user['name'] = form.name.data
        user['email'] = form.email.data
        flash('User updated successfully!', 'success')
        return redirect(url_for('list_users'))
    
    return render_template('users/edit.html', form=form, user=user)

@app.route('/users/<int:user_id>/delete', methods=['POST'])
def delete_user(user_id):
    global users
    users = [u for u in users if u['id'] != user_id]
    flash('User deleted successfully!', 'success')
    return redirect(url_for('list_users'))

CSRF Protection

Flask-WTF automatically protects against CSRF attacks:

# In your template
<form method="POST">
    {{ form.hidden_tag() }}  <!-- CSRF token -->
    <!-- Your form fields -->
</form>

# In your app configuration
app.config['WTF_CSRF_ENABLED'] = True
app.config['WTF_CSRF_SECRET_KEY'] = 'your-csrf-secret-key'

Form Rendering with Bootstrap

Make forms look professional:

<!-- templates/base.html -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">

<!-- templates/user_form.html -->
{% extends "base.html" %}

{% block content %}
<div class="container mt-5">
    <div class="row justify-content-center">
        <div class="col-md-6">
            <h1 class="mb-4">{{ title }}</h1>
            
            {% with messages = get_flashed_messages(with_categories=true) %}
            {% if messages %}
            {% for category, message in messages %}
            <div class="alert alert-{{ 'success' if category == 'success' else 'danger' }} alert-dismissible fade show" role="alert">
                {{ message }}
                <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
            </div>
            {% endfor %}
            {% endif %}
            {% endwith %}
            
            <form method="POST" novalidate>
                {{ form.hidden_tag() }}
                
                <div class="mb-3">
                    {{ form.name.label(class="form-label") }}
                    {{ form.name(class="form-control" + (" is-invalid" if form.name.errors else "")) }}
                    {% if form.name.errors %}
                    <div class="invalid-feedback">
                        {% for error in form.name.errors %}{{ error }}{% endfor %}
                    </div>
                    {% endif %}
                </div>
                
                <div class="mb-3">
                    {{ form.email.label(class="form-label") }}
                    {{ form.email(class="form-control" + (" is-invalid" if form.email.errors else "")) }}
                    {% if form.email.errors %}
                    <div class="invalid-feedback">
                        {% for error in form.email.errors %}{{ error }}{% endfor %}
                    </div>
                    {% endif %}
                </div>
                
                {{ form.submit(class="btn btn-primary") }}
            </form>
        </div>
    </div>
</div>
{% endblock %}

Practical Examples

Example 1: Registration Form

from flask import Flask, render_template, redirect, url_for, flash
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, BooleanField
from wtforms.validators import DataRequired, Email, EqualTo, Length

app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'

class RegistrationForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired(), Length(min=2, max=20)])
    email = StringField('Email', validators=[DataRequired(), Email()])
    password = PasswordField('Password', validators=[DataRequired()])
    confirm_password = PasswordField('Confirm Password', 
                                   validators=[DataRequired(), EqualTo('password')])
    accept_terms = BooleanField('I accept the terms and conditions', 
                               validators=[DataRequired()])
    submit = SubmitField('Sign Up')

@app.route('/register', methods=['GET', 'POST'])
def register():
    form = RegistrationForm()
    
    if form.validate_on_submit():
        # Here you would create the user account
        flash(f'Account created for {form.username.data}!', 'success')
        return redirect(url_for('home'))
    
    return render_template('register.html', title='Register', form=form)

@app.route('/')
def home():
    return render_template('home.html')

if __name__ == '__main__':
    app.run(debug=True)

Example 2: Contact Form with Email

from flask import Flask, render_template, flash, redirect, url_for
from flask_wtf import FlaskForm
from flask_mail import Mail, Message
from wtforms import StringField, TextAreaField, SubmitField
from wtforms.validators import DataRequired, Email

app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
app.config['MAIL_SERVER'] = 'smtp.gmail.com'
app.config['MAIL_PORT'] = 587
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = 'your-email@gmail.com'
app.config['MAIL_PASSWORD'] = 'your-password'

mail = Mail(app)

class ContactForm(FlaskForm):
    name = StringField('Name', validators=[DataRequired()])
    email = StringField('Email', validators=[DataRequired(), Email()])
    subject = StringField('Subject', validators=[DataRequired()])
    message = TextAreaField('Message', validators=[DataRequired()])
    submit = SubmitField('Send')

@app.route('/contact', methods=['GET', 'POST'])
def contact():
    form = ContactForm()
    
    if form.validate_on_submit():
        msg = Message(
            subject=f"Contact Form: {form.subject.data}",
            sender=form.email.data,
            recipients=['your-email@gmail.com'],
            body=f"""
            From: {form.name.data} <{form.email.data}>
            
            {form.message.data}
            """
        )
        mail.send(msg)
        
        flash('Your message has been sent!', 'success')
        return redirect(url_for('contact'))
    
    return render_template('contact.html', form=form)

if __name__ == '__main__':
    app.run(debug=True)

Example 3: Search Form

from flask import Flask, render_template, request
from flask_wtf import FlaskForm
from wtforms import StringField, SelectField, SubmitField

app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'

class SearchForm(FlaskForm):
    query = StringField('Search')
    category = SelectField('Category', choices=[
        ('all', 'All Categories'),
        ('books', 'Books'),
        ('electronics', 'Electronics'),
        ('clothing', 'Clothing')
    ])
    sort_by = SelectField('Sort by', choices=[
        ('relevance', 'Relevance'),
        ('price_low', 'Price: Low to High'),
        ('price_high', 'Price: High to Low'),
        ('newest', 'Newest First')
    ])
    submit = SubmitField('Search')

# Mock search results
products = [
    {'id': 1, 'name': 'Python Book', 'category': 'books', 'price': 29.99},
    {'id': 2, 'name': 'Laptop', 'category': 'electronics', 'price': 999.99},
    {'id': 3, 'name': 'T-Shirt', 'category': 'clothing', 'price': 19.99},
]

@app.route('/search', methods=['GET', 'POST'])
def search():
    form = SearchForm()
    results = []
    
    if form.validate_on_submit() or request.method == 'GET':
        query = form.query.data or request.args.get('q', '')
        category = form.category.data or request.args.get('category', 'all')
        
        # Filter results
        results = products
        if query:
            results = [p for p in results if query.lower() in p['name'].lower()]
        if category != 'all':
            results = [p for p in results if p['category'] == category]
    
    return render_template('search.html', form=form, results=results)

if __name__ == '__main__':
    app.run(debug=True)

Best Practices

1. Always Validate Input

# Good
class SafeForm(FlaskForm):
    name = StringField('Name', validators=[DataRequired(), Length(max=100)])

# Bad - No validation
name = request.form.get('name')  # Could be anything!

2. Use CSRF Protection

# Always include in templates
<form method="POST">
    {{ form.hidden_tag() }}
    <!-- form fields -->
</form>

3. Handle File Uploads Securely

from werkzeug.utils import secure_filename

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in {'png', 'jpg', 'jpeg'}

if file and allowed_file(secure_filename(file.filename)):
    # Safe to save
    pass

4. Use Flash Messages

# In route
flash('Success message!', 'success')
flash('Error message!', 'error')

# In template
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}

5. Redirect After POST

# POST-REDIRECT-GET pattern
if form.validate_on_submit():
    # Process form
    flash('Success!', 'success')
    return redirect(url_for('some_route'))  # Redirect prevents double submission

Practice Exercises

Exercise 1: User Registration

Create a registration system with:

  • Username, email, password fields
  • Password confirmation
  • Terms acceptance checkbox
  • Email validation
  • Password strength requirements

Exercise 2: Blog Comment System

Build a comment form with:

  • Name and email fields
  • Comment text area
  • CAPTCHA or simple math verification
  • Comment moderation (approve/reject)
  • Nested replies

Exercise 3: Product Review Form

Create a product review system with:

  • Star rating (1-5)
  • Review title and text
  • Photo upload
  • Review filtering and sorting
  • Average rating calculation

Exercise 4: Survey Application

Build a multi-page survey with:

  • Different question types (text, radio, checkbox, select)
  • Progress indicator
  • Save progress functionality
  • Results summary page
  • Export responses to CSV

Summary

Forms handle user input and validation in Flask applications:

Basic Form Handling:

@app.route('/submit', methods=['GET', 'POST'])
def submit():
    if request.method == 'POST':
        data = request.form.get('field_name')
        # Process data
    return render_template('form.html')

Flask-WTF Forms:

class MyForm(FlaskForm):
    name = StringField('Name', validators=[DataRequired()])
    submit = SubmitField('Submit')

@app.route('/form', methods=['GET', 'POST'])
def my_form():
    form = MyForm()
    if form.validate_on_submit():
        # Process valid form
        return redirect(url_for('success'))
    return render_template('form.html', form=form)

Key Concepts:

  • Form validation with WTForms
  • CSRF protection
  • File uploads
  • Flash messages for feedback
  • POST-REDIRECT-GET pattern

Next: Databases - storing and retrieving data with SQLAlchemy! 💾