Skip to content

Validation decorator

The validate_arguments 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; it provides an extremely easy way to apply validation to your code with minimal boilerplate.

In Beta

The validate_arguments decorator is in beta, it has been added to pydantic in v1.5 on a provisional basis. It may change significantly in future releases and its interface will not be concrete until v2. Feedback from the community while it's still provisional would be extremely useful; either comment on #1205 or create a new issue.

Example of usage:

from pydantic import validate_arguments, ValidationError

@validate_arguments
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=' ')
print(b)
#> b'x x x x'

try:
    c = repeat('hello', 'wrong')
except ValidationError as exc:
    print(exc)
    """
    1 validation error for Repeat
    count
      value is not a valid integer (type=type_error.integer)
    """

(This script is complete, it should run "as is")

Argument Types🔗

Argument types are inferred from type annotations on the function, arguments without a type decorator are considered as Any. Since validate_arguments internally uses a standard BaseModel, all types listed in types can be validated, including pydantic models and custom types. As with the rest of pydantic, types can be coerced by the decorator before they're passed to the actual function:

from pathlib import Path
from typing import Pattern, Optional

from pydantic import validate_arguments, DirectoryPath

@validate_arguments
def find_file(path: DirectoryPath, regex: Pattern, max=None) -> Optional[Path]:
    for i, f in enumerate(path.glob('**/*')):
        if max and i > max:
            return
        if f.is_file() and regex.fullmatch(str(f.relative_to(path))):
            return f

print(find_file('/etc/', '^sys.*'))
#> /etc/sysctl.conf
print(find_file('/etc/', '^foobar.*', max=3))
#> None

(This script is complete, it should run "as is")

A few notes: * through they're passed as strings path and regex are converted to a Path object and regex respectively, by the decorator * max has no type annotation, so will be considered as Any by the decorator

Type coercion like this can be extremely helpful but also confusing or not desired, see below for a discussion of validate_arguments's limitations in this regard.

Function Signatures🔗

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

  • positional or keyword arguments with or without defaults
  • variable positional arguments defined via * (often *args)
  • variable keyword arguments defined via ** (often **kwargs)
  • keyword only arguments - arguments after *,
  • positional only arguments - arguments before , / (new in python 3.8)

To demonstrate all the above parameter types:

from pydantic import validate_arguments

@validate_arguments
def pos_or_kw(a: int, b: int = 2) -> str:
    return f'a={a} b={b}'

print(pos_or_kw(1))
#> a=1 b=2
print(pos_or_kw(a=1))
#> a=1 b=2
print(pos_or_kw(1, 3))
#> a=1 b=3
print(pos_or_kw(a=1, b=3))
#> a=1 b=3

@validate_arguments
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_arguments
def pos_only(a: int, b: int = 2, /) -> str:  # python 3.8 only
    return f'a={a} b={b}'

print(pos_only(1))
#> a=1 b=2
print(pos_only(1, 2))
#> a=1 b=2

@validate_arguments
def var_args(*args: int) -> str:
    return str(args)

print(var_args(1))
#> (1,)
print(var_args(1, 2))
#> (1, 2)
print(var_args(1, 2, 3))
#> (1, 2, 3)

@validate_arguments
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_arguments
def armageddon(
    a: int,
    /,  # python 3.8 only
    b: int,
    c: int = None,
    *d: int,
    e: int,
    f: int = None,
    **g: int
) -> str:
    return f'a={a} b={b} c={c} d={d} e={e} f={f} g={g}'

print(armageddon(1, 2, e=3))
#> a=1 b=2 c=None d=() e=3 f=None g={}
print(armageddon(1, 2, 3, 4, 5, 6, c=7, e=8, f=9, g=10, spam=11))
#> a=1 b=2 c=7 d=(4, 5, 6) e=8 f=9 g={'spam': 11}

(This script is complete, it should run "as is")

Usage with mypy🔗

The validate_arguments decorator should work "out of the box" with mypy since it's defined to return a function with the same signature as the function it decorates. The only limitation is that since we trick mypy into thinking the function returned by the decorator is the same as the function being decorated; access to the raw function or other attributes will require type: ignore.

Raw function🔗

The raw function which was decorated is accessible, this is useful if in some scenarios you trust your input arguments and want to call the function in the most performant way (see notes on performance below):

from pydantic import validate_arguments

@validate_arguments
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'

(This script is complete, it should run "as is")

Async Functions🔗

validate_arguments can also be used on async functions:

import asyncio
from pydantic import PositiveInt, ValidationError, validate_arguments

@validate_arguments
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())
        """
        [
            {
                'loc': ('user_id',),
                'msg': 'ensure this value is greater than 0',
                'type': 'value_error.number.not_gt',
                'ctx': {'limit_value': 0},
            },
        ]
        """

asyncio.run(main())

(This script is complete, it should run "as is")

Limitations🔗

validate_arguments has been released on a provisional basis without all the bells and whistles, which may be added later, see #1205 for some more discussion of this.

In particular:

Validation Exception🔗

Currently upon validation failure, a standard pydantic ValidationError is raised, see model error handling.

This is helpful since it's str() method provides useful details of the error which occurred and methods like .errors() and .json() can be useful when exposing the errors to end users, however ValidationError inherits from ValueError not TypeError which may be unexpected since python would raise a TypeError upon invalid or missing arguments. This may be addressed in future by either allow a custom error or raising a different exception by default, or both.

Coercion and Stictness🔗

pydantic currently leans on the side of trying to coerce types rather than raise an error if a type is wrong, see model data conversion and validate_arguments is no different.

See #1098 and other issues with the "strictness" label for a discussion of this. If pydantic gets a "strict" mode in future, validate_arguments will have an option to use this, it may even become the default for the decorator.

Performance🔗

We've made a big effort to make pydantic as performant as possible (see the benchmarks) and argument inspect and model creation is only performed once when the function is defined, however there will still be a performance impact to using the validate_arguments decorator compared to calling the raw function.

In many situations this will have little or no noticeable effect, however be aware that validate_arguments is not an equivalent or alternative to function definitions in strongly typed languages; it never will be.

Return Value🔗

The return value of the function is not validated against its return type annotation, this may be added as an option in future.

Config and Validators🔗

Custom Config and validators are not yet supported.

Model fields and reserved arguments🔗

The following names may not be used by arguments since they can be used internally to store information about the function's signature:

  • v__args
  • v__kwargs
  • v__positional_only

These names (together with "args" and "kwargs") may or may not (depending on the function's signature) appear as fields on the internal pydantic model accessible via .model thus this model isn't especially useful (e.g. for generating a schema) at the moment.

This should be fixable in future as the way error are raised is changed.