Как загрузить файл через FastAPI: пошаговая инструкция
Настроим эндпоинт для загрузки файлов в FastAPI за 15–20 минут: от установки зависимостей до валидации размера и типа. На выходе — рабочий сервер, который принимает файлы и сохраняет их на диск.

Что понадобится до начала
Инструкция написана для FastAPI 0.110+ и Python 3.10+. Загрузка файлов в FastAPI опирается на библиотеку python-multipart — без неё фреймворк не сможет разобрать multipart/form-data. Убедитесь, что у вас есть:
- Python 3.10 или новее — проверьте командой
python --version; - виртуальное окружение (venv, Poetry, uv — любое на выбор);
- права на запись в каталог, куда будете сохранять файлы;
- любой HTTP-клиент для проверки: браузер с Swagger UI,
curlили Postman.
Шаг 1. Устанавливаем зависимости
Активируйте виртуальное окружение и выполните:
pip install "fastapi[standard]" python-multipart uvicorn
Пакет fastapi[standard] подтянет сам фреймворк и Pydantic. uvicorn — ASGI-сервер, на котором мы запустим приложение. python-multipart нужен именно для разбора загружаемых файлов.
Проверка: команда pip list | grep fastapi должна показать версию 0.110 или выше.
Шаг 2. Создаём минимальный эндпоинт
Создайте файл main.py:
from pathlib import Path
from fastapi import FastAPI, UploadFile
app = FastAPI()
UPLOAD_DIR = Path("uploads")
UPLOAD_DIR.mkdir(exist_ok=True)
@app.post("/upload/")
async def upload_file(file: UploadFile):
"""Принимает один файл и сохраняет на диск."""
dest = UPLOAD_DIR / file.filename
content = await file.read()
dest.write_bytes(content)
return {
"filename": file.filename,
"size_bytes": len(content),
"content_type": file.content_type,
}
Параметр file: UploadFile говорит FastAPI, что эндпоинт ожидает multipart/form-data с полем file. Класс UploadFile предоставляет асинхронные методы read, write, seek и close, а также атрибуты filename, content_type и size.
Проверка: запустите сервер:
uvicorn main:app --reload
Откройте в браузере http://127.0.0.1:8000/docs — в Swagger UI появится эндпоинт POST /upload/ с кнопкой выбора файла. Загрузите любой файл: ответ должен вернуть JSON с именем, размером и MIME-типом.
Шаг 3. Загрузка нескольких файлов за один запрос
Измените сигнатуру, чтобы принимать список:
from fastlib import FastAPI, UploadFile
from fastapi import FastAPI, UploadFile
from pathlib import Path
app = FastAPI()
UPLOAD_DIR = Path("uploads")
UPLOAD_DIR.mkdir(exist_ok=True)
@app.post("/upload-many/")
async def upload_many(files: list[UploadFile]):
"""Принимает несколько файлов."""
saved = []
for f in files:
content = await f.read()
dest = UPLOAD_DIR / f.filename
dest.write_bytes(content)
saved.append({"filename": f.filename, "size_bytes": len(content)})
return {"uploaded": saved}
Проверка через curl:
curl -X POST http://127.0.0.1:8000/upload-many/ \
-F "files=@photo1.jpg" \
-F "files=@photo2.jpg"
Ответ должен содержать массив uploaded с двумя элементами.
Шаг 4. Валидация: тип и размер файла
Без проверки пользователь может загрузить исполняемый файл или архив на десятки гигабайт. Добавим ограничения прямо в эндпоинте:
from fastapi import FastAPI, UploadFile, HTTPException
from pathlib import Path
app = FastAPI()
UPLOAD_DIR = Path("uploads")
UPLOAD_DIR.mkdir(exist_ok=True)
ALLOWED_TYPES = {"image/jpeg", "image/png", "application/pdf"}
MAX_SIZE = 10 * 1024 * 1024 # 10 МБ
@app.post("/upload/")
async def upload_file(file: UploadFile):
# Проверяем MIME-тип, который передал клиент
if file.content_type not in ALLOWED_TYPES:
raise HTTPException(
status_code=400,
detail=f"Тип {file.content_type} не разрешён. Допустимы: {', '.join(ALLOWED_TYPES)}",
)
content = await file.read()
if len(content) > MAX_SIZE:
raise HTTPException(
status_code=413,
detail=f"Файл слишком большой: {len(content)} байт, лимит {MAX_SIZE} байт.",
)
dest = UPLOAD_DIR / file.filename
dest.write_bytes(content)
return {"filename": file.filename, "size_bytes": len(content)}
Значениеcontent_typeформирует клиент, а не сервер. Для критичных сценариев проверяйте содержимое файла дополнительно — например, через библиотекуpython-magic, которая определяет тип по сигнатуре байтов, а не по заголовку запроса.
Проверка: попробуйте загрузить .exe или файл больше 10 МБ — сервер должен вернуть 400 или 413 с понятным сообщением.
Шаг 5. Потоковое чтение больших файлов
Вызов await file.read() загружает весь файл в оперативную память. Для файлов в десятки мегабайт это допустимо, но при сотнях мегабайт процесс может выйти за лимиты. Читайте порциями:
import aiofiles
from fastapi import FastAPI, UploadFile
from pathlib import Path
app = FastAPI()
UPLOAD_DIR = Path("uploads")
UPLOAD_DIR.mkdir(exist_ok=True)
CHUNK_SIZE = 1024 * 1024 # 1 МБ
@app.post("/upload-stream/")
async def upload_stream(file: UploadFile):
dest = UPLOAD_DIR / file.filename
total = 0
async with aiofiles.open(dest, "wb") as out:
while chunk := await file.read(CHUNK_SIZE):
await out.write(chunk)
total += len(chunk)
return {"filename": file.filename, "size_bytes": total}
Установите aiofiles заранее: pip install aiofiles. Теперь в памяти находится не больше одного мегабайта за раз.
Проверка: загрузите файл больше 50 МБ и следите за потреблением памяти процесса — оно не должно расти пропорционально размеру файла.
UploadFile vs bytes: когда что использовать
FastAPI позволяет принять тело файла как bytes напрямую: file: bytes = File(). Разница существенна:
| Критерий | UploadFile | bytes = File() |
|---|---|---|
| Хранение | Временный файл на диске при превышении порога (по умолчанию 1 МБ) | Целиком в оперативной памяти |
| Метаданные | filename, content_type, size | Нет — только сырые байты |
| Потоковое чтение | Да, через read(chunk) | Нет — данные уже в памяти |
| Подходит для | Файлы любого размера | Маленькие файлы до нескольких МБ, когда метаданные не нужны |
В большинстве случаев UploadFile — правильный выбор. bytes удобен, когда вы принимаете иконку или аватар размером до пары сотен килобайт и хотите сразу передать содержимое дальше, не работая с файловым объектом.
* ASGI (Asynchronous Server Gateway Interface) — спецификация интерфейса между веб-сервером и Python-приложением. В отличие от синхронного WSGI, ASGI поддерживает асинхронную обработку запросов, WebSocket и HTTP/2. Uvicorn — одна из популярных реализаций ASGI-сервера.
Если не получилось
Пять ошибок, с которыми сталкиваются чаще всего:
ModuleNotFoundError: No module named 'multipart'— не установленpython-multipart. Выполнитеpip install python-multipartи перезапустите сервер.422 Unprocessable Entityпри отправке через Postman — убедитесь, что в Body выбранform-data, ключ поля называется именноfile(как параметр функции), а тип поля переключён на File, а не Text.PermissionError: [Errno 13]при записи — у процесса нет прав на каталогuploads/. Проверьте владельца и права:ls -la uploads/.- Файл загружается, но пустой (0 байт) — вы дважды вызвали
await file.read()без промежуточногоawait file.seek(0). Второй вызов читает с конца и возвращает пустые байты. - Сервер зависает на большом файле — вы читаете всё в память через
await file.read()без чанков. Переходите на потоковое чтение из шага 5 или ограничьте размер через Nginx-директивуclient_max_body_sizeперед FastAPI.
Безопасность: о чём стоит помнить
Рабочий эндпоинт — это полдела. Прежде чем выпускать его в прод, закройте базовые риски:
- Имя файла от пользователя ненадёжно. Значение
file.filenameможет содержать../и указывать за пределы каталога загрузок. Генерируйте имя сами — через UUID или хеш содержимого — и сохраняйте оригинальное имя отдельно в базе. - Ограничивайте размер на уровне реверс-прокси. Даже с проверкой в коде, Nginx или Caddy должны отсекать слишком большие запросы раньше: так вы не тратите ресурсы приложения на чтение мусора.
- Не отдавайте загруженные файлы напрямую. Каталог
uploads/не должен быть доступен как статика без проверки прав. Иначе любой файл доступен по прямому URL.
Что дальше
Загрузка файлов — входная точка. Следующие шаги зависят от задачи:
- Если файлы нужно хранить долго — подключайте S3-совместимое хранилище (MinIO для локальной разработки, S3 или аналог в проде) вместо локальной файловой системы.
- Если нужна индикация прогресса — загрузка через
multipart/form-dataне даёт серверу отправлять промежуточный статус; рассмотрите подход с presigned URL, когда клиент грузит файл напрямую в хранилище. - Если эндпоинт публичный — добавьте rate limiting и аутентификацию до того, как кто-нибудь заполнит диск.
Актуальные параметры класса UploadFile и примеры с дополнительными полями формы описаны в официальной документации FastAPI — раздел «Request Files».
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: FastAPI — Request Files (официальная документация), Starlette — UploadFile reference
Частые вопросы
Обязательно ли устанавливать python-multipart отдельно?
Можно ли передать вместе с файлом дополнительные поля формы — например, описание?
Что будет, если два пользователя загрузят файл с одинаковым именем одновременно?
Как ограничить максимальный размер файла, если FastAPI стоит за Nginx?
Инструкция написана для FastAPI 0.110+. Если у меня более ранняя версия, что изменится?
Почему await file.read() возвращает пустые байты при повторном вызове?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.