In this tutorial, we are going to build a simple Flask login Form, it’s going to be a simple demonstration of Flask web flow and field validations.


  • Python 3.8.5
  • Flask 2.0.1
  • Max OS X



The support for web form handling that comes with Flask is a bare minimum, so to handle Web forms in this example, I am going to use flask-wtf so this extension is going to give me additional features such as field validation.

Flask extensions are regular python packages that are installed with pip, so pip install flask-wtf works well.

Note- Make sure to activate the virtual environment.

(venv) flask-tutorials % pip install flask-wtf
Collecting flask-wtf
  Downloading Flask_WTF-0.15.1-py2.py3-none-any.whl (13 kB)
Requirement already satisfied: itsdangerous in ./venv/lib/python3.8/site-packages (from flask-wtf) (2.0.1)
Collecting WTForms
  Downloading WTForms-2.3.3-py2.py3-none-any.whl (169 kB)
     |████████████████████████████████| 169 kB 1.8 MB/s 
Requirement already satisfied: Flask in ./venv/lib/python3.8/site-packages (from flask-wtf) (2.0.1)
Requirement already satisfied: MarkupSafe in ./venv/lib/python3.8/site-packages (from WTForms->flask-wtf) (2.0.1)
Requirement already satisfied: click>=7.1.2 in ./venv/lib/python3.8/site-packages (from Flask->flask-wtf) (8.0.1)
Requirement already satisfied: Werkzeug>=2.0 in ./venv/lib/python3.8/site-packages (from Flask->flask-wtf) (2.0.1)
Requirement already satisfied: Jinja2>=3.0 in ./venv/lib/python3.8/site-packages (from Flask->flask-wtf) (3.0.1)
Installing collected packages: WTForms, flask-wtf
Successfully installed WTForms-2.3.3 flask-wtf-0.15.1

To use the flask-wtf extension, the Flask application instance needs to be configured with a secret key. This secret key is used in the Falsk application for several things related to security.

In our case flask-wtf is going to use it to protect the web forms against a common attack called cross-site-request-forgery. So we have to configure this secrete-key in the application as a configuration variable.

The configuration variables in Flask can be configured in many ways, the easiest way to set them as app.config using dict syntax.

app.config['SECRET_KEY'] = 'secret-key'

Flask Login Form:

Application Structure:

(venv) flask-login-form %
├── app
│   ├──
│   ├──
│   ├──
│   └── templates
│       ├── header.html
│       ├── index.html
│       └── login.html

class Config(object):
    SECRET_KEY = 'my-secrete-key'

from flask import Flask
from config import Config

app = Flask(__name__)
from app import routes

When using flask-wtfto work with web forms, each form is represented by a python class. So here is the LoginForm class which represents the web form.

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired

class LoginForm(FlaskForm):
    user_name  = StringField('UserName', validators=[DataRequired()])
    password = PasswordField('Password', validators=[DataRequired()])
    submit = SubmitField('Sign In')

Form classes are inherited from FlaskForm, provided by flask-wtfand the fields of the form are represented as class variables. On the above form, I have taken two form fields user_name and password, where user_name is StringField type and password is PasswordField type.

You may be noticed in the import statements, the *Fieldclasses are not directly coming from flask_wtfdirectly but coming from one of its dependencies called wtforms

*Fieldclasses accept some arguments, the first argument that I passed to StringField and PasswordField classes is that the label name that I want to use in the formwhen it is present to the user.

As a second argument, I defined validators, this is a very important aspect of creating a form and one of the reasons why flask-wtfand wtformsare great. We can define validation functions for each field and these are going to be executed automatically by the framework itself.

In the above example, I defined a required field validation, so that, I used a DataRequiredvalidator. This is going to prevent the user from submitting the field empty. I did the same thing for the password field too.

Note that in both cases the validator argument was set to a list, this is because we can assign more than one validator to each field, and also note that the validator itself is an object that needs to be created.

Create an HTML template for the page that renders the above form.

This template is going to extends from the header template as we discussed in the jinja template inheritance.

{% extends "header.html" %}
{% block content %}
<h1> Sign In</h1>
<form action="", method="post">
    {{ form.hidden_tag() }}
    <p>{{ form.submit() }}</p>
{% endblock %}

The formelement is going to have two attributes action and method. The action attribute indicates that what URL that browser should use when submitting the information when the user entered in the form. In the above form action, I have given an empty string, which means I am telling the browser to submit the form to the URL that currently in the address bar.

The method attribute sets the type of request, that is going to be used for the form submission.

Inside the form I have to set my form fields, this is an area where flask-wtf helps a lot because all the fields that I added to the form class already know how to render them selfs in an HTML page, so all we need to do is just use the template place holders for the label and fields.

The form.user_name.labelreferenced to the label that I set in the user_name class variable inLoginFormclass, and in the next line I invoked the user_name field as a function, this is going to output the HTML for this field.

If we need to customize in any way appearance of the field element, we can add additional attributes as keyword arguments in this function call. In this case, I just added sizeargument to make the user_name and password fields both 32 characters long.

Finally, we need a submit button.

The form.hidden_tag() is going to render a hidden field that going to add to the form to protect against the cross-site-scripting.

Create a route that maps the login URL to the form page.

from app import app
from flask import render_template, flash, redirect
from app.forms import LoginForm

def index():
    user_info = {
    return render_template('index.html', user=user_info)

@app.route('/login', methods=['GET','POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        if == 'admin' and == 'admin':
            flash('login successful')
            return redirect('index')
    return render_template('login.html', form=form)

In the above, I have created a new route for /loginURL, I just started this function by creating an instance of LoginForm class. To make this example simple I am validating the user with hardcoded values.

form.validate_on_submit() function is going to evaluate to true when the request comes with the form data and all those fields in the form are valid.

flash() function is going to print a message on the webpage that we are going to redirect, In this case, it’s going to print the ‘login success’ message on the index page when the user validation successful.

Create header.html template.

        <title>Generic Blog</title>
    <h2 style="color:#093657">My Blog</h2>
        <a href="/login">Login</a>
        {% with messages = get_flashed_messages()%}
        {% if messages %}
        {% for message in messages %}
            <li> {{message}}</li>
        {% endfor %}
        {% endif %}
        {% endwith %}
        {% block content %} {% endblock %}

In the above header.html template, I obtained the flask messages that is coming from the /login route using get_flash_messages() function.

Create an index page that will appear on successful login.


{% extends "header.html" %}

{% block content %}
<h1 style="color:#093657"> Welcome
    {% if user %}
    {% else %}
    {% endif %}
{% endblock %}

Finally, prepare the login form, it typically includes a header and content blocks.

{% extends "header.html" %}
{% block content %}
<h1> Sign In</h1>
<form action="", method="post">
    {{ form.hidden_tag() }}
    <p>{{ form.submit() }}</p>
{% endblock %}

Run the app:

(venv) flask-login-form % flask run                  
 * Serving Flask app '' (lazy loading)
 * Running on (Press CTRL+C to quit)


Accessing Login Form

Python - Flask Login Form Example login page

Login Success:

Python - Flask Login Form Example success

Login Error:

Python - Flask Login Form Example Error


Download Source from GIT:

Happy Learning 🙂