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 of the functionality you need.

In this article we’ll walk through setting up and using Flask 2.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 2.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 async, use pip install flask[async]. See the “Using async” section below for more on this.

A basic Flask app

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

from flask import Flask

app = Flask(__name__)

@app.route("https://www.infoworld.com/")
def home():
    return "Hello, world"

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

Here is 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.
  • 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 just the site root ("https://www.infoworld.com/") and the response is just the string "Hello, world".

To run the app, 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 “Hello, world.”

Note that you can name the main file of your Flask application anything, but calling it app.py allows Flask to 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 app 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 below.

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 /hi/ followed by a name, 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.

Route variables can also be type-constrained. 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 as datapath. For instance, if the route were /show/<path:datapath> and we used the URL /show/main/info, then main/info would be passed along as 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.

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()
    else:
        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 also lets you use app.get and app.post as shortcuts. The above routes could also be decorated 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 error message, and the 404 error code. 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, and pass in variables to be used in the template.

Here is 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 app, whenever a 404 error is generated, the result returned to the client will be generated by the page_not_found function. 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 on deploying to most common hosting options, as well as details on how to host Flask apps 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. With coroutines now a standard feature in Python, Flask 2.0 supports async methods for route handlers. However, async support comes as an add-on. You need to use pip install flask[async] to install this feature.

@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 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 © 2021 IDG Communications, Inc.


#started #Flask

Source link