AWS Lambda with Python: A Practical Guide
Build serverless functions with AWS Lambda and Python — handlers, events, layers, environment variables, API Gateway integration, and best practices.
What you'll learn
- ✓How Lambda functions work and the execution model
- ✓Writing handlers for API Gateway, S3, and SQS events
- ✓Layers, environment variables, and packaging dependencies
- ✓Cold starts, timeouts, and performance optimization
Prerequisites
- •Python basics
- •An AWS account with IAM permissions
AWS Lambda runs your code without provisioning servers. You upload a function, define a trigger, and AWS handles scaling, patching, and availability. You pay only for the compute time you use.
The handler function
Every Lambda function has a handler — a function that receives an event and a context.
def handler(event, context):
name = event.get("name", "World")
return {
"statusCode": 200,
"body": f"Hello, {name}!"
}
event: the input data (JSON parsed into a dict)context: runtime info (function name, memory, time remaining)
API Gateway integration
When Lambda is triggered by API Gateway, the event contains HTTP request details.
import json
def handler(event, context):
method = event["httpMethod"]
path = event["path"]
body = json.loads(event.get("body") or "{}")
query = event.get("queryStringParameters") or {}
return {
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({
"message": f"{method} {path}",
"query": query,
"input": body,
}),
}
S3 event trigger
Process files uploaded to S3:
import boto3
s3 = boto3.client("s3")
def handler(event, context):
for record in event["Records"]:
bucket = record["s3"]["bucket"]["name"]
key = record["s3"]["object"]["key"]
response = s3.get_object(Bucket=bucket, Key=key)
content = response["Body"].read().decode("utf-8")
print(f"Processing {key} from {bucket}")
print(f"Content length: {len(content)}")
return {"processed": len(event["Records"])}
SQS trigger
def handler(event, context):
failed = []
for record in event["Records"]:
try:
body = json.loads(record["body"])
process_message(body)
except Exception as e:
failed.append(record["messageId"])
return {
"batchItemFailures": [
{"itemIdentifier": mid} for mid in failed
]
}
Environment variables
import os
DB_HOST = os.environ["DB_HOST"]
DB_NAME = os.environ.get("DB_NAME", "mydb")
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
Set them in the Lambda console, SAM template, or CDK.
Packaging dependencies
With a requirements.txt
pip install -r requirements.txt -t ./package
cd package && zip -r ../deployment.zip .
cd .. && zip deployment.zip handler.py
aws lambda update-function-code --function-name my-func --zip-file fileb://deployment.zip
With Lambda layers
Layers let you share dependencies across functions.
mkdir -p layer/python
pip install requests -t layer/python
cd layer && zip -r ../my-layer.zip python
aws lambda publish-layer-version \
--layer-name my-dependencies \
--zip-file fileb://my-layer.zip \
--compatible-runtimes python3.12
Cold starts
The first invocation of a Lambda (or after idle time) has extra latency — the “cold start.” The runtime initializes, your code loads, and dependencies import.
Minimizing cold starts
- Keep packages small. Fewer dependencies = faster imports.
- Initialize outside the handler. Code at module level runs once per cold start.
- Use provisioned concurrency for latency-sensitive functions.
import boto3
# Runs once per cold start — reused across invocations
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table("users")
def handler(event, context):
# This runs every invocation
user_id = event["userId"]
response = table.get_item(Key={"id": user_id})
return response.get("Item")
Context object
def handler(event, context):
print(f"Function: {context.function_name}")
print(f"Memory: {context.memory_limit_in_mb} MB")
print(f"Time remaining: {context.get_remaining_time_in_millis()} ms")
print(f"Request ID: {context.aws_request_id}")
Error handling
class NotFoundError(Exception):
pass
def handler(event, context):
try:
user_id = event.get("userId")
if not user_id:
return {"statusCode": 400, "body": "Missing userId"}
user = get_user(user_id)
return {"statusCode": 200, "body": json.dumps(user)}
except NotFoundError:
return {"statusCode": 404, "body": "User not found"}
except Exception as e:
print(f"Error: {e}")
return {"statusCode": 500, "body": "Internal error"}
Testing locally
if __name__ == "__main__":
test_event = {
"httpMethod": "GET",
"path": "/users",
"queryStringParameters": {"id": "123"},
}
result = handler(test_event, None)
print(json.dumps(result, indent=2))
Summary
Lambda functions are stateless, event-driven, and scale automatically. Initialize expensive resources outside the handler, keep packages lean, handle errors gracefully, and use layers to share dependencies. Start with a simple API Gateway trigger and expand to S3, SQS, and EventBridge as your architecture grows.
Related articles
- AWS AWS Lambda Best Practices: Performance, Cost, Security
Master AWS Lambda best practices for optimizing performance, reducing cold starts, controlling costs, and securing your serverless functions in production.
- AWS Building REST APIs with API Gateway
Build serverless REST APIs with AWS API Gateway — resources, methods, Lambda integration, authorization, throttling, and deployment stages.
- AWS Deploying to ECS Fargate
Deploy containerized apps to AWS ECS Fargate — task definitions, services, load balancers, auto-scaling, and CI/CD deployment.
- AWS S3 Operations with Boto3
Master S3 operations in Python — upload, download, presigned URLs, multipart uploads, lifecycle policies, and event notifications.