Python Framework

Flask

Flask is the minimal Python micro-framework. You get routing, a request object and Jinja templates in a single file, then add exactly the extensions you need as the app grows.

beginner14 min readUpdated Sep 16, 2026
app.py
python
# app.py
from flask import Flask, jsonify

app = Flask(__name__)


@app.get("/health")
def health():
    return jsonify(status="ok")


if __name__ == "__main__":
    app.run(debug=True)
Released
2010
Language
Python
Style
Minimal, unopinionated
Core idea
One app object
Templates
Jinja2
Current line
3.x

Why it matters

Why Flask is a great first framework

One file to start

Import Flask, create an app, add a decorator and run. There is no project generator and no required folder layout to learn first.

You choose the pieces

The core is small on purpose. Database, auth, forms and migrations come from well-maintained extensions you opt into.

Plain Python throughout

Routes are functions, configuration is a dictionary, and the request is an object you can read. Nothing hides behind a large framework API.

The big picture

App, route, blueprint

One app object, decorators that map URLs to functions, and blueprints that group routes as the project grows.

The app object

Initialize

Flask(__name__) creates the application. It holds configuration, registers routes and is the object the WSGI server calls.

Routes

Match

The @app.route decorator binds a URL rule to a function. Converters capture typed values such as integers and paths.

Blueprints

Compose

A Blueprint groups routes, templates and static files into a module that can be registered on the app, often more than once.

At a glance

The Flask toolbox

Decorator routing

@app.get("/posts/<int:id>") binds a URL pattern to a view function.

Request and response

Read request.args, request.form and request.get_json(); return strings, dicts or Response objects.

Jinja2 templates

render_template fills HTML with data and escapes variables by default.

Blueprints

Split a large app into modules with prefixes and their own templates.

Extensions

SQLAlchemy, Login, Migrate and friends add the pieces the core leaves out.

Test client

app.test_client() makes requests in-process with no server running.

A short history

From an April Fools joke to a web staple

  1. 2010

    A joke becomes a framework

    Armin Ronacher releases Flask, built on Werkzeug and Jinja2, after an April Fools project gained real interest.

    10
  2. 2011

    The extension ecosystem

    Flask-SQLAlchemy and other extensions establish the "small core, opt-in pieces" model.

    11
  3. 2018

    Flask 1.0

    After years of 0.x releases, Flask commits to a stable public API.

    18
  4. 2021

    Flask 2.0

    Async views and short decorators like @app.get arrive, modernising the developer experience.

    21
  5. 2023

    Flask 3.0

    A leaner core removes long-deprecated APIs and tightens the request context.

    23
  6. Today

    Still the small default

    Flask remains the go-to choice for small services, prototypes and teaching web fundamentals.

    Today

The complete guide

Flask: Everything you need to know

What is Flask?

Flask is a minimal web framework for Python. It gives you an application object, a way to map URLs to functions, and a request and response model. Everything else — databases, authentication, forms, background jobs — is a decision you make later, usually by adding an extension.

That smallness is the whole point. Django has an opinion about how a project should be structured; Flask has almost none. For a small API, an internal tool or a first web project, that freedom is a feature. You can read the entire framework in an afternoon and understand every line of your own app.

Flask is built on two long-standing libraries: Werkzeug for the WSGI plumbing and Jinja2 for templates. Both are mature and well documented, and knowing that they are underneath explains most of Flask’s behaviour.

The smallest Flask app

A complete Flask application can fit in a few lines.

# app.py
from flask import Flask

app = Flask(__name__)


@app.get("/")
def index():
    return "Hello, world"


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

Flask(__name__) creates the application and tells it where to find templates and static files. The decorator binds a URL to the function below it. app.run() starts the development server. The if __name__ == "__main__" guard keeps the server from starting when the module is imported by a test or a WSGI server.

Routing with decorators

The @app.route decorator is the heart of Flask. It registers a URL rule and the function that answers it.

@app.route("/posts")
def list_posts():
    return "All posts"


@app.route("/posts/<int:post_id>")
def get_post(post_id):
    return f"Post {post_id}"

Rules can contain variable parts written in angle brackets. Flask matches the URL, converts the value and passes it to the function as a keyword argument. If no rule matches, Flask returns a 404 automatically.

URL variables and HTTP methods

Converters make variables type-safe before your code runs. The common ones are string (the default), int, float, path (which allows slashes) and uuid.

@app.get("/users/<username>")
def profile(username):
    return f"Profile for {username}"


@app.get("/files/<path:filepath>")
def serve_file(filepath):
    return f"File at {filepath}"

Flask 2.0 added shortcuts for each method. @app.get, @app.post, @app.put, @app.patch and @app.delete are clearer than passing a methods list, and a single function can answer several methods.

@app.route("/posts", methods=["GET", "POST"])
def posts():
    if request.method == "POST":
        return create_post()
    return list_posts()

The request object

