Search code examples
pythonpydantic

Pydantic non-default argument follows default argument


I don't understand why the code:

from typing import Optional
from pydantic import Field
from pydantic.dataclasses import dataclass

@dataclass
class Klass:
    field1: str = Field(min_length=1)
    field2: str = Field(min_length=1)
    field3: Optional[str]

throws the error:

TypeError: non-default argument 'field3' follows default argument

if by default Field default kwarg is PydanticUndefined. Why are field1 and field2 default arguments?

I'm using python 3.8 and pydantic 2.6

I tried field3: Optional[str] = Field(...) and it works. I expected the code block above to work because all fields are required and none has default values.


Solution

  • TL;DR

    • field3 is a required argument with type Optional[str], not an optional argument, because you didn't assign anything to field3 in the class definition.

    • field1 and field2 are technically optional, because the Field object you assign to each provides a default of PydanticUndefined. That value, though, causes a validation error at runtime if you don't supply another argument in its place.


    The dataclass decorator is constructing a def statement to define your class's __init__ method that looks something like

    def __init__(self, field1=PydanticUndefined, field2=PydanticUndefined, field3):
        ...
    

    The constructed statement is then execed, which is why you get the error about a non-default argument when the class is defined, rather than when you try to instantiate the class.

    To make field3 optional, you have to provide a default value.

    field3: Optional[str] = None
    

    This makes the defined statement something like

    def __init__(self, field1=PydanticUndefined, field2=PydanticUndefined, field3=None):
       ...
    

    You can't (as far as I know) make field1 or field2 truly required; the PydanticUndefined value just causes __init__ to raise a ValidationError rather than a TypeError if no explicit argument is passed.

    >>> Klass()
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "/Users/chepner/py311/lib/python3.11/site-packages/pydantic/_internal/_dataclasses.py", line 134, in __init__
        s.__pydantic_validator__.validate_python(ArgsKwargs(args, kwargs), self_instance=s)
    pydantic_core._pydantic_core.ValidationError: 2 validation errors for Klass
    field1
      Field required [type=missing, input_value=ArgsKwargs(()), input_type=ArgsKwargs]
        For further information visit https://errors.pydantic.dev/2.5/v/missing
    field2
      Field required [type=missing, input_value=ArgsKwargs(()), input_type=ArgsKwargs]
        For further information visit https://errors.pydantic.dev/2.5/v/missing
    

    I haven't dug into the source to see exactly how that happens, but I assume it's something resembling

    def __init__(self, field1=PydanticUndefined, ...):
        if field1 is PydanticUndefined:
            # prepare ValidationError exception
        if field2 is PydanticUndefined:
            # prepare ValidationError exception
        if <ValidationError needs to be raised>:
            raise ValidationError(...)
    

    If desired, you can provide "real" default values for field1 and field2 by adding the default keyword argument to Field.