pydantic: field defined with `Field()` makes pydantic to have a "RecursionError: maximum recursion depth exceeded"

Initial Checks

  • I confirm that I’m using Pydantic V2

Description

pydantic 2.x leads to a recursion error when running the example code.

(....)

  File "/home/luminoso/PycharmProjects/pydantic2datamodel-bug-report/venv/lib/python3.11/site-packages/pydantic/_internal/_repr.py", line 55, in __repr_str__
    return join_str.join(repr(v) if a is None else f'{a}={v!r}' for a, v in self.__repr_args__())
                                                                            ^^^^^^^^^^^^^^^^^^^^
RecursionError: maximum recursion depth exceeded

to test, try to parse an object:

Model.parse_obj({"Settings": {"name": "foobar"}})

(Despite pydantic 2 deprecated parse_obj() the problem also happens with pydantic v2 model_validate())

A workaround is not to use the Field(), as so:

class Model(BaseModel):
    Settings: Settings

Where the problem doesn’t happen.

And the solution is to downgrade pydantic 1.10.12, where the problem does not occur with both with or without the Field() definition.

Example Code

from __future__ import annotations

from pydantic.fields import Field
from pydantic.main import BaseModel


class Settings(BaseModel):
    name: str


class Model(BaseModel):
    Settings: Settings = Field(..., title="a nice title")

Python, Pydantic & OS Version

pydantic version: 2.3.0
        pydantic-core version: 2.6.3
          pydantic-core build: profile=release pgo=true
                 install path: /var/home/luminoso/PycharmProjects/pydantic2datamodel-bug-report/venv/lib/python3.11/site-packages/pydantic
               python version: 3.11.4 (main, Jun  7 2023, 00:00:00) [GCC 13.1.1 20230511 (Red Hat 13.1.1-2)]
                     platform: Linux-6.1.50-200.fc38.x86_64-x86_64-with-glibc2.37
     optional deps. installed: ['email-validator', 'typing-extensions']

Selected Assignee: @dmontagu

About this issue

  • Original URL
  • State: closed
  • Created 10 months ago
  • Reactions: 1
  • Comments: 17 (7 by maintainers)

Commits related to this issue

Most upvoted comments

any reason why this issue is closed? 😦

If the issue is related to python then why the same exact example runs with pydantic v1?

It only works with from __future__ import annotations. When I said it has to do with how Python finds references it might be slightly incorrect: Pydantic defines a lot of extra utilities to determine what type hints have been used. So this might differ from Pydantic v1 where it could be able to determine the type correctly when type hints are evaluated as strings thanks to the future import.

While I don’t think supporting this use case with or without the future import (I don’t know if it is even possible without this import) is a good idea, a better error message could be a good improvement.

For this use case, an alias can be used, or reference the Settings class under a different name (not pretty):

from pydantic import BaseModel, Field


class Settings(BaseModel):
    name: str

_Settings = Settings

class Model(BaseModel):
    settings: Settings = Field(..., title="a nice title", alias="Settings")
    # or
    Settings: _Settings  = Field(..., title="a nice title")

I played with this a bit, and it looks like it comes down to the fact that when from __future__ import annotations is present, the namespace of the class is ignored when using typing.get_type_hints, whereas when it is not present, it is not ignored.

I’m not sure the details here, so maybe it is fixable. I thought I was onto something by removing the localns = dict(vars(base)) line from pydantic._internal._typing_extra.get_cls_type_hints_lenient, which doesn’t break any tests except one designed to demonstrate this exact misbehavior. But I realized that removing that actually makes it stop matching the logic for resolving annotations when you don’t have from __future__ import annotations:

# from __future__ import annotations  # can uncomment this to get the other outcome below

from typing import get_type_hints


class Settings:
    pass


class Model:
    class Settings:
        pass

    x: Settings


print(get_type_hints(Model))
# with `from __future__ import annotations`: {'x': <class '__main__.Settings'>}
# without `from __future__ import annotations`: {'x': <class '__main__.Model.Settings'>}

(Note there is nothing specific to pydantic above, it’s all standard library only.)

So, unless there is a way to determine whether from __future__ import annotations was imported in the module in which the class was defined, I don’t see a way to make the change that is undeniably an improvement. (And even if you could determine that, it is arguably still a breaking change to the current behavior that would likely be painful to resolve if you were affected.)

Given all of this, I think the best course of action is to just treat this as an unfortunately unfixable “misbehavior”, at least until PEP649 is live (at which point I think the improvements that will bring would merit making the arguably-breaking change to always reflect the semantics of that PEP).

For what it’s worth, if you don’t have another workaround available, you might also find the approach from tests.test_edge_cases.test_invalid_forward_ref_model useful as a way to work around collisions between field names and type names.

Just following up since the prior issue was closed - is this an impossible to fix bug? Or would it be possible to at least have an informative error message saying you cant have a field with the same name as the type?

It seems odd it only happens when you have an explicit Field object and not otherwise, but idk anything about the implementation that might explain that