Flask – Introduction & Setup

Introduction

Flask is a lightweight WSGI web application framework for Python. Created by Armin Ronacher in 2010, it started as an April Fools’ joke that merged two libraries — Werkzeug (a WSGI toolkit) and Jinja2 (a template engine) — into a single package. The joke turned out to be genuinely useful, and Flask quickly became one of the most popular Python web frameworks in the world.

Flask calls itself a “micro-framework,” but do not let that label fool you. “Micro” means the core is deliberately small and unopinionated. Flask gives you routing, request handling, templates, and a development server. Everything else — database access, form validation, authentication, file uploads — you choose and plug in yourself. This is a feature, not a limitation. It means you understand every piece of your stack, and you never carry dead weight from features you do not use.

The two pillars that Flask is built on deserve a brief mention:

  • Werkzeug — A comprehensive WSGI (Web Server Gateway Interface) utility library. It handles HTTP request and response objects, URL routing, and the development server. WSGI is the standard interface between Python web applications and web servers, defined in PEP 3333. Every Python web framework speaks WSGI under the hood.
  • Jinja2 — A modern template engine for Python. It lets you write HTML templates with embedded Python-like expressions, template inheritance, filters, and macros. If you have used Django templates, Jinja2 will feel familiar but more powerful.

Flask’s philosophy is simple: give developers the tools they need and get out of their way. There is no ORM you are forced to use, no admin panel you never asked for, no project structure you must follow. You make the decisions. This tutorial will take you from zero to a working Flask application, covering everything a professional developer needs to know to build real applications with Flask.


Flask vs Django

The “Flask or Django?” question comes up in every Python web development discussion. The answer depends entirely on what you are building and how you prefer to work.

Choose Flask When

  • You want control over your stack. Flask lets you pick your own ORM (SQLAlchemy, Peewee, raw SQL), your own authentication system, your own form library. You assemble exactly what you need.
  • You are building an API or microservice. Flask’s lightweight nature is perfect for REST APIs and microservices where you do not need server-rendered HTML, an admin panel, or a built-in ORM.
  • You are learning web development. Flask’s simplicity means you understand what is happening at every layer. There is no magic. A Flask “Hello World” is five lines of code.
  • Your project has unusual requirements. When your application does not fit the standard “database-backed website” mold, Flask’s flexibility shines. Real-time dashboards, machine learning model serving, IoT backends — Flask adapts to your use case.
  • You want a small footprint. Flask installs in seconds and has minimal dependencies. Your deployment is lighter, your container images are smaller, and your cold start times are faster.

Choose Django When

  • You are building a content-heavy website. Django’s built-in admin panel, ORM, authentication, and form handling save enormous amounts of time for traditional web applications.
  • You need batteries included. Django ships with user management, session handling, CSRF protection, an admin interface, database migrations, and more. If you need all of these, Django saves you from integrating them yourself.
  • Your team values convention over configuration. Django has strong opinions about project structure and patterns. This means every Django project looks similar, which helps large teams maintain consistency.
  • You need rapid prototyping of data-driven apps. Django’s ORM and admin interface let you go from data model to working CRUD interface in minutes.

Neither framework is objectively better. Flask gives you freedom; Django gives you structure. Senior developers often use both — Flask for APIs and services, Django for full-stack web applications. This tutorial focuses on Flask because understanding it teaches you how web frameworks actually work, which makes you better with any framework.


Installation

Before installing Flask, set up a virtual environment. If you are not familiar with virtual environments and pip, review our Python Advanced – Virtual Environments & pip tutorial first. Never install Flask globally — always isolate your project dependencies.

Create a Virtual Environment

# Create a project directory
mkdir flask-tutorial && cd flask-tutorial

# Create a virtual environment
python3 -m venv venv

# Activate it
# macOS / Linux
source venv/bin/activate

# Windows
venv\Scripts\activate

Install Flask

# Install Flask
pip install flask

# Verify the installation
python -c "import flask; print(flask.__version__)"

Flask installs its dependencies automatically: Werkzeug, Jinja2, MarkupSafe, ItsDangerous, Click, and Blinker. You do not need to install these separately.

Freeze Your Dependencies

