Pydantic validate_call 验证装饰器实战指南:基于类型注解的函数入参与返回值校验
2026/9/11 0:11:50 网站建设 项目流程

Pydantic validate_call 验证装饰器实战指南:基于类型注解的函数入参与返回值校验

【免费下载链接】pydanticData validation using Python type hints项目地址: https://gitcode.com/GitHub_Trending/py/pydantic

pydantic.validate_call装饰器让普通 Python 函数也能像 Pydantic 模型一样,在调用前依据类型注解自动完成参数的解析、类型强制转换与校验,失败时抛出标准ValidationError。本指南以官方概念文档 validation_decorator.md 为主体,结合仓库内装饰器的实际实现源码与测试用例,系统讲解其用法、支持的参数形态、配置方式与已知限制,帮助你在接口封装、CLI 参数校验、数据管道等场景中以极简样板代码获得类型安全的函数调用层。

快速上手:一个装饰器完成函数入参校验

validate_call()装饰器允许在函数真正被调用之前,利用函数的类型注解对传入参数进行解析与校验。其底层复用了 Pydantic 模型创建与初始化的同一套机制(详见 Validators 中关于校验器的说明),但对使用者而言,它提供了一种极简样板代码即可为既有代码加上校验能力的方式:

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] """

可以看到,count参数声明为int,传入字符串'4'会被自动转换为整数4;而传入无法解析的'wrong'时,会抛出ValidationError,错误信息中明确标识了出错参数的位置(1,即第二个位置参数)、错误类型int_parsing与原始输入值。

从源码实现看,装饰器的入口位于 pydantic/validate_call_decorator.py。它支持两种调用形态:作为裸装饰器@validate_call,或带参数调用@validate_call(...)。装饰过程的核心步骤是:

  1. 通过_check_function_type校验被装饰对象必须是函数、方法、partial或 lambda,并具备合法签名;
  2. 构造 pydantic/_internal/_validate_call.py 中的ValidateCallWrapper,用GenerateSchema基于函数签名生成 Core Schema,并借助create_schema_validator创建__pydantic_validator__
  3. 调用时把(args, kwargs)包装为pydantic_core.ArgsKwargs,交给 validator 的validate_python完成校验,校验通过后再执行真正的函数体。

关键细节:校验只发生在调用入口,装饰器的__call__逻辑(res = self.__pydantic_validator__.validate_python(pydantic_core.ArgsKwargs(args, kwargs)))与实际函数体是分离的,因此校验失败不会执行函数体。

参数类型:从注解推断,未注解默认为 Any

参数类型直接从函数的类型注解推断;若某个参数没有注解,则按Any处理,即不做任何校验与转换。文档中列出的全部类型(types 与 custom types)都可以被校验,包括 Pydantic 模型本身。

与 Pydantic 其他部分一致,装饰器默认会对类型做强制转换(coercion),转换完成后再把值传给真实函数:

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)
  1. include_equal没有类型注解,因此被推断为Any,不做校验;
  2. 虽然d1是字符串,但会在调用前被转换为date对象(输出为True)。

类型强制转换非常有用,但也可能带来困惑或不符合某些场景的预期(参见 模型数据转换 的讨论)。如果需要关闭转换,可以通过 自定义配置 开启 严格模式。测试 tests/test_validate_call.py 验证了strict=True'foo'无法通过int校验、元组无法通过list校验的行为。

注意:默认不校验返回值。默认情况下,函数返回值不会被校验。需要校验返回值时,将装饰器的validate_return参数设为True即可。从源码看,当validate_return=True时,ValidateCallWrapper会基于返回注解再生成一个__return_pydantic_validator__;对异步函数,它会先await协程拿到结果再校验(见 pydantic/_internal/_validate_call.py)。

函数签名:支持全部参数形态的组合

validate_call()设计为可与所有可能的参数配置及其任意组合配合使用:

  • 带默认值或不带默认值的位置/关键字参数;
  • 仅限关键字参数:*,之后的参数;
  • 仅限位置参数:, /之前的参数;
  • 可变位置参数:*定义的*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}

从源码层面看,参数形态信息来自_typing_extra.signature_no_eval取得的函数签名(见 pydantic/validate_call_decorator.py),并由GenerateSchema中的generate_schema转换为arguments类 Core Schema。测试用例(如 tests/test_validate_call.py)验证了各种调用组合:foo(*[1, 2])foo(a=1, b=2)foo(1, b=2)均能正确解析;而缺少必填参数会报missing_argument,多余的位置参数报unexpected_positional_argument,多余的关关键字参数报unexpected_keyword_argument,同一参数被重复传值则报multiple_argument_values

Unpack+TypedDict标注可变关键字参数

Unpack与 TypedDict 可用来给函数的可变关键字参数做细粒度注解(对应 PEP 692 与相关规范章节),该能力自v2.10起可用:

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)

仓库测试进一步验证了该特性的边界行为(tests/test_validate_call.py):

  • **kwargs: Unpack[int](非 TypedDict)会触发PydanticUserError,错误码unpack-typed-dict
  • TypedDict 字段与显式参数名重叠(如def foo(a: int, b: int, **kwargs: Unpack[TD]))会报overlapping-unpack-typed-dict,但仅限位置参数a: int, /)不与**kwargs冲突;
  • TypedDict(total=False)中的Required字段仍然必须提供;
  • closed=True的 TypedDict 不接受额外键,会报extra_forbidden
  • extra_items会约束额外键的值类型。

用 Field() 描述函数参数

Field()函数也可以与装饰器配合,为参数附加校验约束与元信息。文档给出了明确的选型建议:

  • 若未使用defaultdefault_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

Field()提供的约束(gtltgelemultiple_ofmax_lengthmin_lengthpattern等)与在模型字段中的行为完全一致,测试 tests/test_validate_call.py 验证了Annotated[int, Field(gt=0), Field(lt=10)]分别触发greater_thanless_than错误。此外,Field(default_factory=...)也受支持(见test_field_can_provide_factory)。

别名(Alias)同样可用

字段别名在装饰器中正常工作,调用时需使用别名作为关键字:

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)

测试还覆盖了更多别名场景:tests/test_validate_call.py 中的test_annotated_use_of_alias(空字符串别名、别名缺省会报missing_argument且原参数名被视为多余关键字)、test_validation_aliasvalidation_aliasAliasChoices('d', 'e'))、test_validate_by_namevalidate_by_name: True时别名与原名可混用)以及test_populate_by_name。配置alias_generator也可生效(test_alias_generator,tests/test_validate_call.py)。

访问原始函数:raw_function

装饰后的函数仍可通过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_functionupdate_wrapper_attributes赋值(pydantic/_internal/_validate_call.py):装饰器通过functools.wraps保留原始函数的__doc____module__等元数据,并手动修正__name____qualname__(对partial对象会显示为partial(func_name)的形式),最后挂载raw_function指向被包裹的原始函数。

异步函数支持

validate_call()同样适用于async函数,校验逻辑一致,且异步调用前同样完成参数校验:

class Connection: async def execute(self, sql, *args): return 'testing@example.com' conn = Connection() import asyncio from pydantic import PositiveInt, ValidationError, validate_call @validate_call async def get_user_email(user_id: PositiveInt): # `conn` 是某个虚构的数据库连接 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()) # 需要:`conn.execute()` 返回 `'testing@example.com'`

示例中user_id: PositiveInt传入-4时抛出的ValidationError,其loc(0,)(第一个参数),错误类型为greater_than。源码层面,update_wrapper_attributes会通过inspect.iscoroutinefunction(wrapped)检测异步函数并返回对应的async def wrapper_function(pydantic/_internal/_validate_call.py),保证装饰后inspect.iscoroutinefunction仍返回True(见测试test_async)。若同时开启validate_return=True,返回值校验也会先await协程结果再执行。

与类型检查器(mypy / pyright)的兼容性

由于validate_call()装饰器保留了被装饰函数的签名(通过functools.wraps与签名修复机制),它与类型检查器(如 mypy、pyright)是兼容的——类型检查器看到的仍是原函数签名,因此参数类型检查可正常进行。测试test_wrap(tests/test_validate_call.py)确认了装饰后inspect.signature返回的签名与原始签名一致。

但受限于当前 Python 类型系统的能力,raw_function等额外属性不会被类型检查器识别,访问它们时通常需要抑制错误(一般通过# type: ignore注释)。

自定义配置(config 参数)

与 Pydantic 模型类似,装饰器的config参数可指定自定义配置(ConfigDict)。下面用arbitrary_types_allowed=True让装饰器接受任意自定义类作为参数类型:

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] """

