Flask-WTF offers simple integration with WTForms. This integration includes optional CSRF handling for greater security.

Source code and issue tracking at GitHub.

Current Version

The current version of Flask-WTF is 0.8.4.

Installing Flask-WTF

Install with pip and easy_install:

pip install Flask-WTF

or download the latest version from version control:

git clone https://github.com/ajford/flask-wtf.git
cd flask-wtf
python setup.py develop

If you are using virtualenv, it is assumed that you are installing Flask-WTF in the same virtualenv as your Flask application(s).

Configuring Flask-WTF

The following settings are used with Flask-WTF:

CSRF_ENABLED default True

CSRF_ENABLED enables CSRF. You can disable by passing in the csrf_enabled parameter to your form:

form = MyForm(csrf_enabled=False)

Generally speaking it’s a good idea to enable CSRF. If you wish to disable checking in certain circumstances - for example, in unit tests - you can set CSRF_ENABLED to False in your configuration.

CSRF support is built using wtforms.ext.csrf; Form is a subclass of SessionSecureForm. Essentially, each form generates a CSRF token deterministically based on a secret key and a randomly generated value stored in the user’s session. You can specify a secret key by passing a value to the secret_key parameter of the form constructor, setting a SECRET_KEY variable on a form class, or setting the config variable SECRET_KEY; if none of these are present, app.secret_key will be used (if this is also not present, then CSRF is impossible; creating a form with csrf_enabled = True will raise an exception).

NOTE: Previous to version 0.5.2, Flask-WTF automatically skipped CSRF validation in the case of AJAX POST requests, as AJAX toolkits added headers such as X-Requested-With when using the XMLHttpRequest and browsers enforced a strict same-origin policy.

However it has since come to light that various browser plugins can circumvent these measures, rendering AJAX requests insecure by allowing forged requests to appear as an AJAX request.

Therefore CSRF checking will now be applied to all POST requests, unless you disable CSRF at your own risk through the options described above.

You can pass in the CSRF field manually in your AJAX request by accessing the csrf field in your form directly:

var params = {'csrf' : '{{ form.csrf_token }}'};

A more complete description of the issue can be found here.

In addition, there are additional configuration settings required for Recaptcha integration : see below.

Creating forms


There will been a change in the Flask-WTF namespace soon. Until version 0.9, Flask-WTF will provide a facade to the more common WTForms fields and validator. However, this facade is proving difficult to maintain and has caused confusion when it does not provide a field/validator that was desired.

You should now import all fields and validators from the main WTForms package, excepting the HTML5 fields (which will be introduced to WTForms soon, see HTML5 widgets for more info) and the Recaptcha field.

Flask-WTF provides you with all the API features of WTForms. For example:

from flask.ext.wtf import Form
from wtforms import TextField, DataRequired

class MyForm(Form):
    name = TextField(name, validators=[DataRequired()])

In addition, a CSRF token hidden field is created. You can print this in your template as any other field:

<form method="POST" action=".">
    {{ form.csrf_token }}
    {{ form.name.label }} {{ form.name(size=20) }}
    <input type="submit" value="Go">

Changed in version 0.6: The csrf field renamed from csrf to csrf_token.

However, in order to create valid XHTML/HTML the Form class has a method hidden_tag which renders any hidden fields, including the CSRF field, inside a hidden DIV tag:

<form method="POST" action=".">
    {{ form.hidden_tag() }}

Using the ‘safe’ filter

The safe filter used to be required with WTForms in Jinja2 templates, otherwise your markup would be escaped. For example:

{{ form.name|safe }}

However widgets in the latest version of WTForms return a HTML safe string so you shouldn’t need to use safe.

Ensure you are running the latest stable version of WTForms so that you don’t need to use this filter everywhere.

File uploads

Instances of the field type FileField automatically draw data from flask.request.files if the form is posted.

The data attribute will be an instance of Werkzeug FileStorage.

For example:

from werkzeug import secure_filename

class PhotoForm(Form):

    photo = FileField("Your photo")

