Ad – 728Γ—90
⚑ Frameworks

Flask Python Tutorial – Build Your First Web App

Flask is a lightweight, "micro" web framework for Python that gives you everything you need to build web applications without imposing rigid conventions. It is the perfect starting point for learning web development with Python β€” simple enough to go from zero to a running app in minutes, yet powerful enough to build production APIs used by companies like Pinterest and LinkedIn. This tutorial takes you from installation to a fully functional REST API.

⏱️ 30 min read 🎯 Advanced πŸ“… Updated 2026

What is Flask?

Flask is a Python web framework built on top of Werkzeug (WSGI toolkit) and Jinja2 (templating engine). It is called a "micro" framework because it ships with minimal built-in tooling β€” no built-in database layer, form validation, or admin interface. Instead, you pick exactly the extensions you need.

FeatureFlaskDjango
PhilosophyMicro β€” add what you needBatteries included
Learning curveGentleSteeper
FlexibilityVery highMore opinionated
Best forAPIs, small–medium appsFull-stack, CMS, large teams
ORMNot built-in (use SQLAlchemy)Built-in Django ORM

Installing Flask

Flask is installed via pip. It is always best practice to work inside a virtual environment:

Shell
# Create and activate a virtual environment
python -m venv venv
source venv/bin/activate        # macOS / Linux
# venv\Scripts\activate         # Windows

# Install Flask
pip install flask

# Verify the installation
python -c "import flask; print(flask.__version__)"
# 3.0.x
πŸ’‘
Always use a virtual environment

A virtual environment isolates your project's dependencies from your system Python and other projects. This prevents version conflicts and keeps your environment reproducible.

Your First Flask Application

A minimal Flask app is just a few lines. Create a file called app.py:

Python – app.py
from flask import Flask

app = Flask(__name__)   # Create the Flask application instance

@app.route("/")         # Register a route for the root URL
def home():
    return "Hello, Flask!"

if __name__ == "__main__":
    app.run(debug=True)  # Start the development server
Shell
python app.py
β–Ά Output
* Running on http://127.0.0.1:5000 * Debug mode: on

Open your browser at http://127.0.0.1:5000 and you will see "Hello, Flask!". That is your first web application running.

⚠️
debug=True is for development only

Debug mode enables auto-reloading on code changes and an interactive debugger in the browser. Never use debug=True in production β€” it exposes your code and allows arbitrary code execution.

Routes and URL Rules

A route maps a URL path to a Python function (called a view function). The @app.route() decorator registers the mapping.

Python
from flask import Flask

app = Flask(__name__)

@app.route("/")
def home():
    return "Home Page"

@app.route("/about")
def about():
    return "About Page"

# Dynamic route β€” captures URL segment as a parameter
@app.route("/user/<username>")
def user_profile(username):
    return f"Profile page for {username}"

# Type-converters: string (default), int, float, path, uuid
@app.route("/post/<int:post_id>")
def show_post(post_id):
    return f"Post #{post_id}"

# Multiple URLs for the same view
@app.route("/home")
@app.route("/index")
def index():
    return "You reached the index!"

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

Visit /user/alice and Flask calls user_profile("alice"). Visit /post/42 and Flask calls show_post(42) with an integer, not a string.

HTTP Methods – GET and POST

By default, routes only accept GET requests. Use the methods argument to support POST, PUT, DELETE, and others:

Python
from flask import Flask, request

app = Flask(__name__)

@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        username = request.form.get("username")
        password = request.form.get("password")
        if username == "admin" and password == "secret":
            return f"Welcome, {username}!"
        return "Invalid credentials", 401
    # GET request β€” show the login form
    return """
    <form method="post">
      <input name="username" placeholder="Username">
      <input name="password" type="password" placeholder="Password">
      <button type="submit">Log In</button>
    </form>
    """
Ad – 336Γ—280

The request Object

Flask's request is a global context object that holds all information about the incoming HTTP request:

Python
from flask import Flask, request

app = Flask(__name__)

@app.route("/data", methods=["GET", "POST"])
def data():
    # --- GET parameters from query string: /data?name=alice&age=30
    name = request.args.get("name", "anonymous")
    age  = request.args.get("age", "unknown")

    # --- POST form data
    form_value = request.form.get("field_name")

    # --- JSON body (for API requests)
    json_data = request.get_json()          # Returns None if not JSON

    # --- Request metadata
    print(request.method)           # GET, POST, etc.
    print(request.url)              # Full URL
    print(request.remote_addr)      # Client IP
    print(request.headers)          # All HTTP headers
    print(request.cookies)          # Cookies dict

    return f"Hello {name}, age {age}"

