What is Werkzeug?

Last updated January 9th, 2024

This article explains what Werkzeug is and how Flask uses it for its core HTTP functionality. Along the way, you'll develop your own WSGI-compatible application using Werkzeug to create a Flask-like web framework!

This article assumes that you have prior experience with Flask. If you're interested in learning more about Flask, check out my course on how to build, test, and deploy a Flask application:

Developing Web Applications with Python and Flask

Contents

Flask Dependencies

You may have already noticed, but every time you install Flask, you also install the following dependencies:

  1. Blinker
  2. Click
  3. ItsDangerous
  4. Jinja
  5. MarkupSafe
  6. Werkzeug

Flask is a wrapper around all of them.

$ pip install Flask
$ pip freeze

blinker==1.7.0
click==8.1.7
Flask==3.0.0
itsdangerous==2.1.2
Jinja2==3.1.2
MarkupSafe==2.1.3
Werkzeug==3.0.1  # !!!

What is Werkzeug?

Werkzeug is a collection of libraries that can be used to create a WSGI (Web Server Gateway Interface) compatible web application in Python.

A WSGI (Web Server Gateway Interface) server is necessary for Python web applications since a web server cannot communicate directly with Python. WSGI is an interface between a web server and a Python-based web application.

Put another way, Werkzeug provides a set of utilities for creating a Python application that can talk to a WSGI server, like Gunicorn.

Want to learn more about WSGI?

Check out 'What is Gunicorn in Python?' and take a look at the Building a Python Web Framework course.

Werkzeug provides the following functionality (which Flask uses):

  1. Request processing
  2. Response handling
  3. URL routing
  4. Middleware
  5. HTTP utilities
  6. Exception handling

It also provides a basic development server with hot reloading.

Let's dive into an example of building a web application using Werkzeug. We'll also look at how Flask implements similar functionality.

Hello World App

As an introduction to Werkzeug, let's start by creating a "Hello World" app using some of the key functionality provided by Werkzeug.

You can find the source code for the project discussed in this article on GitLab: https://gitlab.com/patkennedy79/werkzeug_movie_app.

Installation

Start by creating a new project:

$ mkdir werkzeug_movie_app
$ cd werkzeug_movie_app
$ python3 -m venv venv
$ source venv/bin/activate
(venv)$

Install Werkzeug, Jinja2, and redis-py:

(venv)$ pip install Werkzeug Jinja2 redis
(venv)$ pip freeze > requirements.txt

Redis will be used as the data storage solution for storing movie data.

Application

Werkzeug is a collection of libraries used to build a WSGI-compatible web application. It doesn't provide a high-level class, like Flask, for scaffolding out a full web application. Instead, you need to create the application yourself from Werkzeug's libraries.

Create a new app.py file in the top-level folder of your project:

from werkzeug.wrappers import Request, Response


class MovieApp(object):
    """Implements a WSGI application for managing your favorite movies."""
    def __init__(self):
        pass

    def dispatch_request(self, request):
        """Dispatches the request."""
        return Response('Hello World!')

    def wsgi_app(self, environ, start_response):
        """WSGI application that processes requests and returns responses."""
        request = Request(environ)
        response = self.dispatch_request(request)
        return response(environ, start_response)

    def __call__(self, environ, start_response):
        """The WSGI server calls this method as the WSGI application."""
        return self.wsgi_app(environ, start_response)


def create_app():
    """Application factory function that returns an instance of MovieApp."""
    app = MovieApp()
    return app

The MovieApp class implements a WSGI-compatible web application, which processes requests from different users and generates responses back to the users. Here's the flow of how this class interfaces with a WSGI server:

Werkzeug Flow Diagram

When a request comes in, it's processed in wsgi_app():

def wsgi_app(self, environ, start_response):
    """WSGI application that processes requests and returns responses."""
    request = Request(environ)
    response = self.dispatch_request(request)
    return response(environ, start_response)

The environment (environ) is automatically processed in the Request class to create a request object. The request is then processed in dispatch_request(). For this initial example, dispatch_request() returns a response of 'Hello World!'. The response is then returned from wsgi_app().

Flask Comparison:

