Skip to content

Validation Decorator

API Documentation

pydantic.validate_call_decorator.validate_call

The validate_call() decorator allows the arguments passed to a function to be parsed and validated using the function's annotations before the function is called.

While under the hood this uses the same approach of model creation and initialisation (see Validators for more details), it provides an extremely easy way to apply validation to your code with minimal boilerplate.

Example of usage:

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

Parameter types

Parameter types are inferred from type annotations on the function, or as Any if not annotated. All types listed in types can be validated, including Pydantic models and custom types. As with the rest of Pydantic, types are by default coerced by the decorator before they're passed to the actual function:

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. Because include_equal has no type annotation, it will be inferred as Any.
  2. Although d1 is a string, it will be converted to a date object.

Type coercion like this can be extremely helpful, but also confusing or not desired (see model data conversion). Strict mode can be enabled by using a custom configuration.

Validating the return value

By default, the return value of the function is not validated. To do so, the validate_return argument of the decorator can be set to True.

Function signatures

The validate_call() decorator is designed to work with functions using all possible parameter configurations and all possible combinations of these:

  • Positional or keyword parameters with or without defaults.
  • Keyword-only parameters: parameters after *,.
  • Positional-only parameters: parameters before , /.
  • Variable positional parameters defined via * (often *args).
  • Variable keyword parameters defined via ** (often **kwargs).
Example
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}

Unpack for keyword parameters

Unpack and typed dictionaries can be used to annotate the variable keyword parameters of a function:

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)

For reference, see the related specification section and PEP 692.

Using the Field() function to describe function parameters

The Field() function can also be used with the decorator to provide extra information about the field and validations. If you don't make use of the default or default_factory parameter, it is recommended to use the annotated pattern (so that type checkers infer the parameter as being required). Otherwise, the Field() function can be used as a default value (again, to trick type checkers into thinking a default value is provided for the parameter).

from typing_extensions 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

Aliases can be used with the decorator as normal:

from typing_extensions 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)
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)

Accessing the original function

The original function which was decorated can still be accessed by using the raw_function attribute. This is useful if in some scenarios you trust your input arguments and want to call the function in the most efficient way (see notes on performance below):

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'

Async functions

validate_call() can also be used on async functions:

class Connection:
    async def execute(self, sql, *args):
        return '[email protected]'


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)
    #> [email protected]
    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 `'[email protected]'`

Compatibility with type checkers

As the validate_call() decorator preserves the decorated function's signature, it should be compatible with type checkers (such as mypy and pyright). However, due to current limitations in the Python type system, the raw_function or other attributes won't be recognized and you will need to suppress the error using (usually with a # type: ignore comment).

Custom configuration

Similarly to Pydantic models, the config parameter of the decorator can be used to specify a custom configuration:

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

Extension — validating arguments before calling a function

In some cases, it may be helpful to separate validation of a function's arguments from the function call itself. This might be useful when a particular function is costly/time consuming.

Here's an example of a workaround you can use for that pattern:

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

Limitations

Validation exception

Currently upon validation failure, a standard Pydantic ValidationError is raised (see model error handling for details). This is also true for missing required arguments, where Python normally raises a TypeError.

Performance

We've made a big effort to make Pydantic as performant as possible. While the inspection of the decorated function is only performed once, there will still be a performance impact when making calls to the function compared to using the original function.

In many situations, this will have little or no noticeable effect. However, be aware that validate_call() is not an equivalent or alternative to function definitions in strongly typed languages, and it never will be.