@app.route("/upload/", methods=("GET", "POST"))
def upload():
    form = PhotoForm()
    if form.validate_on_submit():
        filename = secure_filename(form.photo.data.filename)
        filename = None

    return render_template("upload.html",

It’s recommended you use werkzeug.secure_filename on any uploaded files as shown in the example to prevent malicious attempts to access your filesystem.

Remember to set the enctype of your HTML form to multipart/form-data to enable file uploads:

<form action="." method="POST" enctype="multipart/form-data">

Note: as of version 0.4 all FileField instances have access to the corresponding FileStorage object in request.files, including those embedded in FieldList instances.

Validating file uploads

Flask-WTF supports validation through the Flask Uploads extension. If you use this (highly recommended) extension you can use it to add validation to your file fields. For example:

from flask.ext.uploads import UploadSet, IMAGES
from flask.ext.wtf import Form, FileField, file_allowed, \

images = UploadSet("images", IMAGES)

class UploadForm(Form):

    upload = FileField("Upload your image",
                                   file_allowed(images, "Images only!")])

In the above example, only image files (JPEGs, PNGs etc) can be uploaded. The file_required validator, which does not require Flask-Uploads, will raise a validation error if the field does not contain a FileStorage object.

HTML5 widgets

Flask-WTF supports a number of HTML5 widgets. Of course, these widgets must be supported by your target browser(s) in order to be properly used.

HTML5-specific widgets are available under the flask.ext.wtf.html5 package:

from flask.ext.wtf.html5 import URLField

class LinkForm():

    url = URLField(validators=[url()])

See the API for more details.


HTML5 widgets have been integrated into WTForms as of 1.0.4dev, and therefore should be in the next release candidate. When this happens, HTML5 widgets will be removed from Flask-WTF in the effort to clean up the namespace.


Flask-WTF also provides Recaptcha support through a RecaptchaField:

from flask.ext.wtf import Form, RecaptchaField
from wtforms import TextField

class SignupForm(Form):
    username = TextField("Username")
    recaptcha = RecaptchaField()

This field handles all the nitty-gritty details of Recaptcha validation and output. The following settings are required in order to use Recaptcha:

  • RECAPTCHA_USE_SSL : default False

RECAPTCHA_OPTIONS is an optional dict of configuration options. The public and private keys are required in order to authenticate your request with Recaptcha - see documentation for details on how to obtain your keys.

Under test conditions (i.e. Flask app testing is True) Recaptcha will always validate - this is because it’s hard to know the correct Recaptcha image when running tests. Bear in mind that you need to pass the data to recaptcha_challenge_field and recaptcha_response_field, not recaptcha:

response = self.client.post("/someurl/", data={
                            'recaptcha_challenge_field' : 'test',
                            'recaptcha_response_field' : 'test'})

If flask.ext-babel is installed then Recaptcha message strings can be localized.


Prior to version 0.8.3, the Recaptcha widget returned a regular Python string, not a Markup string, which required the user to wrap the output of the widget within a Markup string. This issue has now been fixed. If you have already wrapped things in your code, this shouldn’t be a problem, as rewrapping a Markup object does not have any detrimental effects.

API changes

The Form class provided by Flask-WTF is the same as for WTForms, but with a couple of changes. Aside from CSRF validation, a convenience method validate_on_submit is added:

from flask import Flask, request, flash, redirect, url_for, \

from flask.ext.wtf import Form
from wtforms import TextField

app = Flask(__name__)

class MyForm(Form):
    name = TextField("Name")

@app.route("/submit/", methods=("GET", "POST"))
def submit():

    form = MyForm()
    if form.validate_on_submit():
        return redirect(url_for("index"))
    return render_template("index.html", form=form)

Note the difference from a pure WTForms solution:

from flask import Flask, request, flash, redirect, url_for, \

from flask.ext.wtf import Form, TextField

app = Flask(__name__)

class MyForm(Form):
    name = TextField("Name")

@app.route("/submit/", methods=("GET", "POST"))
def submit():

    form = MyForm(request.form)
    if request.method == "POST" and form.validate():
        return redirect(url_for("index"))
    return render_template("index.html", form=form)

validate_on_submit will automatically check if the request method is PUT or POST.

You don’t need to pass request.form into your form instance, as the Form automatically populates from request.form unless alternate data is specified. Pass in None to suppress this. Other arguments are as with wtforms.Form.


class flask.ext.wtf.Form(formdata=<class flask_wtf.form._Auto at 0x35bbdb8>, obj=None, prefix='', csrf_context=None, secret_key=None, csrf_enabled=None, *args, **kwargs)

