DEV Community

Cover image for 6 Python Techniques for Building High-Performance Serverless Applications
Nithin Bharadwaj
Nithin Bharadwaj

Posted on

6 Python Techniques for Building High-Performance Serverless Applications

As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!

Python has transformed how I approach serverless application development. After implementing dozens of production serverless systems, I've identified six powerful techniques that consistently deliver exceptional results. These approaches have helped me create flexible, scalable solutions while significantly reducing operational complexity.

Lambda Functions with Python

AWS Lambda functions work beautifully with Python. The runtime environment supports recent Python versions, and the execution model pairs perfectly with Python's concise syntax. I've found that organizing Lambda code into small, focused functions produces the most maintainable solutions.

import json
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
    logger.info("Event received: %s", json.dumps(event))

    try:
        # Extract parameters from the event
        name = event.get('name', 'World')

        # Business logic
        message = process_greeting(name)

        # Return success response
        return {
            'statusCode': 200,
            'body': json.dumps({'message': message})
        }
    except Exception as e:
        logger.error("Error: %s", str(e))
        # Return error response
        return {
            'statusCode': 500,
            'body': json.dumps({'error': str(e)})
        }

def process_greeting(name):
    return f"Hello, {name}!"
Enter fullscreen mode Exit fullscreen mode

For complex applications, I separate business logic from the Lambda handler. This approach enables better unit testing and makes code easier to maintain. The handler becomes a thin adapter between AWS and your application logic.

Leveraging AWS SDK with Boto3

Boto3 provides a Pythonic interface to AWS services. I regularly use it to interact with services like DynamoDB, S3, and SQS within serverless applications.

import boto3
import uuid
import json
from datetime import datetime

def store_user_data(user_data):
    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table('Users')

    # Generate a unique ID
    user_id = str(uuid.uuid4())

    # Add metadata
    user_data['user_id'] = user_id
    user_data['created_at'] = datetime.utcnow().isoformat()

    # Store in DynamoDB
    table.put_item(Item=user_data)

    # Return the generated ID
    return user_id

def lambda_handler(event, context):
    body = json.loads(event.get('body', '{}'))

    # Store user data
    user_id = store_user_data(body)

    # Return success response
    return {
        'statusCode': 201,
        'body': json.dumps({'user_id': user_id})
    }
Enter fullscreen mode Exit fullscreen mode

One useful pattern I've adopted is creating reusable service classes that encapsulate AWS interactions. This approach promotes code reuse and simplifies testing with mocks.

class UserService:
    def __init__(self, table_name='Users', dynamodb_resource=None):
        self.dynamodb = dynamodb_resource or boto3.resource('dynamodb')
        self.table = self.dynamodb.Table(table_name)

    def create_user(self, user_data):
        user_id = str(uuid.uuid4())
        item = {
            'user_id': user_id,
            'created_at': datetime.utcnow().isoformat(),
            **user_data
        }
        self.table.put_item(Item=item)
        return user_id

    def get_user(self, user_id):
        response = self.table.get_item(Key={'user_id': user_id})
        return response.get('Item')
Enter fullscreen mode Exit fullscreen mode

Serverless Framework for Deployment

The Serverless Framework streamlines deployment of Python applications. I define infrastructure as code, which makes deployments repeatable and version-controlled.

A typical serverless.yml for a Python service might look like:

service: user-api

provider:
  name: aws
  runtime: python3.9
  region: us-east-1
  environment:
    USERS_TABLE: ${self:service}-users-${opt:stage, 'dev'}
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:PutItem
        - dynamodb:GetItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
        - dynamodb:Query
      Resource: !GetAtt UsersTable.Arn

functions:
  createUser:
    handler: handlers/users.create
    events:
      - http:
          path: users
          method: post
          cors: true

  getUser:
    handler: handlers/users.get
    events:
      - http:
          path: users/{userId}
          method: get
          cors: true

resources:
  Resources:
    UsersTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:provider.environment.USERS_TABLE}
        BillingMode: PAY_PER_REQUEST
        AttributeDefinitions:
          - AttributeName: user_id
            AttributeType: S
        KeySchema:
          - AttributeName: user_id
            KeyType: HASH
Enter fullscreen mode Exit fullscreen mode

I organize my code into separate modules based on functionality, with a dedicated handler file for each resource. This structure scales well as applications grow.

FastAPI for Serverless APIs

FastAPI has become my preferred framework for building serverless APIs due to its performance, type checking, and automatic documentation.

from fastapi import FastAPI, HTTPException
from mangum import Mangum
from pydantic import BaseModel
from uuid import uuid4
from datetime import datetime

app = FastAPI()

# In-memory database for demonstration
users_db = {}

class User(BaseModel):
    name: str
    email: str
    age: int = None

class UserResponse(BaseModel):
    id: str
    name: str
    email: str
    age: int = None
    created_at: str

@app.post("/users", response_model=UserResponse, status_code=201)
def create_user(user: User):
    user_id = str(uuid4())
    created_at = datetime.utcnow().isoformat()

    user_data = user.dict()
    users_db[user_id] = {**user_data, "id": user_id, "created_at": created_at}

    return users_db[user_id]

@app.get("/users/{user_id}", response_model=UserResponse)
def get_user(user_id: str):
    if user_id not in users_db:
        raise HTTPException(status_code=404, detail="User not found")

    return users_db[user_id]