# Save your dependencies
pip freeze > requirements.txt

Your requirements.txt should look something like this:

blinker==1.9.0
click==8.1.7
Flask==3.1.0
itsdangerous==2.2.0
Jinja2==3.1.4
MarkupSafe==3.0.2
Werkzeug==3.1.3

Your First Flask Application

Let us build the classic “Hello World” application. Create a file called app.py.

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello, World!"

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

Let us break down every line:

  • from flask import Flask — Import the Flask class. An instance of this class is your WSGI application.
  • app = Flask(__name__) — Create an application instance. The __name__ argument tells Flask where to look for templates, static files, and other resources. When you run the file directly, __name__ is "__main__". When imported as a module, it is the module name.
  • @app.route("/") — A decorator that tells Flask which URL should trigger this function. The "/" means the root URL of your application.
  • def hello() — The view function. Flask calls this when someone visits the root URL. Whatever it returns becomes the HTTP response body.
  • app.run(debug=True) — Starts the built-in development server with debug mode enabled.

Running the Application

You have two options for running your Flask app:

# Option 1: Run the file directly
python app.py

# Option 2: Use the flask command (recommended)
flask --app app run --debug

Both methods start the development server on http://127.0.0.1:5000. Open that URL in your browser and you should see “Hello, World!”.

Debug Mode

Debug mode provides two critical features during development:

  • Automatic reloader — The server restarts automatically when you change your code. No more manually stopping and starting the server every time you make an edit.
  • Interactive debugger — When your code raises an exception, Flask shows a detailed traceback in the browser with an interactive debugger that lets you execute Python code at any frame in the stack.

Warning: Never enable debug mode in production. The interactive debugger allows arbitrary code execution on your server. We will cover this in more detail in the Common Pitfalls section.


Project Structure

Flask does not enforce a project structure, which is both its strength and a potential pitfall. Here are two structures that work well depending on your project size.

Small Application (Single Module)

For small apps, APIs, or prototypes, a flat structure works fine:

flask-tutorial/
├── app.py              # Application code
├── templates/          # Jinja2 templates
│   ├── base.html
│   ├── index.html
│   └── about.html
├── static/             # CSS, JS, images
│   ├── css/
│   │   └── style.css
│   └── js/
│       └── main.js
├── requirements.txt
├── .env
├── .gitignore
└── venv/

Larger Application (Package Structure)

When your application grows beyond a few hundred lines, organize it as a Python package using the application factory pattern:

flask-tutorial/
├── app/
│   ├── __init__.py     # Application factory (create_app)
│   ├── routes/
│   │   ├── __init__.py
│   │   ├── main.py     # Main blueprint
│   │   ├── auth.py     # Auth blueprint
│   │   └── api.py      # API blueprint
│   ├── models/
│   │   ├── __init__.py
│   │   └── user.py
│   ├── templates/
│   │   ├── base.html
│   │   ├── index.html
│   │   └── auth/
│   │       ├── login.html
│   │       └── register.html
│   ├── static/
│   │   ├── css/
│   │   └── js/
│   └── config.py
├── tests/
│   ├── __init__.py
│   ├── conftest.py
│   └── test_routes.py
├── requirements.txt
├── .env
├── .gitignore
└── run.py

Application Factory Pattern

The application factory is a function that creates and configures your Flask app. It is the recommended pattern for any non-trivial Flask application because it allows you to create multiple instances with different configurations (useful for testing), avoids circular imports, and keeps your application modular.

# app/__init__.py
from flask import Flask

def create_app(config_name="default"):
    app = Flask(__name__)

    # Load configuration
    if config_name == "testing":
        app.config.from_object("app.config.TestingConfig")
    elif config_name == "production":
        app.config.from_object("app.config.ProductionConfig")
    else:
        app.config.from_object("app.config.DevelopmentConfig")

    # Register blueprints
    from app.routes.main import main_bp
    app.register_blueprint(main_bp)

    from app.routes.auth import auth_bp
    app.register_blueprint(auth_bp, url_prefix="/auth")

    return app
# run.py
from app import create_app

app = create_app()

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

Configuration

