Flask Basics | Get Started with the Python Web Framework
이 글의 핵심
Learn Flask from scratch: routes, Jinja2 templates, forms, JSON APIs, and sessions. Hands-on examples with @app.route, render_template, and jsonify for Python web backends.
Introduction
Flask is a minimal, extensible micro web framework. Unlike batteries-included stacks, it ships a small core—routing, request handling, Jinja2 templating, and development tooling—and you add extensions for databases, forms, and auth. That design keeps startup friction low, but you must make deliberate architectural choices as the app grows.
Production tip: treat debug=True as development-only. It enables an interactive debugger that must never be exposed on the public internet.
1. Core concepts and architecture
WSGI: Flask sits on the Web Server Gateway Interface. A WSGI server (Gunicorn, uWSGI) runs your app callable; the development server is not meant for load.
Application and request contexts: current_app and g (request-scoped) are populated per request. Use g for data you load once in a before_request handler (for example, the current user id after auth).
Factories: A common pattern is an create_app(config_name) function that returns Flask(__name__), applies configuration, registers blueprints, and returns the app. This enables different configs for test, development, and production and simplifies pytest fixtures.
# app/__init__.py
from flask import Flask
def create_app():
app = Flask(__name__)
app.config.from_object("config.Config")
from .main import main_bp
app.register_blueprint(main_bp)
return app
Typical project layout in production: app/ package, config.py, wsgi.py (imports app = create_app()), tests/, and optional migrations/.
2. Getting started
Installation
pip install flask
# Common stack for tutorials below:
pip install flask-wtf SQLAlchemy flask-sqlalchemy
Hello World
# app.py
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello, Flask!"
if __name__ == "__main__":
app.run(debug=True)
python app.py
# http://127.0.0.1:5000
3. Routing and URL building
Converters: string, int, float, path (keeps slashes), uuid. Custom converters are registerable for domain-specific slugs.
URL building with url_for: always generate links by endpoint name, not by hard-coded paths. It respects URL prefixes from blueprints and works behind reverse proxies when SERVER_NAME or proxy headers are configured.
from flask import Flask, url_for, redirect, abort
app = Flask(__name__)
@app.route("/user/<username>")
def profile(username):
return f"Profile: {username}"
@app.route("/post/<int:post_id>")
def post(post_id):
return f"Post {post_id}"
with app.test_request_context():
assert url_for("profile", username="alice") == "/user/alice"
assert url_for("post", post_id=42) == "/post/42"
HTTP methods and APIs:
from flask import request, jsonify
@app.route("/items", methods=["GET", "POST"])
def items():
if request.method == "POST":
body = request.get_json(silent=True) or {}
return jsonify(created=body), 201
return jsonify(items=[])
Static files: url_for("static", filename="css/main.css") maps to static/css/main.css by default.
Tip: for REST APIs, group related routes and return consistent error shapes; use 404, 400, and 409 with JSON bodies to help API clients.
4. Jinja2 templates in depth
Flask auto-configures Jinja2. Escaping is on by default for {{ var }}—use | safe only for trusted HTML.
Inheritance and blocks:
<!-- templates/base.html -->
<!DOCTYPE html>
<html>
<head><title>{% block title %}App{% endblock %}</title></head>
<body>{% block body %}{% endblock %}</body>
</html>
<!-- templates/home.html -->
{% extends "base.html" %}
{% block title %}Home{% endblock %}
{% block body %}
<h1>Welcome</h1>
{% include "_flash.html" %}
{% endblock %}
Filters and tests: {{ price|round(2) }}, {% if users is defined and users|length > 0 %}.
Macros (reusable UI fragments):
{% macro field(name, label) %}
<label for="{{ name }}">{{ label }}</label>
<input id="{{ name }}" name="{{ name }}">
{% endmacro %}
Context processors inject global template variables (e.g., current year, feature flags) without passing them in every render_template call.
@app.context_processor
def inject_config():
return {"app_name": "MyApp", "now_year": 2026}
Cache busting for assets: use url_for with a version query or integrate a bundler; for simple sites, a config ASSET_VERSION in the context processor works.
5. Forms and validation
Raw forms work with request.form and request.args, but Flask-WTF (WTForms + CSRF) is the standard for HTML forms.
# forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, Email, Length
class LoginForm(FlaskForm):
email = StringField("Email", validators=[DataRequired(), Email()])
password = PasswordField("Password", validators=[DataRequired(), Length(min=8)])
submit = SubmitField("Sign in")
# views.py
from flask import render_template, flash, redirect, url_for
from .forms import LoginForm
@app.route("/login", methods=["GET", "POST"])
def login():
form = LoginForm()
if form.validate_on_submit():
# authenticate user…
flash("Logged in.", "success")
return redirect(url_for("dashboard"))
return render_template("login.html", form=form)
In login.html, render {{ form.hidden_tag() }} for the CSRF token, then loop fields or render field by field. Never disable CSRF for public POST routes without a replacement strategy.
API tip: for JSON-only endpoints, use Pydantic or a schema library to validate request.get_json(); CSRF is often replaced by token-based auth (Bearer JWT) rather than form tokens.
6. Database integration with SQLAlchemy
Flask-SQLAlchemy wires sessions to the app context so you rarely leak connections. Define models, then db.create_all() in development (use Alembic for migrations in production).
# models.py
from datetime import datetime
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(120), unique=True, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
# in create_app
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///app.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
db.init_app(app)
with app.app_context():
db.create_all()
Session pattern in a view:
@app.post("/register")
def register():
u = User(email=request.form["email"])
db.session.add(u)
try:
db.session.commit()
except Exception:
db.session.rollback()
return jsonify(error="email taken"), 409
return jsonify(id=u.id), 201
Practical tip: use a connection string from environment variables; never commit secrets. For PostgreSQL in production, pool settings and pool_pre_ping=True (SQLAlchemy 1.4+) help with stale connections.
7. Authentication and session management
Flask’s session is signed, not encrypted—do not store secrets or PII. Store a user id and load the user from the database per request, or use Flask-Login for session management helpers.
Minimal session login:
import secrets
from flask import session, redirect, request
from werkzeug.security import check_password_hash, generate_password_hash
app.secret_key = secrets.token_hex(32) # load from env in production
# store: users[id] = { "email": ..., "password_hash": ... }
@app.post("/login")
def login():
user = get_user_by_email(request.form.get("email"))
if not user or not check_password_hash(user["password_hash"], request.form.get("password", "")):
return redirect(url_for("login_form"))
session["uid"] = user["id"]
return redirect(url_for("dashboard"))
Security checklist: hash passwords with scrypt or bcrypt (passlib or werkzeug), set SESSION_COOKIE_HTTPONLY and SESSION_COOKIE_SECURE behind HTTPS, rotate SECRET_KEY with a key rotation plan, and add rate limiting (Flask-Limiter) on auth routes.
8. Blueprints for modularity
Blueprints let you split routes, templates, and static files by feature (auth, api, admin).
# app/auth/routes.py
from flask import Blueprint, render_string
auth_bp = Blueprint("auth", __name__, url_prefix="/auth")
@auth_bp.get("/me")
def me():
return render_string("ok")
# app/__init__.py
from .auth.routes import auth_bp
app.register_blueprint(auth_bp)
Tip: name endpoints blueprint_name.function_name when calling url_for("auth.me") if the blueprint is registered with a name. Keep circular imports in check by importing views inside register_blueprints or using smaller modules.
9. Deployment: Gunicorn and Docker
Gunicorn is a common production WSGI server:
gunicorn -w 4 -b 0.0.0.0:8000 "app:create_app()"
Workers: a rule of thumb is (2 × CPU) + 1 for I/O-bound apps; profile your workload. Put nginx or another reverse proxy in front for TLS, HTTP/2, and static file serving.
Docker sketch:
FROM python:3.12-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["gunicorn", "-w", "2", "-b", "0.0.0.0:8000", "wsgi:app"]
Health checks: expose /healthz that hits the database and returns 200 only when dependencies are reachable. Tip: set X-Forwarded-* handling if you terminate TLS at the load balancer (ProxyFix in Werkzeug).
10. Flask vs Django vs FastAPI
| Concern | Flask | Django | FastAPI |
|---|---|---|---|
| Defaults | Minimal core; you choose extensions | ORM, admin, auth patterns built-in | Async-first, automatic OpenAPI docs |
| Ecosystem | Huge extension catalog | “Batteries included” with opinionated structure | Pydantic-centric; great for APIs |
| HTML sites | Jinja2 + forms fits naturally | Server-rendered with Django templates | Often paired with a separate SPA or Jinja2 |
| Async | Sync-first; async experimental patterns | 4.1+ async views, sync still common | Native async/await for I/O |
| Best when | Small services, custom stacks, learning WSGI | CRUD apps, admin-heavy projects | High-throughput JSON APIs, typing-first design |
Rule of thumb: pick Django when you want an integrated ORM, migrations, and admin; FastAPI when the product is mostly JSON with strict schemas and performance matters; Flask when you want a thin layer and full control over composition.
11. JSON API quick reference
from flask import jsonify, request
USERS = [{"id": 1, "name": "Alice"}]
@app.get("/api/users")
def get_users():
return jsonify(USERS)
@app.get("/api/users/<int:user_id>")
def get_user(user_id):
u = next((x for x in USERS if x["id"] == user_id), None)
if not u:
return jsonify(error="not found"), 404
return jsonify(u)
For larger APIs, consider Flask-RESTX, flask-smorest, or switching the API layer to FastAPI while keeping Flask for server-rendered pages.
12. Sessions and cookies (recap)
from flask import session, redirect, request, url_for
app.config["SECRET_KEY"] = "change-me-in-env"
@app.route("/login", methods=["POST"])
def login():
# validate credentials, then:
session["username"] = request.form["username"]
return redirect(url_for("dashboard"))
@app.route("/dashboard")
def dashboard():
if "username" not in session:
return redirect(url_for("login_page"))
return f"Welcome, {session['username']}!"
@app.route("/logout")
def logout():
session.pop("username", None)
return redirect(url_for("index"))
13. Practical example: in-memory blog API
from datetime import datetime
from flask import Flask, jsonify, request
app = Flask(__name__)
POSTS = []
@app.get("/api/posts")
def list_posts():
return jsonify(POSTS)
@app.post("/api/posts")
def create_post():
data = request.get_json() or {}
if "title" not in data:
return jsonify(error="title required"), 400
post = {
"id": len(POSTS) + 1,
"title": data["title"],
"content": data.get("content", ""),
"created_at": datetime.utcnow().isoformat() + "Z",
}
POSTS.append(post)
return jsonify(post), 201
if __name__ == "__main__":
app.run(debug=True)
Summary
- Flask is a WSGI microframework—routing, Jinja2, and extensions for everything else.
- Routing and
url_forkeep URLs maintainable; use converters and method lists deliberately. - Jinja2—inheritance, macros, and context processors keep templates DRY.
- Flask-WTF adds CSRF and validation to HTML forms.
- Flask-SQLAlchemy wires sessions to the app lifecycle; use migrations in production.
- Auth—hash passwords, protect sessions, and avoid storing secrets in client-visible cookies.
- Blueprints scale structure by feature.
- Deploy with Gunicorn + a reverse proxy; Docker standardizes runtimes.
- Compare with Django (full stack) and FastAPI (async APIs) by project needs.
Next steps
- [Django basics](/en/blog/python-series-12-django-basics/
- [REST API design](/en/blog/python-series-13-rest-api/
Related posts
- [Python environment setup | Install on Windows & Mac](/en/blog/python-series-01-environment-setup/
- [Django Basics](/en/blog/python-series-12-django-basics/
- [Python REST APIs | Build API Servers with Flask and Django](/en/blog/python-series-13-rest-api/
- [Express.js Complete Guide: Node.js Web Framework and REST](/en/blog/nodejs-series-04-express/
- [Express REST API Tutorial for Node.js | Routing](/en/blog/nodejs-express-rest-api-tutorial/
Keywords
Python, Flask, Web Development, Backend, REST API, Framework, Jinja2, SQLAlchemy, Gunicorn, Docker