MovieApp is an simplified version of the Flask class.

Within the Flask class, the wsgi_app() is the actual WSGI application that interfaces with the WSGI server. Also, dispatch_request() and full_dispatch_request() are used to do the request dispatching, which matches the URL to the applicable view function and handles exceptions.

Development Server

Add the following code to the bottom of app.py to run the Werkzeug development server:

if __name__ == '__main__':
    # Run the Werkzeug development server to serve the WSGI application (MovieApp)
    from werkzeug.serving import run_simple
    app = create_app()
    run_simple('127.0.0.1', 5000, app, use_debugger=True, use_reloader=True)

Run the application:

(venv)$ python app.py

Navigate to http://127.0.0.1:5000 to see the 'Hello World!' Message.

Flask Comparison:

Within the Flask class, there's an equivalent run() method that utilizes the Werkzeug development server.

Middleware for Serving Static Files

In web applications, middleware is a software component that can be added to the request/response processing pipeline to perform a specific function.

One important function for a web server/application to perform is serving static files (CSS, JavaScript, and image files). Werkzeug provides a middleware for this functionality called SharedDataMiddleware.

SharedDataMiddleware is ideally suited for working with the Werkzeug development server to serve static files.

For a production environment, you'll want to switch out the Werkzeug development server and SharedDataMiddleware for a web server such as Nginx and a WSGI server such as Gunicorn.

To utilize SharedDataMiddleware, start by adding a new folder called "static" to the project with "css" and "img" folders:

├── app.py
├── requirements.txt
└── static
    ├── css
    └── img

Within the "static/img" folder, add the Flask logo from https://gitlab.com/patkennedy79/werkzeug_movie_app/-/blob/main/static/img/flask.png. Save it as flask.png.

Next, expand the application factory function:

def create_app():
    """Application factory function that returns an instance of MovieApp."""
    app = MovieApp()
    app.wsgi_app = SharedDataMiddleware(app.wsgi_app, {
        '/static': os.path.join(os.path.dirname(__file__), 'static')
    })
    return app

Update the imports at the top:

import os

from werkzeug.middleware.shared_data import SharedDataMiddleware
from werkzeug.wrappers import Request, Response

Now when a request is processed by the Werkzeug applicaiton (app), it will first be routed to SharedDataMiddleware to determine if a static file has been requested:

Werkzeug Middleware Diagram

If a static file is requested, SharedDataMiddleware will generate the response with the static file. Otherwise, the request is passed down the chain to the Werkzeug app for processing in wsgi_app().

To see SharedDataMiddleware in action, run the server and navigate to http://127.0.0.1:5000/static/img/flask.png to view the Flask logo.

For a full list of middleware solutions provided by Werkzeug, check out the Middleware docs.

Flask Comparison:

Flask does not utilize the SharedDataMiddleware. It takes a different approach for serving static files. By default, if a static folder exists, Flask automatically adds a new URL rule to serve the static files up.

To illustrate this concept, run flask routes in the top-level project of a Flask application and you will see:

(venv)$ flask routes

Endpoint     Methods  Rule
-----------  -------  -----------------------
index        GET      /
static       GET      /static/<path:filename>

Templates

As is typically done in a Flask project, we'll use Jinja for the templating engine for our app.

Start by adding a new folder called "templates" to the project:

├── app.py
├── requirements.txt
├── static
│   ├── css
│   └── img
│       └── flask.png
└── templates

In order to utilize Jinja, expand the constructor of the MovieApp class:

def __init__(self):
    """Initializes the Jinja templating engine to render from the 'templates' folder."""
    template_path = os.path.join(os.path.dirname(__file__), 'templates')
    self.jinja_env = Environment(loader=FileSystemLoader(template_path),
                                 autoescape=True)

Add the import:

from jinja2 import Environment, FileSystemLoader

Flask Comparison:

Flask utilizes Jinja Environment as well to create the templating engine.

Within the MovieApp class, add a new render_template() method:

def render_template(self, template_name, **context):
    """Renders the specified template file using the Jinja templating engine."""
    template = self.jinja_env.get_template(template_name)
    return Response(template.render(context), mimetype='text/html')

