Python and relative imports in AWS Lambda Functions

You are writing an AWS Lambda function and there is always that point that the file becomes to big.
You create a second file and refactor your code to do a relative import.
You deploy your code and run it, and it fails… Sounds familiar?

If you look at the PEP 8 — Style Guide for Python Code.
You will read that absolute imports are recommended. But, explicit relative imports are an acceptable alternative to absolute imports.
This means that you should favor absolute imports. And only use relative imports within a package.

Sample code

Let’s create a hello world lambda function to show this. So we have the following code in a file called hello_world/app.py:

import json

def lambda_handler(event, context):
    return {
        "statusCode": 200,
        "body": json.dumps({"message": "hello world"}),
    }

For simplicity reasons I used the AWS Serverless Application Model.
The templates contain the following resource:

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.9
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /hello
            Method: get

The most important thing is the Handler definition. The value app.lambda_handler translates to:

Use the app.py file and invoke the lambda_handler method.

We will now move the payload to a file called hello_world/response.py:

def get_response() -> dict:
    return {"message": "hello world"}

And update the hello_world/app.py:

import json
from .response import get_response

def lambda_handler(event, context):

    return {
        "statusCode": 200,
        "body": json.dumps(get_response()),
    }

You run your test and it works, nice! So we will now deploy our code and that it also works. And it fails…

[ERROR] Runtime.ImportModuleError: Unable to import module 'app': attempted relative import with no known parent package
Traceback (most recent call last):

The content of the hello_world folder ends up in the /var/task/ folder. And not the folder itself.
So it might look like that the code is in a package but from the Lambda runtime it’s not.

The solution

You can solve this by creating a package. How you might ask? Create a hello_world folder in the hello_world folder.
Move all files except the requirements.txt into the created folder.

Example of the folder structure

Next you need to update the Handler in the template:

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello_world/
      Handler: hello_world.app.lambda_handler
      Runtime: python3.9
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /hello
            Method: get

Now the value of the Handler translates to:

In the hello_world package you use the app.py file and invoke the lambda_handler method.

When you deploy you will now notice that the relative import works.

Conclusion

I would always tell you to deploy your python code in a package. It makes it easier to use many files and organize your code better.
It also allows you to separate the Lambda invocation logic from your business logic. Making it easier to test and maintain.

Image by Gerd Altmann from Pixabay

Joris has been working with the AWS cloud since 2009 and focussing on building event driven architectures. While working with the cloud from (almost) the start he has seen most of the services being launched. Joris strongly believes in automation and infrastructure as code and is open to learn new things and experiment with them, because that is the way to learn and grow. In his spare time he enjoys running and runs a small micro brewery from his home.
Share this article: Tweet this post / Post on LinkedIn