配置在源码中被封装为ConfigWrapper(config)(pydantic/_internal/_validate_call.py),再传入GenerateSchemacreate_schema_validator,因此与模型配置共享同一套解析与生效逻辑。除上述示例外,仓库测试验证了这些配置的可用性:

  • strict=True:禁止隐式类型转换(test_config_strict);
  • validate_by_name=True/populate_by_name=True:别名之外同时接受原参数名(test_validate_by_nametest_populate_by_name);
  • alias_generator:自动生成参数别名(test_alias_generator);
  • field_title_generator:影响生成 JSON Schema 时的字段标题(test_json_schema_custom_title)。

扩展模式:先校验、后调用昂贵函数

某些场景下,你可能想把"参数校验"与"函数调用"分离——例如目标函数执行代价很高或耗时很长时,可以先用装饰器封装一个返回闭包的函数,让校验发生在最外层:

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(1, 2)的调用参数先被校验,但真正的计算逻辑(foo()闭包)尚未执行;之后任意时刻再调用foo()即可。这样既避免了昂贵计算在无效输入上浪费资源,也把校验点前移到了数据入口。

装饰器可接受的函数类型与常见误用

结合 pydantic/validate_call_decorator.py 的_check_function_type与测试(tests/test_validate_call.py),validate_call支持:普通函数、lambda、类方法(含__init____new____call__)、实例方法、staticmethodclassmethodfunctools.partial。以下误用会抛出带错误码validate-call-typePydanticUserError