This method takes the template_name and any variables to pass to the templating engine (**context). It then generates a Response using the render() method from Jinja.

Flask Comparison:

Doesn't the render_template() function look familiar? The Flask flavor is one of the most used functions in Flask.

To see render_template() in action, update dispatch_request() to render a template:

def dispatch_request(self, request):
    """Dispatches the request."""
    return self.render_template('base.html')

All requests to the app will now render the templates/base.html template.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Werkzeug Movie App</title>

    <!-- CSS file for styling the application -->
    <link rel="stylesheet" href="/static/css/style.css" type="text/css">
</head>
<body>
    <h1>Werkzeug Movie App</h1>
    {% block body %}
    {% endblock %}
</body>
</html>

Make sure to add this template to the "templates" folder and save a copy of https://gitlab.com/patkennedy79/werkzeug_movie_app/-/blob/main/static/css/style.css to static/css/style.css.

Run the server. Navigate to http://127.0.0.1:5000. You should now see:

Werkzeug Routing Example - Homepage

Routing

Routing means to match the URL to the appropriate view function. Werkzeug provides a Map class that allows you to match URLs to view functions using Rule objects.

Let's create the Map object in the MovieApp constructor to illustrate how this works:

def __init__(self):
    """Initializes the Jinja templating engine to render from the 'templates' folder."""
    template_path = os.path.join(os.path.dirname(__file__), 'templates')
    self.jinja_env = Environment(loader=FileSystemLoader(template_path),
                                 autoescape=True)
    self.url_map = Map([
        Rule('/', endpoint='index'),
        Rule('/movies', endpoint='movies'),
    ])

Don't forget the import:

from werkzeug.routing import Map, Rule

Each Rule object defines a URL and the view function (endpoint) to call if the URL is matched:

    self.url_map = Map([
        Rule('/', endpoint='index'),
        Rule('/movies', endpoint='movies'),
    ])

For example, when the homepage ('/') is requested, the index view function should be called.

Flask Comparison:

One of the amazing features of Flask is the @route decorator, which is used to assign a URL to a view function. This decorator updates the url_map for the Flask app, similar to the hand-coded url_map that we defined above.

In order to utilize the URL mapping, dispatch_request() needs to be updated:

def dispatch_request(self, request):
    """Dispatches the request."""
    adapter = self.url_map.bind_to_environ(request.environ)
    try:
        endpoint, values = adapter.match()
        return getattr(self, endpoint)(request, **values)
    except HTTPException as e:
        return e

Now when a request comes in to dispatch_request(), the url_map will be utilized to attempt to match() the URL to an entry. If the URL requested is included in the url_map, then the applicable view function (endpoint) will be called. If the URL is not found in the url_map, then an exception is raised.

Exception handling will be covered shortly!

Add the import:

from werkzeug.exceptions import HTTPException

We've specified two view functions in the url_map, so let's create them now within the MovieApp class:

def index(self, request):
    return self.render_template('base.html')

def movies(self, request):
    return self.render_template('movies.html')

While templates/base.html was created in the previous section, templates/movies.html needs to be created now:

{% extends "base.html" %}

{% block body %}
<div class="table-container">
    <table>
        <!-- Table Header -->
        <thead>
            <tr>
                <th>Index</th>
                <th>Movie Title</th>
            </tr>
        </thead>

        <!-- Table Elements (Rows) -->
        <tbody>
            <tr>
                <td>1</td>
                <td>Knives Out</td>
            </tr>
            <tr>
                <td>2</td>
                <td>Pirates of the Caribbean</td>
            </tr>
            <tr>
                <td>3</td>
                <td>Inside Man</td>
            </tr>
        </tbody>
    </table>
</div>
{% endblock %}

This template file utilizes template inheritance to use base.html as the parent template. It generates a table of three movies.

http://127.0.0.1:5000 should look the same:

Werkzeug Routing Example - Homepage

However, if you navigate to http://127.0.0.1:5000/movies, you'll now see the table of movies:

Werkzeug Routing Example - List of Movies

Exception Handling

Try navigating to http://127.0.0.1:5000/movies2:

Werkzeug Exception Handling Example - Invalid URL