Incoming data lives on the global request object, which Flask populates for the current request. It is a proxy, so importing it once at the top of the module is safe.

from flask import request

@app.get("/search")
def search():
    query = request.args.get("q", "")
    page = request.args.get("page", 1, type=int)
    return {"query": query, "page": page}


@app.post("/posts")
def create_post():
    payload = request.get_json()
    title = payload["title"]
    return {"title": title}, 201

request.args holds query-string values, request.form holds form posts, request.get_json() parses a JSON body, request.files holds uploads, and request.headers gives the raw headers. Because these are multi-value mappings, use .get() and provide defaults rather than indexing blindly.

Responses and JSON

A view can return a string, a dictionary (which Flask serialises to JSON), a tuple of (body, status) or (body, status, headers), or a full Response object.

from flask import jsonify, make_response

@app.post("/posts")
def create_post():
    post = {"id": 1, "title": "Hello"}
    return jsonify(post), 201


@app.get("/ping")
def ping():
    response = make_response({"pong": True})
    response.headers["Cache-Control"] = "no-store"
    return response

jsonify is the explicit way to return JSON: it serialises the data and sets the content type. Returning a dictionary works too, but jsonify is clearer about intent and handles more types.

Templates with Jinja2

render_template loads a file from the templates/ folder and renders it with the data you pass. Jinja2 escapes variables by default, which protects against XSS.

from flask import render_template

@app.get("/posts/<int:post_id>")
def get_post(post_id):
    post = {"id": post_id, "title": "Hello", "body": "First post"}
    return render_template("post.html", post=post)
<!-- templates/post.html -->
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>{{ post.title }}</title>
  </head>
  <body>
    <h1>{{ post.title }}</h1>
    <p>{{ post.body }}</p>
  </body>
</html>

Jinja supports {% extends %} for inheritance, {% include %} for fragments, {% for %} and {% if %} for logic, and filters such as {{ value|upper }}. Keep templates presentational; if the logic grows, move it into the view.

Static files

Flask serves anything in the static/ folder at /static/... by default. Reference it with url_for, which builds the URL from the endpoint name.

<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}" />

In production, let a reverse proxy or CDN serve static files instead of the Python process. It is faster and frees the app to handle requests.

Blueprints

A Blueprint is a collection of routes and templates that can be registered on an app. It is how a single-module Flask app grows into a project without becoming a mess.

# blog/views.py
from flask import Blueprint, jsonify

bp = Blueprint("blog", __name__, url_prefix="/blog")


@bp.get("/")
def index():
    return jsonify(posts=[])


@bp.get("/<int:post_id>")
def detail(post_id):
    return jsonify(id=post_id)
# app.py
from blog.views import bp as blog_bp

app.register_blueprint(blog_bp)

Blueprints carry their own URL prefix, templates and static files, and one blueprint can be registered multiple times with different prefixes. They also make testing and code review easier because each feature is self-contained.

The application factory

Once you use blueprints, the natural next step is a factory function that builds the app. This is the standard structure for anything larger than a demo.

# app.py
from flask import Flask

from blog.views import bp as blog_bp


def create_app(config_object="config.Config"):
    app = Flask(__name__)
    app.config.from_object(config_object)

    app.register_blueprint(blog_bp)

    return app
# wsgi.py
from app import create_app

app = create_app()

The factory makes tests trivial: build an app with a test configuration, run the client, and throw it away. It also avoids importing a half-configured global app, which is the root of most circular-import problems in Flask.

Extensions

Because the core is minimal, the ecosystem provides the rest. A few extensions cover most projects:

  • Flask-SQLAlchemy integrates SQLAlchemy’s ORM and manages the session per request.
  • Flask-Migrate wraps Alembic so schema changes are versioned.
  • Flask-Login handles user sessions and the current_user proxy.
  • Flask-WTF adds forms, CSRF protection and file uploads.
  • Flask-CORS controls cross-origin requests for APIs.
# extensions.py
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

db = SQLAlchemy()
migrate = Migrate()
# app.py
from extensions import db, migrate


def create_app():
    app = Flask(__name__)
    app.config.from_object("config.Config")

    db.init_app(app)
    migrate.init_app(app, db)

    return app

The init_app pattern lets an extension be created without an app and bound later, which keeps the factory clean and the extension importable by models.

Configuration

Configuration is a plain mapping on app.config. Load it from a class, a file or the environment, and keep secrets out of source control.

# config.py
import os


class Config:
    SECRET_KEY = os.environ["SECRET_KEY"]
    SQLALCHEMY_DATABASE_URI = os.environ["DATABASE_URL"]
    JSON_SORT_KEYS = False
app.config.from_object("config.Config")
app.config.from_prefixed_env()

from_prefixed_env() reads FLASK_* environment variables, which is convenient in containers. Use different classes or files per environment, and never commit the real SECRET_KEY — it signs sessions and CSRF tokens.