# Lambda handler
handler = Mangum(app)
Enter fullscreen mode Exit fullscreen mode

FastAPI's validation and documentation features have saved me countless hours of debugging and documentation work. The Mangum adapter seamlessly connects FastAPI to AWS Lambda.

Event-Driven Architecture with Python

Python's simplicity makes it ideal for implementing event-driven patterns in serverless applications. I frequently build systems where events trigger processing chains across multiple services.

def process_order_created(event, context):
    """Handle order.created events by initiating payment processing"""
    for record in event['Records']:
        # Parse the SQS message
        message = json.loads(record['body'])
        order = message['detail']

        # Process the order
        try:
            result = process_payment(order)

            # Publish success event
            publish_event(
                'order.payment.processed', 
                {'order_id': order['id'], 'status': 'success', 'payment_id': result['payment_id']}
            )
        except Exception as e:
            # Publish failure event
            publish_event(
                'order.payment.failed',
                {'order_id': order['id'], 'status': 'failed', 'error': str(e)}
            )

def publish_event(event_type, detail):
    """Publish an event to EventBridge"""
    client = boto3.client('events')

    response = client.put_events(
        Entries=[
            {
                'Source': 'payment-service',
                'DetailType': event_type,
                'Detail': json.dumps(detail)
            }
        ]
    )
    return response
Enter fullscreen mode Exit fullscreen mode

This pattern creates loosely coupled services that can scale independently. Each function handles a specific event type and may generate new events for downstream processing.

Effective State Management

Managing state in serverless applications requires careful design. I use a combination of techniques depending on the application needs.

For simple applications, environment variables work well:

import os
import json
import boto3

API_KEY = os.environ['API_KEY']
SERVICE_URL = os.environ['SERVICE_URL']
ENVIRONMENT = os.environ['ENVIRONMENT']

def lambda_handler(event, context):
    # Use environment variables for configuration
    client = boto3.client('sns')

    # Process event
    result = process_data(event.get('data', {}))

    # Publish to different topics based on environment
    if ENVIRONMENT == 'production':
        topic_arn = 'arn:aws:sns:us-east-1:123456789012:production-notifications'
    else:
        topic_arn = 'arn:aws:sns:us-east-1:123456789012:development-notifications'

    client.publish(
        TopicArn=topic_arn,
        Message=json.dumps(result)
    )

    return {'status': 'success'}
Enter fullscreen mode Exit fullscreen mode

For more complex state, I use purpose-built database services:

def get_user_session(session_id):
    """Retrieve user session from DynamoDB"""
    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table('UserSessions')

    response = table.get_item(Key={'session_id': session_id})
    return response.get('Item')

def update_session_state(session_id, state_data):
    """Update session state in DynamoDB"""
    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table('UserSessions')

    # Update specific attributes in the session
    update_expression = "SET "
    expression_values = {}

    for key, value in state_data.items():
        update_expression += f"#{key} = :{key}, "
        expression_values[f":{key}"] = value

    # Remove trailing comma and space
    update_expression = update_expression[:-2]

    # Create expression attribute names
    expression_names = {f"#{key}": key for key in state_data.keys()}

    table.update_item(
        Key={'session_id': session_id},
        UpdateExpression=update_expression,
        ExpressionAttributeNames=expression_names,
        ExpressionAttributeValues=expression_values
    )
Enter fullscreen mode Exit fullscreen mode

For cross-cutting concerns like logging and monitoring, I implement middleware patterns:

import json
import time
import logging
import traceback
from functools import wraps

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def middleware(func):
    @wraps(func)
    def wrapper(event, context):
        # Request logging
        request_id = context.aws_request_id
        logger.info(f"Request started: {request_id}")
        logger.info(f"Event: {json.dumps(event)}")

        start_time = time.time()

        try:
            # Execute the handler
            response = func(event, context)

            # Calculate duration
            duration = time.time() - start_time

            # Log success
            logger.info(f"Request completed: {request_id}, Duration: {duration:.2f}s")

            return response
        except Exception as e:
            # Calculate duration
            duration = time.time() - start_time

            # Log error with stack trace
            logger.error(f"Request failed: {request_id}, Duration: {duration:.2f}s")
            logger.error(f"Error: {str(e)}")
            logger.error(traceback.format_exc())

            # Return formatted error response
            return {
                'statusCode': 500,
                'body': json.dumps({
                    'error': str(e),
                    'requestId': request_id
                })
            }

    return wrapper

# Usage
@middleware
def lambda_handler(event, context):
    # Business logic here
    return {
        'statusCode': 200,
        'body': json.dumps({'message': 'Success'})
    }
Enter fullscreen mode Exit fullscreen mode

Python's serverless capabilities have revolutionized how I build cloud applications. These six techniques - Lambda functions, Boto3 integration, Serverless Framework deployment, FastAPI APIs, event-driven architecture, and effective state management - form the foundation of my serverless Python toolkit.

By combining these approaches, I've built systems that scale automatically, require minimal operational overhead, and deliver exceptional business value. The flexibility of Python paired with serverless architecture gives me the freedom to focus on solving business problems rather than managing infrastructure.

I encourage you to experiment with these techniques in your next Python serverless project. Start with a small, focused function and gradually expand as you become comfortable with the serverless paradigm. The productivity gains and operational benefits make the learning curve well worth the effort.


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!

Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Top comments (0)