← Back to FastAPI
Lesson 1.4 · Getting Started

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.

Beginner30 min

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.

request flowFrom URL text to a Python valuestep 1 / 3

1 - The route matches

/books/2 fits the pattern /books/{book_id}. The text "2" is cut out of the URL.

path
/books/2
pattern
/books/{book_id}
raw value
"2" (text)
no match
/users/a/b -> 404

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.

Small dictionary
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.

Example 1 - path_params.py
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}
Output - 19 requests with TestClient
/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.

Error types you met above
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.

Output - what odd numbers turn into
/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.

Output - ../ inside a :path parameter (curl --path-as-is)
/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.

Path or query?
Which book?Path: /books/2
Which shop, which book?Path: /shops/7/books/3
Only python booksQuery: /books?genre=python (Lesson 1.5)
Page 2, 10 per pageQuery: /books?skip=10&limit=10

Path parameters at a glance

Declare

Same name in path and function.

@app.get("/books/{book_id}")
def get_book(book_id: int):
Limits

ge, gt, le, lt.

page: Annotated[int, Path(ge=1, le=100)]
Description

Shown in /docs.

Path(description="Page number, 1 to 100")
Fixed choices

An Enum.

class Size(str, Enum): small = "small" ...
With slashes

Check it before using it!

@app.get("/files/{file_path:path}")
Several

Any 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.

In the docs

“Run path_params.py with fastapi dev and open /docs. How does /shirts/{size} look different from /books/{book_id}?”

Keep zeros

“Make a route /codes/{code} where "007" and "7" are different. Which type do you need?”

No negatives

“Add Path(ge=1) to get_book. What does /books/-1 return now, and why is that better?”

Safe files

“Change get_file to refuse any file_path that contains "..". Test it with curl --path-as-is.”

What usually goes wrong

Different names in path and function

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):
An int for codes that keep their zeros

"007" became 7. If leading zeros matter, use str.

No limits on numbers

/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)]):
Trusting a :path value

"../../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

How do you get the "2" from /books/2 as an integer?
@app.get("/books/{book_id}") and def get_book(book_id: int).
What does "loc": ["path", "page"] mean in an error?
The problem is in the path parameter called page.
What did /books/007 turn into?
The integer 7.
How do you allow only small, medium and large?
Declare a str Enum with those values and use it as the type.

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. 1.

    Why did /users/a/b give 404?

  2. 2.

    Which of these became valid ints: "+2", "1_000", "1e3"?

  3. 3.

    What does Path(ge=1, le=100) do for page=101?

  4. 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...