Python v2 Programming Model¶
This document provides a deep dive into the Python v2 programming model for Azure Functions. The v2 model uses Python decorators to define triggers, bindings, and routes — replacing the file-based function.json configuration from v1.
flowchart LR
A[Decorators in function_app.py and blueprints] --> B[Python worker indexes functions]
B --> C[Function metadata registered with host]
C --> D[Azure Functions host builds trigger and route map]
D --> E[Invocations dispatched to Python worker] v1 vs. v2: What Changed¶
The v1 programming model required a specific directory structure with one folder per function, each containing a function.json configuration file and a __init__.py entry point:
# v1 structure (LEGACY — do not use)
MyFunctionApp/
host.json
requirements.txt
HealthFunction/
function.json # Trigger/binding config
__init__.py # Function code
InfoFunction/
function.json
__init__.py
The v2 model eliminates this structure entirely. Functions are defined inline using decorators, and the project structure is flat:
# v2 structure (CURRENT)
apps/python/
host.json
requirements.txt
function_app.py # App entry point
blueprints/
health.py # Functions organized by domain
info.py
requests.py
Key Differences¶
| Aspect | v1 Model | v2 Model |
|---|---|---|
| Function config | function.json per function | Python decorators |
| Discovery | Host scans filesystem | Worker indexes decorators |
| Project structure | One folder per function | Flat or modular (your choice) |
| Feature flag | Not needed | Not required on current runtimes (4.x+) |
| Bindings | JSON properties | Decorator parameters |
| Entry point | __init__.py per folder | function_app.py (single entry) |
| Modularity | Folders | Blueprint class |
FunctionApp — The Application Entry Point¶
Every v2 Azure Functions application starts with a FunctionApp instance in function_app.py. This is the file the Python worker imports at startup.
The FunctionApp class serves as the root container for all functions. You can register functions directly on the app or use Blueprints for modular organization.
Direct Registration¶
For small applications with a few functions, register directly on the FunctionApp:
import azure.functions as func
import json
app = func.FunctionApp()
@app.route(route="health", methods=["GET"])
def health(req: func.HttpRequest) -> func.HttpResponse:
return func.HttpResponse(
json.dumps({"status": "healthy"}),
mimetype="application/json"
)
@app.route(route="info", methods=["GET"])
def info(req: func.HttpRequest) -> func.HttpResponse:
return func.HttpResponse(
json.dumps({"name": "azure-functions-python-guide"}),
mimetype="application/json"
)
This approach is simple but becomes unwieldy with many functions.
Blueprint — Modular Organization¶
The Blueprint class allows you to group related functions into separate modules. This is the recommended pattern for applications with more than a handful of functions.
Defining a Blueprint¶
# blueprints/health.py
import azure.functions as func
import json
from datetime import datetime, timezone
bp = func.Blueprint()
@bp.route(route="health", methods=["GET"])
def health(req: func.HttpRequest) -> func.HttpResponse:
body = {
"status": "healthy",
"timestamp": datetime.now(timezone.utc).isoformat(),
"version": "1.0.0"
}
return func.HttpResponse(
json.dumps(body),
mimetype="application/json"
)
Registering Blueprints¶
# function_app.py
import azure.functions as func
app = func.FunctionApp()
from blueprints.health import bp as health_bp
from blueprints.info import bp as info_bp
from blueprints.requests import bp as requests_bp
from blueprints.dependencies import bp as dependencies_bp
from blueprints.exceptions import bp as exceptions_bp
app.register_functions(health_bp)
app.register_functions(info_bp)
app.register_functions(requests_bp)
app.register_functions(dependencies_bp)
app.register_functions(exceptions_bp)
When register_functions() is called, all decorated functions from the Blueprint are merged into the FunctionApp's function index.
@app.route() vs. @bp.route()¶
| Decorator | Defined On | Use When |
|---|---|---|
@app.route() | FunctionApp | Small apps, quick prototypes, single-file applications |
@bp.route() | Blueprint | Production apps, multiple function groups, team collaboration |
Both decorators accept the same parameters. The only difference is where the function is registered. Blueprints provide better separation of concerns and make testing individual modules easier.
HttpRequest Object¶
The func.HttpRequest object provides access to all parts of the incoming HTTP request:
@bp.route(route="example/{id}", methods=["GET", "POST"])
def example(req: func.HttpRequest) -> func.HttpResponse:
# HTTP method
method = req.method # "GET" or "POST"
# Route parameters (from URL path)
item_id = req.route_params.get("id")
# Query parameters
page = req.params.get("page", "1")
# Headers
auth_header = req.headers.get("Authorization", "")
content_type = req.headers.get("Content-Type", "")
# Request body (raw bytes)
raw_body = req.get_body()
# Request body (as JSON — raises ValueError if not valid JSON)
try:
json_body = req.get_json()
except ValueError:
json_body = None
# URL
url = req.url # Full request URL
Key Properties and Methods¶
| Property/Method | Type | Description |
|---|---|---|
req.method | str | HTTP method (GET, POST, PUT, etc.) |
req.url | str | Full request URL |
req.headers | dict-like | Request headers (case-insensitive keys) |
req.params | dict | Query string parameters |
req.route_params | dict | URL path parameters from route template |
req.get_body() | bytes | Raw request body |
req.get_json() | dict/list | Parsed JSON body (raises ValueError on invalid JSON) |
HttpResponse Object¶
The func.HttpResponse object represents the response sent back to the client:
# Simple text response
return func.HttpResponse("OK", status_code=200)
# JSON response
return func.HttpResponse(
json.dumps({"key": "value"}),
mimetype="application/json",
status_code=200
)
# Response with custom headers
response = func.HttpResponse(
json.dumps({"created": True}),
mimetype="application/json",
status_code=201
)
response.headers["X-Request-Id"] = "abc-123"
response.headers["Cache-Control"] = "no-store"
return response
# Error response
return func.HttpResponse(
json.dumps({"error": "Not found"}),
mimetype="application/json",
status_code=404
)
Constructor Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
body | str or bytes | None | Response body |
status_code | int | 200 | HTTP status code |
headers | dict | {} | Response headers |
mimetype | str | None | Content-Type header value |
charset | str | None | Character encoding |
Route Configuration¶
Route Prefix¶
All HTTP trigger routes are prefixed with /api/ by default. This is configured in host.json:
With the default prefix, a function with route="health" is accessible at /api/health. To remove the prefix entirely:
Route Templates¶
Routes support parameterized segments:
@bp.route(route="users/{user_id}/orders/{order_id}", methods=["GET"])
def get_order(req: func.HttpRequest) -> func.HttpResponse:
user_id = req.route_params["user_id"]
order_id = req.route_params["order_id"]
# ...
Route parameters are always strings. Cast them as needed:
Complete Blueprint Example¶
Here is a complete blueprint demonstrating common patterns:
# blueprints/products.py
import azure.functions as func
import json
import logging
bp = func.Blueprint()
# In-memory store for demo purposes
_products = {
"1": {"id": "1", "name": "Widget", "price": 9.99},
"2": {"id": "2", "name": "Gadget", "price": 24.99},
}
@bp.route(route="products", methods=["GET"])
def list_products(req: func.HttpRequest) -> func.HttpResponse:
"""List all products."""
logging.info("Listing %d products", len(_products))
return func.HttpResponse(
json.dumps(list(_products.values())),
mimetype="application/json"
)
@bp.route(route="products/{product_id}", methods=["GET"])
def get_product(req: func.HttpRequest) -> func.HttpResponse:
"""Get a single product by ID."""
product_id = req.route_params.get("product_id")
product = _products.get(product_id)
if product is None:
return func.HttpResponse(
json.dumps({"error": "Product not found"}),
mimetype="application/json",
status_code=404
)
return func.HttpResponse(
json.dumps(product),
mimetype="application/json"
)
@bp.route(route="products", methods=["POST"])
def create_product(req: func.HttpRequest) -> func.HttpResponse:
"""Create a new product."""
try:
body = req.get_json()
except ValueError:
return func.HttpResponse(
json.dumps({"error": "Invalid JSON"}),
mimetype="application/json",
status_code=400
)
product_id = str(len(_products) + 1)
product = {"id": product_id, "name": body["name"], "price": body["price"]}
_products[product_id] = product
logging.info("Created product: %s", product_id)
return func.HttpResponse(
json.dumps(product),
mimetype="application/json",
status_code=201
)
Register it in function_app.py:
This creates three endpoints: - GET /api/products — List all products - GET /api/products/{product_id} — Get a product by ID - POST /api/products — Create a new product