Flask applications need configuration for things like secret keys, database URLs, debug flags, and third-party API credentials. Flask provides several ways to manage configuration, and choosing the right approach matters for both security and maintainability.

Configuration Classes

The cleanest approach is to define configuration as Python classes. Each class represents an environment.

# config.py
import os

class Config:
    """Base configuration shared by all environments."""
    SECRET_KEY = os.environ.get("SECRET_KEY", "fallback-secret-key")
    SQLALCHEMY_TRACK_MODIFICATIONS = False

class DevelopmentConfig(Config):
    """Development-specific configuration."""
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = os.environ.get(
        "DATABASE_URL", "sqlite:///dev.db"
    )

class TestingConfig(Config):
    """Testing-specific configuration."""
    TESTING = True
    SQLALCHEMY_DATABASE_URI = "sqlite:///test.db"

class ProductionConfig(Config):
    """Production-specific configuration."""
    DEBUG = False
    SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL")
    # In production, SECRET_KEY must be set via environment variable
    SECRET_KEY = os.environ["SECRET_KEY"]

Loading Configuration

# Load from a class
app.config.from_object("config.DevelopmentConfig")

# Load from environment variable pointing to a config file
app.config.from_envvar("APP_CONFIG_FILE")

# Set individual values
app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024  # 16 MB upload limit

Using python-dotenv

For local development, storing environment variables in a .env file is standard practice. Install python-dotenv and Flask will automatically load .env files.

pip install python-dotenv
# .env (add this to .gitignore!)
FLASK_APP=app
FLASK_DEBUG=1
SECRET_KEY=my-super-secret-key-change-in-production
DATABASE_URL=sqlite:///dev.db
from dotenv import load_dotenv
load_dotenv()  # Load variables from .env file

import os
secret = os.environ.get("SECRET_KEY")

Never commit .env files to version control. They contain secrets. Add .env to your .gitignore and provide a .env.example file with placeholder values so teammates know which variables to set.


Routing Basics

Routing is the mechanism that maps URLs to Python functions. In Flask, you define routes using the @app.route() decorator.

Basic Routes

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

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

@app.route("/contact")
def contact():
    return "Contact Page"

Variable Rules

You can capture parts of the URL as variables and pass them to your view function. Flask supports several converter types.

# String (default) - accepts any text without slashes
@app.route("/user/<username>")
def user_profile(username):
    return f"Profile: {username}"

# Integer
@app.route("/post/<int:post_id>")
def show_post(post_id):
    return f"Post #{post_id}"

# Float
@app.route("/price/<float:amount>")
def show_price(amount):
    return f"Price: ${amount:.2f}"

# Path - like string but accepts slashes
@app.route("/files/<path:filepath>")
def serve_file(filepath):
    return f"Serving: {filepath}"

# UUID
@app.route("/item/<uuid:item_id>")
def get_item(item_id):
    return f"Item: {item_id}"

HTTP Methods

By default, routes only respond to GET requests. Use the methods parameter to accept other HTTP methods.

from flask import request, redirect, url_for, render_template, jsonify

@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        username = request.form["username"]
        password = request.form["password"]
        # Authenticate user...
        return redirect(url_for("index"))
    return render_template("login.html")

@app.route("/api/users", methods=["GET"])
def list_users():
    users = [{"id": 1, "name": "Folau"}, {"id": 2, "name": "Sione"}]
    return jsonify(users)

@app.route("/api/users", methods=["POST"])
def create_user():
    data = request.get_json()
    # Create user...
    return jsonify({"id": 3, "name": data["name"]}), 201

@app.route("/api/users/<int:user_id>", methods=["PUT"])
def update_user(user_id):
    data = request.get_json()
    # Update user...
    return jsonify({"id": user_id, "name": data["name"]})

@app.route("/api/users/<int:user_id>", methods=["DELETE"])
def delete_user(user_id):
    # Delete user...
    return "", 204

URL Building with url_for()

url_for() generates URLs based on function names rather than hardcoded paths. This is critical because if you change a URL pattern, all url_for() calls automatically update.

from flask import url_for

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

@app.route("/user/<username>")
def profile(username):
    return f"Profile: {username}"

