Pydantic validate_call 验证装饰器实战指南基于类型注解的函数入参与返回值校验【免费下载链接】pydanticData validation using Python type hints项目地址: https://gitcode.com/GitHub_Trending/py/pydanticpydantic.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) # bhellohellohello b repeat(x, 4, separatorb ) print(b) # bx 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 [typeint_parsing, input_valuewrong, input_typestr] 可以看到count参数声明为int传入字符串4会被自动转换为整数4而传入无法解析的wrong时会抛出ValidationError错误信息中明确标识了出错参数的位置1即第二个位置参数、错误类型int_parsing与原始输入值。从源码实现看装饰器的入口位于 pydantic/validate_call_decorator.py。它支持两种调用形态作为裸装饰器validate_call或带参数调用validate_call(...)。装饰过程的核心步骤是通过_check_function_type校验被装饰对象必须是函数、方法、partial或 lambda并具备合法签名构造 pydantic/_internal/_validate_call.py 中的ValidateCallWrapper用GenerateSchema基于函数签名生成 Core Schema并借助create_schema_validator创建__pydantic_validator__调用时把(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_equalFalse) - 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_equalTrue)include_equal没有类型注解因此被推断为Any不做校验虽然d1是字符串但会在调用前被转换为date对象输出为True。类型强制转换非常有用但也可能带来困惑或不符合某些场景的预期参见 模型数据转换 的讨论。如果需要关闭转换可以通过 自定义配置 开启 严格模式。测试 tests/test_validate_call.py 验证了strictTrue下foo无法通过int校验、元组无法通过list校验的行为。注意默认不校验返回值。默认情况下函数返回值不会被校验。需要校验返回值时将装饰器的validate_return参数设为True即可。从源码看当validate_returnTrue时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 fa{a} b{b} print(pos_or_kw(1, b3)) # a1 b3 validate_call def kw_only(*, a: int, b: int 2) - str: return fa{a} b{b} print(kw_only(a1)) # a1 b2 print(kw_only(a1, b3)) # a1 b3 validate_call def pos_only(a: int, b: int 2, /) - str: return fa{a} b{b} print(pos_only(1)) # a1 b2 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(a1)) # {a: 1} print(var_kwargs(a1, b2)) # {a: 1, b: 2} validate_call def armageddon( a: int, /, b: int, *c: int, d: int, e: int None, **f: int, ) - str: return fa{a} b{b} c{c} d{d} e{e} f{f} print(armageddon(1, 2, d3)) # a1 b2 c() d3 eNone f{} print(armageddon(1, 2, 3, 4, 5, 6, d8, e9, f10, spam11)) # a1 b2 c(3, 4, 5, 6) d8 e9 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(a1, b2)、foo(1, b2)均能正确解析而缺少必填参数会报missing_argument多余的位置参数报unexpected_positional_argument多余的关关键字参数报unexpected_keyword_argument同一参数被重复传值则报multiple_argument_values。用UnpackTypedDict标注可变关键字参数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(x1, y2)仓库测试进一步验证了该特性的边界行为tests/test_validate_call.py**kwargs: Unpack[int]非 TypedDict会触发PydanticUserError错误码unpack-typed-dictTypedDict 字段与显式参数名重叠如def foo(a: int, b: int, **kwargs: Unpack[TD])会报overlapping-unpack-typed-dict但仅限位置参数a: int, /不与**kwargs冲突TypedDict(totalFalse)中的Required字段仍然必须提供closedTrue的 TypedDict 不接受额外键会报extra_forbiddenextra_items会约束额外键的值类型。用 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(gt10)]): 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 [typegreater_than, input_value1, input_typeint] validate_call def return_value(value: str Field(defaultdefault value)): return value print(return_value()) # default valueField()提供的约束gt、lt、ge、le、multiple_of、max_length、min_length、pattern等与在模型字段中的行为完全一致测试 tests/test_validate_call.py 验证了Annotated[int, Field(gt0), Field(lt10)]分别触发greater_than与less_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(gt10, aliasnumber)]): return num how_many(number42)测试还覆盖了更多别名场景tests/test_validate_call.py 中的test_annotated_use_of_alias空字符串别名、别名缺省会报missing_argument且原参数名被视为多余关键字、test_validation_aliasvalidation_alias与AliasChoices(d, e)、test_validate_by_namevalidate_by_name: True时别名与原名可混用以及test_populate_by_name。配置alias_generator也可生效test_alias_generatortests/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) # bhellohellohello b repeat.raw_function(good bye, 2, separatorb, ) print(b) # bgood bye, good bye在源码中raw_function由update_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 testingexample.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) # testingexample.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() 返回 testingexample.com示例中user_id: PositiveInt传入-4时抛出的ValidationError其loc为(0,)第一个参数错误类型为greater_than。源码层面update_wrapper_attributes会通过inspect.iscoroutinefunction(wrapped)检测异步函数并返回对应的async def wrapper_functionpydantic/_internal/_validate_call.py保证装饰后inspect.iscoroutinefunction仍返回True见测试test_async。若同时开启validate_returnTrue返回值校验也会先await协程结果再执行。与类型检查器mypy / pyright的兼容性由于validate_call()装饰器保留了被装饰函数的签名通过functools.wraps与签名修复机制它与类型检查器如 mypy、pyright是兼容的——类型检查器看到的仍是原函数签名因此参数类型检查可正常进行。测试test_wraptests/test_validate_call.py确认了装饰后inspect.signature返回的签名与原始签名一致。但受限于当前 Python 类型系统的能力raw_function等额外属性不会被类型检查器识别访问它们时通常需要抑制错误一般通过# type: ignore注释。自定义配置config 参数与 Pydantic 模型类似装饰器的config参数可指定自定义配置ConfigDict。下面用arbitrary_types_allowedTrue让装饰器接受任意自定义类作为参数类型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 fFoobar({self.v}) validate_call(configConfigDict(arbitrary_types_allowedTrue)) 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 [typeis_instance_of, input_value1, input_typeint] 1 Input should be an instance of Foobar [typeis_instance_of, input_value2, input_typeint] 配置在源码中被封装为ConfigWrapper(config)pydantic/_internal/_validate_call.py再传入GenerateSchema与create_schema_validator因此与模型配置共享同一套解析与生效逻辑。除上述示例外仓库测试验证了这些配置的可用性strictTrue禁止隐式类型转换test_config_strictvalidate_by_nameTrue/populate_by_nameTrue别名之外同时接受原参数名test_validate_by_name、test_populate_by_namealias_generator自动生成参数别名test_alias_generatorfield_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(a1, b2) print(foo()) # 3这里validate_foo(1, 2)的调用参数先被校验但真正的计算逻辑foo()闭包尚未执行之后任意时刻再调用foo()即可。这样既避免了昂贵计算在无效输入上浪费资源也把校验点前移到了数据入口。装饰器可接受的函数类型与常见误用结合 pydantic/validate_call_decorator.py 的_check_function_type与测试tests/test_validate_call.pyvalidate_call支持普通函数、lambda、类方法含__init__、__new__、__call__、实例方法、staticmethod、classmethod及functools.partial。以下误用会抛出带错误码validate-call-type的PydanticUserError误用形式报错提示内置函数如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__无合法签名的函数doesnt have a valid signaturepartial(partial(...))等嵌套Partial of ... is invalid ...注意由于functools.partial对象没有__name__与__qualname__源码会将其命名为partial(原函数名)pydantic/_internal/_validate_call.py。限制Limitations校验异常的类型目前校验失败时抛出的是标准的 PydanticValidationError由 pydantic-core 提供。即使缺少必填参数也不会像原生 Python 那样抛TypeError而是同样抛出ValidationError错误类型如missing_argument、missing_keyword_only_argument、missing_positional_only_argument见测试test_args、test_kwargs、test_positional_only。该错误能定位被拒绝的参数与值。如果你还需要在周边 trace 上下文中保留这些细节Logfire 可以在被装饰的调用失败时记录它们。性能PerformancePydantic 在性能上做了大量努力对装饰函数的签名检查与 Schema 生成只执行一次ValidateCallWrapper构造时完成见 pydantic/_internal/_validate_call.py除非启用defer_build延迟构建此时首次调用才触发_create_validators。尽管如此相比直接调用原始函数经由装饰器的每次调用仍存在一定的性能开销。在多数场景下这种开销几乎无感但请务必认识到validate_call()不等同于、也无法替代强类型语言中的函数定义未来也不会变成那样。它只是一种运行时校验手段不是编译期类型系统。在追求极致性能的热路径上可以考虑在信任输入时通过raw_function绕过校验直接调用。小结validate_call是 Pydantic 中用类型注解描述校验规则理念在普通函数上的延伸无需定义模型类仅加一个装饰器即可获得入参解析、类型转换、约束校验、异步支持、返回值校验validate_returnTrue、自定义配置与 JSON Schema 生成等完整能力。它适合为脚本入口、RPC/CLI 边界、数据管道节点等位置快速建立可信边界同时与Field()、Annotated 模式、别名、严格模式等 Pydantic 生态能力无缝衔接。相关更深入的细节可继续阅读 字段定义与约束、类型体系、严格模式 与 校验器机制。【免费下载链接】pydanticData validation using Python type hints项目地址: https://gitcode.com/GitHub_Trending/py/pydantic创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考