误用形式报错提示
内置函数(如breakpointInput built-in function ... is not supported
staticmethod/classmethod先于validate_call应用The @staticmethod decorator should be applied after @validate_call(应把@classmethod/@staticmethod上面
直接装饰类validate_call应作用于函数,请装饰__init____new__
装饰可调用实例应显式装饰__call__
无合法签名的函数doesn't have a valid signature
partial(partial(...))等嵌套Partial of ... is invalid ...

注意:由于functools.partial对象没有__name____qualname__,源码会将其命名为partial(原函数名)(pydantic/_internal/_validate_call.py)。

限制(Limitations)

校验异常的类型

目前校验失败时抛出的是标准的 PydanticValidationError(由 pydantic-core 提供)。即使缺少必填参数,也不会像原生 Python 那样抛TypeError,而是同样抛出ValidationError(错误类型如missing_argumentmissing_keyword_only_argumentmissing_positional_only_argument,见测试test_argstest_kwargstest_positional_only)。

该错误能定位被拒绝的参数与值。如果你还需要在周边 trace 上下文中保留这些细节,Logfire 可以在被装饰的调用失败时记录它们。

性能(Performance)

Pydantic 在性能上做了大量努力:对装饰函数的签名检查与 Schema 生成只执行一次ValidateCallWrapper构造时完成,见 pydantic/_internal/_validate_call.py;除非启用defer_build延迟构建,此时首次调用才触发_create_validators)。尽管如此,相比直接调用原始函数,经由装饰器的每次调用仍存在一定的性能开销。

在多数场景下这种开销几乎无感;但请务必认识到:validate_call()不等同于、也无法替代强类型语言中的函数定义,未来也不会变成那样。它只是一种"运行时校验"手段,不是编译期类型系统。在追求极致性能的热路径上,可以考虑在信任输入时通过raw_function绕过校验直接调用。

小结

validate_call是 Pydantic 中"用类型注解描述校验规则"理念在普通函数上的延伸:无需定义模型类,仅加一个装饰器,即可获得入参解析、类型转换、约束校验、异步支持、返回值校验(validate_return=True)、自定义配置与 JSON Schema 生成等完整能力。它适合为脚本入口、RPC/CLI 边界、数据管道节点等位置快速建立"可信边界",同时与Field()、Annotated 模式、别名、严格模式等 Pydantic 生态能力无缝衔接。相关更深入的细节可继续阅读 字段定义与约束、类型体系、严格模式 与 校验器机制。

【免费下载链接】pydanticData validation using Python type hints项目地址: https://gitcode.com/GitHub_Trending/py/pydantic

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询