# In your code or templates:
with app.test_request_context():
    print(url_for("index"))                          # /
    print(url_for("profile", username="folau"))      # /user/folau
    print(url_for("static", filename="css/style.css"))  # /static/css/style.css

Always use url_for() instead of hardcoding URLs. It handles URL encoding, respects your application’s root path, and makes refactoring painless.


Request and Response

Flask provides a request object that gives you access to all incoming HTTP request data, and several utilities for building responses.

The Request Object

from flask import request

@app.route("/search")
def search():
    # Query string parameters: /search?q=flask&page=2
    query = request.args.get("q", "")
    page = request.args.get("page", 1, type=int)
    return f"Searching for '{query}' on page {page}"

@app.route("/submit", methods=["POST"])
def submit():
    # Form data (application/x-www-form-urlencoded or multipart/form-data)
    name = request.form.get("name")
    email = request.form.get("email")
    return f"Received: {name}, {email}"

@app.route("/api/data", methods=["POST"])
def receive_json():
    # JSON data (application/json)
    data = request.get_json()
    return jsonify({"received": data})

@app.route("/upload", methods=["POST"])
def upload_file():
    # File uploads
    file = request.files.get("document")
    if file:
        file.save(f"./uploads/{file.filename}")
        return f"Uploaded: {file.filename}"
    return "No file provided", 400

Other Useful Request Attributes

@app.route("/debug-request")
def debug_request():
    info = {
        "method": request.method,              # GET, POST, etc.
        "url": request.url,                    # Full URL
        "path": request.path,                  # /debug-request
        "host": request.host,                  # localhost:5000
        "headers": dict(request.headers),      # All headers
        "content_type": request.content_type,  # Request content type
        "remote_addr": request.remote_addr,    # Client IP address
        "cookies": dict(request.cookies),      # All cookies
    }
    return jsonify(info)

Building Responses

from flask import make_response, jsonify, redirect, url_for

# Simple string response (200 OK by default)
@app.route("/simple")
def simple():
    return "Hello"

# String with custom status code
@app.route("/not-found-example")
def custom_status():
    return "This page does not exist", 404

# JSON response
@app.route("/api/status")
def api_status():
    return jsonify({"status": "healthy", "version": "1.0.0"})

# Full control with make_response
@app.route("/custom")
def custom_response():
    resp = make_response("Custom Response", 200)
    resp.headers["X-Custom-Header"] = "MyValue"
    resp.set_cookie("visited", "true", max_age=3600)
    return resp

# Redirect
@app.route("/old-page")
def old_page():
    return redirect(url_for("index"))

Templates with Jinja2

Returning HTML strings from view functions is impractical for real applications. Flask uses the Jinja2 template engine to separate your presentation logic from your application logic. Templates live in a templates/ directory by default.

Basic Template Rendering

from flask import render_template

@app.route("/")
def index():
    return render_template("index.html", title="Home", user="Folau")

@app.route("/posts")
def posts():
    all_posts = [
        {"id": 1, "title": "Flask Basics", "author": "Folau"},
        {"id": 2, "title": "Jinja2 Templates", "author": "Folau"},
        {"id": 3, "title": "Flask Routing", "author": "Folau"},
    ]
    return render_template("posts.html", posts=all_posts)

Template Inheritance

Template inheritance is one of Jinja2’s most powerful features. You define a base template with blocks that child templates can override. This eliminates HTML duplication across pages.

<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}My Flask App{% endblock %}</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
    <nav>
        <a href="{{ url_for('index') }}">Home</a>
        <a href="{{ url_for('about') }}">About</a>
        <a href="{{ url_for('posts') }}">Posts</a>
    </nav>

    <main>
        {% block content %}{% endblock %}
    </main>

    <footer>
        <p>&copy; 2026 My Flask App</p>
    </footer>
</body>
</html>
<!-- templates/index.html -->
{% extends "base.html" %}

{% block title %}Home - My Flask App{% endblock %}

{% block content %}
<h1>Welcome, {{ user }}!</h1>
<p>This is a Flask application with Jinja2 templates.</p>
{% endblock %}

Variables and Expressions

<!-- Output a variable -->
<h1>{{ title }}</h1>

<!-- Access object attributes -->
<p>{{ user.name }}</p>

