Lesson 3 - Path Parameters

Welcome to Path Parameters

So far your endpoints have lived at fixed URLs like /tasks. But a real Task Manager needs to answer questions about one specific task — “show me task 3,” “show me task 7.” You don’t want a separate function for every task; you want a single endpoint that captures the number from the URL and works for any task. That captured piece of the URL is a path parameter, and it’s one of the most-used features in all of FastAPI. The best part is that the same type hints you’ve already been writing turn each path parameter into something FastAPI reads, converts, and validates for you.

In this lesson you’ll declare your first path parameter, watch FastAPI convert /items/42 into a real integer (not the text "42"), see the exact error it returns when someone sends a bad value, learn why the order you define your routes in matters, and finish by restricting a parameter to a fixed set of allowed values with an Enum.

By the end of this lesson, you will be able to:

  • Declare a path parameter and read it inside your function
  • Use a type hint to get automatic conversion and validation, and read the 422 error FastAPI returns for bad input
  • Order your routes correctly so fixed paths win over variable ones
  • Restrict a path parameter to a fixed set of values using an Enum

Let’s begin.


Declaring a Path Parameter

A path parameter is a part of the URL path that carries data. You mark it in your route by wrapping a name in curly braces, then you receive its value by adding a function argument with the same name. Here is the smallest possible example:

from fastapi import FastAPI

app = FastAPI()

@app.get("/items/{item_id}")
def get_item(item_id: int):
    return {"item_id": item_id, "type": type(item_id).__name__}

Two things are connected here. In the decorator, "/items/{item_id}" says “the part of the URL where I’ve written {item_id} is not fixed text — capture whatever is there and call it item_id.” Then the function argument item_id: int receives that captured value. The name in the braces and the name of the argument must match — that’s how FastAPI knows which argument to fill in.

The interesting part is the type hint : int. We’ve added a temporary line, "type": type(item_id).__name__, just to see what type FastAPI hands us. Let’s send a request to /items/42:

GET /items/42 -> 200 {'item_id': 42, 'type': 'int'}

Read that output carefully. URLs are always text — the browser sends the characters 4 and 2. But the response shows 'type': 'int', and item_id is 42 with no quotes around it. FastAPI saw your : int hint, took the text "42" from the URL, and converted it into a real Python integer before your function ran. That means inside your function you can do real math (item_id + 1) without converting anything yourself. This automatic conversion from URL text to the type you asked for is the first thing path parameters give you.


Automatic Validation and the 422 Error

Conversion only works when the value actually fits the type. What happens if someone requests /items/abc? The text "abc" is not a number, so FastAPI cannot turn it into an int. Instead of crashing or passing garbage to your function, it stops the request and sends back a structured error:

GET /items/abc status -> 422

That 422 is an HTTP status code meaning the request was understood but the data in it was invalid. FastAPI doesn’t just say “no” — it tells the client exactly what went wrong. Here is the real response body for /items/abc:

{
  "detail": [
    {
      "type": "int_parsing",
      "loc": [
        "path",
        "item_id"
      ],
      "msg": "Input should be a valid integer, unable to parse string as an integer",
      "input": "abc"
    }
  ]
}

Let’s unpack this, because you’ll see this shape again and again in FastAPI. The whole body is an object with one key, detail, holding a list of errors (a list because a request can have several problems at once). Each error is an object with:

  • type — a short machine-readable label for the kind of error. Here it’s "int_parsing", meaning “we tried to parse this as an integer and couldn’t.”
  • loc — short for location, telling you exactly where the bad value was. ["path", "item_id"] means “in the URL path, the item_id part.”
  • msg — a human-readable explanation: Input should be a valid integer, unable to parse string as an integer.
  • input — the actual bad value it received, "abc", so you can see what was sent.

The crucial idea is when this happens: the validation runs before your function is ever called. Your get_item code never executed for the /items/abc request — FastAPI rejected it at the door. You wrote zero validation code, yet bad input can never reach the inside of your function. That’s the safety net the type hint buys you.

What does 422 mean?

422 stands for Unprocessable Entity. It’s the status code FastAPI uses when a request reaches your API correctly but contains data that fails validation — the wrong type, a missing required value, or a value outside the allowed set. It is different from 404 (the URL doesn’t exist) and 500 (your code crashed). A 422 means “I understood your request, but I can’t accept this data,” and the detail list always tells you why. Because validation happens before your function runs, you never have to write this check yourself.


Why Route Order Matters

Path parameters match anything in their slot, and that creates a subtle trap. Imagine you want two endpoints under /users: one for the special URL /users/me (the currently logged-in user), and one for any user by id, /users/{user_id}. The problem is that /users/me also matches the pattern /users/{user_id} — the text "me" is a perfectly valid value to capture into user_id.

FastAPI checks routes in the order you define them and uses the first one that matches. So the order you write them in decides who wins. Define the fixed path first:

@app.get("/users/me")
def read_me():
    return {"user_id": "the current user"}

@app.get("/users/{user_id}")
def read_user(user_id: str):
    return {"user_id": user_id}

Now requests behave exactly as you’d want:

GET /users/me  -> 200 {'user_id': 'the current user'}
GET /users/123 -> 200 {'user_id': '123'}

/users/me hits the specific route because it was defined first, and /users/123 falls through to the variable route. If you had written the /users/{user_id} route first, it would have matched /users/me too — capturing "me" as a user_id — and your read_me function would never run. The rule to remember: fixed paths before variable paths. When two routes could match the same URL, define the more specific one earlier.


