Skip to main content

Collections and Containers

Handling collections like lists, dictionaries, and tuples involves defining schemas that specify the types of items, keys, and values, along with constraints like length and strictness.

Lists and Sequences

To validate a list, use list_schema. In lax mode (the default), it accepts any iterable except for strings, bytes, and dictionaries. In strict mode, it only accepts actual Python list objects.

from pydantic_core import SchemaValidator, core_schema

# Define a list of integers with length constraints
schema = core_schema.list_schema(
items_schema=core_schema.int_schema(),
min_length=1,
max_length=3
)
v = SchemaValidator(schema)

# Validates and converts items
assert v.validate_python([1, '2', 3]) == [1, 2, 3]

# Accepts other iterables in lax mode
assert v.validate_python((1, 2)) == [1, 2]
assert v.validate_python({1, 2}) == [1, 2]

Sets and Frozensets

Validation for set and frozenset works similarly to lists but returns the respective collection type.

# Set validation
set_v = SchemaValidator(core_schema.set_schema(core_schema.int_schema()))
assert set_v.validate_python([1, '2', 1]) == {1, 2}

# Frozenset validation
frozenset_v = SchemaValidator(core_schema.frozenset_schema(core_schema.int_schema()))
assert frozenset_v.validate_python([1, 2]) == frozenset({1, 2})

Dictionaries and Mappings

Use dict_schema to validate mappings. You can specify schemas for both keys and values.

schema = core_schema.dict_schema(
keys_schema=core_schema.str_schema(),
values_schema=core_schema.int_schema()
)
v = SchemaValidator(schema)

# Validates keys and values
assert v.validate_python({'a': 1, 'b': '2'}) == {'a': 1, 'b': 2}

# Accepts custom Mapping types
from collections.abc import Mapping

class MyMapping(Mapping):
def __init__(self, d): self._d = d
def __getitem__(self, k): return self._d[k]
def __iter__(self): return iter(self._d)
def __len__(self): return len(self._d)

assert v.validate_python(MyMapping({'x': '10'})) == {'x': 10}

Tuples

The tuple_schema supports both fixed-position items and variadic items (PEP 646 style).

Fixed-Length Positional Tuples

Define a list of schemas corresponding to each position in the tuple.

# Matches exactly (int, str)
schema = core_schema.tuple_schema([
core_schema.int_schema(),
core_schema.str_schema()
])
v = SchemaValidator(schema)
assert v.validate_python((1, 'hello')) == (1, 'hello')

Variadic Tuples

Use variadic_item_index to indicate which schema in items_schema should repeat for variable-length parts.

# Matches (int, str, str, ..., float)
# The item at index 1 (str_schema) is variadic
schema = core_schema.tuple_schema(
[
core_schema.int_schema(),
core_schema.str_schema(),
core_schema.float_schema()
],
variadic_item_index=1,
)
v = SchemaValidator(schema)
assert v.validate_python((1, 'a', 'b', 'c', 1.5)) == (1, 'a', 'b', 'c', 1.5)

# Matches (int, ...)
schema_all_ints = core_schema.tuple_schema(
[core_schema.int_schema()],
variadic_item_index=0
)
v_ints = SchemaValidator(schema_all_ints)
assert v_ints.validate_python((1, 2, 3)) == (1, 2, 3)

Generators and Lazy Validation

The generator_schema validates iterators and generators. Unlike other collections, validation is lazy: errors are only raised when a violating value is actually yielded from the generator.

from typing import Iterator

def my_generator() -> Iterator[Any]:
yield 1
yield 'not an int'

schema = core_schema.generator_schema(items_schema=core_schema.int_schema())
v = SchemaValidator(schema)

# Validation happens during iteration, not call
gen = v.validate_python(my_generator())
assert next(gen) == 1

import pytest
from pydantic_core import ValidationError

with pytest.raises(ValidationError):
next(gen) # Raises error here because 'not an int' fails int_schema

Common Constraints

Most collection schemas support these shared parameters:

ParameterDescription
min_lengthMinimum number of items required.
max_lengthMaximum number of items allowed.
strictIf True, rejects types that require conversion (e.g., rejects tuples for list_schema).
fail_fastIf True, stops validation and raises an error on the first invalid item found.

Using Fail Fast

By default, pydantic-core collects all errors in a collection. Use fail_fast=True to stop immediately.

schema = core_schema.list_schema(
items_schema=core_schema.int_schema(),
fail_fast=True
)
v = SchemaValidator(schema)

try:
v.validate_python(['a', 'b', 'c'])
except ValidationError as e:
# Only contains the error for the first item 'a'
assert len(e.errors()) == 1

Troubleshooting

Lax Mode Rejections

Even in lax mode, list_schema and tuple_schema explicitly reject strings and bytes to prevent accidental iteration over characters.

v = SchemaValidator(core_schema.list_schema(core_schema.str_schema()))
# This will raise a ValidationError even though strings are iterable
# v.validate_python("abc")

Dictionary Key Coercion

When validating dictionaries, if a key fails validation but can be represented as a string, the error location will use that string representation.

v = SchemaValidator(core_schema.dict_schema(values_schema=core_schema.int_schema()))
# Error loc will be ('(1, 2)',) if the key is the tuple (1, 2)