Flask

軽量で柔軟性の高いマイクロWebフレームワーク。シンプルながら拡張可能な設計で、プロトタイプから本格運用まで対応。

PythonフレームワークBackendWebマイクロフレームワークWerkzeugJinja2

GitHub概要

pallets/flask

The Python micro framework for building web applications.

スター70,157
ウォッチ2,106
フォーク16,518
作成日:2010年4月6日
言語:Python
ライセンス:BSD 3-Clause "New" or "Revised" License

トピックス

flaskjinjapalletspythonweb-frameworkwerkzeugwsgi

スター履歴

pallets/flask Star History
データ取得日時: 2025/8/13 01:43

フレームワーク

Flask

概要

Flaskは、Pythonで書かれた軽量で柔軟性の高いマイクロWebフレームワークです。シンプルで拡張可能な設計により、必要な機能だけを選択して使用できる高い自由度が特徴です。

詳細

Flaskは2010年にArmin Ronacherによって開発されたPythonのマイクロWebフレームワークで、「マイクロ」は機能が制限されているのではなく、フレームワークのコア部分をシンプルに保ち、必要に応じて拡張できる設計を意味しています。WerkzeugとJinja2をベースに構築され、開発者が必要な機能だけを選択して使用できる高い自由度が特徴です。Werkzeug(WSGIユーティリティライブラリ)によるリクエスト・レスポンス処理、Jinja2による強力なテンプレートエンジン、ブループリントによるアプリケーションのモジュール化機能を提供します。Flask-SQLAlchemy(ORM)、Flask-Login(認証)、Flask-Mail(メール送信)等の豊富な拡張エコシステムにより、小規模なプロトタイプから中規模のWebアプリケーションまで対応可能です。Pinterest、Netflix、LinkedIn、Airbnb等の企業でマイクロサービスやAPI開発で採用されており、Pythonの柔軟性を活かした迅速な開発とカスタマイズ性を重視するプロジェクトに最適です。

メリット・デメリット

メリット

  • シンプルで軽量: 最小限のコア機能で学習コストが低い
  • 高い柔軟性: 必要な機能のみを選択して追加可能
  • 豊富な拡張エコシステム: Flask-SQLAlchemy、Flask-Login等の充実した拡張
  • 迅速なプロトタイピング: 小規模なアプリケーション開発が効率的
  • カスタマイズ性: 他のライブラリとの組み合わせが容易
  • 強力なテンプレート: Jinja2による高機能なテンプレートエンジン
  • 豊富なドキュメント: 詳細で分かりやすい公式ドキュメント

デメリット

  • 標準機能の少なさ: ORM、認証、管理画面が標準では非搭載
  • 拡張の選択負担: 多数の選択肢から適切な拡張を選ぶ必要がある
  • 大規模開発の限界: エンタープライズレベルの開発では制約がある
  • 設定の複雑化: 拡張機能の組み合わせで設定が煩雑になる場合がある
  • セキュリティ: セキュリティ対策が開発者任せになりがち

主要リンク

書き方の例

Hello World

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello_world():
    return "<p>Hello, World!</p>"

@app.route("/user/<name>")
def user(name):
    return f"<p>Hello, {name}!</p>"

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

テンプレートとルーティング

from flask import Flask, render_template, request

app = Flask(__name__)

@app.route("/")
def index():
    return render_template("index.html")

@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        username = request.form["username"]
        password = request.form["password"]
        # ログイン処理
        return f"Welcome, {username}!"
    return render_template("login.html")

@app.route("/posts/<int:post_id>")
def show_post(post_id):
    # データベースから投稿を取得
    post = {"id": post_id, "title": "Sample Post", "content": "This is a sample post."}
    return render_template("post.html", post=post)

ブループリントによるモジュール化

# auth.py
from flask import Blueprint, render_template, request, session, redirect, url_for
from werkzeug.security import check_password_hash, generate_password_hash

auth = Blueprint('auth', __name__, url_prefix='/auth')

@auth.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        # ユーザー登録処理
        return redirect(url_for('auth.login'))
    return render_template('auth/register.html')