<!-- Access dictionary keys -->
<p>{{ config['SECRET_KEY'] }}</p>

<!-- Expressions -->
<p>{{ items | length }} items in your cart</p>

Control Structures

<!-- Conditionals -->
{% if user %}
    <p>Hello, {{ user.name }}!</p>
{% elif guest %}
    <p>Hello, guest!</p>
{% else %}
    <p>Please log in.</p>
{% endif %}

<!-- Loops -->
<ul>
{% for post in posts %}
    <li>
        <a href="{{ url_for('show_post', post_id=post.id) }}">
            {{ post.title }}
        </a>
        <span>by {{ post.author }}</span>
    </li>
{% else %}
    <li>No posts found.</li>
{% endfor %}
</ul>

<!-- Loop with index -->
{% for item in items %}
    <p>{{ loop.index }}. {{ item }}</p>
{% endfor %}

Filters

Jinja2 filters transform values. They are applied using the pipe | operator.

<!-- Built-in filters -->
{{ name | capitalize }}
{{ name | upper }}
{{ name | lower }}
{{ description | truncate(100) }}
{{ items | length }}
{{ price | round(2) }}
{{ html_content | safe }}  <!-- Render HTML without escaping -->
{{ list | join(", ") }}
{{ date | default("No date") }}

By default, Jinja2 auto-escapes HTML in variables to prevent XSS attacks. The | safe filter tells Jinja2 you trust the content and it should be rendered as raw HTML. Use this sparingly and only with content you control.


Static Files

Static files — CSS, JavaScript, images — live in the static/ directory. Flask automatically serves this directory at the /static URL path.

Directory Structure

static/
├── css/
│   ├── style.css
│   └── bootstrap.min.css
├── js/
│   ├── main.js
│   └── jquery.min.js
└── images/
    ├── logo.png
    └── favicon.ico

Referencing Static Files in Templates

Always use url_for('static', filename=...) to reference static files. Never hardcode paths.

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

<!-- JavaScript -->
<script src="{{ url_for('static', filename='js/main.js') }}"></script>

<!-- Images -->
<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo">

<!-- Favicon -->
<link rel="icon" href="{{ url_for('static', filename='images/favicon.ico') }}">

Using url_for() ensures correct paths regardless of where your application is mounted. If your app runs at /myapp/ instead of /, url_for() adjusts automatically. Hardcoded paths would break.


Error Handling

Flask lets you register custom error handlers so your users see friendly, branded error pages instead of generic browser defaults.

Custom Error Pages

from flask import render_template

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

@app.errorhandler(500)
def internal_server_error(error):
    return render_template("errors/500.html"), 500

@app.errorhandler(403)
def forbidden(error):
    return render_template("errors/403.html"), 403
<!-- templates/errors/404.html -->
{% extends "base.html" %}

{% block title %}Page Not Found{% endblock %}

{% block content %}
<div class="error-page">
    <h1>404</h1>
    <h2>Page Not Found</h2>
    <p>The page you are looking for does not exist.</p>
    <a href="{{ url_for('index') }}">Go back home</a>
</div>
{% endblock %}

Error Handling for APIs

For JSON APIs, return JSON error responses instead of HTML.

from flask import jsonify
from werkzeug.exceptions import HTTPException

@app.errorhandler(HTTPException)
def handle_http_exception(error):
    response = jsonify({
        "error": error.name,
        "message": error.description,
        "status_code": error.code,
    })
    response.status_code = error.code
    return response

@app.errorhandler(Exception)
def handle_generic_exception(error):
    # Log the error for debugging
    app.logger.error(f"Unhandled exception: {error}", exc_info=True)
    response = jsonify({
        "error": "Internal Server Error",
        "message": "An unexpected error occurred.",
        "status_code": 500,
    })
    response.status_code = 500
    return response

Aborting Requests

Use abort() to immediately stop a request and return an error status code.

from flask import abort

@app.route("/admin")
def admin_panel():
    if not current_user.is_admin:
        abort(403)  # Forbidden
    return render_template("admin.html")

@app.route("/api/users/<int:user_id>")
def get_user(user_id):
    user = User.query.get(user_id)
    if user is None:
        abort(404)  # Not Found
    return jsonify(user.to_dict())