Error handling

Register error handlers to return consistent responses for the whole app. Handlers can be global or scoped to a blueprint.

from flask import jsonify
from werkzeug.exceptions import HTTPException


@app.errorhandler(404)
def not_found(error):
    return jsonify(error="not_found"), 404


@app.errorhandler(Exception)
def unhandled(error):
    if isinstance(error, HTTPException):
        return error
    app.logger.exception(error)
    return jsonify(error="internal_error"), 500

The handler for HTTPException is important: Flask’s built-in errors are exceptions too, and a blanket Exception handler would otherwise turn a 404 into a 500.

Testing

Flask’s test client makes requests in-process, with no server and no network.

import pytest

from app import create_app


@pytest.fixture
def client():
    app = create_app("config.TestConfig")
    with app.test_client() as client:
        yield client


def test_health(client):
    response = client.get("/health")
    assert response.status_code == 200
    assert response.get_json() == {"status": "ok"}


def test_missing_post(client):
    response = client.get("/posts/999")
    assert response.status_code == 404

The factory and test_client() work together: each test gets a fresh app, so state cannot leak between tests. Override configuration for tests and use an in-memory database when you add one.

WSGI and deployment

Flask is a WSGI application, which means any WSGI server can run it. The development server is for development only.

pip install gunicorn
gunicorn --workers 4 --bind 0.0.0.0:8000 "app:create_app()"

In production, run Gunicorn or uWSGI behind Nginx, terminate TLS at the proxy, and serve static files from the proxy or a CDN. Set DEBUG=False, configure logging, and read secrets from the environment. Containerising the app makes the deployment portable across hosts.

Best practices

  • Start with a single module, then move to a factory and blueprints as it grows.
  • Use url_for to build URLs instead of hardcoding paths.
  • Keep configuration in classes or environment variables, never in code.
  • Use jsonify and explicit status codes for API responses.
  • Validate every piece of input before it reaches your logic.
  • Register error handlers so failures return a consistent shape.
  • Use the test client and a fresh app per test.
  • Never run the development server in production.

Common mistakes

  • Leaving debug=True on in production, which exposes an interactive debugger.
  • Importing a global app everywhere and creating circular imports.
  • Reading request.json without handling malformed or missing bodies.
  • Hardcoding URLs in templates instead of using url_for.
  • Building a monolithic app.py with hundreds of routes.
  • Forgetting CSRF protection when using forms.
  • Storing secrets in config.py and committing it.
  • Assuming Flask has an ORM or auth built in, then reinventing both badly.

Where to go next

Flask is the fastest way to learn what a web framework actually does, because so little is hidden. When you want an ORM, migrations, auth and an admin without assembling them, read the Django guide. When you want automatic validation and API docs from type hints, move to FastAPI. And to design the endpoints you just built, the REST APIs guide is the natural companion. If you add a database, the backend roadmap covers SQL, the language underneath the ORM.

Returning JSON

jsonify serialises Python data, sets the application/json content type and handles status codes. json.dumps leaves you to build the whole response yourself.

Prefer
from flask import jsonify

@app.get("/posts/<int:id>")
def get_post(id):
    return jsonify(id=id, title="Hello")
Avoid
import json
from flask import Response

@app.get("/posts/<int:id>")
def get_post(id):
    body = json.dumps({"id": id, "title": "Hello"})
    return Response(body, mimetype="application/json")

Creating the app

A factory function lets tests build a fresh app with test configuration, while a module-level app is created once when the module is imported and is awkward to reconfigure.

Prefer
def create_app(config=None):
    app = Flask(__name__)
    app.config.from_object(config or "config.Config")
    register_blueprints(app)
    return app
Avoid
app = Flask(__name__)
app.config["SECRET_KEY"] = "dev"

# imported everywhere; hard to test in isolation

Trade-offs

Is Flask the right default?

Flask optimises for freedom and a small surface area. That is liberating for small apps and a burden once the app grows.

Strengths

  • You understand the whole thing

    There is very little magic. Every route, config value and extension is visible, which makes Flask an excellent framework for learning how the web works.

  • Minimal and flexible

    You pick the database, the validation library and the auth strategy instead of inheriting decisions you did not make.

  • Perfect for small services

    A JSON microservice or an internal tool is often a single file, and nothing in the framework fights that.

Trade-offs

  • You assemble the stack

    Auth, migrations, forms and admin are your responsibility. Each extension has its own patterns and its own learning curve.

  • Conventions are yours to invent

    Nothing enforces a project layout, so two Flask apps can look completely different. Large teams need their own house style.

  • Less comes out of the box

    Security headers, ORM behaviour and validation are opt-in. You must remember to add them rather than being protected by default.

FAQ

Frequently asked questions

Keep learning

Related topics from the roadmap.

$ start learning

Ready to start learning Flask?

Our interactive tutorial walks you through Flask step by step — with quizzes and real code you can run in the browser.