Skip to content

Overview

Python

These are the official Python 3 templates maintained by OpenFaaS Ltd.

The python3-http template is recommended for most Python functions. Use python3-http-debian when a dependency requires native compilation — if a package fails to build on Alpine, switch to the Debian variant.

Template Base OS Use when
python3-http Alpine Default choice. Pure Python packages only.
python3-http-debian Debian Native C extensions required — SQL drivers, Kafka, Pandas, image manipulation.
python3-flask Alpine Direct access to Flask Response, e.g. for SSE streaming.
python3-flask-debian Debian Flask Response with native C extensions.

Note

The python3-http template is the best option for most users, followed by python3-http-debian for when C/C++ based pip modules are required.

All templates use the of-watchdog, Flask for HTTP routing, and Waitress as the production WSGI server.

Downloading the templates

Using template pull with the repository's URL:

faas-cli template pull https://github.com/openfaas/python-flask-template

Using the template store, since they are both within the same repository, either command will fetch down both templates.

faas-cli template store pull python3-http
faas-cli template store pull python3-http-debian

Using your stack.yml file:

configuration:
    templates:
        - name: python3-http

Python 3 versions

The python3-http template supports a dynamic Python version through a build argument.

It is recommended that you put the version within the OpenFaaS YAML file:

functions:
  postgres-fn:
    lang: python3-http
+    build_args:
+      - PYTHON_VERSION=3.12

The option can also be passed via a CLI flag, however this is not recommended as it is easy to forget or miss it:

faas-cli build --build-arg PYTHON_VERSION=3.11

Event and Context Data

The function handler is passed two arguments, event and context.

The event contains data about the request, including:

  • body
  • headers
  • method
  • query
  • path

The context contains basic information about the function, including:

  • hostname

Responses must be returned as a dictionary with the following keys:

  • statusCode - the HTTP status code to return as a number
  • body - the response body, which can be a string, dict, or list
  • headers - (optional) a dictionary of response headers to return

Response Bodies

By default, the template will automatically attempt to set the correct Content-Type header for you based on the type of response.

For example, returning a dict object type will automatically attach the header Content-Type: application/json and returning a string type will automatically attach the Content-Type: text/html, charset=utf-8 for you.

Accessing Event Data

Accessing request body:

def handle(event, context):
    return {
        "statusCode": 200,
        "body": "You said: " + str(event.body)
    }

Consuming the request path:

def handle(event, context):
    if event.path == '/hello':
        return {
            "statusCode": 200,
            "body": "Hello, World!"
        }

    return {
        "statusCode": 404,
        "body": "Not Found"
    }

This could be useful if you needed to serve up a static file or metadata to an external tool or service that is integrated with your functions. An example would be a HTTP readiness probe that checks the /healthz endpoint to see if a database is connected.

Accessing request method:

def handle(event, context):
    if event.method == 'GET':
        return {
            "statusCode": 200,
            "body": "GET request"
        }
    else:
        return {
            "statusCode": 405,
            "body": "Method not allowed"
        }

Accessing request query string arguments:

def handle(event, context):
    return {
        "statusCode": 200,
        "body": {
            "name": event.query['name']
        }
    }

Accessing request headers:

def handle(event, context):
    return {
        "statusCode": 200,
        "body": {
            "content-type-received": event.headers.get('Content-Type')
        }
    }

Return a JSON body with a Content-Type

def handle(event, context):
    return {
        "statusCode": 200,
        "body": {"message": "Hello from OpenFaaS!"},
        "headers": {
            "Content-Type": "application/json"
        }
    }

Custom status codes and response bodies

Successful response status code and JSON response body

def handle(event, context):
    return {
        "statusCode": 200,
        "body": {
            "key": "value"
        }
    }

Successful response status code and string response body

def handle(event, context):
    return {
        "statusCode": 201,
        "body": "Object successfully created"
    }

Failure response status code and JSON error message

def handle(event, context):
    return {
        "statusCode": 400,
        "body": {
            "error": "Bad request"
        }
    }

Custom Response Headers

Setting custom response headers

def handle(event, context):
    return {
        "statusCode": 200,
        "body": {
            "key": "value"
        },
        "headers": {
            "Location": "https://www.example.com/"
        }
    }

Native dependencies

As explained in the introduction, only the Debian variant of the Python template is suitable for building native dependencies. Why? For one, many libraries are available as pre-compiled wheels, meaning they can be imported without any compilation. Secondly, Alpine Linux requires so many packages to be added to build code that it becomes larger than the Debian base. Thirdly, Alpine Linux is not compatible with many native libraries because it uses its own C library called musl.

If a pre-compiled wheel isn't available for your chosen package, then you can use a build option to add a build toolchain. Build options are an abstracted list of packages to install, grouped together.

functions:
  postgres-fn:
    lang: python3-http
+    build_options:
+      - libpq

The current list of build_options for the Debian-based template is available in the templates repository in the template.yml file. Pull requests and contributions are welcome, however packages can be specified even when they are not present as a build option.

Alternatively, individual packages within apt can be specified through build_args:

functions:
  postgres-fn:
    lang: python3-http
