Search code examples
pythonpython-3.xstack-overflowequalitypydantic

Pydantic exclude field from __eq__ to avoid recursion error


I have a pydantic model like this:

class SomeModel(pydantic.BaseModel):
    name: str
    content: str
    previous_model: typing.Optional["SomeModel"] = None

My code look like this, this is greatly simplified, In my real code there are many and circular dependencies occur by chance occasionally rather than being purposefully created:

models = [
   SomeModel("bob", "2"),
   SomeModel("bob", "2"),
]
models[0].previous_model = models[1]
models[1].previous_model = models[0]

models.remove(models[0])

This throws the following error:

File "c:\Users\username\project-name\src\main.py", line 108, in run_all
    models.remove(models[0])
  File "pydantic\main.py", line 902, in pydantic.main.BaseModel.__eq__
  File "pydantic\main.py", line 445, in pydantic.main.BaseModel.dict
  File "pydantic\main.py", line 861, in _iter
  File "pydantic\main.py", line 736, in pydantic.main.BaseModel._get_value
  File "pydantic\main.py", line 445, in pydantic.main.BaseModel.dict
  File "pydantic\main.py", line 861, in _iter
  File "pydantic\main.py", line 736, in pydantic.main.BaseModel._get_value
  File "pydantic\main.py", line 445, in pydantic.main.BaseModel.dict
  File "pydantic\main.py", line 861, in _iter
  File "pydantic\main.py", line 736, in pydantic.main.BaseModel._get_value
  File "pydantic\main.py", line 445, in pydantic.main.BaseModel.dict
  File "pydantic\main.py", line 861, in _iter
  File "pydantic\main.py", line 736, in pydantic.main.BaseModel._get_value
  File "pydantic\main.py", line 445, in pydantic.main.BaseModel.dict
  File "pydantic\main.py", line 861, in _iter

... Snip several hunderd more lines

  File "pydantic\main.py", line 734, in pydantic.main.BaseModel._get_value
  File "pydantic\main.py", line 304, in pydantic.main.ModelMetaclass.__instancecheck__
RecursionError: maximum recursion depth exceeded

I don't really need the previous_model field to be included in the equality at all. Is there a way to exclude it so that my stack doesn't overflow? This field is irrelevant for the purpose equality in this case.


Solution

  • When pydantic generates __repr__, it iterates over its arguments. That makes sense, that's one of the key selling points. But it doesn't work well in your scenario, you'd have to omit previous_node from __repr__ to make it work. You can either skip previous_node in __repr_args__ or return something simpler in __repr__. To give you a very simplified example that is working

    
    import typing
    
    import pydantic
    
    
    class SomeModel(pydantic.BaseModel):
        name: str
        content: str
        previous_model: typing.Optional["SomeModel"] = None
    
        def __repr__(self):
            return self.name
    
    
    SomeModel.update_forward_refs()
    
    
    models = [
       SomeModel(name="bob", content="2"),
       SomeModel(name="bob", content="2"),
    ]
    models[0].previous_model = models[1]
    models[1].previous_model = models[0]
    
    models.remove(models[0])
    
    print(models)
    

    Less simple version that's closer to how pydantic behaves but will also work in your case

    
    import typing
    
    import pydantic
    
    
    class SomeModel(pydantic.BaseModel):
        name: str
        content: str
        previous_model: typing.Optional["SomeModel"] = None
    
        def __repr_args__(self, *args, **kwargs):
            args = self.dict(exclude={'previous_model',})
            return list(args.items())
    
    
    SomeModel.update_forward_refs()
    
    
    models = [
       SomeModel(name="bob", content="2"),
       SomeModel(name="bob", content="2"),
    ]
    models[0].previous_model = models[1]
    models[1].previous_model = models[0]
    
    models.remove(models[0])