Intermediate

You’ve written Python scripts, maybe built some command-line tools, and now you want to build something others can access through a browser or call from a mobile app. Flask is the fastest path from Python knowledge to a working web application. It’s a lightweight web framework that gives you just what you need — routing, templates, request handling, and JSON responses — without the complexity of larger frameworks like Django.

Flask is a third-party package, so you’ll need to install it with pip install flask. Once installed, a minimal Flask app runs in fewer than 10 lines. Flask works great for REST APIs, small web applications, internal tools, and prototyping ideas quickly. When your app grows large and needs built-in admin panels, ORM, and authentication systems, you might switch to Django — but for most projects, Flask’s simplicity is its superpower.

In this tutorial, you’ll build a Flask web application from scratch. You’ll learn how routes map URLs to functions, how to render HTML templates with Jinja2, how to handle GET and POST form submissions, how to return JSON for API endpoints, and how to use Flask’s development server. By the end, you’ll have a working contact book web application that demonstrates all these concepts together.

Flask: Quick Example

Here’s the smallest possible Flask app — a web server that responds to HTTP requests:

# app.py
from flask import Flask

app = Flask(__name__)

@app.route('/')
def home():
    return '

Hello, World!

Your Flask app is running.

' @app.route('/about') def about(): return '

A simple Flask application.

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

To run it:

$ pip install flask
$ python app.py
 * Running on http://127.0.0.1:5000
 * Debug mode: on

Open http://127.0.0.1:5000 in a browser and you’ll see “Hello, World!”. The @app.route('/') decorator registers the home function as the handler for requests to the root URL. The string you return becomes the HTTP response body. The debug=True option enables the auto-reloader (restarts when you change files) and the interactive debugger — never use this in production.

What Is Flask and When Should You Use It?

Flask is a “micro” web framework for Python. “Micro” doesn’t mean small or limited — it means Flask provides the core tools (routing, request handling, templates) and lets you add everything else (database, authentication, forms) as separate packages you choose yourself. This makes Flask lightweight, flexible, and easy to understand.

FeatureFlaskDjangoFastAPI
Learning curveLowMedium-HighMedium
Built-in ORMNo (use SQLAlchemy)YesNo
Best forAPIs, small apps, prototypesLarge full-stack appsHigh-performance APIs
TemplatesJinja2Django templatesNo built-in
Async supportLimited (Flask 2.0+)LimitedFirst-class

Flask is the right choice when you want to build something functional quickly without learning a large framework, when you need a REST API backend, or when you want full control over which libraries you use for database access, authentication, and other concerns.

Routes and View Functions

A route maps a URL pattern to a Python function. When a browser requests that URL, Flask calls the function and returns its return value as the HTTP response. Routes can include dynamic segments (variables in the URL) captured with angle brackets:

# routes.py
from flask import Flask

app = Flask(__name__)

# Static route
@app.route('/')
def index():
    return '

Home Page

' # Dynamic route: captures a string segment @app.route('/user/') def user_profile(username): return f'

Profile: {username}

' # Dynamic route with type conversion (int only) @app.route('/post/') def show_post(post_id): return f'

Post ID: {post_id} (type: {type(post_id).__name__})

' # Route that accepts GET and POST @app.route('/submit', methods=['GET', 'POST']) def submit(): from flask import request if request.method == 'POST': return '

Form submitted!

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

Example URLs and responses:

GET /              -> Home Page
GET /user/alice    -> Profile: alice
GET /post/42       -> Post ID: 42 (type: int)
GET /post/abc      -> 404 Not Found (not an int)

The <int:post_id> converter ensures Flask only matches the route when the URL segment is a valid integer, and automatically converts it for you. If someone requests /post/abc, Flask returns a 404 response. Flask also supports <float:> and <path:> converters.

HTML Templates with Jinja2

Returning raw HTML strings from view functions gets unwieldy fast. Flask uses the Jinja2 template engine to render HTML files stored in a templates/ directory. Templates can include Python-like logic: loops, conditionals, and variable interpolation.

Template File Structure

Flask looks for templates in a folder named templates next to your app.py. Create this structure:

# Project structure:
# myapp/
#   app.py
#   templates/
#     base.html
#     index.html
#     user.html

A base template defines the common page structure that other templates extend:

<!-- templates/base.html -->
<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}My App{% endblock %}</title>
</head>
<body>
    <nav><a href="/">Home</a> | <a href="/users">Users</a></nav>
    <main>{% block content %}{% endblock %}</main>
</body>
</html>
<!-- templates/index.html -->
{% extends "base.html" %}

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

{% block content %}
<h1>Welcome, {{ name }}!</h1>
<ul>
{% for item in items %}
    <li>{{ item }}</li>
{% endfor %}
</ul>
{% endblock %}
# app_templates.py
from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html',
        name='Alice',
        items=['Learn Flask', 'Build an API', 'Deploy to cloud']
    )

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

The render_template() function loads the template file, substitutes the variables you pass as keyword arguments, and returns the resulting HTML string. Jinja2’s {{ variable }} syntax outputs values, while {% for %}, {% if %}, and {% block %} are control structures.

Handling Forms and POST Requests

Web forms submit data as HTTP POST requests. Flask’s request object gives you access to form data, query parameters, and request headers. Use request.form.get() for form fields (not request.form[]) to avoid KeyError if a field is missing:

# form_handling.py
from flask import Flask, render_template, request, redirect, url_for

app = Flask(__name__)

# In-memory storage (use a database in production)
contacts = []

@app.route('/contacts')
def contact_list():
    return render_template('contacts.html', contacts=contacts)

@app.route('/contacts/add', methods=['GET', 'POST'])
def add_contact():
    if request.method == 'POST':
        name = request.form.get('name', '').strip()
        email = request.form.get('email', '').strip()

        if name and email:  # Basic validation
            contacts.append({'name': name, 'email': email})
            return redirect(url_for('contact_list'))
        else:
            error = 'Both name and email are required.'
            return render_template('add_contact.html', error=error)

    return render_template('add_contact.html', error=None)

The Post/Redirect/Get pattern is critical for form handling: after a successful POST, redirect the user to a GET route. This prevents the “Are you sure you want to resubmit the form?” browser dialog when the user refreshes the page. The url_for('contact_list') call generates the URL for the contact_list view function — use this instead of hardcoding URLs to keep your routes maintainable.

Building a JSON API

Flask makes it straightforward to return JSON responses for API endpoints. Use jsonify() to convert Python dicts and lists into properly formatted JSON responses with the correct Content-Type header:

# json_api.py
from flask import Flask, jsonify, request

app = Flask(__name__)

# Simple in-memory task list
tasks = [
    {'id': 1, 'title': 'Learn Flask', 'done': True},
    {'id': 2, 'title': 'Build an API', 'done': False},
]

@app.route('/api/tasks', methods=['GET'])
def get_tasks():
    return jsonify({'tasks': tasks, 'count': len(tasks)})

@app.route('/api/tasks/', methods=['GET'])
def get_task(task_id):
    task = next((t for t in tasks if t['id'] == task_id), None)
    if task is None:
        return jsonify({'error': 'Task not found'}), 404
    return jsonify(task)

@app.route('/api/tasks', methods=['POST'])
def create_task():
    data = request.get_json()
    if not data or 'title' not in data:
        return jsonify({'error': 'title is required'}), 400

    new_task = {
        'id': max(t['id'] for t in tasks) + 1,
        'title': data['title'],
        'done': data.get('done', False)
    }
    tasks.append(new_task)
    return jsonify(new_task), 201

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

Test with curl:

$ curl http://127.0.0.1:5000/api/tasks
{"tasks": [{"id": 1, "title": "Learn Flask", "done": true}, ...], "count": 2}

$ curl -X POST http://127.0.0.1:5000/api/tasks \
  -H "Content-Type: application/json" \
  -d '{"title": "Deploy to production"}'
{"done": false, "id": 3, "title": "Deploy to production"}

Return the appropriate HTTP status code as the second argument to jsonify(): 200 for success (default), 201 for created, 400 for bad request, 404 for not found. This tells API clients what happened without them needing to parse the response body.

Real-Life Example: Contact Book Web App

Let’s bring everything together in a complete contact book application with a list view, add form, and JSON API endpoint:

# contact_book.py
from flask import Flask, render_template, request, redirect, url_for, jsonify

app = Flask(__name__)

contacts = [
    {'id': 1, 'name': 'Alice Chen', 'email': 'alice@example.com', 'phone': '555-0101'},
    {'id': 2, 'name': 'Bob Smith', 'email': 'bob@example.com', 'phone': '555-0102'},
]
next_id = 3