@auth.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        # ログイン処理
        session['user_id'] = username
        return redirect(url_for('index'))
    return render_template('auth/login.html')

# app.py
from flask import Flask
from auth import auth

app = Flask(__name__)
app.register_blueprint(auth)

データベース連携(Flask-SQLAlchemy)

from flask import Flask, render_template, request, redirect, url_for
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///blog.db'
app.config['SECRET_KEY'] = 'your-secret-key'
db = SQLAlchemy(app)

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), nullable=False)
    content = db.Column(db.Text, nullable=False)
    date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)

@app.route("/")
def home():
    posts = Post.query.order_by(Post.date_posted.desc()).all()
    return render_template("home.html", posts=posts)

@app.route("/create", methods=['GET', 'POST'])
def create_post():
    if request.method == 'POST':
        post = Post(
            title=request.form['title'],
            content=request.form['content']
        )
        db.session.add(post)
        db.session.commit()
        return redirect(url_for('home'))
    return render_template("create_post.html")

if __name__ == "__main__":
    with app.app_context():
        db.create_all()
    app.run(debug=True)

API開発とJSON レスポンス

from flask import Flask, jsonify, request
from functools import wraps

app = Flask(__name__)

# ダミーデータ
books = [
    {"id": 1, "title": "Python入門", "author": "山田太郎"},
    {"id": 2, "title": "Flask実践", "author": "佐藤花子"}
]

