Урок курса
Query-параметры и фильтрация каталога
FastAPI для начинающих: API с базой данных и тестамиPath-параметры брали значения прямо из сегментов пути — /api/rooms/2 давал room_id = 2. Query-параметры устроены иначе: они находятся в части URL после ?. Именно с ними мы будем фильтровать каталог переговорных.
Как аргумент попадает из query-строки в функцию
Для простых аргументов int, str и bool FastAPI по умолчанию применяет такое правило: если имя аргумента функции совпадает с именем сегмента пути в фигурных скобках — значение берётся из пути. Если не совпадает — FastAPI ищет его в query-строке.
Вот конкретный пример. Маршрут объявлен как @app.get("/api/rooms") — без переменных сегментов. Тогда аргумент min_capacity: int = 0 получит значение из ?min_capacity=10, а не из пути, которого там попросту нет.
Имя в функции должно совпадать с именем параметра в URL. Если написать ?min_capacity=10, а в функции принять capacity, FastAPI не свяжет их: capacity будет искаться как отдельный query-параметр.
Здесь кроется частая ошибка при работе с path-параметрами. Возьмём маршрут:
@app.get("/api/rooms/{room_id}")
def get_room(id: int):
...
Аргумент называется id, а в шаблоне пути стоит {room_id}. FastAPI не свяжет id с сегментом пути — он будет ожидать ?id=... в query-строке. Запрос GET /api/rooms/1 без этого параметра получит ответ 422.
Исправление простое — согласовать имена:
@app.get("/api/rooms/{room_id}")
def get_room(room_id: int):
...
Теперь room_id найдётся в пути, и запрос GET /api/rooms/1 отработает корректно. Для фильтра списка ситуация обратная: min_capacity не упоминается в фигурных скобках маршрута "/api/rooms", поэтому FastAPI читает его из query-строки — никакой дополнительной аннотации для этого не нужно.
Фильтр отсутствует, true или false
Чтобы фильтр был необязательным, аргументу задают значение по умолчанию. Для числового порога это выглядит так:
def list_rooms(min_capacity: int = 0):
Если ?min_capacity в запросе нет, FastAPI подставит 0, и условие capacity >= 0 пропустит все комнаты.
С булевым фильтром нужно различать три состояния: фильтр не передан, передан true и передан false. Значение 0 здесь не подойдёт — нужен None:
from typing import Optional
def list_rooms(min_capacity: int = 0, has_projector: Optional[bool] = None):
Optional[bool] означает, что аргумент может быть True, False или None. Именно = None определяет, что получит функция, если параметр не передан. Импорт Optional из typing обязателен для Python 3.8 и 3.9; начиная с 3.10 можно писать bool | None, но Optional работает везде.
Как FastAPI преобразует значения из строки:
?has_projector=true→True?has_projector=false→False?has_projector=1→True?has_projector=0→False- параметр отсутствует →
None
Если передать ?has_projector=maybe, FastAPI вернёт 422 ещё до вызова функции — строка maybe не является допустимым булевым значением.
Важно не путать это с обычным Python. Выражение bool("false") возвращает True, потому что непустая строка — истинна. FastAPI делает другое: он анализирует содержимое строки и преобразует её в настоящий True или False. Это разные механизмы, и полагаться на bool() вместо аннотации типа нельзя.
Два условия фильтрации одного каталога
Каталог хранится как словарь rooms_by_id, где ключ — числовой id, а значение — словарь с полями комнаты. Нас интересуют два поля: capacity (количество мест) и has_projector (булево наличие проектора).
Фильтрация работает в два шага. Первый — отсев по вместимости:
result = [r for r in rooms_by_id.values() if r["capacity"] >= min_capacity]
Мы берём все значения словаря и оставляем только те комнаты, где мест не меньше, чем запрошено. Если min_capacity=0 (значение по умолчанию), условие пройдут все комнаты.
Второй шаг — фильтр по проектору, но только если он был передан:
if has_projector is not None:
result = [r for r in result if r["has_projector"] == has_projector]
Проверка is not None принципиальна. Если написать if has_projector:, то явно переданный False тоже не пройдёт условие — Python посчитает его ложным, и фильтрация по комнатам без проектора молча отключится. Проверка is not None отличает «фильтр не задан» от «фильтр задан как False».
Когда has_projector=False, второй шаг оставит только комнаты с has_projector == False — то есть именно те, где проектора нет. Когда параметр не передан и равен None, шаг пропускается, и ограничений по оборудованию нет.
Оба условия применяются последовательно к одному списку, поэтому результат — пересечение: комнаты, удовлетворяющие обоим критериям одновременно.
При этом rooms_by_id остаётся нетронутым. Мы создаём новые списки через list comprehension, а не удаляем записи из словаря. Следующий запрос снова получит полный каталог.
Полный маршрут и проверка результатов запросов
Каталог содержит три переговорные: Лондон (6 мест, проектор есть), Берлин (12 мест, проектора нет), Токио (4 места, проектор есть).
from fastapi import FastAPI
from typing import Optional
app = FastAPI()
rooms_by_id = {
1: {"id": 1, "name": "Лондон", "floor": 2, "capacity": 6, "has_projector": True},
2: {"id": 2, "name": "Берлин", "floor": 3, "capacity": 12, "has_projector": False},
3: {"id": 3, "name": "Токио", "floor": 1, "capacity": 4, "has_projector": True},
}
@app.get("/api/rooms")
def list_rooms(min_capacity: int = 0, has_projector: Optional[bool] = None):
result = [r for r in rooms_by_id.values() if r["capacity"] >= min_capacity]
if has_projector is not None:
result = [r for r in result if r["has_projector"] == has_projector]
return result
Проверим разные комбинации запросов:
GET /api/rooms— оба параметра по умолчанию, фильтры не применяются. Ответ 200: все три комнаты.GET /api/rooms?min_capacity=6— остаются Лондон (6 ≥ 6) и Берлин (12 ≥ 6). Токио (4) не проходит. Ответ 200: два объекта.GET /api/rooms?min_capacity=6&has_projector=true— из Лондона и Берлина проектор есть только у Лондона. Ответ 200: один объект.GET /api/rooms?has_projector=false— без ограничения по вместимости, только без проектора. Ответ 200: только Берлин.GET /api/rooms?min_capacity=100— ни одна комната не вмещает 100 человек. Ответ 200: пустой массив[].GET /api/rooms?min_capacity=abc— строкаabcне является целым числом. FastAPI вернёт 422 до вызова функции.GET /api/rooms?has_projector=maybe— недопустимое булево значение. Тоже 422.
После любого из этих запросов GET /api/rooms без параметров снова вернёт все три комнаты: словарь rooms_by_id не изменился.

Попробуйте решить
Маршрут принимает has_projector: Optional[bool] = None. Клиент отправляет GET /api/rooms?has_projector=true. Какое значение получит функция-обработчик?
Продолжить с проверкой и прогрессом
Откройте интерактивный раннер с заданиями урока.
