How To Write Serverless Functions in Python

By Michael Lavers

Editor’s note: This is an updated version of a blog post that originally appeared on the blog from IOpipe, which is now part of New Relic.

As you’ve undoubtedly heard, serverless has grown in leaps and bounds since the release of AWS Lambda. And while the idea of not having to manage a server and paying only for the compute resources you use—not to mention out-of-the-box horizontal autoscaling—may sound appealing, how do you know where to start?

If you’re a Python developer interested in serverless, you might have heard of the Serverless Framework, but maybe you find the prospect of working in an unfamiliar ecosystem like Node.js daunting. Maybe you’re unclear on what the framework does, or if it’s overkill for folks just getting started.

If these concerns speak to you, you’ve come to the right place. In this blog post, I’ll give you a brief tour of the Serverless Framework and show you how to use it to build a serverless application in Python.

A few considerations

Before we jump in, let’s get some questions out of the way.

Is my app/workload right for serverless?

This is probably the most important question you need to ask. While serverless has a lot to offer, it isn’t ideal for all apps/workloads. For example, the current maximum duration for an AWS Lambda function invocation is 15 minutes. If your app/workload can’t be broken up into 15 minute chunks, then serverless isn’t the best option.

For example, if your app uses WebSockets to maintain a persistent connection with the server, this connection would be closed and need to be reestablished every 15 minutes if you used AWS Lambda. You’d also be paying for all that compute time for a workload that’s really just keeping a socket open. AWS recently introduced WebSocket support for API Gateway , which gets around the persistent connection issue described above by breaking up every WebSocket exchange into its own function invocation, but the long-running persistent workloads caveat still applies.

If your app, on the other hand, is mostly stateless, like a REST or GraphQL API, then serverless may be a great fit, as HTTP requests rarely last 30 seconds, let alone 15 minutes.

Serverless really shines for workloads that might have spikes or periods of low activity. When your app/workload spikes, AWS Lambda provides considerable horizontal scaling— by default it can handle 1,000 concurrent requests out-of-the-box (but you can increase this limit). And when your app/workload is in a period of low activity, your meter doesn’t run at full tilt, which can save you a lot on operating expenses. Think about it: Most apps/workloads serve a range of timezones, so why pay full price to run your app/workload when your customers are sleeping?

If you’re still not sure whether or not your app/workload is a good fit, here’s a handy calculator to compare AWS Lambda to EC2.

Should I use Python 2 or 3?

The Python ecosystem has gone through a lot of changes in the past decade—the most significant being the release of Python 3 and the transition of many codebases from Python 2.x to 3.x. For new serverless projects, we recommend Python 3.x. While Python 2.7 has served many of us well, it no longer receives updates. So, if you haven’t already started your transition to 3.x, there’s no time like the present.

If you have an existing Python 2.7 project, you’re in luck, as AWS Lambda still supports 2.7, but you should seriously consider porting your code to Python 3.x as soon as possible. The advice we give in the rest of this post is compatible for both versions.

Should I use a web API or a worker?

Before we go over the serverless tools available to Python, let’s drill down a little more into our app/workload. If your web app serves a frontend with several web assets (HTML, JavaScript, CSS, and images), don’t serve these with a function. That’s not to say that you can’t—just that you shouldn’t. Remember, with AWS Lambda you pay for the time your function runs. It doesn’t make much sense to spend this time serving web assets. In fact, since your frontend likely has many web assets, this could turn a simple task into an expensive liability. For serving web assets, consider a content delivery network (CDN)—Amazon CloudFront is an AWS service built specifically for this purpose. (Check out their guide on how to use it with S3.)

But that really only covers your web app’s frontend. What if your app/workload doesn’t have a frontend at all? We’re going to break down the apps/workloads we talk about in this post into two categories: web APIs (REST, GraphQL, etc.), and workers. Hopefully you’re already thinking about what parts of your app will be served via a web API and what parts can be worker tasks that run in the background, so you can pick the right tool for your project.

Building a “hello world” function on the Serverless Framework

The Serverless Framework is a well-established leader and for good reason. They’ve put considerable time and effort into the developer experience to make it one of the most intuitive and accessible serverless tools out there. It also offers a comprehensive feature that supports multiple cloud vendors, in addition to AWS Lambda, and has a growing plugin ecosystem. For a Python developer, the Serverless Framework is a great starting point.