+    build_args:
+      ADDITIONAL_PACKAGE: "libpq-dev gcc python3-dev"

Example with PostgreSQL

stack.yml

version: 1.0
provider:
  name: openfaas
  gateway: http://127.0.0.1:8080
functions:
  pgfn:
    lang: python3-http-debian
    handler: ./pgfn
    image: pgfn:latest
    build_options:
      - libpq
    environment:
      db_host: "postgresql.default.svc.cluster.local"
    secrets:
      - db-password

The build_options: libpq shorthand installs the packages needed to compile psycopg2. If you need more control over which packages are installed, you can use build_args instead:

    build_args:
      ADDITIONAL_PACKAGE: "libpq-dev gcc python3-dev"

The database host is set as an environment variable so it can be changed per deployment without rebuilding the image. The database password is stored as an OpenFaaS secret to keep it out of environment variables and the function image.

Create the secret before deploying the function:

faas-cli secret create db-password --from-literal='passwd'

requirements.txt

psycopg2==2.9.3

Create a database and table to use with the example:

CREATE DATABASE main;

\c main;

CREATE TABLE users (
    name TEXT,
);

-- Insert the original PostgreSQL author's name into the test table:

INSERT INTO users (name) VALUES ('Michael Stonebraker');

handler.py:

The handler reads the database password from the mounted secret and the host from the db_host environment variable set in stack.yaml. It opens a connection, queries the users table, and returns the results.

import os
import psycopg2

def handle(event, context):

    try:
        password = read_secret('db-password')

        # Connect using the host from the db_host env var
        # and the password from the mounted secret.
        conn = psycopg2.connect(
            dbname='main',
            user='postgres',
            port=5432,
            host=os.getenv('db_host'),
            password=password
        )
    except Exception as e:
        print("DB error {}".format(e))
        return {
            "statusCode": 500,
            "body": e
        }

    cur = conn.cursor()
    cur.execute("""SELECT * from users;""")
    rows = cur.fetchall()

    return {
        "statusCode": 200,
        "body": rows
    }

def read_secret(name):
    with open("/var/openfaas/secrets/" + name, "r") as f:
        return f.read().strip()

Always read secrets from an OpenFaaS secret at /var/openfaas/secrets/secret-name. The use of environment variables for sensitive values is an anti-pattern — they are visible via the OpenFaaS API.

Authenticate a function

To authenticate a function with a pre-shared secret, or API token, first create a secret, bind that secret to the function, then read it at runtime and validate it.

Create a new pre-shared secret:

openssl rand -base64 32 > python-auth-token.txt

Then create a new secret in OpenFaaS:

faas-cli secret create python-auth-token \
    --from-file python-auth-token.txt

Create a new function called python-auth:

faas-cli new --lang python3-http \
    python-auth

Bind the secret to the function:

version: 1.0
provider:
  name: openfaas
  gateway: http://127.0.0.1:8080
functions:
  python-auth:
    lang: python3-http-debian
+    secrets:
+    - python-auth-token

Now edit the function's handler.py file to read the secret and validate it:

def read_secret(name):
    with open("/var/openfaas/secrets/" + name) as f:
        return f.read().strip()

def handle(event, context):
    token = read_secret("python-auth-token")

    if not "Authorization" in event.headers:
        return {
            "statusCode": 401,
            "body": "Unauthorized"
        }

    if event.headers["Authorization"] != "Bearer {}".format(token):
        return {
            "statusCode": 401,
            "body": "Unauthorized"
        }

    return {
        "statusCode": 200,
        "body": "Access granted"
    }

Deploy the function with faas-cli up, then invoke it:

curl -i https://127.0.0.1:8080/function/python-auth \
    -H "Authorization: Bearer $(cat ./python-auth-token.txt)"

HTTP/2 200
Access granted

How to perform package upgrades at build time

The base images for the official OpenFaaS templates come from the Docker Hub, these images are built with automation and should always have the latest apk or apt packages installed.

That said, if you need to upgrade the images sooner, or are using an older image that was mirrored from the Docker Hub, you can add a --build-arg flag or build_args: entry in stack.yaml to force an upgrade on each build.

functions:
  fn1:
    lang: python3-http
    build_args:
      - UPGRADE_PACKAGES: "true"
faas-cli publish --filter fn1\
    --build-arg UPGRADE_PACKAGES=true

Add static files to your function

A common use-case for static files is when you want to serve HTML, lookup information from a JSON manifest or render some kind of templates.

With the python templates, static files and folders can just be added to the handler directory and will be copied into the function image.

To read a file e.g data.json back at runtime you can do the following:

def handle(event, context):
    if event.path=="/static":
        # Get the directory where this handler.py file is located
        current_dir = os.path.dirname(os.path.abspath(__file__))
        data_file_path = os.path.join(current_dir, 'data.json')

        # Read the data.json file
        with open(data_file_path, 'r') as file:
            data = json.load(file)

        return {
            "statusCode": 200,
            "body": json.dumps(data),
            "headers": {
                "Content-Type": "application/json"
            }
        }
    else:
        return {
            "statusCode": 200,
            "body": "Hello from OpenFaaS!"
        }

Examples