Templates with Jinja2

Instead of returning HTML strings, Flask uses Jinja2 templates β€” HTML files with special syntax for variables, conditionals, and loops. Store templates in a templates/ folder next to your app:

Python – app.py
from flask import Flask, render_template

app = Flask(__name__)

@app.route("/users")
def users():
    user_list = [
        {"name": "Alice", "role": "admin"},
        {"name": "Bob",   "role": "editor"},
        {"name": "Carol", "role": "viewer"},
    ]
    return render_template("users.html", users=user_list, title="User List")
HTML – templates/users.html
<!DOCTYPE html>
<html>
<head><title>{{ title }}</title></head>
<body>
  <h1>{{ title }}</h1>
  <ul>
    {% for user in users %}
      <li>{{ user.name }} β€” <em>{{ user.role }}</em></li>
    {% endfor %}
  </ul>
  {% if users|length == 0 %}
    <p>No users found.</p>
  {% endif %}
</body>
</html>

Jinja2 syntax uses {{ variable }} for output and {% tag %} for logic. Common tags include for, if, elif, else, block, and extends.

Template Inheritance

Jinja2 supports template inheritance to avoid repeating your HTML boilerplate across pages:

HTML – templates/base.html
<!DOCTYPE html>
<html>
<head>
  <title>{% block title %}My App{% endblock %}</title>
  <link rel="stylesheet" href="/static/style.css">
</head>
<body>
  <nav><a href="/">Home</a> | <a href="/about">About</a></nav>
  <main>{% block content %}{% endblock %}</main>
  <footer>&copy; 2024 MyApp</footer>
</body>
</html>
HTML – templates/home.html
{% extends "base.html" %}

{% block title %}Home – MyApp{% endblock %}

{% block content %}
  <h1>Welcome!</h1>
  <p>This content is injected into the base template.</p>
{% endblock %}

Returning JSON – Building an API

Flask makes it trivial to return JSON responses, which is the foundation of REST APIs:

Python
from flask import Flask, jsonify, request, abort

app = Flask(__name__)

# In-memory "database"
books = [
    {"id": 1, "title": "Fluent Python", "author": "Luciano Ramalho"},
    {"id": 2, "title": "Clean Code",    "author": "Robert C. Martin"},
]

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

# GET a single book
@app.route("/api/books/<int:book_id>", methods=["GET"])
def get_book(book_id):
    book = next((b for b in books if b["id"] == book_id), None)
    if book is None:
        abort(404)   # Returns a 404 JSON error
    return jsonify(book)

# POST β€” create a new book
@app.route("/api/books", methods=["POST"])
def create_book():
    data = request.get_json()
    if not data or "title" not in data:
        abort(400, description="title is required")
    new_book = {
        "id": max(b["id"] for b in books) + 1,
        "title": data["title"],
        "author": data.get("author", "Unknown"),
    }
    books.append(new_book)
    return jsonify(new_book), 201   # 201 Created

# DELETE a book
@app.route("/api/books/<int:book_id>", methods=["DELETE"])
def delete_book(book_id):
    global books
    books = [b for b in books if b["id"] != book_id]
    return "", 204   # 204 No Content

if __name__ == "__main__":
    app.run(debug=True)
β–Ά Test with curl
# GET all books curl http://localhost:5000/api/books # POST a new book curl -X POST -H "Content-Type: application/json" \ -d '{"title": "Python Tricks", "author": "Dan Bader"}' \ http://localhost:5000/api/books # DELETE book #1 curl -X DELETE http://localhost:5000/api/books/1

Flask Project Structure

For anything beyond a single file, organise your Flask project like this:

Project Layout
my_flask_app/
β”œβ”€β”€ app/
β”‚   β”œβ”€β”€ __init__.py        # create_app() factory function
β”‚   β”œβ”€β”€ routes/
β”‚   β”‚   β”œβ”€β”€ __init__.py
β”‚   β”‚   β”œβ”€β”€ main.py        # Blueprint: general pages
β”‚   β”‚   └── api.py         # Blueprint: API endpoints
β”‚   β”œβ”€β”€ models.py          # SQLAlchemy database models
β”‚   β”œβ”€β”€ templates/
β”‚   β”‚   β”œβ”€β”€ base.html
β”‚   β”‚   └── home.html
β”‚   └── static/
β”‚       β”œβ”€β”€ style.css
β”‚       └── app.js
β”œβ”€β”€ tests/
β”‚   └── test_routes.py
β”œβ”€β”€ config.py              # Configuration classes
β”œβ”€β”€ requirements.txt
└── run.py                 # Entry point: from app import create_app

