Lesson 4 - Query Parameters

Welcome to Query Parameters

In the last lesson you used path parameters to pick out which resource you wanted — GET /tasks/3 returns task 3. But often a client doesn’t want a single resource by id; it wants a list, shaped in some way: “give me the next ten tasks,” “only show the ones that are done,” “skip the first five.” That shaping information goes in the query string — the part of a URL after the ?, written as ?key=value pairs. FastAPI calls these query parameters, and the wonderful part is that you declare them exactly the way you declared path parameters: as typed function arguments. The only difference is where the value comes from.

This lesson is about reading those optional inputs from the URL and letting FastAPI validate them for you.

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

  • Explain what a query parameter is and how it differs from a path parameter
  • Turn any non-path function argument into a query parameter
  • Use defaults to make a parameter optional, or omit a default to make it required
  • Read boolean and optional (str | None) query parameters with automatic validation

Let’s begin.


What a Query Parameter Is

Look closely at a real URL with a query string:

/tasks?skip=5&limit=3

Everything before the ? is the path (/tasks). Everything after it is the query string: a list of key=value pairs joined by &. Here there are two pairs — skip=5 and limit=3. Each pair is a query parameter.

In FastAPI, the rule for creating one is simple: any function argument whose name is not part of the path becomes a query parameter. You don’t need a new decorator or any special syntax — you just add an argument. Here is an endpoint that lists tasks with pagination controls:

from fastapi import FastAPI

app = FastAPI()

@app.get("/tasks")
def list_tasks(skip: int = 0, limit: int = 10):
    return {"skip": skip, "limit": limit}

Notice the path is just /tasks — there are no {curly braces} in it. So when FastAPI sees the arguments skip and limit, it knows they can’t come from the path. It reads them from the query string instead. A request to /tasks?skip=5&limit=3 fills skip with 5 and limit with 3:

GET /tasks?skip=5&limit=3  ->  {"skip": 5, "limit": 3}

That skip/limit pair is the classic pagination pattern: “skip the first N results, then return at most M of them.” It lets a client page through a long list of tasks a screenful at a time, without your endpoint changing at all.


Defaults Make a Parameter Optional

Here is the most important idea in this lesson, and it’s a piece of plain Python you already know: a default value makes the parameter optional.

In the example above, both arguments have defaults (skip: int = 0 and limit: int = 10). That means the client can leave them out entirely, and FastAPI fills in the default. Let’s add a third parameter and test what happens when we send no query string at all. (done: bool is a boolean — we’ll dig into booleans in the next section.)

from fastapi import FastAPI

app = FastAPI()

@app.get("/tasks")
def list_tasks(skip: int = 0, limit: int = 10, done: bool = False):
    return {"skip": skip, "limit": limit, "done": done}

Requesting /tasks with nothing after it returns the defaults:

GET /tasks  ->  {"skip": 0, "limit": 10, "done": False}

Every value came from the defaults because the client supplied none. Now send all three:

GET /tasks?skip=5&limit=3&done=true  ->  {"skip": 5, "limit": 3, "done": True}

The flip side is just as important: a parameter with no default is required. If you had written def list_tasks(limit: int) with no = 10, then a request to /tasks (with no limit in the query string) would be rejected with a validation error, because you declared a value you didn’t provide a fallback for. So the presence or absence of a default is what decides required-versus-optional — there’s no special keyword for it. For pagination and filters you almost always want defaults, so the endpoint works out of the box and clients only send what they want to override.

Path = which resource, query = how to shape it

A clean way to remember the difference: a path parameter identifies which resource (/tasks/3 — that specific task), while a query parameter says how to filter, paginate, or shape a result (/tasks?done=true&limit=5). Path parameters are part of the address and are always required; query parameters are extras after the ? and are optional whenever you give them a default.


Booleans and Automatic Validation

Query parameters arrive as text — a URL is just a string. But because you declared types, FastAPI converts and validates each value before your function runs, exactly as it did for path parameters in Lesson 3.

The done: bool parameter is a good example. A boolean is a true/false value, and FastAPI is generous about how clients express it. It accepts true/false, and also 1/0:

GET /tasks?done=true  ->  {"skip": 0, "limit": 10, "done": True}
GET /tasks?done=1     ->  {"skip": 0, "limit": 10, "done": True}
GET /tasks?done=0     ->  {"skip": 0, "limit": 10, "done": False}

A boolean flag like this is perfect for filtering: ?done=true means “only finished tasks,” ?done=false means “only unfinished ones.” The client sends a word; your code receives a real Python bool.

Validation works the same way for numbers. Because limit is declared int, sending something that isn’t a number is rejected before your function ever runs:

GET /tasks?limit=abc  ->  422 Unprocessable Entity

The response body explains exactly what went wrong:

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