But Python developers may want to note a big caveat about the Serverless Framework—it’s written in Node.js, which may not be every Python dev’s first choice. But like any good tool, if done right, you shouldn’t even notice what language it is implemented in, and that case could certainly be made here for the Serverless Framework. You’ll still need to install Node and NPM, but you won’t need to know any JavaScript.

Let’s give it a try.

Step 1: Set up the project

First, install Node and NPM:

npm install -g serverless

You can access the CLI using either serverless or the shorthand sls. Let’s create a project:

mkdir ~/my-serverless-project cd ~/my-serverless-project sls create -n my-serverless-project -t aws-python3

Here, I’ve created a directory called my-serverless-project and created a project using sls create. I’ve also specified a template with -t aws-python3. Serverless comes bundled with several templates that set some sensible defaults for you in serverless.yml. In this case, I’m specifying the AWS template for Python 3.6. If your project is Python 2.7, use aws-python2. There are other templates for other languages and clouds, but that’s outside of the scope of this guide.

The -n my-serverless-project specifies a service name, and you can change this to whatever you want to name your project. Now, let’s take a look at the contents of the my-serverless-project directory. Run:

cat serverless.yml

The file serverless.yml comes loaded with several helpful comments explaining each section of the config. (I recommend reading through these comments, as it will be helpful later on.)

Step 2: Write and deploy your function

Let’s write a hello world equivalent of a function:

def handler(event, context): return {"message": "hi there"}

Save that in your my-serverless-project directory as hello.py. We commonly refer to functions as handlers, but you can name your functions whatever you want.

Now that you have a function, make the Serverless Framework aware of it by adding it to serverless.yml. Edit serverless.yml and replace the functions section with the following:

functions: hello: handler: hello.handler

Now save serverless.yml. To deploy this function, you’ll need to make sure you’ve configured your AWS credentials.

When you’re ready to deploy, run the following:

sls deploy

Deployment may take several moments. Essentially the Serverless Framework:

  1. Creates a CloudFormation template based on serverless.yml
  2. Compresses the CloudFormation template and hello.py into a zip archive
  3. Creates an S3 bucket and uploads the zip archive to it
  4. Executes the CloudFormation template, which includes configuring an AWS Lambda function, and points it to the S3 zip archive

You could do all of these steps manually, but why would you want to if the framework can automate it for you? When your deploy is complete, test it with the following command:

sls invoke -f hello

You should see the following response:

{"message": "hi there"}

Congratulations, you’ve just created your first serverless function.

Building an advanced function

Now, we’ll do something a little more challenging—we’ll make an HTTP request and return the result.

Step 1: Create the HTTP request function

Let’s create a new file called httprequest.py and add the following:

import requests def handler(event, context): r = requests.get("https://news.ycombinator.com/news") return {"content": r.text}

Update the functions section of serverless.yml:

functions: hello: handler: hello.handler httprequest: handler: httprequest.handler

Now re-deploy the function:

sls deploy sls invoke -f httprequest

You should now see an ImportError. This is because requests is not installed. With AWS Lambda, you need to bundle any libraries you want to use with your function.

You could run pip install requests -t to install the requests wheel (and its dependencies), but AWS Lambda runs on 64-bit Linux. So what do you do if you’re running Mac OS? Or Windows? Or FreeBSD?

Thankfully Serverless comes with a plugin ecosystem to fill the gaps. Specifically we want to install serverless-python-requirements:

sls plugin install -n serverless-python-requirements

Add the following lines to the end of serverless.yml:

plugins: - serverless-python-requirements

This plugin enables requirements.txt support, so add a requirements.txt file to your project directory:

echo "requests" >> requirements.txt

Now the requirements will be installed and bundled automatically the next time you deploy.

But we haven’t solved our compilation problem yet. To do that you’ll need to add a custom section to serverless.yml. This section is where you can add custom configuration options, but it’s also where plugins look for their own config options. Our new custom section should look like this:

custom: pythonRequirements: dockerizePip: true

This section tells the serverless-python-requirements plugin to compile the Python packages in a Docker container before bundling them in the zip archive to ensure they’re compiled for 64-bit Linux. You’ll also need to install Docker in order for this to work, but after you do, this plugin will automatically handle the dependencies you define in requirements.txt.

Now deploy and invoke your function again:

sls deploy sls invoke -f httprequest

Even if you are running 64-bit Linux, this is way cleaner, don’t you think?

Step 2: Set up a worker for the HTTP request function

Before we continue, let’s explain how events and context are useful.

Functions are event-driven, so when you invoke a one, you’re actually triggering an event within AWS Lambda. The first argument of your function contains the event that triggered the function, which is represented within AWS Lambda as a JSON object, but what is passed to Python is a dict of that object. When you run sls invoke -f hello, an empty dict is passed to the function. But if it’s an API request, it would contain the entire HTTP request represented as a dict. In other words, the event dict acts as your function’s input parameters, and your function returns the output. With AWS Lambda, your output needs to be JSON serializable. (Here’s some example events you might see with AWS Lambda.)

The second argument is the AWS Lambda context, which is a Python object with useful metadata about the function and the current invocation. For example, every invocation has a aws_request_id which is useful if you want to track down what happened in a specific invocation within your logs. (See the AWS Lambda docs for more information about the context object.) You probably won’t need to worry about the context object right away, but you’ll eventually find it useful when debugging.

So, how are events useful? Well, if your app/workload can work with a JSON serializable input and produce a JSON serializable output, you can plug it right into an AWS Lambda function.

So far you’ve already implemented what you need for a worker. Let’s say you wanted to run your httprequest function every 10 minutes; to do that, add the following to serverless.yml:

functions: httprequest: handler: httprequest.handler events: - schedule: rate(10 minutes)

And deploy the function:

sls deploy

Now httprequest is triggered automatically every ten minutes. If you want more fine-grained control, you can specify a specific time at which your function should be triggered. You can also build more complex workflows using Amazon Simple Notification Service (SNS), Amazon Simple Queue Service (SQS), or other AWS services.

Step 3: Set up a web API for the HTTP request function

Earlier I mentioned that an HTTP request can be represented as an event. In the case of web APIs, Amazon’s API Gateway service can trigger events for our function. In addition to this, API Gateway provides a hostname that can receive HTTP requests, transform those HTTP requests into an event object, invoke our function, and collect the response and pass it on to the requester as an HTTP response. That might sound complex, but thankfully the Serverless Framework abstracts away much of this for us.

So, add an HTTP endpoint to serverless.yml:

functions: webapi: handler: webapi.handler events: - http: path: / method: get

This looks a lot like our scheduled worker task earlier, doesn’t it? Like in that task, you configured this handler to handle http events and specified path (our HTTP request path) and a method (the HTTP method this handler will handle). Since we’ve added a new handler, we’ll need to create that in webapi.py:

import json def handler(event, context): return {"statusCode": 200, "body": json.dumps({"message": "I'm an HTTP response"})}

This handler will accept an event from the API Gateway and respond with a JSON serializable dict. Within the dict we have two keys: statusCode, which is the HTTP status code we want the API Gateway to respond with, and body, which contains the HTTP body of the response serialized as JSON. API Gateway expects the HTTP response body to be a string, so if we want our web API to respond with JSON, we need to serialize it before handing it back to API Gateway.

Now deploy the function again:

sls deploy

The Serverless Framework will provide an endpoint:

endpoints: GET - https://XXXXXXXXXX.execute-api.us-east-1.amazonaws.com/dev

What just happened here? In short, the Serverless Framework created our new function and then configured an AWS API Gateway to point to the function. The endpoint returned is the one provided by API Gateway.

Try your endpoint:

curl https://XXXXXXXXXX.execute-api.us-east-1.amazonaws.com/dev

You should see the following response:

{"message": "I'm an HTTP response"}

Congratulations, you’ve just created your first serverless web API! You might have noticed that the URL provided by the API Gateway provides is pretty ugly. It would be a lot nicer if it could be something more readable like https://api.mywebapi.com/. Well, there’s a plugin for that, too.

Cleaning up

If you were playing along, you now have three serverless functions and an API Gateway. But these were really just examples to help you get started with serverless development. You’ll probably want to clean up your project; to do so run:

sls remove

And the Serverless Framework will take care of the rest.

Want to learn about monitoring, visualizing, and troubleshooting your functions? Visit New Relic Serverless for AWS Lambda to learn more and request a demo.

In the meantime, check out this explainer video from RedMonk, “What is Serverless Observability And Why Does It Matter?”: