Skip to content

When Config.validate_assignment is true, raising an unexpected exception type in a field validator results in the field being removed from model.__dict__ #2044

@johnsabath

Description

@johnsabath

Checks

  • I added a descriptive title to this issue
  • I have searched (google, github) for similar issues and couldn't find anything
  • I have read and followed the docs and still think this is a bug

Disclaimer

While I understand that the docs state that validators should only raise exceptions of ValueError, TypeError, or AssertionError, it's not unreasonable to assume that other Exception types could be raised by accident/ignorance. Since Pydantic is acting on these unexpected exceptions in a destructive way, I still felt the need to report a bug.

Bug

This is a regression that was introduced with Pydantic 3.7

If a field validator raises an exception that isn't of type ValueError, TypeError, or AssertionError, while Config.validate_assignment is set to True, the field that was being validated is removed from model.__dict__ and can no longer be read or assigned.

Problematic LoC
As far as I can tell, the destructive self.__dict__.pop(name) operation could be replaced with self.__dict__[name].
This way even if an unhandled exception occurs afterwards, the field will still exist on __dict__ and will still have its original value set.

Reproduction of the issue:

from pydantic import validator, BaseModel

class Model(BaseModel):
    foo: str = None

    class Config:
        validate_assignment = True

    @validator('foo')
    def validate_fields(cls, value):
        if value == 'raise_exception':
            raise Exception('Example validator error')
        return value

if __name__ == '__main__':
    model = Model(foo='field1_value')

    print('Model before raising exception in validator')
    print(f'  model={model}')
    print(f'  model.__fields__={model.__fields__}')
    print(f'  model.__dict__={model.__dict__}')

    try:
        model.foo = 'raise_exception'
    except Exception:
        print('Caught exception as expected')

    print('Model after raising exception in validator')
    print(f'  model={model}')
    print(f'  model.__fields__={model.__fields__}')
    print(f'  model.__dict__={model.__dict__}')

    # Raises KeyError: 'foo'
    # File "pydantic/main.py", line 387, in pydantic.main.BaseModel.__setattr__
    model.foo = 'field1_value'

Output:

Model before raising exception in validator
  model=foo='field1_value'
  model.__fields__={'foo': ModelField(name='foo', type=Optional[str], required=False, default=None)}
  model.__dict__={'foo': 'field1_value'}
Caught exception as expected
Model after raising exception in validator
  model=
  model.__fields__={'foo': ModelField(name='foo', type=Optional[str], required=False, default=None)}
  model.__dict__={}
Traceback (most recent call last):
  File "/Users/john.sabath/random/pydantic-issue/main.py", line 45, in <module>
    model.foo = 'field1_value'
  File "/Users/john.sabath/venv/pydantic-issue/lib/python3.7/site-packages/pydantic/main.py", line 387, in __setattr__
    original_value = self.__dict__.pop(name)
KeyError: 'foo'

Metadata

Metadata

Assignees

No one assigned

    Labels

    bug V1Bug related to Pydantic V1.X

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions