原作:Pydantic 官方文档贡献者,Validation Decorator。本文根据 2026 年 10 月 5 日读取的 latest 页面完整译编,适用于 Pydantic v2 API;Unpack 示例在原页标明 v2.10 加入。本次未安装依赖或执行示例,代码中的输出均来自原文。
validate_call() 会在调用函数之前,按照函数的类型注解解析并校验传入参数。底层使用与模型创建和初始化相似的机制,但作为装饰器使用时,不需要为每个函数手写一套模型代码。

从一个重复文本函数开始
from pydantic import ValidationError, validate_call
@validate_call
def repeat(s: str, count: int, *, separator: bytes = b'') -> bytes:
b = s.encode()
return separator.join(b for _ in range(count))
a = repeat('hello', 3)
print(a)
#> b'hellohellohello'
b = repeat('x', '4', separator=b' ')
print(b)
#> b'x x x x'
try:
c = repeat('hello', 'wrong')
except ValidationError as exc:
print(exc)
"""
1 validation error for repeat
1
Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='wrong', input_type=str]
"""
第一个调用传入整数 3,返回重复三次的 bytes。第二个调用的 count 是字符串 ‘4’,默认转换规则会把它解析成整数;separator 必须是关键字参数。’wrong’ 无法解析成整数,于是抛出 ValidationError。错误位置中的数字表示位置参数索引。
参数类型、类型转换和返回值
参数类型来自函数注解。没有注解的参数视为 Any;Pydantic 支持的类型,包括模型和自定义类型,都可用于参数。默认情况下,装饰器可能在把值传进函数前转换类型:
from datetime import date
from pydantic import validate_call
@validate_call
def greater_than(d1: date, d2: date, *, include_equal=False) -> date: # (1)
if include_equal:
return d1 >= d2
else:
return d1 > d2
d1 = '2000-01-01' # (2)
d2 = date(2001, 1, 1)
greater_than(d1, d2, include_equal=True)
原例中的字符串日期会转换成 date 对象;include_equal 没有注解,因而按 Any 处理。这种转换可能很方便,也可能不符合业务要求;可以用自定义配置开启严格模式。
原文勘误:上面 greater_than 的返回注解为 date,函数体却返回比较结果 bool。这不是日期返回值的正确示范。原文默认不检查返回值,因此这一错误可能不会在调用时暴露。下面是编辑修订版:只把返回注解改成 bool、为 include_equal 补 bool 注解,并显式开启返回校验;与原例差异已全部列明。
from datetime import date
from pydantic import validate_call
@validate_call(validate_return=True)
def greater_than(d1: date, d2: date, *, include_equal: bool = False) -> bool:
return d1 >= d2 if include_equal else d1 > d2
返回值默认不校验;设置 validate_return=True 才会检查。检查发生在函数执行之后,若函数已经发送消息、写数据库或扣款,返回值校验失败也不会自动撤销这些副作用。类型校验更不能替代身份验证、授权或事务控制。
支持的函数签名
装饰器支持普通位置或关键字参数、默认参数、星号之后的仅关键字参数、斜杠之前的仅位置参数、*args 可变位置参数、**kwargs 可变关键字参数,以及这些形式的组合:
from pydantic import validate_call
@validate_call
def pos_or_kw(a: int, b: int = 2) -> str:
return f'a={a} b={b}'
print(pos_or_kw(1, b=3))
#> a=1 b=3
@validate_call
def kw_only(*, a: int, b: int = 2) -> str:
return f'a={a} b={b}'
print(kw_only(a=1))
#> a=1 b=2
print(kw_only(a=1, b=3))
#> a=1 b=3
@validate_call
def pos_only(a: int, b: int = 2, /) -> str:
return f'a={a} b={b}'
print(pos_only(1))
#> a=1 b=2
@validate_call
def var_args(*args: int) -> str:
return str(args)
print(var_args(1))
#> (1,)
print(var_args(1, 2, 3))
#> (1, 2, 3)
@validate_call
def var_kwargs(**kwargs: int) -> str:
return str(kwargs)
print(var_kwargs(a=1))
#> {'a': 1}
print(var_kwargs(a=1, b=2))
#> {'a': 1, 'b': 2}
@validate_call
def armageddon(
a: int,
/,
b: int,
*c: int,
d: int,
e: int = None,
**f: int,
) -> str:
return f'a={a} b={b} c={c} d={d} e={e} f={f}'
print(armageddon(1, 2, d=3))
#> a=1 b=2 c=() d=3 e=None f={}
print(armageddon(1, 2, 3, 4, 5, 6, d=8, e=9, f=10, spam=11))
#> a=1 b=2 c=(3, 4, 5, 6) d=8 e=9 f={'f': 10, 'spam': 11}
编者注:组合示例的 e: int = None 原样保留,它把 int 注解与 None 默认值放在一起。不要把一次调用成功当成默认值也严格符合注解的证据;实际业务若允许 None,应明确写 Optional[int] 或 int | None,并按版本配置默认值校验。
用 TypedDict 与 Unpack 描述关键字集合
可变关键字参数不一定只能“所有值是同一种类型”。通过 TypedDict 和 Unpack,可以描述预期键及每个键的类型:
from typing_extensions import TypedDict, Unpack
from pydantic import validate_call
class Point(TypedDict):
x: int
y: int
@validate_call
def add_coords(**kwargs: Unpack[Point]) -> int:
return kwargs['x'] + kwargs['y']
add_coords(x=1, y=2)
这里的 Point 要求 x 和 y 两个整数字段。原页标注这一能力自 v2.10 提供;还需匹配 Python 与 typing_extensions 版本。相关标准为 PEP 692。
用 Field 添加约束、默认值和别名
Field 可以补充参数约束与说明。如果不需要 default 或 default_factory,推荐用 Annotated 模式,使类型检查器仍将参数识别为必填。需要默认值时,可以把 Field 放到参数默认值位置:
from typing import Annotated
from pydantic import Field, ValidationError, validate_call
@validate_call
def how_many(num: Annotated[int, Field(gt=10)]):
return num
try:
how_many(1)
except ValidationError as e:
print(e)
"""
1 validation error for how_many
0
Input should be greater than 10 [type=greater_than, input_value=1, input_type=int]
"""
@validate_call
def return_value(value: str = Field(default='default value')):
return value
print(return_value())
#> default value
第一例要求 num 大于 10;第二例提供默认字符串。别名同样有效:
from typing import Annotated
from pydantic import Field, validate_call
@validate_call
def how_many(num: Annotated[int, Field(gt=10, alias='number')]):
return num
how_many(number=42)
外部调用使用 number=42,函数内部仍使用 num。校验规则只检查你声明的条件;例如一个合法整数仍可能远超业务上合理的重复次数、页大小或任务量。对不可信入口应补充范围和长度限制。
直接访问原函数
装饰后仍可通过 raw_function 访问原始函数。原文将它用于输入已经可信、希望减少重复校验开销的场景:
from pydantic import validate_call
@validate_call
def repeat(s: str, count: int, *, separator: bytes = b'') -> bytes:
b = s.encode()
return separator.join(b for _ in range(count))
a = repeat('hello', 3)
print(a)
#> b'hellohellohello'
b = repeat.raw_function('good bye', 2, separator=b', ')
print(b)
#> b'good bye, good bye'
编者注:raw_function 有意绕过装饰器的输入转换与检查,不应在公开 API 或其他不可信入口当作加速开关。确认数据可信也不等于用户有权执行该操作。
异步函数同样适用
validate_call 可以装饰 async 函数。下面使用 PositiveInt 限制用户编号,并用参数占位符执行查询:
class Connection:
async def execute(self, sql, *args):
return 'testing@example.com'
conn = Connection()
# ignore-above
import asyncio
from pydantic import PositiveInt, ValidationError, validate_call
@validate_call
async def get_user_email(user_id: PositiveInt):
# `conn` is some fictional connection to a database
email = await conn.execute('select email from users where id=$1', user_id)
if email is None:
raise RuntimeError('user not found')
else:
return email
async def main():
email = await get_user_email(123)
print(email)
#> testing@example.com
try:
await get_user_email(-4)
except ValidationError as exc:
print(exc.errors())
"""
[
{
'type': 'greater_than',
'loc': (0,),
'msg': 'Input should be greater than 0',
'input': -4,
'ctx': {'gt': 0},
'url': 'https://errors.pydantic.dev/2/v/greater_than',
}
]
"""
asyncio.run(main())
# requires: `conn.execute()` that will return `'testing@example.com'`
编者注:Connection 是说明性替身,execute 无论 SQL 和参数是什么都返回 testing@example.com。它不是数据库驱动、鉴权方案或可部署的访问层。示例使用 $1 参数绑定,比把输入直接拼进 SQL 更合适,但真实应用还要限制连接凭据、验证访问权限,并匹配驱动的参数语法。不要把“编号为正整数”当成可以读取任意用户邮件的授权。
与静态类型检查器的关系
装饰器保留原函数签名,因此通常能与 mypy、pyright 等类型检查器兼容。Python 类型系统对动态增加的属性存在限制,raw_function 等属性可能无法被识别,必要时可在经过审查的具体位置使用 # type: ignore,而不是关闭整个模块的检查。
自定义配置
与 Pydantic 模型类似,装饰器可以接受 config。下面允许 Foobar 这样的任意类型,但依然要求传入对象是该类的实例:
from pydantic import ConfigDict, ValidationError, validate_call
class Foobar:
def __init__(self, v: str):
self.v = v
def __add__(self, other: 'Foobar') -> str:
return f'{self} + {other}'
def __str__(self) -> str:
return f'Foobar({self.v})'
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
def add_foobars(a: Foobar, b: Foobar):
return a + b
c = add_foobars(Foobar('a'), Foobar('b'))
print(c)
#> Foobar(a) + Foobar(b)
try:
add_foobars(1, 2)
except ValidationError as e:
print(e)
"""
2 validation errors for add_foobars
0
Input should be an instance of Foobar [type=is_instance_of, input_value=1, input_type=int]
1
Input should be an instance of Foobar [type=is_instance_of, input_value=2, input_type=int]
"""
arbitrary_types_allowed 不会自动验证对象内部的 v 属性是否符合业务条件。若需要严格输入类型,可显式使用 @validate_call(config=ConfigDict(strict=True));严格模式改变转换规则,并不新增业务授权。
将参数验证与昂贵操作分开
当函数耗时或成本高时,可以先验证参数,再返回一个稍后执行的闭包:
from pydantic import validate_call
@validate_call
def validate_foo(a: int, b: int):
def foo():
return a + b
return foo
foo = validate_foo(a=1, b=2)
print(foo())
#> 3
调用 validate_foo 时参数已被检查;调用返回的 foo 才执行 a+b。这是原文给出的模式示例,并非完整任务队列。若闭包保存可变对象或依赖会变化的外部状态,实际执行时仍需考虑状态是否改变。
异常与性能边界
校验失败抛出的是 Pydantic ValidationError;缺少必填参数也是如此,而普通 Python 函数通常会抛 TypeError。外层错误处理应按这个差异设计。错误对象可能包含被拒绝的参数和值,记录日志或发送给客户端前要脱敏。原文提到 Logfire 可把失败参数记录到追踪上下文,使用时同样要控制敏感数据外传。
函数签名的检查只在装饰时完成一次,但每次调用仍有校验开销。许多场景难以感知这个差异,热点路径仍需在实际负载下测量。validate_call 不是强类型语言函数定义的替代品,也不是安全边界的全部。
审核结论:示例未见硬编码真实秘密或拼接用户输入执行命令;已确认返回注解错误、raw_function 绕过检查、默认值注解不一致和说明性数据库替身的限制。源码没有在此环境运行,原文输出与本文编辑修订都不代表本次实测。
来源与归属:Pydantic 文档贡献者 / Pydantic Services Inc.;中文译编:未完纪编辑部。阅读原文。全文翻译、转载及配图按权利人授权使用;原作者及项目归属保留。
示例代码许可证
随示例代码保留项目版权与许可通知。来源:官方 LICENSE。代码许可与本文其他素材的权利分别适用。
许可全文
The MIT License (MIT) Copyright (c) 2017 to present Pydantic Services Inc. and individual contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.












暂无评论内容