FastAPI 文件上传核心类型 `UploadFile`:属性、异步方法与底层实现全解
在 FastAPI 中,接收客户端上传的文件是构建 Web 应用(如图片、视频、文档处理服务)的常见需求,而 UploadFile 正是官方推荐的首选类型。当你把路径操作函数的参数声明为 UploadFile 类型时,FastAPI 会自动从 multipart/form-data 请求体中解析出上传的文件,并提供文件元数据与一套可 await 的异步读写接口。阅读完本文,你将掌握 UploadFile 的导入方式、全部公开属性与方法、与 bytes/File() 的搭配使用、可选与多文件上传实战,以及它在仓库源码(fastapi/datastructures.py)中的继承与校验机制。
认识 UploadFile
从 fastapi 顶层导入
根据 reference/uploadfile.md 的定义,UploadFile 可以从 fastapi 包直接导入:
from fastapi import UploadFile
之所以能直接导入,是因为它被作为顶层公共 API 导出。查看 fastapi/init.py 可以看到这一行:
from .datastructures import UploadFile as UploadFile
与 File、Form、Body、Request 等类型一样,UploadFile 属于 FastAPI 对外暴露的核心数据类型之一。
一个最简可运行示例
将 UploadFile 用作路径操作函数参数,即可接收上传文件:
from typing import Annotated
from fastapi import FastAPI, File, UploadFile
app = FastAPI()
@app.post("/files/")
async def create_file(file: Annotated[bytes, File()]):
return {"file_size": len(file)}
@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile):
return {"filename": file.filename}
这段示例正是 docs_src/request_files/tutorial001_an_py310.py 的完整源码,同时也出现在 fastapi/datastructures.py 的类文档字符串中。同一份请求体,bytes 模式把全部内容读入内存,UploadFile 模式则交给文件对象托管。
与 Starlette 的继承关系
从源码看,FastAPI 的 UploadFile 直接继承自 Starlette 的 UploadFile:
from starlette.datastructures import UploadFile as StarletteUploadFile
class UploadFile(StarletteUploadFile):
...
如官方教程在 tutorial/request-files.md 中所指出的:FastAPI 的 UploadFile 直接继承自 Starlette 的 UploadFile,并额外补充了一些必要部分,使其与 Pydantic 以及 FastAPI 的其他部件兼容。这些“额外部分”包括:
- 类属性上的
Annotated[...]+Doc(...)元数据声明(见 datastructures.py); - 一套针对 Pydantic 校验与 JSON Schema 生成的钩子方法(见 datastructures.py)。
何时应该使用 UploadFile 而非 bytes
FastAPI 支持两种接收文件的参数声明方式:
bytes类型:FastAPI 会把文件整个读出来,你收到的是内容字节串。缺点显而易见——全部内容都会驻留在内存中,只适合体积很小的文件。UploadFile类型:内部使用 spooled file(暂存文件) 机制,具体表现为:
- 文件先存于内存,超过一定大小上限后自动落盘;
- 因此处理图片、视频、大型二进制等大文件时不会耗尽内存;
- 可以获取上传文件的元数据(文件名、MIME 类型、请求头等);
- 提供 file-like 的
async接口; - 底层暴露真实的 Python
SpooledTemporaryFile对象,可直接传给任何期待 file-like 对象的第三方库。
使用 UploadFile 时无需写 File()
与 bytes 不同,声明 UploadFile 参数时不强制要求 File(),直接写:
async def create_upload_file(file: UploadFile):
return {"filename": file.filename}
即可。这是因为 FastAPI 能从 UploadFile 这一类型本身推断出它属于文件上传字段,而不是查询参数或 JSON body 参数。
提示:官方教程在 tutorial/request-files.md 中强调,声明文件 body 时通常需要显式使用
File(),否则参数会被解释成查询参数或 JSON body。UploadFile配合类型注解能规避歧义,但若要附加描述等元数据,仍需File(description=...),详见下文。
公开属性一览
根据参考文档 reference/uploadfile.md 列出、以及 datastructures.py 中带类型与说明的实现,UploadFile 拥有以下公开属性:
| 属性 | 类型 | 说明 |
|---|---|---|
file |
BinaryIO |
标准的 Python 文件对象(非异步)。本质是一个 SpooledTemporaryFile。在普通 def 函数中直接同步访问它 |
filename |
str | None |
客户端上传的原始文件名,例如 myimage.jpg |
size |
int | None |
文件大小,单位为字节 |
headers |
Headers |
本次请求的请求头 |
content_type |
str | None |
请求的 Content-Type,即 MIME / media type,例如 image/jpeg |
其中 filename、content_type、file 三个属性在官方教程 tutorial/request-files.md 中被重点强调:file 是真正的 Python 文件对象,可直接传给其他库。
异步方法与线程池执行原理
UploadFile 提供四个公开的 async 方法(见 datastructures.py):
| 方法签名 | 作用 | 语义 |
|---|---|---|
await read(size: int = -1) -> bytes |
从文件读取字节 | 不传 size(默认 -1)时读取全部内容 |
await write(data: bytes) -> None |
向文件写入字节 | 请求中读到的文件一般不会使用它 |
await seek(offset: int) -> None |
移动到文件中的字节位置 | 后续 read/write 都从该位置继续 |
await close() -> None |
关闭文件 | 释放底层资源 |
所有方法都通过 return await super().write(data) / super().read(size) 这类形式委托给 Starlette 基类,内部实际操作的是那个 SpooledTemporaryFile。
关键原理:这些方法注释与实现都明确写到 —— 为了让它们可 await、与异步兼容,FastAPI 会把这些文件操作放入线程池(threadpool)执行再返回结果。也就是说,阻塞式的磁盘 IO 不会阻塞事件循环。
典型用法,在 async def 路径操作函数中:
contents = await myfile.read()
而如果你处于普通的 def(非异步)路径操作函数中,可以绕过异步方法直接访问底层同步文件对象:
contents = myfile.file.read()
这一点在 fastapi/datastructures.py 的类文档字符串中也有明确说明:普通 def 函数应使用 upload_file.file 访问原生同步 Python 文件。
seek 的典型场景
await myfile.seek(0) 会把指针移回文件开头。当你先执行过一次 await myfile.read() 想再次读取内容时(例如先校验后保存),就需要先 seek 回起点。
前置条件:安装 python-multipart
要真正接收上传文件,必须先安装 python-multipart,因为上传文件以 “form data” 形式发送,需要该库负责 multipart/form-data 的解析。官方教程 tutorial/request-files.md 给出的命令是:
$ uv add python-multipart
若使用 pip 环境,等价操作是 pip install python-multipart。未安装时运行涉及文件上传的应用会报错提示。
深入理解 “Form Data” 编码
HTML 表单(<form></form>)向服务端发送数据时,默认使用一种区别于 JSON 的编码。理解这点有助于你判断参数应如何声明:
- 表单不含文件时,通常用
application/x-www-form-urlencoded编码; - 表单包含文件时,编码为
multipart/form-data;一旦你使用了File,FastAPI 就知道要从 body 的正确位置取出文件。
一个重要的声明限制
同一个路径操作中,你可以声明多个 File 和 Form 参数,但不能再声明期望以 JSON 形式接收的 Body 字段——因为请求体此时是 multipart/form-data 而非 application/json。官方文档指出这并非 FastAPI 的限制,而是 HTTP 协议本身的特性。
实战进阶:元数据、可选文件与多文件上传
给 UploadFile 追加元数据:File()
参考文档与教程都说明,你可以把 File() 与 UploadFile 组合使用,以附加描述等额外元数据(见教程 tutorial/request-files.md 的 “UploadFile with Additional Metadata” 一节与配套源码 docs_src/request_files/tutorial001_03_an_py310.py):
@app.post("/files/")
async def create_file(
file: Annotated[UploadFile, File(description="A file read as UploadFile")],
):
...
从底层看,File 本质上是返回 params.Form 的函数。查看 fastapi/param_functions.py 的函数签名可以发现:
media_type参数默认值为"multipart/form-data",它影响生成的 OpenAPI 文档,但当前版本并不参与数据解析;- 其余参数(
alias、title、description、examples、json_schema_extra等)与Form/Body保持一致,最终通过params.Form(...)构造字段信息。
补充事实:教程中注释提到
File是直接继承自Form的类;而从fastapi导入的Query、Path、File等,实际是返回特殊类的函数(File(description=...)这种调用形式即来源于此)。源码中File确实定义为一个函数并委托给params.Form,两者表述可相互印证。
可选文件上传
通过标准类型注解把默认值设为 None,即可让文件变成可选(见 docs_src/request_files/tutorial001_02_an_py310.py):
@app.post("/files/")
async def create_file(file: Annotated[bytes | None, File()] = None):
if not file:
return {"message": "No file sent"}
return {"file_size": len(file)}
@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile | None = None):
if not file:
return {"message": "No upload file sent"}
return {"filename": file.filename}
多文件同时上传
如果表单里同一个字段上传了多个文件,可以把参数声明为 UploadFile 或 bytes 的 list,FastAPI 会把它们组装成列表交给你(见 docs_src/request_files/tutorial002_an_py310.py):
from typing import Annotated
from fastapi import FastAPI, File, UploadFile
from fastapi.responses import HTMLResponse
app = FastAPI()
@app.post("/files/")
async def create_files(files: Annotated[list[bytes], File()]):
return {"file_sizes": [len(file) for file in files]}
@app.post("/uploadfiles/")
async def create_upload_files(files: list[UploadFile]):
return {"filenames": [file.filename for file in files]}
@app.get("/")
async def main():
content = """
<body>
<form action="/files/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
<form action="/uploadfiles/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
</body>
"""
return HTMLResponse(content=content)
关键点在于 HTML 表单必须写 enctype="multipart/form-data"、method="post" 且 <input> 带 multiple 属性,同时同一表单字段名(此处都是 files)对应服务端列表参数名。多文件同样支持配合 File() 追加元数据(见 docs_src/request_files/tutorial003_an_py310.py)。
源码级透视:Pydantic 兼容与 Schema 生成
作为 FastAPI 的顶层类型,UploadFile 必须能融入 Pydantic 的校验体系。这一部分在 datastructures.py 中实现得相当克制:
@classmethod
def _validate(cls, __input_value: Any, _: Any) -> "UploadFile":
if not isinstance(__input_value, StarletteUploadFile):
raise ValueError(f"Expected UploadFile, received: {type(__input_value)}")
return cast(UploadFile, __input_value)
@classmethod
def __get_pydantic_json_schema__(cls, core_schema, handler):
return {"type": "string", "contentMediaType": "application/octet-stream"}
@classmethod
def __get_pydantic_core_schema__(cls, source, handler):
from ._compat.v2 import with_info_plain_validator_function
return with_info_plain_validator_function(cls._validate)
可以这样理解这段实现:
- 校验:
_validate只接受StarletteUploadFile实例(FastAPI 自身的UploadFile是其子类,故天然通过),否则抛出ValueError,并通过with_info_plain_validator_function注册为 Pydantic 的 plain validator; - 文档:在 OpenAPI/JSON Schema 层面,
UploadFile被描述为一个string类型且contentMediaType为application/octet-stream,从而让 Swagger UI 等工具能正确呈现上传控件。
仓库中还为上述教程示例配备了完整测试,例如 tests/test_tutorial/test_request_files/test_tutorial001.py、tests/test_tutorial/test_request_files/test_tutorial001_02.py、tests/test_tutorial/test_request_files/test_tutorial002.py 等,覆盖了基础上传、可选文件与多文件场景,可作为阅读源码时的行为对照。
小结
UploadFile从fastapi顶层导入,继承自 Starlette 的UploadFile,并用少量代码补齐了 Pydantic 校验与 Schema 生成能力(见 fastapi/init.py 与 fastapi/datastructures.py);- 属性
file、filename、size、headers、content_type提供文件对象与元数据;方法read、write、seek、close均为 async 版本,底层在线程池中执行; - 相比
bytes整读入内存,UploadFile借助SpooledTemporaryFile更省内存,适合大文件; - 上传依赖
python-multipart解析multipart/form-data;同一路径操作中不能混用BodyJSON 字段; - 支持与
File()组合声明元数据、通过= None实现可选、通过list[UploadFile]实现多文件上传; - 想进一步深入,可继续阅读参考页 reference/uploadfile.md 与配套教程 tutorial/request-files.md,并结合 docs_src/request_files 下的可运行示例动手实践。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00