Path Parameters
Path parameters are variable segments of the URL captured by name. FastAPI uses your Python type hints to validate and coerce them automatically — an invalid type triggers a 422 before your handler runs.
By the end of this lesson you can: declare typed path parameters, add metadata and constraints with Path, validate enums in paths, and handle multiple path parameters in a single route.
Declaring a Path Parameter
from fastapi import APIRouter
router = APIRouter(prefix="/items", tags=["items"])
@router.get("/{item_id}")
async def get_item(item_id: int) -> dict:
return {"item_id": item_id}
The {item_id} segment in the path is captured and passed to the function. FastAPI converts it to int — if the request sends /items/abc, FastAPI returns:
{
"detail": [{"type": "int_parsing", "loc": ["path", "item_id"], "msg": "..."}]
}
No handler code runs. Validation is free.
Type Coercion in Paths
| Type hint | Accepted path segments | Rejected |
|---|---|---|
int | 123, 0, -1 | abc, 1.5 |
float | 1.5, 3, -0.1 | abc |
str | Any text | — |
bool | true, false, 1, 0 | yes, no |
uuid.UUID | Valid UUID4 string | Malformed UUID |
import uuid
from fastapi import APIRouter
router = APIRouter()
@router.get("/users/{user_id}")
async def get_user(user_id: uuid.UUID) -> dict:
return {"user_id": str(user_id)}
@router.get("/posts/{slug}")
async def get_post(slug: str) -> dict:
return {"slug": slug}
Path with Annotated and Path
Use Annotated and Path to add validation constraints and OpenAPI metadata:
from typing import Annotated
from fastapi import APIRouter, Path
router = APIRouter(prefix="/items", tags=["items"])
@router.get("/{item_id}")
async def get_item(
item_id: Annotated[
int,
Path(
title="The item ID",
description="Must be a positive integer",
ge=1, # >= 1
le=1_000_000, # <= 1,000,000
example=42,
),
],
) -> dict:
return {"item_id": item_id}
Available Path constraints:
| Constraint | Type | Meaning |
|---|---|---|
ge | numeric | Greater than or equal |
gt | numeric | Greater than |
le | numeric | Less than or equal |
lt | numeric | Less than |
min_length | str | Minimum string length |
max_length | str | Maximum string length |
pattern | str | Regex pattern |
Enum Path Parameters
When a path segment must be one of a fixed set of values, use an Enum:
from enum import Enum
from fastapi import APIRouter
class ReportFormat(str, Enum):
json = "json"
csv = "csv"
xml = "xml"
router = APIRouter(prefix="/reports", tags=["reports"])
@router.get("/{report_id}/export/{format}")
async def export_report(report_id: int, format: ReportFormat) -> dict:
return {"report_id": report_id, "format": format.value}
FastAPI automatically documents the enum values in OpenAPI — users can only send json, csv, or xml. Any other value returns a 422.
Multiple Path Parameters
A single route can have several path parameters:
from fastapi import APIRouter, Path
from typing import Annotated
router = APIRouter(prefix="/orgs", tags=["organizations"])
@router.get("/{org_id}/repos/{repo_id}/issues/{issue_number}")
async def get_issue(
org_id: Annotated[str, Path(min_length=1, max_length=50)],
repo_id: Annotated[str, Path(min_length=1, max_length=100)],
issue_number: Annotated[int, Path(ge=1)],
) -> dict:
return {
"org": org_id,
"repo": repo_id,
"issue": issue_number,
}
Path Parameters with Slash (/) in Value
By default {param} does not match /. To allow a file path value:
from fastapi import APIRouter
router = APIRouter()
@router.get("/files/{file_path:path}")
async def read_file(file_path: str) -> dict:
# file_path captures "a/b/c.txt" from /files/a/b/c.txt
return {"path": file_path}
:path converters bypass path segment validation. Always sanitize the value to prevent path traversal attacks.
Common Pitfalls
| Pitfall | Cause / Symptom | Fix |
|---|---|---|
| 422 when passing a string that looks like int | Using int type hint on non-numeric IDs | Use str for slug/UUID identifiers, not int |
Route matching /me as UUID | /me declared after /{user_id: UUID} | Put /me before /{user_id} |
| Negative IDs accepted | No ge=1 constraint | Use Path(ge=1) with Annotated |
| Enum values not in docs | Didn't inherit from str | Always inherit from both str and Enum: class X(str, Enum) |
Path traversal via :path | Unvalidated file paths | Strip leading /, validate against allowed directories |
Hands-On Practice
from enum import Enum
from typing import Annotated
from fastapi import APIRouter, Path, HTTPException
class Category(str, Enum):
electronics = "electronics"
clothing = "clothing"
books = "books"
router = APIRouter(prefix="/catalog", tags=["catalog"])
_items = {
1: {"name": "Laptop", "category": "electronics"},
2: {"name": "T-Shirt", "category": "clothing"},
3: {"name": "Python Cookbook", "category": "books"},
}
@router.get("/{category}")
async def list_by_category(category: Category) -> list[dict]:
return [
{"id": k, **v}
for k, v in _items.items()
if v["category"] == category.value
]
@router.get("/{category}/{item_id}")
async def get_item_in_category(
category: Category,
item_id: Annotated[int, Path(ge=1, title="Item ID")],
) -> dict:
item = _items.get(item_id)
if not item or item["category"] != category.value:
raise HTTPException(404, detail="Item not found in category")
return {"id": item_id, **item}
uvicorn app.routers.catalog:router --reload --port 8003
curl "http://localhost:8003/catalog/electronics"
curl "http://localhost:8003/catalog/books/3"
curl "http://localhost:8003/catalog/electronics/99" -w "\n%{http_code}"
curl "http://localhost:8003/catalog/furniture" -w "\n%{http_code}"
# → 422 — furniture is not a valid Category enum value