Skip to main content

Function and Argument Validation

Function and argument validation in this codebase is primarily handled by a set of core schemas that bridge the gap between raw data validation and Python function execution. The system is designed to take various input formats (like tuples, dictionaries, or specialized ArgsKwargs objects), validate them against a function's signature, execute the function, and optionally validate the return value.

The Call Lifecycle

The central component for function validation is the CallSchema. It orchestrates the entire process of validating inputs, calling a target function, and validating the output.

CallSchema

Defined in pydantic_core/core_schema.py, the CallSchema acts as a wrapper around a Python callable. It requires an arguments_schema to handle the input mapping and a function to execute.

from pydantic_core import SchemaValidator, core_schema as cs

def my_function(a: int, b: int) -> int:
return a + b

# A CallSchema that validates two integers and calls my_function
schema = cs.call_schema(
arguments=cs.arguments_schema([
cs.arguments_parameter(name='a', schema=cs.int_schema()),
cs.arguments_parameter(name='b', schema=cs.int_schema()),
]),
function=my_function,
return_schema=cs.int_schema()
)

v = SchemaValidator(schema)
# Validating and calling with positional arguments
assert v.validate_python((1, 2)) == 3

The CallSchema follows this internal flow:

  1. Argument Validation: The input is passed to the arguments_schema. This schema must return a tuple in the format (args_tuple, kwargs_dict).
  2. Function Execution: The function is called using the validated args_tuple and kwargs_dict.
  3. Return Validation: If a return_schema is provided, the result of the function call is validated against it before being returned to the caller.

Defining Function Arguments

The mapping from input data to function parameters is handled by ArgumentsSchema or the newer ArgumentsV3Schema. These schemas define how positional and keyword arguments are extracted and validated.

ArgumentsSchema and ArgumentsParameter

ArgumentsSchema is the standard way to define a function's signature. It uses a list of ArgumentsParameter objects to describe individual parameters.

Each ArgumentsParameter has a mode that determines how it can be supplied:

  • positional_only: Must be provided as a positional argument.
  • positional_or_keyword: Can be provided either by position or by name (default).
  • keyword_only: Must be provided as a keyword argument.
# Example of different parameter modes in ArgumentsSchema
args_schema = cs.arguments_schema([
cs.arguments_parameter(name='a', schema=cs.int_schema(), mode='positional_only'),
cs.arguments_parameter(name='b', schema=cs.int_schema(), mode='positional_or_keyword'),
cs.arguments_parameter(name='c', schema=cs.int_schema(), mode='keyword_only'),
])

For variadic arguments (*args and **kwargs), ArgumentsSchema provides top-level fields:

  • var_args_schema: A schema to validate all extra positional arguments.
  • var_kwargs_schema: A schema to validate extra keyword arguments.
  • var_kwargs_mode: Determines if keyword arguments are 'uniform' (all same type) or 'unpacked-typed-dict'.

ArgumentsV3Schema (Experimental)

ArgumentsV3Schema is a modern alternative found in pydantic_core/core_schema.py. It simplifies the structure by moving variadic logic directly into the ArgumentsV3Parameter modes.

Supported modes in ArgumentsV3Parameter include:

  • var_args: Represents *args.
  • var_kwargs_uniform: Represents **kwargs where all values share a schema.
  • var_kwargs_unpacked_typed_dict: Represents **kwargs mapped to a TypedDict.
# Using ArgumentsV3Schema for complex signatures
v3_schema = cs.arguments_v3_schema([
cs.arguments_v3_parameter(name='a', schema=cs.int_schema(), mode='positional_only'),
cs.arguments_v3_parameter(name='args', schema=cs.int_schema(), mode='var_args'),
cs.arguments_v3_parameter(name='kwargs', schema=cs.str_schema(), mode='var_kwargs_uniform'),
])

v = SchemaValidator(v3_schema)
# Returns ((a, *args), {**kwargs})
assert v.validate_python({'a': 1, 'args': [2, 3], 'kwargs': {'key': 'val'}}) == ((1, 2, 3), {'key': 'val'})

Basic Callable Validation

For scenarios where you only need to verify if a value is a callable (without necessarily calling it or validating its internal signature), the codebase provides CallableSchema.

# Simple check if the input is a Python callable
schema = cs.callable_schema()
v = SchemaValidator(schema)

assert v.validate_python(lambda x: x) == (lambda x: x)
# v.validate_python(123) # Raises ValidationError

Integration with Pydantic

While these schemas are defined in pydantic-core, they are heavily utilized by the main Pydantic library, specifically for the @validate_call decorator.

  • Schema Generation: pydantic/_internal/_generate_schema.py contains logic to inspect Python function signatures and automatically generate the corresponding ArgumentsSchema.
  • Validation Logic: pydantic/_internal/_validate_call.py uses these generated schemas to wrap functions, ensuring that every call to a decorated function is validated against its defined CoreSchema.

When using CallSchema manually, the input to validate_python can be a tuple (for positional-only functions), a dict (for keyword-heavy functions), or an ArgsKwargs object from pydantic_core which explicitly separates the two. Regardless of the input format, the ArgumentsSchema ensures the function receives the correct Python calling convention.