Practical Example: Build a Simple Blog

Let us pull everything together and build a simple blog application with multiple routes, template inheritance, and static CSS. This example demonstrates real-world Flask patterns you will use in production applications.

Application Code

# app.py
from flask import Flask, render_template, abort

app = Flask(__name__)

# Simulated blog data (in a real app, this comes from a database)
POSTS = [
    {
        "id": 1,
        "title": "Getting Started with Flask",
        "slug": "getting-started-with-flask",
        "author": "Folau Kaveinga",
        "date": "2026-02-20",
        "summary": "Learn the basics of Flask, the lightweight Python web framework.",
        "content": (
            "Flask is a micro-framework for Python that gives you the essentials "
            "for building web applications without imposing unnecessary complexity. "
            "In this post, we explore the core concepts that make Flask a favorite "
            "among Python developers."
        ),
    },
    {
        "id": 2,
        "title": "Understanding Jinja2 Templates",
        "slug": "understanding-jinja2-templates",
        "author": "Folau Kaveinga",
        "date": "2026-02-22",
        "summary": "Deep dive into Jinja2 template engine features and best practices.",
        "content": (
            "Jinja2 is the template engine that powers Flask's HTML rendering. "
            "It supports template inheritance, filters, macros, and more. "
            "Mastering Jinja2 is essential for building maintainable Flask applications."
        ),
    },
    {
        "id": 3,
        "title": "Flask Routing and URL Building",
        "slug": "flask-routing-and-url-building",
        "author": "Folau Kaveinga",
        "date": "2026-02-25",
        "summary": "Master Flask routing, variable rules, and URL generation.",
        "content": (
            "Routing is the backbone of any web application. Flask makes it "
            "intuitive with decorators and supports variable rules, multiple "
            "HTTP methods, and dynamic URL building with url_for()."
        ),
    },
]


@app.route("/")
def index():
    return render_template("blog/index.html", posts=POSTS)


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


@app.route("/post/<int:post_id>")
def post_detail(post_id):
    post = next((p for p in POSTS if p["id"] == post_id), None)
    if post is None:
        abort(404)
    return render_template("blog/post.html", post=post)


@app.errorhandler(404)
def page_not_found(error):
    return render_template("blog/404.html"), 404


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

Base Template

<!-- templates/blog/base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}Flask Blog{% endblock %}</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/blog.css') }}">
</head>
<body>
    <header>
        <nav>
            <div class="nav-brand">Flask Blog</div>
            <div class="nav-links">
                <a href="{{ url_for('index') }}">Home</a>
                <a href="{{ url_for('about') }}">About</a>
            </div>
        </nav>
    </header>

    <main>
        {% block content %}{% endblock %}
    </main>

    <footer>
        <p>Built with Flask &mdash; &copy; 2026</p>
    </footer>
</body>
</html>

Home Page Template

<!-- templates/blog/index.html -->
{% extends "blog/base.html" %}

{% block title %}Home - Flask Blog{% endblock %}

{% block content %}
<h1>Latest Posts</h1>
{% for post in posts %}
<article class="post-card">
    <h2>
        <a href="{{ url_for('post_detail', post_id=post.id) }}">{{ post.title }}</a>
    </h2>
    <div class="post-meta">{{ post.author }} &middot; {{ post.date }}</div>
    <p>{{ post.summary }}</p>
    <a href="{{ url_for('post_detail', post_id=post.id) }}" class="read-more">
        Read more &rarr;
    </a>
</article>
{% endfor %}
{% endblock %}

Post Detail Template

<!-- templates/blog/post.html -->
{% extends "blog/base.html" %}

{% block title %}{{ post.title }} - Flask Blog{% endblock %}

{% block content %}
<article class="post-full">
    <h1>{{ post.title }}</h1>
    <div class="post-meta">{{ post.author }} &middot; {{ post.date }}</div>
    <div class="post-content">
        {{ post.content }}
    </div>
    <a href="{{ url_for('index') }}">&larr; Back to all posts</a>
</article>
{% endblock %}

About Page Template

<!-- templates/blog/about.html -->
{% extends "blog/base.html" %}