The page returned is the default error page when a URL is not found in url_map:

def dispatch_request(self, request):
    """Dispatches the request."""
    adapter = self.url_map.bind_to_environ(request.environ)
    try:
        endpoint, values = adapter.match()
        return getattr(self, endpoint)(request, **values)
    except HTTPException as e:
        return e

Additionally, you should see the following in the console:

127.0.0.1 - - [07/Mar/2021 12:13:17] "GET /movies2 HTTP/1.1" 404 -

Let's create a custom error page by expanding dispatch_request():

def dispatch_request(self, request):
    """Dispatches the request."""
    adapter = self.url_map.bind_to_environ(request.environ)
    try:
        endpoint, values = adapter.match()
        return getattr(self, endpoint)(request, **values)
    except NotFound:
        return self.error_404()
    except HTTPException as e:
        return e

Update the import:

from werkzeug.exceptions import HTTPException, NotFound

Now when a URL is not found in the url_map, it will be handled by calling error_404(). Create this new method within the MovieApp class:

def error_404(self):
    response = self.render_template("404.html")
    response.status_code = 404
    return response

Create templates/404.html:

{% extends "base.html" %}

{% block body %}
<div class="error-description">
    <h2>Page Not Found (404)</h2>
    <h4>What you were looking for is just not there!</h4>
    <h4><a href="/">Werkzeug Movie App</a></h4>
</div>
{% endblock %}

Now when you navigate to http://127.0.0.1:5000/movies2, you should see a friendly message:

Werkzeug Exception Handling Example - Custom Error Page

Flask Comparison:

When full_dispatch_request() in the Flask class detects an exception, it will be handled gracefully in handle_user_exceptions(). Flask allows custom error pages for all HTTP error codes as well.

Request Processing

In this section, we'll add a form to the app to allow the user to input their favorite movies.

Redis

As mentioned, we'll be using Redis, an in-memory data structure store, to persist the movies due to its fast read/write speed and ease of setup.

Install and run Redis.

The quickest way to get Redis up and running is with Docker:

$ docker run --name some-redis -d -p 6379:6379 redis

To check that the Redis container is running:

$ docker ps

To stop the running Redis container:

$ docker stop some-redis  # Use name of Docker container

If you're not a Docker user, check out these resources:

In order to utilize Redis, start by updating the MovieApp constructor to create an instance of StrictRedis:

def __init__(self, config):  # Updated!!
    """Initializes the Jinja templating engine to render from the 'templates' folder,
    defines the mapping of URLs to view methods, and initializes the Redis interface."""
    template_path = os.path.join(os.path.dirname(__file__), 'templates')
    self.jinja_env = Environment(loader=FileSystemLoader(template_path),
                                 autoescape=True)
    self.url_map = Map([
        Rule('/', endpoint='index'),
        Rule('/movies', endpoint='movies'),
    ])
    self.redis = StrictRedis(config['redis_host'], config['redis_port'],
                             decode_responses=True)  # New!!

Additionally, the constructor (__init__()) has an additional argument (config), which is used for creating the instance of StrictRedis.

Import:

from redis import StrictRedis

The configuration parameters that are passed in to the constructor need to be specified in the application factory function:

def create_app():
    """Application factory function that returns an instance of MovieApp."""
    app = MovieApp({'redis_host': '127.0.0.1', 'redis_port': 6379})
    app.wsgi_app = SharedDataMiddleware(app.wsgi_app, {
        '/static': os.path.join(os.path.dirname(__file__), 'static')
    })
    return app

Form Processing

In order to allow the user to add a movie to the Redis storage, we need to add a new view function in url_map:

def __init__(self, config):
    """Initializes the Jinja templating engine to render from the 'templates' folder,
    defines the mapping of URLs to view methods, and initializes the Redis interface."""

    ...

    self.url_map = Map([
        Rule('/', endpoint='index', methods=['GET']),
        Rule('/movies', endpoint='movies', methods=['GET']),
        Rule('/add', endpoint='add_movie', methods=['GET', 'POST']),  # !!!
    ])

    ...

The Rule entries in url_map have been expanded to specify the HTTP methods that are allowed for each URL. Additionally, the '/add' URL has been added:

Rule('/add', endpoint='add_movie', methods=['GET', 'POST']),

If the '/add' URL is requested with either the GET or POST methods, then the add_movie() view function will be called.

Next, we need to create the add_movie() view function in the MovieApp class:

def add_movie(self, request):
    """Adds a movie to the list of favorite movies."""
    if request.method == 'POST':
        movie_title = request.form['title']
        self.redis.lpush('movies', movie_title)
        return redirect('/movies')
    return self.render_template('add_movie.html')

Import:

from werkzeug.utils import redirect

If a GET request is made to '/add', then add_movie() will render the templates/add_movie.html file. If a POST request is made to '/add', then the form data is stored in the Redis storage in the movies list and the user is redirected to the list of movies.

Create the templates/add_movie.html template file:

{% extends "base.html" %}

{% block body %}
<div class="form-container">
    <form method="post">
        <div class="field">
            <label for="movieTitle">Movie Title:</label>
            <input type="text" id="movieTitle" name="title"/>
        </div>
        <div class="field">
            <button type="submit">Submit</button>
        </div>
    </form>
</div>
{% endblock %}

Display Movies

Since we're now storing the movies in Redis, the movie() view function needs to be updated to read from the movies list in Redis:

def movies(self, request):
    """Displays the list of favorite movies."""
    movies = self.redis.lrange('movies', 0, -1)
    return self.render_template('movies.html', movies=movies)

The list of movies is being passed to the templates/movies.html template file, which needs to be updated to loop through this list to create the table of movies:

{% extends "base.html" %}

{% block body %}
<div class="table-container">
    <table>
        <!-- Table Header -->
        <thead>
            <tr>
                <th>Index</th>
                <th>Movie Title</th>
            </tr>
        </thead>

        <!-- Table Elements (Rows) -->
        <tbody>
            {% for movie in movies %}
            <tr>
                <td>{{ loop.index }}</td>
                <td>{{ movie }}</td>
            </tr>
            {% endfor %}
        </tbody>
    </table>
</div>
{% endblock %}

To see the form processing in action, start by navigating to http://127.0.0.1:5000/add and adding a new movie:

Werkzeug Request Processing Example

After submitting the form, you should be automatically redirected to the list of movies (which may include movies previously added):

Processing Example - List of Movies

That's it!

Why Not Use Werkzeug Instead of Flask?

Werkzeug provides much of the key functionality found in Flask, but Flask adds a number of powerful features, like:

  1. Sessions
  2. Application and Request contexts
  3. Blueprints
  4. Request callback functions
  5. Utilities:
    1. @route decorator
    2. url_for() function
  6. CLI commands
  7. Exception handling
  8. Test client
  9. Flask shell
  10. Logging
  11. Signals
  12. Extensions

As with any web framework -- Don't re-invent the wheel! Flask is a much better option (when compared to Werkzeug) for web development based on its rich feature set and large collection of extensions.

Conclusion

This article provided an overview of Werkzeug, which is one of the key components of Flask, by showing how to build a simple web application using Werkzeug. While it's important to understand how the underlying libraries work in Flask, the complexity of creating a web application using Werkzeug should illustrate how easy it is to develop a web app using Flask!

Additionally, if you're interested in learning how to test a Werkzeug application, check out the tests for the Werkzeug Movie App: https://gitlab.com/patkennedy79/werkzeug_movie_app/-/tree/main/tests.

If you'd like to learn more about Flask, be sure to check out my course -- Developing Web Applications with Python and Flask.

Cheers!

Patrick Kennedy

Patrick Kennedy

Patrick is a software engineer from the San Francisco Bay Area with experience in C++, Python, and JavaScript. His favorite areas of teaching are Vue and Flask. In his free time, he enjoys spending time with his family and cooking.

Share this tutorial

Featured Course

Developing Web Applications with Python and Flask

This course focuses on teaching the fundamentals of Flask by building and testing a web application using Test-Driven Development (TDD).

Featured Course

Developing Web Applications with Python and Flask

This course focuses on teaching the fundamentals of Flask by building and testing a web application using Test-Driven Development (TDD).