The status code 422 means “I understood the request, but the data in it is invalid.” Notice the "loc" field says ["query", "limit"] — FastAPI even tells the client where the bad value was: in the query string, in the parameter named limit. You wrote no validation code for this; the single type hint limit: int produced it.


Optional Parameters with str | None

Sometimes you want a parameter the client may leave out, but there’s no sensible default value to invent — a search box, for example. If nobody typed anything, the right answer isn’t "" or "all"; it’s “no search term was given.” Python expresses “this is a string, or it might be nothing” with the type str | None, and you make it optional by defaulting it to None:

from fastapi import FastAPI

app = FastAPI()

@app.get("/search")
def search(q: str | None = None):
    return {"q": q}

Read q: str | None = None as: “q is a string or None, and if the client doesn’t send it, it’s None.” When the client omits q, your function can check for None and behave accordingly:

GET /search          ->  {"q": null}
GET /search?q=report ->  {"q": "report"}

In the first request there was no q at all, so it came through as None (which appears as null in JSON). In the second, FastAPI read report from the query string. This str | None = None pattern is how you say “this input is genuinely optional” — and in your real endpoint you’d write something like if q is None: return all_tasks to decide what to do when no filter was supplied. For the Task Manager, this is exactly how you’d add a search-by-keyword feature alongside the pagination and done filters you already have.


Practice Exercises

Exercise 1: Read the URL

Given the endpoint def list_tasks(skip: int = 0, limit: int = 10, done: bool = False), what dictionary is returned for each request: (a) /tasks, (b) /tasks?limit=2, (c) /tasks?done=true&skip=4?

Hint

Any parameter the client leaves out falls back to its default. (a) {"skip": 0, "limit": 10, "done": False}. (b) only limit is overridden: {"skip": 0, "limit": 2, "done": False}. (c) skip and done are overridden, limit stays default: {"skip": 4, "limit": 10, "done": True}. The order of pairs in the query string doesn’t matter — FastAPI matches by name.

Exercise 2: Required vs optional

You want a /tasks endpoint where limit is required (the client must always say how many to return) but skip is optional. How would you write the function signature?

Hint

Give skip a default and give limit none: def list_tasks(limit: int, skip: int = 0). Now /tasks?limit=5 works, but /tasks with no limit is rejected with a 422 error, because limit has no fallback value. The presence of a default is the only thing that decides required versus optional.

Exercise 3: Predict the validation

For def list_tasks(limit: int = 10, done: bool = False), which requests succeed and which return 422: (a) /tasks?limit=3, (b) /tasks?limit=three, (c) /tasks?done=1, (d) /tasks?done=maybe?

Hint

(a) succeeds — 3 is a valid int. (b) returns 422 — "three" can’t be parsed as an integer. (c) succeeds — FastAPI accepts 1 as True for a bool. (d) returns 422 — "maybe" isn’t a recognized boolean. FastAPI checks all of this before your function body runs.


Summary

Query parameters are the ?key=value pairs after the ? in a URL, and they’re how a client tells your endpoint how to filter, paginate, or shape a result. In FastAPI you create one by adding a function argument whose name is not part of the path — there’s no special syntax. Just like path parameters, query parameters are converted and validated against their type hints, so a bad value (like limit=abc) is rejected with a clear 422 error before your code runs. A default value makes a parameter optional; the absence of a default makes it required. Booleans accept true/false and 1/0, and the str | None = None pattern expresses a parameter the client may genuinely leave out.

Key Concepts

  • Query string — the part of a URL after ?, made of key=value pairs joined by &.
  • Query parameter — any function argument not named in the path; read from the query string.
  • Default = optional — a default value lets the client omit the parameter; no default makes it required.
  • Validation — typed query params are converted and checked just like path params (bad input → 422).
  • Path vs query — path picks which resource; query says how to filter or shape the result.

Why This Matters

Almost every real “list” endpoint needs pagination and filtering — without them, asking for “all tasks” could mean returning thousands of rows at once. Query parameters are the standard, REST-friendly way to offer those controls, and FastAPI lets you add them with a single typed argument that also gets validation and documentation for free. The required-versus-optional rule you learned here — defaults make things optional — comes back constantly as your API grows, so internalizing it now will pay off in every endpoint you build from this point on.


Next Steps

Continue to Lesson 5 - Guided Project: Read-Only Task Manager API

Put paths and query parameters together to build a complete read-only Task Manager API that lists, filters, and returns individual tasks.

Back to Module Overview

Return to the Getting Started with FastAPI module overview


Continue Building Your Skills

You can now read inputs from both parts of a URL: the path tells your endpoint which resource a client wants, and query parameters tell it how to filter, paginate, and shape the answer. In the next lesson you’ll combine everything from this module into a single guided project — a read-only Task Manager API that lists tasks, filters them, and returns one by id.