def authenticate(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        token = request.headers.get('Authorization')
        if not token or token != 'Bearer your-api-token':
            return jsonify({'error': 'Unauthorized'}), 401
        return f(*args, **kwargs)
    return decorated_function

@app.route("/api/books", methods=["GET"])
@authenticate
def get_books():
    return jsonify({"books": books})

@app.route("/api/books", methods=["POST"])
@authenticate
def add_book():
    data = request.get_json()
    new_book = {
        "id": len(books) + 1,
        "title": data.get("title"),
        "author": data.get("author")
    }
    books.append(new_book)
    return jsonify({"book": new_book, "message": "Book created successfully"}), 201

@app.route("/api/books/<int:book_id>", methods=["GET"])
@authenticate
def get_book(book_id):
    book = next((book for book in books if book["id"] == book_id), None)
    if book:
        return jsonify({"book": book})
    return jsonify({"error": "Book not found"}), 404

フォーム処理とバリデーション

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

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

class ContactForm(FlaskForm):
    name = StringField('名前', validators=[DataRequired(), Length(min=2, max=50)])
    email = StringField('メールアドレス', validators=[DataRequired(), Email()])
    message = TextAreaField('メッセージ', validators=[DataRequired(), Length(min=10)])
    submit = SubmitField('送信')

class RegistrationForm(FlaskForm):
    username = StringField('ユーザー名', validators=[DataRequired(), Length(min=4, max=25)])
    email = StringField('メールアドレス', validators=[DataRequired(), Email()])
    password = PasswordField('パスワード', validators=[DataRequired(), Length(min=8)])
    confirm_password = PasswordField('パスワード確認', 
                                   validators=[DataRequired(), EqualTo('password')])
    submit = SubmitField('登録')

@app.route("/contact", methods=['GET', 'POST'])
def contact():
    form = ContactForm()
    if form.validate_on_submit():
        name = form.name.data
        email = form.email.data
        message = form.message.data
        
        # メール送信やデータベース保存処理
        flash(f'{name}様、お問い合わせありがとうございます!', 'success')
        return redirect(url_for('contact'))
    
    return render_template("contact.html", form=form)

@app.route("/register", methods=['GET', 'POST'])
def register():
    form = RegistrationForm()
    if form.validate_on_submit():
        # ユーザー登録処理
        flash('登録が完了しました!', 'success')
        return redirect(url_for('login'))
    return render_template("register.html", form=form)

認証とセッション管理

from flask import Flask, render_template, request, session, redirect, url_for, flash
from werkzeug.security import generate_password_hash, check_password_hash
from functools import wraps

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

# ダミーユーザーデータベース
users = {
    "admin": {
        "password": generate_password_hash("password123"),
        "email": "[email protected]"
    }
}

def login_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if 'username' not in session:
            flash('ログインが必要です。', 'error')
            return redirect(url_for('login'))
        return f(*args, **kwargs)
    return decorated_function

@app.route("/login", methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        
        if username in users and check_password_hash(users[username]['password'], password):
            session['username'] = username
            flash('ログインに成功しました!', 'success')
            return redirect(url_for('dashboard'))
        else:
            flash('ユーザー名またはパスワードが間違っています。', 'error')
    
    return render_template("login.html")

@app.route("/logout")
def logout():
    session.pop('username', None)
    flash('ログアウトしました。', 'info')
    return redirect(url_for('login'))

@app.route("/dashboard")
@login_required
def dashboard():
    username = session['username']
    return render_template("dashboard.html", username=username)

@app.route("/profile")
@login_required
def profile():
    username = session['username']
    user_data = users.get(username, {})
    return render_template("profile.html", username=username, user_data=user_data)

エラーハンドリング

from flask import Flask, render_template, jsonify, request
import logging
from datetime import datetime

app = Flask(__name__)

# ログ設定
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

@app.errorhandler(404)
def not_found_error(error):
    return render_template('errors/404.html'), 404

@app.errorhandler(500)
def internal_error(error):
    logger.error(f'Server Error: {error}')
    return render_template('errors/500.html'), 500

@app.errorhandler(403)
def forbidden_error(error):
    return render_template('errors/403.html'), 403

# APIエラーハンドリング
@app.errorhandler(400)
def bad_request(error):
    if request.path.startswith('/api/'):
        return jsonify({'error': 'Bad Request', 'message': str(error)}), 400
    return render_template('errors/400.html'), 400

# カスタム例外クラス
class ValidationError(Exception):
    def __init__(self, message, status_code=400):
        super().__init__()
        self.message = message
        self.status_code = status_code

@app.errorhandler(ValidationError)
def handle_validation_error(error):
    if request.path.startswith('/api/'):
        return jsonify({'error': 'Validation Error', 'message': error.message}), error.status_code
    flash(f'エラー: {error.message}', 'error')
    return redirect(request.referrer or url_for('index'))

# ログ機能付きAPIエンドポイント
@app.route("/api/data/<int:data_id>")
def get_data(data_id):
    try:
        # データ取得処理
        if data_id <= 0:
            raise ValidationError("Invalid data ID")
        
        data = {"id": data_id, "name": f"Data {data_id}", "timestamp": datetime.now().isoformat()}
        logger.info(f'Data retrieved: {data_id}')
        return jsonify(data)
    
    except Exception as e:
        logger.error(f'Error retrieving data {data_id}: {str(e)}')
        raise

テスト

# test_app.py
import pytest
from flask import url_for
from app import app, users

@pytest.fixture
def client():
    app.config['TESTING'] = True
    app.config['WTF_CSRF_ENABLED'] = False
    with app.test_client() as client:
        with app.app_context():
            yield client

def test_home_page(client):
    response = client.get('/')
    assert response.status_code == 200
    assert b'Hello, World!' in response.data

def test_login_page(client):
    response = client.get('/login')
    assert response.status_code == 200

def test_login_success(client):
    response = client.post('/login', data={
        'username': 'admin',
        'password': 'password123'
    }, follow_redirects=True)
    assert response.status_code == 200
    assert b'dashboard' in response.data

def test_login_failure(client):
    response = client.post('/login', data={
        'username': 'admin',
        'password': 'wrongpassword'
    })
    assert response.status_code == 200
    assert b'間違っています' in response.data

def test_protected_route_without_login(client):
    response = client.get('/dashboard')
    assert response.status_code == 302  # リダイレクト

def test_api_endpoint(client):
    response = client.get('/api/data/1')
    assert response.status_code == 200
    assert response.json['id'] == 1

def test_api_error_handling(client):
    response = client.get('/api/data/0')
    assert response.status_code == 400
    assert 'error' in response.json

# テスト実行コマンド
# pip install pytest
# pytest
# pytest -v --cov=app