Image Processing in Python with Pillow


A lot of applications use digital images, and with this there is usually a need to process the images used. If you are building your application with Python and need to add image processing features to it, there are various libraries you could use. Some popular ones are OpenCVscikit-imagePython Imaging Library and Pillow.

We won’t debate on which library is the best here, they all have their merits. This article will focus on Pillow, a library that is powerful, provides a wide array of image processing features, and is simple to use.

Pillow is a fork of the Python Imaging Library (PIL). PIL is a library that offers several standard procedures for manipulating images. It’s a powerful library, but hasn’t been updated since 2011 and doesn’t support Python 3. Pillow builds on this, adding more features and support for Python 3. It supports a range of image file formats such as PNG, JPEG, PPM, GIF, TIFF and BMP. We’ll see how to perform various operations on images such as cropping, resizing, adding text to images, rotating, greyscaling, e.t.c using this library.

Installation and Project Setup

Before installing Pillow, there are some prerequisites that must be satisfied. These vary for different operating systems. We won’t list the different options here, you can find the prerequisites for your particular OS in this installation guide.

After installing the prerequisite libraries, you can install Pillow with `pip:

$ pip install Pillow

To follow along, you can download the images (coutesy of Unsplash) that we’ll use in the article. You can also use your own images.

All examples will assume the required images are in the same directory as the python script file being run.

The Image Object

A crucial class in the Python Imaging Library is the Image class. It is defined in the Image module and provides a PIL image on which manipulation operations can be carried out. An instance of this class can be created in several ways: by loading images from a file, creating images from scratch or as a result of processing other images. We’ll see all these in use.

To load an image from a file, we use the open() function in the Image module passing it the path to the image.

from PIL import Image

image ='unsplash_01.jpg')

If successful, the above returns an Image object. If there was a problem opening the file, an IOError exception will be raised.

After obtaining an Image object, you can now use the methods and attributes defined by the class to process and manipulate it. Let’s start by displaying the image. You can do this by calling the show() method on it. This displays the image on an external viewer (usually xv on Unix, and the Paint program on Windows).

You can get some details about the image using the object’s attributes.

# The file format of the source file.
print(image.format) # Output: JPEG

# The pixel format used by the image. Typical values are “1”, “L”, “RGB”, or “CMYK.”
print(image.mode) # Output: RGB

# Image size, in pixels. The size is given as a 2-tuple (width, height).
print(image.size) # Output: (1200, 776)

# Colour palette table, if any.
print(image.palette) # Output: None

For more on what you can do with the Image class, check out the documentation.

Changing Image Type

When you are done processing an image, you can save it to file with the save()method, passing in the name that will be used to label the image file. When saving an image, you can specify a different extension from its original and the saved image will be converted to the specified format.

image ='unsplash_01.jpg')'new_image.png')

The above creates an Image object loaded with the unsplash_01.jpg image and saves it to a new file new_image.png. Pillow sees the file extension has been specified as PNG and so it converts it to PNG before saving it to file. You can provide a second argument to save() to explicitly specify a file format. This'new_image.png', 'PNG') will do the same thing as the previous save(). Usually it’s unnecessary to supply this second argument as Pillow will determine the file storage format to use from the filename extension, but if you’re using non-standard extensions, then you should always specify the format this way.

Resizing Images

To resize an image, you call the resize() method on it, passing in a two-integer tuple argument representing the width and height of the resized image. The function doesn’t modify the used image, it instead returns another Image with the new dimensions.

image ='unsplash_01.jpg')
new_image = image.resize((400, 400))'image_400.jpg')

print(image.size) # Output: (1200, 776)
print(new_image.size) # Output: (400, 400)

The resize() method returns an image whose width and height exactly match the passed in value. This could be what you want, but at times you might find that the images returned by this function aren’t ideal. This is mostly because the function doesn’t account for the image’s Aspect Ratio, so you might end up with an image that either looks stretched or squished.

You can see this in the newly created image from the above code: image_400.jpg. It looks a bit squished horizontally.

Resized Image

If you want to resize images and keep their aspect ratios, then you should instead use the thumbnail() function to resize them. This also takes a two-integer tuple argument representing the maximum width and maximum height of the thumbnail.

image ='unsplash_01.jpg')
image.thumbnail((400, 400))'image_thumbnail.jpg')

print(image.size) # Output: (400, 258)

The above will result in an image sized 400×258, having kept the aspect ratio of the original image. As you can see below, this results in a better looking image.

Thumbnail Image

Another significant difference between the resize() and thumbnail()functions is that the resize() function ‘blows out’ an image if given parameters that are larger than the original image, while the thumbnail() function doesn’t. For example, given an image of size 400×200, a call to resize((1200, 600)) will create a larger sized image 1200×600, thus the image will have lost some definition and is likely to be blurry compared to the original. On the other hand, a call to thumbnail((1200, 600)) using the original image, will result in an image that keeps its size 400×200 since both the width and height are less than the specified maximum width and height.


When an image is cropped, a rectangular region inside the image is selected and retained while everything else outside the region is removed. With the Pillow library, you can crop an image with the crop() method of the Image class. The method takes a box tuple that defines the position and size of cropped region and returns an Image object representing the cropped image. The coordinates for the box are (left, upper, right, lower). The cropped section includes the left column and the upper row of pixels and goes up to (but doesn’t include) the right column and bottom row of pixels. This is better explained with an example.

image ='unsplash_01.jpg')
box = (150, 200, 600, 600)
cropped_image = image.crop(box)'cropped_image.jpg')

This is the resulting image:

Cropped Image

The Python Imaging Library uses a coordinate system that starts with (0, 0) in the upper left corner. The first two values of the box tuple specify the upper left starting position of the crop box. The third and fourth values specify the distance in pixels from this starting position towards the right and bottom direction respectively. The coordinates refer to positions between the pixels, so the region in the above example is exactly 450×400 pixels.

Pasting an Image onto Another Image

Pillow enables you to paste an image onto another one. Some example use cases where this could be useful is in the protection of publicly available images by adding watermarks on them, the branding of images by adding a company logo and in any other case where there is a need to merge two images.

Pasting is done with the paste() function. This modifies the Image object in place, unlike the other processing functions we’ve looked at so far that return a new Image object. Because of this, we’ll first make a copy our demo image before performing the paste, so that we can continue with the other examples with an unmodified image.

image ='unsplash_01.jpg')
logo ='logo.png')
image_copy = image.copy()
position = ((image_copy.width - logo.width), (image_copy.height - logo.height))
image_copy.paste(logo, position)'pasted_image.jpg')

In the above, we load in two images unsplash_01.jpg and logo.png, then make a copy of the former with copy(). We want to paste the logo image onto the copied image and we want it to be placed on the bottom right corner. This is calculated and saved in a tuple. The tuple can either be a 2-tuple giving the upper left corner, a 4-tuple defining the left, upper, right, and lower pixel coordinate, or None (same as (0, 0)). We then pass this tuple to paste() together with the image that will be pasted.

You can see the result below.

Pasted Image With Solid Pixels

That’s not the result we were expecting.

By default, when you perform a paste, transparent pixels are pasted as solid pixels, thus the black (white on some OSs) box surrounding the logo. Most of the times, this isn’t what you want. You can’t have your watermark covering the underlying image’s content. We would rather have transparent pixels appear as such.

To achieve this, you need to pass in a third argument to the paste() function. This argument is the transparency mask Image object. A mask is an Image object where the alpha value is significant, but its green, red, and blue values are ignored. If a mask is given, paste() updates only the regions indicated by the mask. You can use either 1L or RGBA images for masks. Pasting an RGBA image and also using it as the mask would paste the opaque portion of the image but not its transparent background. If you modify the paste as shown below, you should have a pasted logo with transparent pixels.

image_copy.paste(logo, position, logo)

Pasted Image With Transparent Pixels

Rotating Images

You can rotate images with Pillow using the rotate() method. This takes an integer or float argument representing the degrees to rotate an image and returns a new Image object of the rotated image. The rotation is done counterclockwise.

image ='unsplash_01.jpg')

image_rot_90 = image.rotate(90)'image_rot_90.jpg')

image_rot_180 = image.rotate(180)'image_rot_180.jpg')

In the above, we save two images to disk: one rotated at 90 degrees, the other at 180. The resulting images are shown below.

90 Degrees Rotation

180 Degrees Rotation

By default, the rotated image keeps the dimensions of the original image. This means that for angles other than multiples of 180, the image will be cut and/or padded to fit the original dimensions. If you look closely at the first image above, you’ll notice that some of it has been cut to fit the original height and its sides have been padded with a black background (transparent pixels on some OSs) to fit the original width. The example below shows this more clearly.


The resulting image is shown below:

18 Degrees Rotation

To expand the dimensions of the rotated image to fit the entire view, you pass a second argument to rotate() as shown below.

image.rotate(18, expand=True).save('image_rot_18.jpg')

Now the contents of the image will be fully visible, and the dimensions of the image will have increased to account for this.

18 Degrees Rotation With Expanded Edges

Flipping Images

You can also flip images to get their mirror version. This is done with the transpose() function. It takes one of the following options: PIL.Image.FLIP_LEFT_RIGHTPIL.Image.FLIP_TOP_BOTTOMPIL.Image.ROTATE_90PIL.Image.ROTATE_180PIL.Image.ROTATE_270 or PIL.Image.TRANSPOSE.

image ='unsplash_01.jpg')

image_flip = image.transpose(Image.FLIP_LEFT_RIGHT)'image_flip.jpg')

The resulting image can be seen below.

Flipped Image

Drawing on Images

With Pillow, you can also draw on an image using the ImageDraw module. You can draw lines, points, ellipses, rectangles, arcs, bitmaps, chords, pieslices, polygons, shapes and text.

from PIL import Image, ImageDraw

blank_image ='RGBA', (400, 300), 'white')
img_draw = ImageDraw.Draw(blank_image)
img_draw.rectangle((70, 50, 270, 200), outline='red', fill='blue')
img_draw.text((70, 250), 'Hello World', fill='green')'drawn_image.jpg')

In the example, we create an Image object with the new() method. This returns an Image object with no loaded image. We then add a rectangle and some text to the image before saving it.

Drawing on Image

Color Transforms

The Pillow library enables you to convert images between different pixel representations using the convert() method. It supports conversions between L (greyscale), RGB and CMYK modes.

In the example below we convert the image from RGBA to L mode which will result in a black and white image.

image ='unsplash_01.jpg')

greyscale_image = image.convert('L')'greyscale_image.jpg')

Greyscale Image

Aside: Adding Auth0 Authentication to a Python Application

Before concluding the article, let’s take a look at how you can add authentication using Auth0 to a Python application. The application we’ll look at is made with Flask, but the process is similar for other Python web frameworks.

Auth0 offers a generous free tier to get started with modern authentication.

Instead of creating an application from scratch, I’ve put together a simple app that you can download to follow along. It is a simple gallery application, that enables the user to upload images to a server and view the uploaded images.

If you downloaded the project files, you will find two folders inside the main directory: complete_without_auth0 and complete_with_auth0. As the name implies, complete_without_auth0 is the project we’ll start with and add Auth0 to.

To run the code, it’s better to create a virtual environment and install the needed packages there. This prevents package clutter and version conflicts in the system’s global Python interpreter.

We’ll cover creating a virtual environment with Python 3. This version supports virtual environments natively, and doesn’t require downloading an external utility (virtualenv) as is the case with Python 2.7. Any version above 3.0 will do.

After downloading the code files, change your Terminal to point to the completed_without_auth0/gallery_demo folder.

$ cd path/to/complete_without_auth0/gallery_demo

Create the virtual environment with the following command.

$ python3 -m venv venv

Then activate it with (on MacOS and Linux):

$ source venv/bin/activate

On Windows:

$ venv\Scripts\activate

To complete the set-up, install the packages listed in the requirements.txt file with:

$ pip3 install -r requirements.txt

This will install flaskflask-bootstrapdotenvpillowrequestspackages and their dependencies.

Then finally, run the app.

$ python

Open http://localhost:3000/ in your browser and you should see the following page.

Index page

When you head over to http://localhost:3000/gallery, you’ll see a blank page. You can head over to http://localhost:3000/upload and upload some images that will then appear in the Gallery.

Upload page

Gallery page

When an image is uploaded, a smaller copy of it is made with the thumbnail()function we looked at earlier, then the two images are saved – the original to the images folder and the thumbnail to the thumbnails folder.

The gallery displays the smaller sized thumbnails and only shows the larger image (inside a modal) when a thumbnail is clicked.

As the app stands, any user can upload an image. This might not be ideal. It might be better to put some protection over this action to prevent abuse or to at least track user uploads. This is where Auth0 comes in. With Auth0, we’ll be able to add authentication to the app with a minimum amount of work.

For the simplicity of the app, most of its functionality is in the file. Here, you can see the set route handlers. The upload() function handles calls to /upload. This is where images are processed before getting saved. We’ll secure this route with Auth0.

from flask import Flask, render_template, redirect, url_for, send_from_directory, request
from flask_bootstrap import Bootstrap
from PIL import Image
from werkzeug.utils import secure_filename
import os

app = Flask(__name__)

APP_ROOT = os.path.dirname(os.path.abspath(__file__))
images_directory = os.path.join(APP_ROOT, 'images')
thumbnails_directory = os.path.join(APP_ROOT, 'thumbnails')
if not os.path.isdir(images_directory):
if not os.path.isdir(thumbnails_directory):

def index():
    return render_template('index.html')

def gallery():
    thumbnail_names = os.listdir('./thumbnails')
    return render_template('gallery.html', thumbnail_names=thumbnail_names)

def thumbnails(filename):
    return send_from_directory('thumbnails', filename)

def images(filename):
    return send_from_directory('images', filename)

def static_files(filename):
    return send_from_directory('./public', filename)

@app.route('/upload', methods=['GET', 'POST'])
def upload():
    if request.method == 'POST':
        for upload in request.files.getlist('images'):
            filename = upload.filename
            # Always a good idea to secure a filename before storing it
            filename = secure_filename(filename)
            # This is to verify files are supported
            ext = os.path.splitext(filename)[1][1:].strip().lower()
            if ext in set(['jpg', 'jpeg', 'png']):
                print('File supported moving on...')
                return render_template('error.html', message='Uploaded files are not supported...')
            destination = '/'.join([images_directory, filename])
            # Save original image
            # Save a copy of the thumbnail image
            image =
            image.thumbnail((300, 170))
  '/'.join([thumbnails_directory, filename]))
        return redirect(url_for('gallery'))
    return render_template('upload.html')

if __name__ == '__main__':'', port=os.environ.get('PORT', 3000))

Setting up Auth0

To set up the app with Auth0, first sign up for an Auth0 account, then navigate to the Dashboard. Click on the New Client button and fill in the name of the client (or leave it at its default. Select Regular Web Applications from the Client type list. On the next page, select the Settings tab where the client ID, client Secret and Domain can be retrieved. Set the Allowed Callback URLs to http://localhost:3000/callback and Allowed Logout URLs to http://localhost:3000/ then save the changes with the button at the bottom of the page.

Auth0 provides the simplest and easiest to use user interface tools to help administrators manage user identities including password resets, creating and provisioning, blocking and deleting users. A generous free tier is offered so you can get started with modern authentication.

Back in your project, create a file labelled .env and save it at the root of the project. Add your Auth0 client credentials to this file. If you are using versioning, remember to not put this file under versioning. We’ll use the value of SECRET_KEY as the app’s secret key. You can/should change it.

AUTH0_CALLBACK_URL = 'http://localhost:3000/callback'
SECRET_KEY = 'F12ZMr47j\3yXgR~X@H!jmM]6Lwf/,4?KT'

Add another file named to the root directory of the project and add the following constants to it.

ACCESS_TOKEN_KEY = 'access_token'
APP_JSON_KEY = 'application/json'
AUTHORIZATION_CODE_KEY = 'authorization_code'
CLIENT_ID_KEY = 'client_id'
CODE_KEY = 'code'
CONTENT_TYPE_KEY = 'content-type'
GRANT_TYPE_KEY = 'grant_type'
PROFILE_KEY = 'profile'
REDIRECT_URI_KEY = 'redirect_uri'

Next, modify the beginning of the file as shown (from the first statement to the point where Bootstrap is instantiated).

from flask import Flask, render_template, redirect, url_for, send_from_directory, request, session
from flask_bootstrap import Bootstrap
from PIL import Image
from werkzeug.utils import secure_filename
from dotenv import Dotenv
from functools import wraps
import os
import constants
import requests

# Load Env variables
env = None

    env = Dotenv('./.env')
except IOError:
    env = os.environ

app = Flask(__name__)
app.secret_key = env['SECRET_KEY']

Here we’ve imported the sessiondotenvfunctoolsconstants and requests modules. There is no need to install any new package. These had already been installed when you ranpip install previously.

We use Dotenv to load environment variables into the file from either the .envfile or from the variables set on the System.

We then set the app’s secret_key. The application will make use of sessions, which allows storing information specific to a user from one request to the next. This is implemented on top of cookies and signs the cookies cryptographically. What this means is that someone could look at the contents of your cookie but not be able to make out the underlying credentials or to successfully modify it, unless they know the secret key used for signing.

Add the following functions to the file. You can add them before the route handler definitions.

# Requires authentication decorator
def requires_auth(f):
    def decorated(*args, **kwargs):
        if is_logged_in():
            return f(*args, **kwargs)
        return redirect('/')
    return decorated

def is_logged_in():
    return constants.PROFILE_KEY in session

Here we define a decorator that will ensure that a user is authenticated before they can access a specific route. The second function simply returns True or False depending on whether there is some user data from Auth0 stored in the session object.

modify the index() and upload() functions as shown.

def index():
    return render_template('index.html', env=env, logged_in=is_logged_in())

@app.route('/upload', methods=['GET', 'POST'])
def upload():
    if request.method == 'POST':
        for upload in request.files.getlist('images'):
            filename = upload.filename
            # Always a good idea to secure a filename before storing it
            filename = secure_filename(filename)
            # This is to verify files are supported
            ext = os.path.splitext(filename)[1][1:].strip().lower()
            if ext in set(['jpg', 'jpeg', 'png']):
                print('File supported moving on...')
                return render_template('error.html', message='Uploaded files are not supported...')
            destination = '/'.join([images_directory, filename])
            # Save original image
            # Save a copy of the thumbnail image
            image =
            image.thumbnail((300, 170))
  '/'.join([thumbnails_directory, filename]))
        return redirect(url_for('gallery'))
    return render_template('upload.html', user=session[constants.PROFILE_KEY])

In index() we pass some variables to the index.html template. We’ll use these later on.

We add the @requires_auth decorator to the upload() function. This will ensure that calls to /upload can only be successful if the user is logged in. Not only will an unauthenticated user not be able to access the upload.html page, but they also won’t be able to POST data to the route.

At the end of the function, we pass a user variable to the upload.htmltemplate.

Next, add the following function to the file.

def callback_handling():
    code = request.args.get(constants.CODE_KEY)
    json_header = {constants.CONTENT_TYPE_KEY: constants.APP_JSON_KEY}
    token_url = 'https://{auth0_domain}/oauth/token'.format(
    token_payload = {
        constants.CLIENT_ID_KEY: env[constants.AUTH0_CLIENT_ID],
        constants.REDIRECT_URI_KEY: env[constants.AUTH0_CALLBACK_URL],
        constants.CODE_KEY: code,
        constants.GRANT_TYPE_KEY: constants.AUTHORIZATION_CODE_KEY

    token_info =, json=token_payload,

    user_url = 'https://{auth0_domain}/userinfo?access_token={access_token}'\

    user_info = requests.get(user_url).json()
    session[constants.PROFILE_KEY] = user_info
    return redirect(url_for('upload'))

The above will be called by the Auth0 server after user authentication. It is the path that we added to Allowed Callback URLs on the Auth0 Dashboard. Here, we use the data sent from Auth0 to make a few more calls to the Auth0 server to eventually get the user’s profile. This will have various details regarding the user. We then save this to the session object.

Modify templates/index.html as shown below.

{% extends "base.html" %}

{% block content %}
<div class="container">
    <div class="row">
      <h1>Hi There!!!</h1>
      <p>Welcome to The Gallery</p>
      {% if logged_in %}
      <p>You can <a href="{{ url_for('upload') }}">upload images</a> or head over to the <a href="{{ url_for('gallery') }}">gallery</a></p>
      {% else %}
      <p><a href="#" class="login">Login</a> to upload images or head over to the <a href="{{ url_for('gallery') }}">gallery</a></p>
      {% endif %}

{% endblock %}

{% block scripts %}
<script src=""></script>
  var AUTH0_CLIENT_ID = '{{env.AUTH0_CLIENT_ID}}';
  var AUTH0_DOMAIN = '{{env.AUTH0_DOMAIN}}';
  var AUTH0_CALLBACK_URL = '{{env.AUTH0_CALLBACK_URL if env.AUTH0_CALLBACK_URL else "http://localhost:3000/callback" }}';
<script src="{{url_for('static_files', filename='auth.js')}}"> </script>
{% endblock %}

In the above, we check for the user’s logged in status and display a different message accordingly. At the end of the file, we add a Flask-Bootstrap block for scripts. This is used to add JavaScript to the file. Calling {{super()}} before adding your own scripts will add Bootstrap and its dependencies (jQuery) before your scripts. If you added this later, then the app will fail as we’ll soon add a JavaScript file that depends on jQuery, so this needs to be loaded for the code to work.

For authentication, the app will use Auth0’s Auth0.js library. This will present a ready made, but customizable, login/signup form. We then create some variables that will be used by the widget.

At the end of the code, we link to an auth.js file. Let’s create this. Create a file named auth.js and save it to the public folder. Add the following to it.

$(document).ready(function() {
  var auth0 = new window.auth0.WebAuth({
    clientID: AUTH0_CLIENT_ID,
    domain: AUTH0_DOMAIN,
    scope: "openid email profile",
    responseType: "code",
    redirectUri: AUTH0_CALLBACK_URL

  $('.login').click(function(e) {

The above instantiates an auth0.WebAuth object, passing it the variables we set previously. We also add a click listener to the Login link that will display the login widget.

In templates/upload.html, you can add the following before the form tag.

<h2>Welcome {{user['nickname']}}.</h2>

This will display the logged in user’s nickname which will be the value before the @ in their email. Look at the User Profile to see what other user information is available to you. The information available will be determined by what is saved on the server. For instance, if the user only uses the email/password authentication, then you won’t be able to get their name or picture, but if they used one of the available Identity Providers like Facebook or Google, then you might get this data.

Run the application. You won’t be able to get the upload form by navigating to /upload. Head to the Home page and use the Login link to bring up the login widget.

Auth0 Lock Widget

After authentication, you will be redirected to the /upload.html page.

Logging Out the User

Add the following function to the file.

def logout():
    return redirect('https://{auth0_domain}/v2/logout?client_id={auth0_client_id}&returnTo={app_url}'\
        .format(auth0_domain=env[constants.AUTH0_DOMAIN], auth0_client_id=env[constants.AUTH0_CLIENT_ID],

When you are implementing the logout functionality in an application, there are typically three layers of sessions you need to consider:

  • Application Session: The first is the session inside the application. Even though your application uses Auth0 to authenticate users, you will still need to keep track of the fact that the user has logged in to your application. In a normal web application this is achieved by storing information inside a cookie. You need to log out the user from your application, by clearing their session.
  • Auth0 session: Next, Auth0 will also keep a session and store the user’s information inside a cookie. Next time when a user is redirected to the Auth0 login screen, the user’s information will be remembered. In order to logout a user from Auth0 you need to clear the SSO cookie.
  • Identity Provider session: The last layer is the Identity Provider, for example Facebook or Google. When you allow users to sign in with any of these providers, and they are already signed into the provider, they will not be prompted to sign in. They may simply be required to give permissions to share their information with Auth0 and in turn your application.

In the code above, we deal with the first two. If we had only cleared the session with session.clear(), then the user would be logged out of the app, but they won’t be logged out of Auth0. On using the app again, authentication would be required to upload images. If they tried to login, the login widget will show the user account that is logged in on Auth0 and the user will only have to click on the email to get Auth0 to send their credentials back to the app which will then be saved to the session object. Here, the user will not be asked to reenter their passowrd.

Lock Widget

You can see the problem here. After a user logs out of the app, another user can log in as them on that computer. Thus, it is also necessary to log the user out of Auth0. This is done with a redirect to https://<YOUR_AUTH0_DOMAIN>/v2/logout. Redirecting the user to this URL clears all single sign-on cookies set by Auth0 for the user.

Although not a common practice, you can force the user to also log out of their identity provider by adding a federated querystring parameter to the logout URL: https://<YOUR_AUTH0_DOMAIN>/v2/logout?federated

We add a returnTo parameter to the URL whose value is a URL that Auth0 should redirect to after logging out the user. For this to work, the URL has to have been added to the Allowed Logout URLs on the Auth0 Dashboard, which we did earlier.

Finally, modify the if_else block in index.html as shown. We add a Logout link to the page if the user is logged in.

{% if logged_in %}
  <p>You can <a href="{{ url_for('upload') }}">upload images</a> or head over to the <a href="{{ url_for('gallery') }}">gallery</a></p>
  <p><a href="{{ url_for('logout') }}">Logout</a></p>
{% else %}
  <p><a href="#" class="login">Login</a> to upload images or head over to the <a href="{{ url_for('gallery') }}">gallery</a></p>
{% endif %}

Run the app and you should now be able to log out.


In this article, we’ve covered some of the more common image processing operations found in applications. Pillow is a powerful library and we definitely haven’t discussed all it can do. If you want to find out more, be sure to read the documentation.

If you’re building a Python application that requires authentication, consider using Auth0 as it is bound to save you loads of time and effort. After signing up, setting up your application with Auth0 is fairly simple. If you need help, you can look through the documentation or post your question in the comment section below.