首页
/ FastAPI 文件上传核心类型 `UploadFile`:属性、异步方法与底层实现全解

FastAPI 文件上传核心类型 `UploadFile`:属性、异步方法与底层实现全解

2026-09-06 18:04:39作者:齐添朝

在 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

FileFormBodyRequest 等类型一样,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 支持两种接收文件的参数声明方式:

  1. bytes 类型:FastAPI 会把文件整个读出来,你收到的是内容字节串。缺点显而易见——全部内容都会驻留在内存中,只适合体积很小的文件。
  2. 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

其中 filenamecontent_typefile 三个属性在官方教程 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 的正确位置取出文件。

一个重要的声明限制

同一个路径操作中,你可以声明多个 FileForm 参数,但不能再声明期望以 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 文档,但当前版本并不参与数据解析;
  • 其余参数(aliastitledescriptionexamplesjson_schema_extra 等)与 Form/Body 保持一致,最终通过 params.Form(...) 构造字段信息。

补充事实:教程中注释提到 File 是直接继承自 Form 的类;而从 fastapi 导入的 QueryPathFile 等,实际是返回特殊类的函数(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}

多文件同时上传

如果表单里同一个字段上传了多个文件,可以把参数声明为 UploadFilebyteslist,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 类型且 contentMediaTypeapplication/octet-stream,从而让 Swagger UI 等工具能正确呈现上传控件。

仓库中还为上述教程示例配备了完整测试,例如 tests/test_tutorial/test_request_files/test_tutorial001.pytests/test_tutorial/test_request_files/test_tutorial001_02.pytests/test_tutorial/test_request_files/test_tutorial002.py 等,覆盖了基础上传、可选文件与多文件场景,可作为阅读源码时的行为对照。

小结

  • UploadFilefastapi 顶层导入,继承自 Starlette 的 UploadFile,并用少量代码补齐了 Pydantic 校验与 Schema 生成能力(见 fastapi/init.pyfastapi/datastructures.py);
  • 属性 filefilenamesizeheaderscontent_type 提供文件对象与元数据;方法 readwriteseekclose 均为 async 版本,底层在线程池中执行;
  • 相比 bytes 整读入内存,UploadFile 借助 SpooledTemporaryFile 更省内存,适合大文件;
  • 上传依赖 python-multipart 解析 multipart/form-data;同一路径操作中不能混用 Body JSON 字段;
  • 支持与 File() 组合声明元数据、通过 = None 实现可选、通过 list[UploadFile] 实现多文件上传;
  • 想进一步深入,可继续阅读参考页 reference/uploadfile.md 与配套教程 tutorial/request-files.md,并结合 docs_src/request_files 下的可运行示例动手实践。
登录后查看全文
热门项目推荐
相关项目推荐