Deploy a serverless Python API to Scaleway Functions using CircleCI
Fullstack Developer and Tech Author
Serverless platforms have revolutionized the way developers build and deploy APIs, eliminating the need to manage servers or underlying infrastructure. With serverless, you can focus entirely on your application logic and let the platform handle scaling, availability, and maintenance.
Scaleway Serverless Functions is a flexible serverless platform that makes it easy to deploy lightweight APIs and background jobs in the cloud. It supports multiple runtimes, including Python, and offers a generous free tier, making it a great choice for startups, hobby projects, and production workloads alike.
But building a robust API is only half the story. To ensure reliability and rapid iteration, you need the kind of solid CI/CD pipeline you can create using CircleCI. CircleCI automates your build, test, and deployment workflows, so every code change is validated and shipped to production with confidence.
In this tutorial, you’ll learn how to combine the power of Scaleway Serverless Functions and CircleCI to create a seamless, automated workflow for serverless Python APIs. We’ll walk through building a simple invoice validation API in Python, packaging it for Scaleway, and setting up continuous deployment so your function goes live automatically after passing tests. By the end, you’ll have a modern, cloud-native workflow that’s fast, reliable, and easy to maintain.
Prerequisites
Here’s what you’ll need to follow along with this tutorial:
- Python (3.12 or later) installed on your local machine
- pip package manager
- zip utility for creating deployment packages
- A CircleCI account
- A GitHub account
- A Scaleway account with billing setup. Scaleway offers 100€ free credits for new users, which is more than enough for this project.
- Scaleway CLI installed on your local machine
- Basic knowledge of Python programming language
Understanding Scaleway Serverless Functions
Scaleway Serverless Functions is a serverless platform that lets you deploy lightweight APIs and background jobs without managing infrastructure. Each function is packaged as a zipped archive containing your code and dependencies, and is executed in response to HTTP requests or events.
If you’re building with Python, Scaleway expects your function’s entry point to be a function named handle in a file called handler.py. The required signature is:
def handle(event: Dict[str, Any], context: Any) -> Dict[str, Any]:
This function receives an event dictionary containing request data and a context object with runtime information. Your function should return a dictionary with statusCode, headers, and body fields to form the HTTP response. When you deploy, Scaleway will automatically look for this handle function and invoke it for each request.
For more details, see the Scaleway handler convention documentation.
Getting started
Now that you understand how Scaleway Functions expects your Python code to be structured, you can set up your project. Create a fresh directory and set up a Python virtual environment:
mkdir invoice-validator-scaleway-python
cd invoice-validator-scaleway-python
python3 -m venv venv
source venv/bin/activate
Your project structure should be similar to this:
.
├── venv/
├── handler.py
├── test_handler.py
├── requirements.txt
└── .gitignore
Remember, your main function logic must be implemented as a handle function in handler.py for Scaleway to detect and invoke your code correctly.
Implement and test the invoice validation handler
In this section, you’ll create the core logic for your serverless API; a Python handler that validates invoice data. You’ll also write unit tests to ensure your handler works as expected.
Creating the handler
Create a file named handler.py in your project directory. Paste this code into it:
import json
from datetime import datetime
from typing import Dict, List, Any
class Invoice:
def __init__(self, amount: float, currency: str, due_date: str):
self.amount = amount
self.currency = currency
self.due_date = due_date
def handle(event: Dict[str, Any], context: Any) -> Dict[str, Any]:
"""
Scaleway Functions handler for invoice validation
"""
try:
if 'httpMethod' in event and event.get('body'):
body = event['body']
if isinstance(body, str):
data = json.loads(body)
else:
data = body
elif 'body' in event:
body = event['body']
if isinstance(body, str):
data = json.loads(body)
else:
data = body
else:
data = event
# Create invoice object
invoice = Invoice(
amount=data.get('amount', 0),
currency=data.get('currency', ''),
due_date=data.get('due_date', '')
)
# Validate invoice
errors = validate_invoice(invoice)
if errors:
return {
'statusCode': 422,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({
'valid': False,
'errors': errors
})
}
return {
'statusCode': 200,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'valid': True})
}
except json.JSONDecodeError:
return {
'statusCode': 400,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'error': 'invalid JSON'})
}
except Exception as e:
return {
'statusCode': 500,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'error': str(e)})
}
def validate_invoice(invoice: Invoice) -> List[str]:
"""
Validate invoice data and return list of errors
"""
errors = []
if invoice.amount <= 0:
errors.append("amount must be greater than 0")
if len(invoice.currency) != 3:
errors.append("currency must be a 3-letter code")
try:
datetime.strptime(invoice.due_date, "%Y-%m-%d")
except ValueError:
errors.append("due_date must be in YYYY-MM-DD format")
return errors
This handler receives an event dictionary containing request data, validates the invoice fields, and returns a dictionary response indicating whether the invoice is valid. It checks that the:
- Amount is greater than zero.
- Currency is a three-letter code.
- Due date is in the correct format (
YYYY-MM-DD).
If any validation fails, it returns a list of errors and sets the HTTP status to 422 Unprocessable Entity.
Create a dependencies file
Create a requirements.txt file to specify your project dependencies:
echo "pytest" > requirements.txt
Install the dependencies in your virtual environment:
pip install -r requirements.txt
Write unit tests for the handler
Next, create a file named test_handler.py in the same directory. Paste this code into it:
import json
from handler import handle, validate_invoice, Invoice
def test_handle_valid_invoice():
"""Test handling of a valid invoice"""
event = {
'body': json.dumps({
'amount': 100,
'currency': 'USD',
'due_date': '2025-08-01'
})
}
result = handle(event, {})
assert result['statusCode'] == 200
response_body = json.loads(result['body'])
assert response_body['valid'] is True
def test_handle_invalid_invoice():
"""Test handling of an invalid invoice"""
event = {
'body': json.dumps({
'amount': -5,
'currency': 'us',
'due_date': 'Aug 1'
})
}
result = handle(event, {})
assert result['statusCode'] == 422
response_body = json.loads(result['body'])
assert response_body['valid'] is False
assert len(response_body['errors']) == 3
assert "amount must be greater than 0" in response_body['errors']
assert "currency must be a 3-letter code" in response_body['errors']
assert "due_date must be in YYYY-MM-DD format" in response_body['errors']
def test_handle_invalid_json():
"""Test handling of invalid JSON"""
event = {
'body': '{"amount": invalid}'
}
result = handle(event, {})
assert result['statusCode'] == 400
response_body = json.loads(result['body'])
assert 'error' in response_body
assert response_body['error'] == 'invalid JSON'
def test_validate_invoice_valid():
"""Test validation of a valid invoice"""
invoice = Invoice(amount=100.50, currency='EUR', due_date='2025-12-31')
errors = validate_invoice(invoice)
assert len(errors) == 0
def test_validate_invoice_invalid_amount():
"""Test validation with invalid amount"""
invoice = Invoice(amount=0, currency='USD', due_date='2025-08-01')
errors = validate_invoice(invoice)
assert "amount must be greater than 0" in errors
def test_validate_invoice_invalid_currency():
"""Test validation with invalid currency"""
invoice = Invoice(amount=100, currency='US', due_date='2025-08-01')
errors = validate_invoice(invoice)
assert "currency must be a 3-letter code" in errors
def test_validate_invoice_invalid_date():
"""Test validation with invalid date format"""
invoice = Invoice(amount=100, currency='USD', due_date='08/01/2025')
errors = validate_invoice(invoice)
assert "due_date must be in YYYY-MM-DD format" in errors
These tests use pytest, a popular Python testing framework. The tests simulate various scenarios including valid invoices, invalid invoices with multiple errors, and JSON parsing errors. Each test verifies that your handler returns the correct status codes and response structures.
By creating these files, you ensure that your handler logic is robust and behaves as expected before deploying to Scaleway Functions.
Running tests
To run the tests, execute this command in your terminal:
python -m pytest test_handler.py -v
Your output should indicate that all tests passed, confirming your handler works as intended:
(venv) ➜ invoice-validator-scaleway-python python -m pytest test_handler.py -v
=================================================================== test session starts ====================================================================
platform darwin -- Python 3.13.3, pytest-8.4.1, pluggy-1.6.0 -- /Users/yemiwebby/confirm-tut/invoice-validator-scaleway-python/venv/bin/python
cachedir: .pytest_cache
rootdir: /Users/yemiwebby/confirm-tut/invoice-validator-scaleway-python
collected 7 items
test_handler.py::test_handle_valid_invoice PASSED [ 14%]
test_handler.py::test_handle_invalid_invoice PASSED [ 28%]
test_handler.py::test_handle_invalid_json PASSED [ 42%]
test_handler.py::test_validate_invoice_valid PASSED [ 57%]
test_handler.py::test_validate_invoice_invalid_amount PASSED [ 71%]
test_handler.py::test_validate_invoice_invalid_currency PASSED [ 85%]
test_handler.py::test_validate_invoice_invalid_date PASSED [100%]
==================================================================== 7 passed in 0.01s =====================================================================
Setting up an API key on Scaleway
To interact with Scaleway services from your CLI or CI/CD pipeline, you’ll need to generate an API key.
Create a new project if you don’t have one yet.
Next, go to the Scaleway API keys page to create a new API key.
In the console:
- Click Generate API Key.
- Select an API key bearer.
- Add an optional description and set the expiration if you would like.
- Select Generate API key.
Note: You can select whether the API key will be used for Object storage and choose a project. This step is not needed for this tutorial.
Once the API key is generated, copy and store both your secret key and access key securely. You will need these credentials to authenticate with Scaleway services. The secret key will only be shown once.
Configure Scaleway CLI
Start by initializing the Scaleway CLI. This will prompt you for your Scaleway Access Key, Secret Key, default region (such as fr-par), and default project. Make sure you have your API keys ready. Run:
scw init
After completing the prompts, your CLI will be configured and ready to interact with Scaleway resources.
Creating a namespace on Scaleway
Before deploying your function, you need to create a namespace on Scaleway. A namespace acts as a logical container for one or more related functions, helping you organize and manage your serverless resources.
To create a new namespace, run:
scw function namespace create name=invoice-namespace
After running the command, you’ll receive output similar to:
ID <namespace-id>
Name invoice-namespace
OrganizationID <organization-id>
ProjectID <project-id>
Status pending
...
Take note of the ID field, which is your namespace ID. You will need this ID when deploying your function.
Your namespace will initially be in pending status. To make sure it is created successfully, run the command scw function namespace get namespace-id=<YOUR-NAMESPACE-ID>. Make sure to replace <YOUR-NAMESPACE-ID> with the ID you just obtained.
Your output should be similar to:
ID <YOUR-NAMESPACE-ID>
Name invoice-namespace
OrganizationID <YOUR-ORGANIZATION-ID>
ProjectID <YOUR-PROJECT-ID>
Status ready
Packaging your function for Scaleway
Scaleway Functions requires your code and dependencies to be packaged as a ZIP archive before deployment. From your project directory, run:
zip -r function.zip handler.py requirements.txt
This command creates a function.zip file containing your handler and requirements files.
Note: Avoid zipping from a parent directory, as this can introduce unwanted folder paths in your archive and cause deployment issues. Always zip from within your project directory.
Your function is now packaged and ready to be deployed to Scaleway.
Deploying with the Scaleway CLI
With your function packaged and your namespace created, you’re ready to deploy to Scaleway Functions. When using this command, replace <YOUR_NAMESPACE_ID> with the actual namespace ID you obtained earlier. Run:
scw function deploy \
name=invoice-validator-python \
namespace-id=<YOUR_NAMESPACE_ID> \
runtime=python312 \
zip-file=function.zip
This command uploads your ZIP archive, creates (or updates) the function in the specified namespace, and sets the runtime to Python 3.12. Once the deployment process completes, the output shown will be similar to:
[+] Building 87.2s (4/4) FINISHED
=> Fetching namespace 0.2s
=> Creating or fetching function 0.1s
=> Uploading function 0.5s
=> Deploying function 86.4s
ID 7c4239b8-55e7-44a6-a587-6c3e203df8ca
Name invoice-validator-python
NamespaceID 1e03c3e0-40fb-40f2-a62d-f36b7fedcf63
Status ready
...
DomainName invoicenamespacep0isecut-invoice-validator-python.functions.fnc.fr-par.scw.cloud
...
Take note of the DomainName field in the output. This is your function’s public URL, which you can use to send HTTP requests to your deployed API.
Test your deployed function
To verify your deployment, try sending a POST request to your function’s URL using curl. For example:
curl -X POST <YOUR-FUNCTION-DOMAIN-NAME> \
-H "Content-Type: application/json" \
-d '{"amount": 150, "currency": "USD", "due_date": "2025-08-01"}'
Note: Make sure to replace <YOUR-FUNCTION-DOMAIN-NAME> with the actual value from the previous step.
You should receive a response like this:
{ "valid": true }
You can also test invalid payloads to confirm your validation logic is working:
curl -X POST <YOUR-FUNCTION-DOMAIN-NAME> \
-H "Content-Type: application/json" \
-d '{"amount": 0, "currency": "USD", "due_date": "2025-08-01"}'
{ "errors": ["amount must be greater than 0"], "valid": false }
curl -X POST <YOUR-FUNCTION-DOMAIN-NAME> \
-H "Content-Type: application/json" \
-d '{"amount": 150, "currency": "DD", "due_date": "2025-08-01"}'
{ "errors": ["currency must be a 3-letter code"], "valid": false }
This confirms that your serverless Python API is live and responding as expected on Scaleway Functions.
Configuring CircleCI
To automate your testing and deployment process, you’ll use CircleCI. This ensures that every code change is automatically tested and, if successful, deployed to Scaleway Functions without manual intervention.
Start by creating a .circleci/config.yml file in your project root. Add this content:
version: 2.1
executors:
python-executor:
docker:
- image: cimg/python:3.12
working_directory: ~/project
jobs:
test:
executor: python-executor
steps:
- checkout
- run:
name: Create virtual environment
command: |
python -m venv venv
source venv/bin/activate
echo 'export PATH="~/project/venv/bin:$PATH"' >> $BASH_ENV
- run:
name: Install dependencies
command: |
source venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt
- run:
name: Run tests
command: |
source venv/bin/activate
python -m pytest test_handler.py -v
deploy:
docker:
- image: cimg/base:stable
steps:
- checkout
- run:
name: Install Scaleway CLI
command: |
curl -s https://raw.githubusercontent.com/scaleway/scaleway-cli/master/scripts/get.sh | sh
scw version
- run:
name: Create Scaleway config file
command: |
mkdir -p /home/circleci/.config/scw
echo "access_key: $SCW_ACCESS_KEY" | tee /home/circleci/.config/scw/config.yaml > /dev/null
echo "secret_key: $SCW_SECRET_KEY" >> /home/circleci/.config/scw/config.yaml
echo "default_region: $SCW_DEFAULT_REGION" >> /home/circleci/.config/scw/config.yaml
echo "default_project_id: $SCW_DEFAULT_PROJECT_ID" >> /home/circleci/.config/scw/config.yaml
echo "default_organization_id: $SCW_DEFAULT_ORGANIZATION_ID" >> /home/circleci/.config/scw/config.yaml
echo "send_telemetry: false" >> /home/circleci/.config/scw/config.yaml
- run:
name: Build and package function
command: |
zip -r function.zip handler.py requirements.txt
- run:
name: Deploy to Scaleway Functions
command: |
scw function deploy name=$SCW_FUNCTION_NAME \
namespace-id=$SCW_NAMESPACE_ID \
runtime=python312 \
zip-file=function.zip
workflows:
build-and-deploy:
jobs:
- test
- deploy:
requires:
- test
filters:
branches:
only:
- main
This CircleCI configuration defines two jobs: test and deploy.
- The
testjob checks out your code, creates a Python virtual environment, installs dependencies, and runs your pytest tests. - The
deployjob installs the Scaleway CLI, configures it using environment variables, packages your Python function as a ZIP file, and deploys it to Scaleway Functions using the CLI.
The workflow ensures that deployment only happens if all tests pass and only on the main/master branches, providing a robust CI/CD pipeline for your serverless Python API.
Once you’ve added this file, save your changes and push your code to GitHub. Add a .gitignore file to exclude unnecessary files like the virtual environment. You can copy the content from this example .gitignore.
Note: Make sure your GitHub branch is named main as CircleCI will only run workflows from the main branch as configured in your .circleci/config.yml file.
Setting up the project in CircleCI
Log into CircleCI and create a project.
After setting up your project, your first build might be triggered automatically.
It’s normal for the pipeline to fail on the first run. You still need to add the required environment variables.
Create environment variables in CircleCI
In CircleCI, go to your project settings and add the following environment variables:
SCW_ACCESS_KEY: Your Scaleway access key.SCW_SECRET_KEY: Your Scaleway secret key.SCW_DEFAULT_REGION: Your Scaleway default region (e.g.,fr-par).SCW_DEFAULT_PROJECT_ID: Your Scaleway default project ID.SCW_DEFAULT_ORGANIZATION_ID: Your Scaleway default organization ID.SCW_FUNCTION_NAME: Your Scaleway function name (e.g.,invoice-validator-python).SCW_NAMESPACE_ID: Your Scaleway namespace ID.
You can find your organization and project IDs in the Scaleway console from the Organization Dashboard and Project Dashboard respectively. The namespace ID is provided in the output of the scw function namespace create command.
After adding these variables, re-run the pipeline. It should now pass successfully.
Conclusion
Congratulations! You have built a complete, production-ready serverless API in Python and deployed it to Scaleway Functions. Along the way, you learned how to structure your handler for Scaleway’s platform, write and test robust validation logic, and automate your workflow using CircleCI. With this setup, every code change is automatically tested and deployed, giving you a fast, reliable, and up-to-date CI/CD pipeline for your serverless projects.
This approach not only saves you from managing infrastructure but also ensures your API is always up-to-date and resilient. You can now extend this foundation with new features, integrate with other services, or adapt the workflow for other serverless use cases.
If you’d like to explore further, check out Scaleway’s documentation for advanced features like environment variables, secrets, and custom domains.
The complete source code for this tutorial is available on GitHub. Happy building!