{% block title %}About - Flask Blog{% endblock %}

{% block content %}
<h1>About This Blog</h1>
<p>This is a simple blog built with Flask to demonstrate core framework concepts
including routing, template inheritance, static files, and error handling.</p>
<p>Flask makes it straightforward to build web applications in Python without
the overhead of a full-stack framework. You choose the components you need
and assemble them yourself.</p>
{% endblock %}

Static CSS

/* static/css/blog.css */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    line-height: 1.6;
    color: #333;
    max-width: 800px;
    margin: 0 auto;
    padding: 0 20px;
}

header nav {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 20px 0;
    border-bottom: 2px solid #eee;
}

.nav-brand {
    font-size: 1.5rem;
    font-weight: bold;
    color: #2563eb;
}

.nav-links a {
    margin-left: 20px;
    text-decoration: none;
    color: #555;
}

.nav-links a:hover { color: #2563eb; }

main { padding: 40px 0; }

.post-card {
    margin-bottom: 30px;
    padding-bottom: 30px;
    border-bottom: 1px solid #eee;
}

.post-card h2 a {
    text-decoration: none;
    color: #1a1a1a;
}

.post-card h2 a:hover { color: #2563eb; }

.post-meta {
    color: #888;
    font-size: 0.9rem;
    margin: 8px 0;
}

.read-more {
    color: #2563eb;
    text-decoration: none;
    font-weight: 500;
}

.post-full h1 { margin-bottom: 10px; }

.post-content {
    margin: 20px 0;
    line-height: 1.8;
}

footer {
    padding: 20px 0;
    border-top: 2px solid #eee;
    text-align: center;
    color: #888;
}

Run the application with python app.py and visit http://127.0.0.1:5000. You now have a working blog with a home page listing posts, individual post pages, an about page, and custom 404 handling. All of it using template inheritance so the navigation and footer are defined once in base.html.


Running in Production

The Flask development server is single-threaded, unoptimized, and not designed to handle real traffic. Never use it in production. Instead, use a production-grade WSGI server like Gunicorn.

Gunicorn

# Install gunicorn
pip install gunicorn

# Run your Flask app with gunicorn
gunicorn app:app --bind 0.0.0.0:8000 --workers 4

# With the application factory pattern
gunicorn "app:create_app()" --bind 0.0.0.0:8000 --workers 4

The --workers 4 flag spawns 4 worker processes to handle requests concurrently. A common rule of thumb is (2 * CPU cores) + 1 workers.

Typical Production Stack

In production, Flask applications typically run behind a reverse proxy:

Client → Nginx (reverse proxy, SSL, static files) → Gunicorn (WSGI server) → Flask (application)

Nginx handles SSL termination, static file serving, and load balancing. Gunicorn runs your Flask application. This separation of concerns gives you better performance, security, and reliability than any single component could provide alone.


Common Pitfalls

1. Debug Mode in Production

This is the most dangerous mistake you can make with Flask. The interactive debugger that runs in debug mode allows anyone who can reach your error page to execute arbitrary Python code on your server. This means full remote code execution — they can read your environment variables, access your database, and compromise your entire system.

# NEVER do this in production
app.run(debug=True)

# Instead, control debug mode via environment variables
import os
app.run(debug=os.environ.get("FLASK_DEBUG", "0") == "1")

2. Circular Imports

This is the most common structural issue in Flask applications. It happens when your application module and your routes module try to import each other.

# BAD: Circular import
# app.py
from flask import Flask
app = Flask(__name__)
from routes import *  # routes.py imports app from here = circular

# GOOD: Use the application factory pattern
# app/__init__.py
def create_app():
    app = Flask(__name__)
    from app.routes import main_bp
    app.register_blueprint(main_bp)
    return app

The application factory pattern solves circular imports because the app object is created inside a function, not at module level. Blueprints do not need to import the app object directly.

3. Hardcoded Configuration

Hardcoding database URLs, secret keys, and API keys directly in your source code is a security risk and makes deployment difficult.

# BAD
app.config["SECRET_KEY"] = "my-secret-key"
app.config["DATABASE_URL"] = "postgresql://user:pass@localhost/db"

# GOOD
app.config["SECRET_KEY"] = os.environ["SECRET_KEY"]
app.config["DATABASE_URL"] = os.environ["DATABASE_URL"]

4. Not Using url_for()

Hardcoding URLs in templates and Python code breaks when you change route paths or deploy your app under a different prefix.

<!-- BAD -->
<a href="/about">About</a>
<link rel="stylesheet" href="/static/css/style.css">

<!-- GOOD -->
<a href="{{ url_for('about') }}">About</a>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">

5. Ignoring the Application Context

Some Flask operations require an application context. If you see “RuntimeError: Working outside of application context,” it means you are trying to use Flask features (like url_for() or current_app) outside of a request or without an active app context.

# If you need app context outside a request (e.g., in a script or test)
with app.app_context():
    url = url_for("index")
    db.create_all()

Best Practices

  1. Use the application factory pattern from the start. Even if your app is small today, refactoring a global app object into a factory later is painful. Start with create_app() and save yourself the trouble.
  2. Adopt blueprints early. Blueprints let you organize routes into logical groups. Even for small apps, a single blueprint is better than decorating a global app object because it makes the transition to multiple blueprints trivial.
  3. Use environment-based configuration. Never hardcode secrets. Use configuration classes, environment variables, and python-dotenv. Your .env file should be in .gitignore.
  4. Set up logging properly. Flask uses Python’s built-in logging module. Configure it to write to files or a centralized logging service in production. app.logger is your starting point.
import logging
from logging.handlers import RotatingFileHandler

if not app.debug:
    handler = RotatingFileHandler("app.log", maxBytes=10240, backupCount=10)
    handler.setLevel(logging.INFO)
    formatter = logging.Formatter(
        "%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]"
    )
    handler.setFormatter(formatter)
    app.logger.addHandler(handler)
    app.logger.setLevel(logging.INFO)
    app.logger.info("Application startup")
  1. Write tests. Flask provides a test client that makes it easy to test your routes without running a server.
import pytest
from app import create_app

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

def test_index(client):
    response = client.get("/")
    assert response.status_code == 200

def test_404(client):
    response = client.get("/nonexistent")
    assert response.status_code == 404
  1. Use a virtual environment. Always. No exceptions. See our virtual environments tutorial for details.
  2. Pin your dependencies. Use pip freeze > requirements.txt so every environment runs the same package versions.
  3. Never use the development server in production. Use Gunicorn, uWSGI, or Waitress behind a reverse proxy like Nginx.

Key Takeaways

  • Flask is a lightweight, flexible Python web framework built on Werkzeug (WSGI toolkit) and Jinja2 (template engine). Its “micro” philosophy means you get the essentials and choose everything else yourself.
  • Choose Flask over Django when you need flexibility, are building APIs or microservices, or want to understand every component in your stack. Choose Django when you need batteries included for content-heavy web applications.
  • Always develop inside a virtual environment. Install Flask with pip install flask and freeze your dependencies with pip freeze > requirements.txt.
  • A Flask “Hello World” is five lines of code. Use flask --app app run --debug to start the development server with auto-reloading and an interactive debugger.
  • Use the application factory pattern (create_app()) for any non-trivial application. It prevents circular imports and enables testing with different configurations.
  • Manage configuration with Python classes, environment variables, and python-dotenv. Never hardcode secrets in source code.
  • Routes map URLs to Python functions using @app.route(). Use variable rules for dynamic URLs and url_for() to generate URLs programmatically.
  • The request object provides access to query parameters, form data, JSON payloads, and file uploads. Use jsonify() and make_response() to build responses.
  • Jinja2 templates support inheritance, variables, control structures, and filters. Always use url_for() in templates to reference routes and static files.
  • Register custom error handlers with @app.errorhandler() to show branded error pages instead of browser defaults.
  • Never run the development server in production. Use Gunicorn or another production WSGI server behind Nginx.
  • The three most common Flask pitfalls are: debug mode in production (security disaster), circular imports (use the factory pattern), and hardcoded configuration (use environment variables).



Subscribe To Our Newsletter
You will receive our latest post and tutorial.
Thank you for subscribing!

required
required


Leave a Reply

Your email address will not be published. Required fields are marked *