Mathew K Analytics

Lesson 2 · FastAPI deep dive

FastAPI Tutorial #2: Path & Query Parameters Explained

Video two of the eighteen-part series: pulling real data straight out of the URL. Typed path parameters, query parameters, defaults, and validation. Let's…

⬇ Download notebookOpen in Colab ↗

📓 Full notebook

Download .ipynb

FastAPI Deep-Dive, Video 2: Path and Query Parameters#

  • Video two of the eighteen-part series: pulling real data straight out of the URL.
  • Typed path parameters, query parameters, defaults, and validation.
  • Let's get into it.

Part 1: Path Parameters - Typed, Automatic Conversion#

from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
@app.get('/items/{item_id}')
def read_item(item_id: int):
    return {'item_id': item_id, 'type': type(item_id).__name__}
client = TestClient(app)
print(client.get('/items/42').json())
{'item_id': 42, 'type': 'int'}

Part 2: Path Parameter Validation Errors#

response = client.get('/items/not-a-number')
print(response.status_code)
print(response.json())
422
{'detail': [{'type': 'int_parsing', 'loc': ['path', 'item_id'], 'msg': 'Input should be a valid integer, unable to parse string as an integer', 'input': 'not-a-number'}]}

Part 3: Query Parameters Basics#

@app.get('/search')
def search(q: str = 'default query', limit: int = 10):
    return {'q': q, 'limit': limit}
print(client.get('/search').json())
print(client.get('/search?q=fastapi&limit=5').json())
{'q': 'default query', 'limit': 10}
{'q': 'fastapi', 'limit': 5}

Part 4: Required vs Optional Query Parameters#

@app.get('/required-search')
def required_search(q: str):
    return {'q': q}
print(client.get('/required-search?q=hello').json())
missing = client.get('/required-search')
print(missing.status_code)
{'q': 'hello'}
422

Part 5: Type Conversion - bool and Other Types#

@app.get('/toggle')
def toggle(active: bool = False):
    return {'active': active}
print(client.get('/toggle?active=true').json())
print(client.get('/toggle?active=0').json())
print(client.get('/toggle').json())
{'active': True}
{'active': False}
{'active': False}

Part 6: Combining Path and Query Parameters#

@app.get('/users/{user_id}/orders')
def user_orders(user_id: int, status: str = 'all', page: int = 1):
    return {'user_id': user_id, 'status': status, 'page': page}
print(client.get('/users/7/orders').json())
print(client.get('/users/7/orders?status=shipped&page=2').json())
{'user_id': 7, 'status': 'all', 'page': 1}
{'user_id': 7, 'status': 'shipped', 'page': 2}

Part 7: Optional Query Parameters with None#

@app.get('/filter')
def filter_items(category: str | None = None):
    if category is None:
        return {'filter': 'none applied'}
    return {'filter': category}
print(client.get('/filter').json())
print(client.get('/filter?category=books').json())
{'filter': 'none applied'}
{'filter': 'books'}

Part 8: Enum Path Parameters - a Fixed Set of Values#

from enum import Enum
class SortOrder(str, Enum):
    asc = 'asc'
    desc = 'desc'
@app.get('/sorted-items/{order}')
def sorted_items(order: SortOrder):
    return {'order': order.value}
print(client.get('/sorted-items/asc').json())
print(client.get('/sorted-items/sideways').status_code)
{'order': 'asc'}
422

Part 9: Multiple Values for the Same Query Parameter#

from fastapi import Query
@app.get('/tags')
def get_by_tags(tag: list[str] = Query(default=[])):
    return {'tags': tag}
print(client.get('/tags?tag=python&tag=fastapi&tag=api').json())
print(client.get('/tags').json())
{'tags': ['python', 'fastapi', 'api']}
{'tags': []}

Part 10: A Real Pattern - a Filtered Search Endpoint#

class Category(str, Enum):
    electronics = 'electronics'
    books = 'books'
    all = 'all'
@app.get('/stores/{store_id}/products')
def store_products(
    store_id: int,
    category: Category = Category.all,
    min_price: float | None = None,
    in_stock: bool = True
):
    return {
        'store_id': store_id, 'category': category.value,
        'min_price': min_price, 'in_stock': in_stock
    }
print(client.get('/stores/3/products').json())
print(client.get('/stores/3/products?category=books&min_price=9.99&in_stock=false').json())
{'store_id': 3, 'category': 'all', 'min_price': None, 'in_stock': True}
{'store_id': 3, 'category': 'books', 'min_price': 9.99, 'in_stock': False}

Wrap-Up: What You Learned#

  • A function parameter matching a {curly-brace} path segment becomes a path parameter, converted by its type hint.
  • An invalid value for a typed path parameter triggers an automatic 422 Unprocessable Entity error with details.
  • Any parameter not in the path template becomes a query parameter; a default value makes it optional.
  • A query parameter with no default becomes required, triggering 422 if it's missing.
  • Query parameters support bool, float, and other basic types, with common truthy strings recognized for bool.
  • Path and query parameters combine freely in one endpoint; FastAPI infers which is which from the path template.
  • str | None with a default of None makes a query parameter optional without forcing a fallback value.
  • Typing a parameter as a Python Enum restricts it to declared members and renders as a dropdown in the docs.
  • Typing a query parameter as list[str] collects repeated occurrences of the same parameter name into one list.
  • That wraps up path and query parameters. Next up: Request Body and Pydantic Models.

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.