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, calledapp
. 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 a200 OK
status code and a MIME type oftext/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
])
- (response, status code [
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.