Restricting Values with an Enum

Sometimes a path parameter shouldn’t accept any value of a type — only a fixed set of choices. A task’s priority is a perfect example: you might allow only "low" or "high", and you want anything else rejected. You could check this by hand inside your function, but FastAPI offers a cleaner way: declare the allowed values as an Enum (short for enumeration — a fixed list of named choices) and use it as the type hint.

To make the values work nicely in URLs, your enum class inherits from both str and Enum. The str part means each member is a string, so it slots straight into a URL; the Enum part means only the listed members are valid:

from enum import Enum
from fastapi import FastAPI

app = FastAPI()

class Priority(str, Enum):
    low = "low"
    high = "high"

@app.get("/tasks/{priority}")
def by_priority(priority: Priority):
    return {"priority": priority.value}

By hinting priority: Priority, you’ve told FastAPI: “only low or high are acceptable here.” A valid request works as expected:

GET /tasks/high -> 200 {'priority': 'high'}

Inside the function, priority is an Enum member, so we use priority.value to get the plain string "high" back out. Now watch what happens with a value that isn’t in the list, like /tasks/medium:

GET /tasks/medium status -> 422
{
  "detail": [
    {
      "type": "enum",
      "loc": [
        "path",
        "priority"
      ],
      "msg": "Input should be 'low' or 'high'",
      "input": "medium",
      "ctx": {
        "expected": "'low' or 'high'"
      }
    }
  ]
}

It’s the same friendly 422 pattern as before, but tuned to the enum: the type is now "enum", and the message even lists the valid options — Input should be ’low’ or ‘high’. An enum gives you a self-documenting parameter (the interactive docs will show the choices as a dropdown) plus automatic rejection of anything outside the set, all from one small class. Whenever a parameter has a known, limited set of valid values, an enum is the right tool.


Practice Exercises

Exercise 1: Read one task by id

Write an endpoint at /tasks/{task_id} where task_id is an integer. Return a dictionary containing the task_id and a made-up title. Then predict: what does a request to /tasks/9 return, and what does /tasks/nine return?

Hint

Use @app.get("/tasks/{task_id}") and def read_task(task_id: int):. A request to /tasks/9 succeeds and task_id is the integer 9. A request to /tasks/nine is rejected with a 422, because "nine" can’t be parsed as an int — and FastAPI’s error type will be "int_parsing".

Exercise 2: Fix the route order

A colleague defined /tasks/{task_id} first and /tasks/recent second, and is confused that /tasks/recent never runs its own function. What’s happening, and how do you fix it?

Hint

The variable route /tasks/{task_id} matches /tasks/recent too — capturing "recent" as task_id — and because it’s defined first, it wins. (If task_id is typed as int, the request would even fail with a 422.) The fix is to define the fixed path /tasks/recent before the variable path /tasks/{task_id}.

Exercise 3: Restrict the status

Create a Status enum that inherits from str, Enum and allows only "open" and "done". Use it in an endpoint /tasks/status/{status}. What status code comes back for /tasks/status/open, and what comes back for /tasks/status/pending?

Hint

Define class Status(str, Enum): with members open = "open" and done = "done", then hint status: Status. /tasks/status/open returns 200 because open is in the set. /tasks/status/pending returns 422 with an error type of "enum" and a message listing the allowed values — pending simply isn’t one of them.


Summary

A path parameter captures data from the URL path: you write {name} in the route and add a matching function argument. The argument’s type hint does the heavy lifting — FastAPI converts the URL text into that type (so /items/42 gives you a real int, not "42") and validates it, returning a structured 422 error before your function runs if the value doesn’t fit. When routes could overlap, order matters: define fixed paths like /users/me before variable ones like /users/{user_id}, because FastAPI uses the first match. And when a parameter has a known set of valid choices, declare them with a str, Enum class so only those values are accepted and the rest are rejected automatically.

Key Concepts

  • Path parameter — a {name} slot in the URL path, received by a same-named function argument.
  • Type conversion — FastAPI turns the URL text into the hinted type (e.g. int) for you.
  • Automatic validation — a bad value yields a 422 with a detail list (type, loc, msg, input), before your code runs.
  • Route order — fixed paths must be defined before variable paths, because the first match wins.
  • Enum-restricted values — a str, Enum class limits a parameter to a fixed set of allowed choices.

Why This Matters

Almost every API needs to act on a specific resource — one task, one user, one order — and path parameters are how clients name that resource in the URL. Getting them right means your endpoints are both convenient (one function serves every task) and safe (bad ids never reach your logic). The conversion-and-validation pattern you saw here is the same one you’ll meet for query parameters and request bodies in the next lessons, so the 422 error shape and the type-driven mindset will keep paying off. And knowing the route-ordering rule will save you from a class of bugs that quietly break “special” URLs in real applications.


Next Steps

Continue to Lesson 4 - Query Parameters

Read optional data from the part of the URL after the ? — with defaults, types, and validation, all from your type hints.

Back to Module Overview

Return to the Getting Started with FastAPI module overview


Continue Building Your Skills

You can now pull data straight out of the URL path and trust FastAPI to convert it, validate it, and reject bad values with a clear error — all from a single type hint. Next you’ll learn query parameters: the optional, named bits of a URL after the ? that let clients filter, sort, and paginate your tasks.