Flask-specific subclass of WTForms SessionSecureForm class.

Flask-specific behaviors: If formdata is not specified, this will use flask.request.form. Explicitly pass formdata = None to prevent this.

csrf_context - a session or dict-like object to use when making CSRF tokens.
Default: flask.session.
secret_key - a secret key for building CSRF tokens. If this isn’t specified,
the form will take the first of these that is defined:
  • the SECRET_KEY attribute on this class
  • the value of flask.current_app.config[“SECRET_KEY”]
  • the session’s secret_key

If none of these are set, raise an exception.

csrf_enabled - whether to use CSRF protection. If False, all csrf behavior
is suppressed. Default: check app.config for CSRF_ENABLED, else True

Wraps hidden fields in a hidden DIV tag, in order to keep XHTML compliance.

New in version 0.3.

Parameters:fields – list of hidden field names. If not provided will render all hidden fields, including the CSRF field.

Checks if form has been submitted. The default case is if the HTTP method is PUT or POST.


Checks if form has been submitted and if so runs validate. This is a shortcut, equivalent to form.is_submitted() and form.validate()

class flask.ext.wtf.RecaptchaField(label='', validators=None, **kwargs)
class flask.ext.wtf.Recaptcha(message=u'Invalid word. Please try again.')

Validates a ReCaptcha.

class flask.ext.wtf.RecaptchaWidget
class flask.ext.wtf.file.FileField(label=None, validators=None, filters=(), description=u'', id=None, default=None, widget=None, _form=None, _name=None, _prefix=u'', _translations=None)

Werkzeug-aware subclass of wtforms.FileField

Provides a has_file() method to check if its data is a FileStorage instance with an actual file.

Deprecated :synonym for data

Return True iff self.data is a FileStorage with file data

class flask.ext.wtf.file.FileAllowed(upload_set, message=None)

Validates that the uploaded file is allowed by the given Flask-Uploads UploadSet.

upload_set : instance of flask.ext.uploads.UploadSet

message : error message

You can also use the synonym file_allowed.

class flask.ext.wtf.file.FileRequired(message=None)

Validates that field has a file.

message : error message

You can also use the synonym file_required.

class flask.ext.wtf.html5.SearchInput(input_type=None)

Creates <input type=search> widget

class flask.ext.wtf.html5.SearchField(label=None, validators=None, filters=(), description=u'', id=None, default=None, widget=None, _form=None, _name=None, _prefix=u'', _translations=None)

TextField using SearchInput by default

class flask.ext.wtf.html5.URLInput(input_type=None)

Creates <input type=url> widget

class flask.ext.wtf.html5.URLField(label=None, validators=None, filters=(), description=u'', id=None, default=None, widget=None, _form=None, _name=None, _prefix=u'', _translations=None)

TextField using URLInput by default

class flask.ext.wtf.html5.EmailInput(input_type=None)

Creates <input type=email> widget

class flask.ext.wtf.html5.EmailField(label=None, validators=None, filters=(), description=u'', id=None, default=None, widget=None, _form=None, _name=None, _prefix=u'', _translations=None)

TextField using EmailInput by default

class flask.ext.wtf.html5.TelInput(input_type=None)

Creates <input type=tel> widget

class flask.ext.wtf.html5.TelField(label=None, validators=None, filters=(), description=u'', id=None, default=None, widget=None, _form=None, _name=None, _prefix=u'', _translations=None)

TextField using TelInput by default

class flask.ext.wtf.html5.NumberInput(input_type=None)

Creates <input type=number> widget

class flask.ext.wtf.html5.IntegerField(label=None, validators=None, **kwargs)

IntegerField using NumberInput by default

class flask.ext.wtf.html5.DecimalField(label=None, validators=None, places=2, rounding=None, **kwargs)

DecimalField using NumberInput by default

class flask.ext.wtf.html5.RangeInput(input_type=None)

Creates <input type=range> widget

class flask.ext.wtf.html5.IntegerRangeField(label=None, validators=None, **kwargs)

IntegerField using RangeInput by default

class flask.ext.wtf.html5.DecimalRangeField(label=None, validators=None, places=2, rounding=None, **kwargs)

DecimalField using RangeInput by default