【Python】Flask + WTFormsでバリデーション付きの入力フォームを作成する

>※この記事は、私が運営している技術ブログ（[hirahira.blog](hirahira.blog)）で公開していた内容を移植・加筆したものです。

FlaskとWTFormsを使ってシンプルなフォーム画面を作成する方法を解説します。

WTFormsとはFlaskでフォームを作成する際に使用されるライブラリです。

WTFormsを使うことにより、バリデーションやセキュリティ対策を簡単に行うことができます。

## 環境
- Windows 11
- Python 3.13.2
- Flask 3.1.0
- WTForms 3.2.1

## 準備
以下の2コマンドを実行して、FlaskとWTFormsをインストールしてください。
- `pip install flask`
- `pip install wtforms`

## 構成
以下の構成でフォルダとファイルを作成します。

├ app.py
└ templates/
　├ register.html
　├ registered.html
　└ formhelpers.html

 ## コード内容の解説
 ### app.py
メインとなる処理を記述しています。

`Registration`クラスで、各入力項目に対するバリデーション内容の定義を行っています。
 ```python:app.py
from flask import Flask, redirect, render_template, request, session, url_for
 
# Formクラス及び使用するフィールドをインポート
from wtforms import (
    Form, BooleanField, IntegerField, PasswordField, StringField,
    SubmitField, TextAreaField)
 
# 使用するvalidatorをインポート
from wtforms.validators import DataRequired, EqualTo, Length, NumberRange

app = Flask(__name__)

# セッションで使用するシークレットキーを設定。本来はランダムな文字列が望ましい
app.config['SECRET_KEY'] = 'secret_key'

# wtformsのFormクラスを継承。それぞれの入力項目に対するバリデーションを設定
class Registration(Form):
    name = StringField('名前：', validators=[DataRequired()])
    age = IntegerField('年齢：', validators=[NumberRange(0, 100, '年齢は0から100の間で入力してください')])
    password = PasswordField('パスワード：', validators=[
        Length(1, 10, 'パスワードは1文字以上10文字以内で入力してください'),
        EqualTo('re_password', 'パスワードが一致しません')
    ])
    re_password = PasswordField('パスワード再入力：')
    comment = TextAreaField('コメント：')
    accept = BooleanField('内容確認：', validators=[DataRequired(message='内容確認にチェックを入れてください')])
    submit = SubmitField('送信')

# POSTかつバリデーションエラーがない場合は、セッションに入力内容を格納してregistered.htmlを表示
@app.route('/', methods=['GET', 'POST'])
def index():
    form = Registration(request.form) 
    if request.method == 'POST' and form.validate():
        # 入力内容をセッションに保存
        session['name'] = form.name.data
        session['age'] = form.age.data
        session['comment'] = form.comment.data
        return redirect(url_for('registered'))
    return render_template('register.html', form=form)

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

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


 ### register.html
入力フォーム画面です。

下で紹介する`render_field`マクロをインポートして使っています。

`{{ form.csrf_token }}`は、CSRF（クロスサイトリクエストフォージェリ）対策用のトークンを生成するための記述です。
 ```html:register.html
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>Form</title>
  </head>
  <body>
    <!-- formhelpers.htmlで定義したrender_fieldマクロをインポート -->
    {% from "formhelpers.html" import render_field %}
    <form method="POST">
      {{ form.csrf_token }}
      {{ render_field(form.name) }}
      {{ render_field(form.age) }}
      {{ render_field(form.password) }}
      {{ render_field(form.re_password) }}
      {{ render_field(form.accept) }}
      {{ render_field(form.comment) }}
      {{ form.submit() }}
    </form>
  </body>
</html>
 ```

 ### formhelpers.html
フォーム画面を整形し、エラー内容を表示する処理を、jinja2のマクロ機能を使って定義しています。

`{{ field(**kwargs)|safe }}`のようにsafeを付けるとHTMLをエスケープせずに表示することができます。（WTForms側でエスケープしてくれる）

 ```html:formhelpers.html
<!-- よく使用する処理をマクロ（関数）として定義 -->
{% macro render_field(field) %}
<dt>{{ field.label }}</dt>
<dd>{{ field(**kwargs)|safe }}</dd>
{% if field.errors %}
<ul class="error">
  {% for error in field.errors %}
    <li>{{ error }}</li>
  {% endfor %}
</ul>
{% endif %}
{% endmacro %}
 ```


 ### registered.html
 登録内容を表示する画面です。

 ```html:registered.html
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>registerd</title>
  </head>
  <body>
    <h1>登録内容</h1>
    <ul>
      <li>名前：{{ session['name'] }}</li>
      <li>年齢：{{ session['age'] }}</li>
      <li>コメント：{{ session['comment'] }}</li>
    </ul>
  </body>
</html>
 ```


## 動作確認
`python app.py` を実行し、http://127.0.0.1:5000 にアクセスするとフォーム画面が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/586427/eb369d2c-4eab-47cf-b659-4c71b8b0d702.png)

不正な値を入力後に送信ボタンをクリックすると、エラーメッセージが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/586427/c981f05e-cad1-4a2f-8d82-6cc592c16907.png)

正しい情報を入力して送信ボタンを押すと`registerd.html`に遷移し、入力した内容が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/586427/76193e5b-23a3-4ce4-957d-227a7bf162c8.png)

## 参考記事
https://flask.palletsprojects.com/en/stable/patterns/wtforms/