Get started with Flask 3.0

Dive head-first into one of the most popular and versatile web frameworks for Python with this quick-start tutorial.

Get started with Flask 3.0
Thinkstock

One reason Python is a prime choice for web development is the breadth of web frameworks available in the language. Among the most popular and useful is Flask, which lets you start simple (“one drop at a time”) but grows with your application to add just about all the functionality you need.

In this article, we’ll walk through setting up and using Flask 3.0 for basic web apps. We’ll also touch on using Jinja2 for templating and dealing with common issues like changing response types and handling redirects.

Setting up Flask

Flask 3.0 is easy to set up. Use pip install flask to install both Flask and all of its dependencies including the Jinja2 templating system.

As with any Python framework, it’s best to create a project using Flask inside a Python virtual environment. This isolates your project from your main Python installation and from other projects that might use Flask and its dependencies (as you might find yourself maintaining different versions for different projects).

Note that if you want to install Flask with support for asynchronous functions or coroutines, use pip install flask[async]. I'll have more about using Flask with async shortly.

A basic Flask application

A simple, one-route Flask application can be written in only a few lines of code. Save the following simple example application in a file named app.py:


from flask import Flask

app = Flask(__name__)

@app.route("/")
def home():
    return "Hello, world"

This application doesn’t do much—it just creates a website with a single route that displays “Hello, world” in the browser.

Here's what each element does:

  • The line app = Flask(__name__) creates a new instance of a Flask application, called app. The Flask class takes an argument that is the name of the application’s module or package. Passing it __name__ (the name of the current module) is a quick way to use the current module as the app’s starting point. In theory, you can use any name, but it's customary to use the module name as a default.
  • The app.route decorator is used to wrap a function and indicate the function to use to deliver a response for a given route. In this case, the route is the site root ("/") and the response is the string "Hello, world".

To run the application, use python -m flask run in the same directory as app.py. You should see something like the following in the console:


 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

If you open a web browser to http://127.0.0.1:5000/, you should see the words, “Hello, world.”

Note that you can name the main file of your Flask application anything, but calling it app.py lets Flask recognize it automatically. To use a different name, you need to first set the FLASK_APP environment variable to the name of the new file minus its extension (e.g., hello for hello.py).

Also note that when you run a Flask application in this fashion, you’re running it using Flask’s built-in test server, which isn’t suited for production deployments. We’ll discuss how to deploy Flask in production soon.

Routes and route variables in Flask

Web applications typically use components of a route as variables that are passed to the route function. Flask lets you do this by way of a special route definition syntax.

In this example, where we have a route in the format of /hi/ followed by a username, the name is extracted and passed along to the function as the variable username.


@app.route("/hi/<username>")
def greet(username):
    return f"Hello, {username}"

Visit this route with /hi/Serdar, and you’ll see “Hello, Serdar” in the browser.

Types for Flask route variables

Route variables can also be type-constrained, by adding type information to them.

For instance, if you use <int:userid>, that ensures userid will only be an integer. If you use <path:datapath>, the part of the URL from that position forward will be extracted into the variable datapath. To that end, if we had a route like /show/<path:datapath>, and used the URL /show/main/info, then main/info would be passed in the variable datapath. (See the Flask documentation for more about type-constraining route variables.)

Note that you need to be careful about using multiple, similar paths with different data types. If you have the route /data/<int:userid> and the route /data/<string:username>, any element in the second position that can’t be matched as an integer will be matched as a string. Avoid these kinds of route structures if you can, as they can become confusing and difficult to debug. Be as unambiguous as you can with your routes.

Route methods in Flask

Route decorators can also specify the methods used to access the route. You can create multiple functions to handle a single route with different methods, like this:


@app.route('/post', methods=['GET'])
def post_message_route_get():
    return show_post_message_form()

@app.route('/post', methods=['POST'])
def post_message_route_post():
    return post_message_to_site()

Or, you can consolidate routes into a single function, and make decisions internally based on the method:


from flask import request

@app.route('/post', methods=['GET', 'POST'])
def post_message_route():
    if request.method == 'POST':
        return post_message_to_site()
    # defaults to GET if not POST
    return show_post_message_form()

Note that we need to import the global request object to access the method property. We’ll explore this in detail later.

Flask 2.0 and higher also let you use app.get and app.post as shortcuts. The above routes could also be expressed as:


@app.get('/post')
def post_message_route_get():
    return show_post_message_form()

@app.post('/post')
def post_message_route_post():
    return post_message_to_site()

Request data in Flask

In the last section, we obtained the method used to invoke a route from the global request object. request is an instance of the Request object, from which we can obtain many other details about the request—its headers, cookies, form data, file uploads, and so on.

Some of the common properties of a Request object include:

  • .args: A dictionary that holds the URL parameters. For instance, a URL with arguments like ?id=1 would be expressed as the dictionary {"id": 1}.
  • .cookies: A dictionary that holds any cookies sent in the request.
  • .files: A dictionary that contains any files uploaded with the request, with the key for each element being the file’s name.
  • .form: A dictionary that contains the request’s form data, if any.
  • .headers: The raw headers for the request.
  • .method: The method used by the request (e.g., GET, POST).

Returning responses in Flask

When a route function returns data, Flask makes a best guess to interpret what has been returned:

  • Response objects are returned as-is. Creating a response object gives you fine-grained control over what you return to the client, but for most use cases you can use one of the items below.
  • Strings, including the output of Jinja2 templates (more on this next), are converted into Response objects, with a 200 OK status code and a MIME type of text/html.
  • Dictionaries are converted into JSON.
  • Tuples can be any of the following:
    • (response, status code [int])
    • (response, headers [list/dict])
    • (response, status code [int], headers [list/dict])

Generally, it’s best to return whatever makes clearest the route function’s job. For instance, a 404 error handler can return a 2-tuple—the 404 error code, and the error message details. This keeps the route function uncluttered.

Templates in Flask

Flask includes the Jinja2 template engine to programmatically generate HTML output from data. You use the render_template function to generate HTML, then pass in variables to be used in the template.

Here's an example of how this looks in a route:


from flask import render_template

@app.route('/hi/<username>')
def greet(username=None):
    return render_template('hello.html', username=username)

Templates referred to by render_template are by default found in a subdirectory of the Flask project directory, named templates. To that end, the following file would be in templates/hello.html:


<!doctype html>
<title>Hi there</title>
{% if username %}
  <h1>Hello {{ username }}!</h1>
{% else %}
  <h1>Hello, whoever you are!</h1>
{% endif %}

Jinja2 templates are something of a language unto themselves, but this snippet should give you an idea of how they work. Blocks delineated with {% %} contain template logic, and blocks with {{ }} contain expressions to be inserted at that point. When we called this template with render_template above, we passed username as a keyword argument; the same would be done for any other variables we’d use.

Note that Jinja2 templates have constraints on the code that can be run inside them, for security’s sake. Therefore, you will want to do as much of the processing as possible for a given page before passing it to a template.

Error handlers in Flask

To create a route that handles a particular class of server error, use the errorhandler decorator:


@app.errorhandler(404)
def page_not_found(error):
    return f"error: {error}"

For this application, whenever a 404 error is generated, the result returned to the client will be generated by the page_not_found function. The error is the exception generated by the application, so you can extract more details from it if needed and pass them back to the client.

Running and debugging Flask in production

The Flask test server mentioned earlier in this article isn’t suitable for deploying Flask in production. For production deployments, use a full WSGI-compatible server, with the app object created by Flask() as the WSGI application.

Flask's documentation has details about deploying to most common hosting options, as well as details for how to host Flask applications yourself—e.g., by way of Apache's mod_wsgi or via uWSGI on Nginx.

Using async in Flask

Originally, Flask had no explicit support for asynchronous functions or coroutines. Coroutines are now a standard feature in Python, and as of version 2.0, Flask supports async methods for route handlers. However, async support in Flask comes as an add-on, so you need to use pip install flask[async] to install this feature.

Here's an example of a Flask async route:


@app.route("/embed/<embed_id>")
async def get_embed(embed_id):
    data = await async_render_embed(embed_id)
    return data

Flask’s async support doesn’t change the fact that it runs as a WSGI application with a single worker to handle incoming requests. If you want to support long-running requests such as WebSocket API connections, using async only in your route functions will not be enough. You may want to consider using the Quart framework, which is API-compatible with Flask but uses the ASGI interface to better handle long-running requests and multiple concurrent requests.

Copyright © 2024 IDG Communications, Inc.