Path Parameters
Read values from the URL itself - /books/2, /shirts/large, /files/docs/report.pdf - with types, limits and fixed choices. Tested with 26 real requests, including the surprising ones: "007", "+2" and "1_000" all become numbers, and "../" passes straight through.
What you will be able to do
- Explain what a path parameter is and when to use one
- Declare path parameters with types: int, str, float
- Add limits with Path(): ge, le, description
- Allow only fixed values with an Enum
- Use {name:path} for values that contain slashes - safely
- Read the 422 errors path parameters produce
The idea, in plain English
Often the URL itself says which thing you want: /books/2 means "book number 2", /users/ravi means "the user ravi". The changing part of the URL is a path parameter. In FastAPI you write it in curly brackets in the path - "/books/{book_id}" - and add a function argument with the same name.
The type hint on that argument does the work. book_id: int means FastAPI takes the text from the URL ("2"), turns it into the integer 2, and refuses the request with a 422 error if it cannot ("abc"). You saw this in Lessons 1.1 and 1.3; now we look at it closely.
In this lesson we build a small bookshop API and send it 26 requests - normal ones, wrong ones and strange ones - to see exactly what FastAPI accepts and what it refuses. Some results surprised us. FastAPI 0.143.0, Pydantic 2.14.0.
Worked example: A small bookshop API: books by id, users by name, pages with limits, shirt sizes from a fixed list, and file paths with slashes.
1 - The route matches
/books/2 fits the pattern /books/{book_id}. The text "2" is cut out of the URL.
What happens to the {book_id} part of the URL - real results from our tests.
Words you will see in this lesson
A few words about parameters.
Path parameterA changing part of the URL path, written in {curly brackets}.Type hintbook_id: int - tells FastAPI what type to convert to and check.ValidationChecking a value against rules: type, limits, allowed choices.Path()Adds rules and docs to a path parameter: ge, le, description.EnumA fixed list of allowed values, like small, medium, large.URL encodingSpecial characters in URLs written as %XX: a space is %20.An everyday example: a flat address
An address like "Block B, Flat 204" has a fixed shape and changing parts. The words "Block" and "Flat" never change; "B" and "204" do. The postman knows "204" must be a flat number - "two hundred and four", not a name.
A path like /shops/{shop_id}/books/{book_id} is the same: fixed words (shops, books) and changing parts (the ids). The type hint is the postman’s knowledge: "this part must be a number".
Example 1 - the bookshop API
Each route shows one idea. get_book: a typed int. get_user: a str, which accepts anything without a slash. get_page: limits with Path(ge=1, le=100). get_shirt: a fixed list of sizes with an Enum. shop_book: two path parameters. get_file: :path, which may contain slashes.
Annotated[int, Path(...)] is the modern way to add rules: the type comes first (int), then the extra information (Path with its rules). FastAPI reads both.
from enum import Enum
from typing import Annotated
from fastapi import FastAPI, HTTPException, Path
app = FastAPI()
BOOKS = {1: "Python Basics", 2: "Web APIs", 3: "Databases"}
@app.get("/books/{book_id}") # {book_id} in the path ...
def get_book(book_id: int): # ... becomes this argument, as an int
if book_id not in BOOKS:
raise HTTPException(status_code=404, detail="Book not found")
return {"book_id": book_id, "title": BOOKS[book_id]}
@app.get("/users/{username}") # a str: anything goes
def get_user(username: str):
return {"username": username, "length": len(username)}
@app.get("/pages/{page}") # limits with Path()
def get_page(page: Annotated[int, Path(ge=1, le=100, description="Page number, 1 to 100")]):
return {"page": page}
class Size(str, Enum): # only these three values are allowed
small = "small"
medium = "medium"
large = "large"
@app.get("/shirts/{size}")
def get_shirt(size: Size):
return {"size": size, "is_large": size is Size.large}
@app.get("/shops/{shop_id}/books/{book_id}") # two path parameters
def shop_book(shop_id: int, book_id: int):
return {"shop_id": shop_id, "book_id": book_id}
@app.get("/files/{file_path:path}") # :path - may contain slashes
def get_file(file_path: str):
return {"file_path": file_path}/books/2 -> 200 {"book_id":2,"title":"Web APIs"}
/books/9 -> 404 {"detail":"Book not found"}
/books/abc -> 422 {"detail":[{"type":"int_parsing","loc":["path","book_id"],"msg":"Input should be a valid integer, unable to parse string as an integer","input":"abc"}
/books/2.5 -> 422 {"detail":[{"type":"int_parsing","loc":["path","book_id"],"msg":"Input should be a valid integer, unable to parse string as an integer","input":"2.5"}
/books/-1 -> 404 {"detail":"Book not found"}
/books/007 -> 404 {"detail":"Book not found"}
/books/%202 -> 200 {"book_id":2,"title":"Web APIs"}
/users/ravi -> 200 {"username":"ravi","length":4}
/users/Ravi Kumar -> 200 {"username":"Ravi Kumar","length":10}
/users/123 -> 200 {"username":"123","length":3}
/pages/5 -> 200 {"page":5}
/pages/0 -> 422 {"detail":[{"type":"greater_than_equal","loc":["path","page"],"msg":"Input should be greater than or equal to 1","input":"0","ctx":{"ge":1}}]}
/pages/101 -> 422 {"detail":[{"type":"less_than_equal","loc":["path","page"],"msg":"Input should be less than or equal to 100","input":"101","ctx":{"le":100}}]}
/shirts/large -> 200 {"size":"large","is_large":true}
/shirts/LARGE -> 422 {"detail":[{"type":"enum","loc":["path","size"],"msg":"Input should be 'small', 'medium' or 'large'","input":"LARGE","ctx":{"expected":"'small', 'medi
/shirts/xl -> 422 {"detail":[{"type":"enum","loc":["path","size"],"msg":"Input should be 'small', 'medium' or 'large'","input":"xl","ctx":{"expected":"'small', 'medium'
/shops/7/books/3 -> 200 {"shop_id":7,"book_id":3}
/files/docs/2026/report.pdf -> 200 {"file_path":"docs/2026/report.pdf"}
/users/a/b -> 404 {"detail":"Not Found"}Reading a 422 error
Every validation error has the same shape, so learn to read it once. "detail" is a list - one entry per problem. In each entry: "type" is the kind of problem (int_parsing, greater_than_equal, enum); "loc" is where it is (["path", "page"] = the path parameter called page); "msg" is a sentence for humans; "input" is what was sent; "ctx" gives the rule that failed ({"ge": 1}).
Notice that the input is always text ("0", "101") - everything in a URL is text until FastAPI converts it.
int_parsingNot a whole number: "abc", "2.5".greater_than_equalSmaller than the ge limit: page 0 with ge=1.less_than_equalBigger than the le limit: page 101 with le=100.enumNot one of the allowed values: "xl", "LARGE".Numbers: more forgiving than you think
We wrote a tiny endpoint that just returns the int it received, and sent it odd numbers. "007" became 7. "+2" became 2. A space before or after - " 2" and "2 " - was removed: 2. "1_000" became 1000, because Python allows _ in numbers. A 20-digit number was accepted as it is. Only "1e3" (scientific notation) was refused.
This is Pydantic’s normal ("lax") mode: it converts when the meaning is clear. Usually that is helpful. But it means /books/007 and /books/7 are the same book - which matters if your ids are codes like "007" that must keep their zeros. Then use a str, not an int.
Also: -1 was accepted as an int and only failed because no book has id -1. If negative values make no sense, say so with Path(ge=1) - then the client gets a clear 422 instead of a confusing 404.
/n/007 -> 200 {"n":7}
/n/%202 -> 200 {"n":2}
/n/+2 -> 200 {"n":2}
/n/2%20 -> 200 {"n":2}
/n/1_000 -> 200 {"n":1000}
/n/1e3 -> 422 {"detail":[{"type":"int_parsing","loc":["path","n"],"msg":"Input should be a valid integer
/n/99999999999999999999 -> 200 {"n":99999999999999999999}Fixed choices with an Enum
When only some values make sense - shirt sizes, order statuses, sort directions - list them in an Enum. FastAPI then accepts only those values, shows them as a drop-down in /docs, and gives a clear error for anything else: "Input should be 'small', 'medium' or 'large'".
Enum values are case-sensitive: "LARGE" was refused. Inside your function, size is an Enum member, so you can compare it safely: size is Size.large.
Slashes, and a safety warning
A normal path parameter stops at a slash: /users/a/b did not match /users/{username} at all (404). If a value may contain slashes - a file path, a category tree - write {file_path:path}. Then /files/docs/2026/report.pdf gives file_path = "docs/2026/report.pdf".
Be careful with what you do with that value. We sent ../../etc/passwd through a running server, and FastAPI passed it to the function exactly as "../../etc/passwd". It does not clean it up. If you use it to open a file, a client could read files outside your folder - the path traversal problem we met in the MCP module (Lesson 4.5 of Agentic AI). Never join it to a folder without checking it.
/files/../../etc/passwd -> {"file_path":"../../etc/passwd"} [200]
/files/a/../b -> {"file_path":"a/../b"} [200]
/files/%2E%2E/secret -> {"file_path":"../secret"} [200]Watch out: Treat every path parameter as untrusted text from the internet. Check it (types, limits, allowed values) before you use it - especially for files, database queries or shell commands.
Path parameter or not?
Use a path parameter when the value says WHICH thing: /books/2, /users/ravi. It is required - a URL without it is a different URL. For optional things that filter or sort - "only python books", "page 2", "newest first" - use query parameters, the next lesson.
Which book?Path: /books/2Which shop, which book?Path: /shops/7/books/3Only python booksQuery: /books?genre=python (Lesson 1.5)Page 2, 10 per pageQuery: /books?skip=10&limit=10Path parameters at a glance
DeclareSame name in path and function.
@app.get("/books/{book_id}")
def get_book(book_id: int):Limitsge, gt, le, lt.
page: Annotated[int, Path(ge=1, le=100)]
DescriptionShown in /docs.
Path(description="Page number, 1 to 100")
Fixed choicesAn Enum.
class Size(str, Enum): small = "small" ...
With slashesCheck it before using it!
@app.get("/files/{file_path:path}")SeveralAny number.
"/shops/{shop_id}/books/{book_id}"Try it yourself
The code does not change. Swap the content string and the program does something else entirely.
“Run path_params.py with fastapi dev and open /docs. How does /shirts/{size} look different from /books/{book_id}?”
“Make a route /codes/{code} where "007" and "7" are different. Which type do you need?”
“Add Path(ge=1) to get_book. What does /books/-1 return now, and why is that better?”
“Change get_file to refuse any file_path that contains "..". Test it with curl --path-as-is.”
What usually goes wrong
The name in {curly brackets} must match the argument name exactly.
✗ @app.get("/books/{book_id}")
def get_book(id: int):✓ @app.get("/books/{book_id}")
def get_book(book_id: int):"007" became 7. If leading zeros matter, use str.
/books/-1 gave a confusing 404. Say what is allowed, so the client gets a clear 422.
✗ def get_page(page: int):✓ def get_page(page: Annotated[int, Path(ge=1)]):"../../etc/passwd" arrived unchanged. Check it before opening files.
Key points
- A path parameter is a {named} part of the path; the function argument with the same name receives it.
- The type hint converts the text and checks it; a failure gives 422 with type, loc, msg and input.
- Conversion is forgiving: "007" -> 7, "+2" -> 2, "1_000" -> 1000; "abc", "2.5" and "1e3" fail.
- Path(ge=..., le=...) adds limits; an Enum allows only fixed values (case-sensitive).
- A normal parameter stops at "/"; {name:path} accepts slashes - and "../" - so check it.
- Use path parameters for WHICH thing; use query parameters for options.
Quick check before you move on
Interview questions
How does FastAPI validate path parameters?
It extracts the text from the matched route, then validates and converts it with Pydantic according to the type hint and any Path() constraints, returning 422 with structured error details on failure.
When would you use an Enum for a path parameter?
When only a fixed set of values is valid - statuses, categories, sizes. It gives validation, documentation with the allowed values, and safe comparisons in code.
What security concern comes with {param:path}?
It accepts slashes and ".." sequences unchanged, so using it in file system access allows path traversal. Normalise and verify it stays inside an allowed directory, or map it to known ids.
Quiz
- 1.
Why did /users/a/b give 404?
- 2.
Which of these became valid ints: "+2", "1_000", "1e3"?
- 3.
What does Path(ge=1, le=100) do for page=101?
- 4.
What did FastAPI do with ../../etc/passwd in a :path parameter?
Comments
Sign in to leave a comment. Your name and photo come from Google; nothing else is shared.
Loading comments...