Flask Blueprints

Blueprints let you split your app into modular components:

Python – app/routes/api.py
from flask import Blueprint, jsonify

api_bp = Blueprint("api", __name__, url_prefix="/api/v1")

@api_bp.route("/status")
def status():
    return jsonify({"status": "ok", "version": "1.0"})
Python – app/__init__.py
from flask import Flask
from .routes.api import api_bp

def create_app(config=None):
    app = Flask(__name__)
    app.config.from_object(config or "config.DevelopmentConfig")
    app.register_blueprint(api_bp)
    return app

Configuration

Python – config.py
import os

class Config:
    SECRET_KEY = os.environ.get("SECRET_KEY", "dev-key-change-in-prod")
    DEBUG = False
    TESTING = False

class DevelopmentConfig(Config):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = "sqlite:///dev.db"

class ProductionConfig(Config):
    SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL")
    # Never hardcode production secrets!

Deploying Flask

Flask's built-in server is not suitable for production. Use a WSGI server like Gunicorn behind a reverse proxy like Nginx:

Shell
# Install Gunicorn
pip install gunicorn

# Run with 4 worker processes
gunicorn -w 4 -b 0.0.0.0:8000 "app:create_app()"

# For Docker: create a Dockerfile
# FROM python:3.12-slim
# WORKDIR /app
# COPY requirements.txt .
# RUN pip install -r requirements.txt
# COPY . .
# CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "app:create_app()"]

πŸ‹οΈ Practical Exercises

  1. Create a Flask app with at least three routes: home, about, and a dynamic /greet/<name> page that renders a Jinja2 template.
  2. Build a simple to-do API (in memory) with GET (list all), POST (create), and DELETE (by id) endpoints. Test using curl or Postman.
  3. Add a /search route that accepts a q query parameter and returns matching items from a list.
  4. Refactor your app to use a Blueprint and the application factory pattern.

πŸ”₯ Challenge: Mini Blog API

Build a REST API for a mini blog with Flask. Requirements: posts have id, title, body, author, and created_at fields. Implement CRUD (GET all, GET one, POST, PUT/PATCH update, DELETE). Add validation β€” title and body are required. Return proper HTTP status codes (200, 201, 204, 400, 404). Store data in memory. Bonus: add pagination via ?page=1&per_page=5 query params.

  • What is Flask and how does it differ from Django?
  • What does Flask(__name__) do? Why pass __name__?
  • How do you handle both GET and POST requests in the same route?
  • What is request.args vs request.form vs request.get_json()?
  • What is Jinja2 and how do you pass variables to a template?
  • What is a Flask Blueprint and why would you use one?
  • What WSGI server would you use to deploy a Flask app in production?
  • What does abort(404) do in Flask?
  • Why should you never use debug=True in production?

πŸ“‹ Summary

  • Flask is a lightweight Python micro-framework for building web apps and APIs.
  • Install with pip install flask inside a virtual environment.
  • Use @app.route("/path") to register URL rules; add methods=["POST"] for non-GET routes.
  • The request object holds query params (.args), form data (.form), and JSON (.get_json()).
  • render_template() renders Jinja2 HTML templates stored in the templates/ folder.
  • jsonify() returns JSON responses with the correct Content-Type header.
  • Blueprints and the application factory pattern keep larger apps maintainable.
  • Use Gunicorn + Nginx to deploy Flask in production; never use the dev server.

Frequently Asked Questions

Is Flask good for large applications? +

Yes, with proper structure. Use Blueprints to split the app into modules, an application factory to manage configuration, and extensions like Flask-SQLAlchemy, Flask-Login, and Flask-Migrate. Many large-scale applications are built on Flask.

What is the difference between Flask and FastAPI? +

FastAPI is an async-first framework with built-in data validation via Pydantic, automatic OpenAPI docs, and native async/await support. Flask is synchronous by default (though Flask 2.x supports async) and is a more established ecosystem. Choose FastAPI for new API-only projects; Flask for projects needing Jinja2 templating or that have a large existing Flask ecosystem.

How do I add a database to Flask? +

Use Flask-SQLAlchemy (pip install flask-sqlalchemy) for an ORM-based approach with SQLite, PostgreSQL, or MySQL. For schema migrations, add Flask-Migrate. For simple key-value needs, Flask-Caching or direct Redis integration work well.

How do I handle CORS in Flask? +

Install Flask-CORS (pip install flask-cors) and apply it: from flask_cors import CORS; CORS(app). You can restrict origins, headers, and methods as needed.