@app.route('/')
def index():
    search = request.args.get('q', '').lower()
    if search:
        results = [c for c in contacts
                   if search in c['name'].lower() or search in c['email'].lower()]
    else:
        results = contacts

    # Return HTML list (simplified -- in production use render_template)
    rows = ''.join(
        f'{c["name"]}{c["email"]}'
        f'{c["phone"]}'
        for c in results
    )
    return f'''
    

Contact Book

Search:
{rows}
NameEmailPhone

Add Contact

''' @app.route('/add', methods=['GET', 'POST']) def add(): global next_id error = None if request.method == 'POST': name = request.form.get('name', '').strip() email = request.form.get('email', '').strip() phone = request.form.get('phone', '').strip() if not name or not email: error = 'Name and email are required.' else: contacts.append({'id': next_id, 'name': name, 'email': email, 'phone': phone}) next_id += 1 return redirect(url_for('index')) return f'''

Add Contact

{'

' + error + '

' if error else ''}
Name:
Email:
Phone:
''' @app.route('/api/contacts') def api_contacts(): return jsonify({'contacts': contacts, 'total': len(contacts)}) if __name__ == '__main__': app.run(debug=True, port=5000)

Test the API endpoint:

$ curl http://127.0.0.1:5000/api/contacts
{
  "contacts": [
    {"email": "alice@example.com", "id": 1, "name": "Alice Chen", "phone": "555-0101"},
    {"email": "bob@example.com", "id": 2, "name": "Bob Smith", "phone": "555-0102"}
  ],
  "total": 2
}

This app demonstrates routing, form handling with the Post/Redirect/Get pattern, query parameter reading (request.args.get()), and JSON API responses in a single file. To extend this into a production-ready app, add a database with Flask-SQLAlchemy, use proper Jinja2 templates instead of HTML strings, add Flask-WTF for form validation and CSRF protection, and deploy with Gunicorn behind an Nginx proxy.

Frequently Asked Questions

Is debug=True safe for production?

No — never use debug=True in production. Debug mode enables the Werkzeug interactive debugger, which lets anyone who triggers an exception run arbitrary Python code in your server. In production, run Flask with Gunicorn or uWSGI: gunicorn app:app. Set FLASK_ENV=production as an environment variable to disable debug mode.

How do I serve static files (CSS, JS, images) with Flask?

Create a folder named static next to your app.py. Flask automatically serves files from this folder at /static/filename. In templates, use url_for('static', filename='style.css') to generate the correct URL. For production, serve static files directly from Nginx or a CDN for better performance.

How do I connect a database to Flask?

The most common choice is Flask-SQLAlchemy, which wraps SQLAlchemy’s ORM with Flask integration. Install it with pip install flask-sqlalchemy and configure app.config['SQLALCHEMY_DATABASE_URI']. For a simpler option for small apps, use the built-in sqlite3 module directly. For production APIs, consider Flask-SQLAlchemy with PostgreSQL.

How do I handle 404 and 500 errors with custom pages?

Use the @app.errorhandler() decorator: @app.errorhandler(404) on a function that returns a custom error page. The function receives the error object as an argument. Return a tuple of (response, status_code) to ensure the correct HTTP status code is sent: return render_template('404.html'), 404.

What are Flask Blueprints?

Blueprints let you split a large Flask app into smaller, modular components. Each blueprint is like a mini Flask app with its own routes, templates, and static files. For example, you might have an auth blueprint for login/logout routes and a dashboard blueprint for the main app. Register blueprints with app.register_blueprint(auth_bp, url_prefix='/auth'). They’re the standard way to organize larger Flask applications.

Conclusion

Flask makes it possible to go from Python developer to web application builder in a single session. In this tutorial, you covered the core building blocks: creating a Flask app and defining routes with @app.route(), using dynamic URL segments with type converters, rendering HTML with Jinja2 templates, handling GET and POST requests with request.form.get(), implementing the Post/Redirect/Get pattern, and returning JSON responses with jsonify().

The contact book project ties all these concepts together. Extend it by adding Flask-SQLAlchemy for persistent storage, Flask-Login for user authentication, and deploying with Gunicorn on any cloud provider.

For deeper learning, see the official Flask documentation and the Jinja2 template documentation.