Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix __post_init__ cause infinite recursion in inheritance #606


Copy link

@Hanaasagi Hanaasagi commented Jun 20, 2019

Change Summary

  • Move _pydantic_post_init into _process_class, so it can use the closure feature to capture parent scope post_init_original method.
  • Remove __post_init_original__ and __post_init_post_parse__ method.

Related issue number

It will resolve #536


  • Unit tests for the changes exist
  • Tests pass on CI and coverage remains at 100%
  • Documentation reflects the changes where applicable
  • HISTORY.rst has been updated
    • if this is the first change since a release, please add a new section
    • include the issue number or this pull request number #<number>
    • include your github username @<whomever>

Why it can resolve the problem

Sorry for my pool English, it is not my native language.

Firstly, we should know

class A:
    def test(self):

class B(A):
    def test(self):


Above code will print <__main__.B object at 0x7fdb1d7a1908>, it is a instance of Class B that we pass to A.test.

In pydantic, if we use the dataclass decorator, it will replace user's __post_init__ to _pydantic_post_init, and save original __post_init__ in __post_init_original__

post_init_original = getattr(_cls, '__post_init__', None)
post_init_post_parse = getattr(_cls, '__post_init_post_parse__', None)
if post_init_original and post_init_original.__name__ == '_pydantic_post_init':
post_init_original = None
_cls.__post_init__ = _pydantic_post_init
cls = dataclasses._process_class(_cls, init, repr, eq, order, unsafe_hash, frozen) # type: ignore
fields: Dict[str, Any] = { (field.type, field.default if field.default != dataclasses.MISSING else Required)
for field in dataclasses.fields(cls)
cls.__post_init_original__ = post_init_original
cls.__post_init_post_parse__ = post_init_post_parse

According to code of issue #536,

from pydantic.dataclasses import dataclass

class A:
    a: int
    def __post_init__(self):

class B(A):
    b: int
    def __post_init__(self):

B(b=1, a=2)

After decorated by dataclass,

In [1]: A.__post_init__
Out[1]: <function pydantic.dataclasses._pydantic_post_init(self: 'DataclassType', *initvars: Any) -> None>

In [2]: A.__post_init_original__
Out[2]: <function __main__.A.__post_init__(self)>

In [3]: B.__post_init__
Out[3]: <function pydantic.dataclasses._pydantic_post_init(self: 'DataclassType', *initvars: Any) -> None>

In [4]: B.__post_init_original__
Out[4]: <function __main__.B.__post_init__(self)>

In [5]: A.__post_init__ is B.__post_init__
Out[5]: True

So when we create an instance of class B, like B(a=1, b=2). Python stdlib's dataclass will call __post_init__ automatically. But it calls pydantic.dataclasses._pydantic_post_initactually(we replace it).

In _pydantic_post_init, it will call original __post_init__ through __post_init_original__

def _pydantic_post_init(self: 'DataclassType', *initvars: Any) -> None:
if self.__post_init_original__:
d = validate_model(self.__pydantic_model__, self.__dict__, cls=self.__class__)[0]
object.__setattr__(self, '__dict__', d)
object.__setattr__(self, '__initialised__', True)
if self.__post_init_post_parse__:

In above code, we call super().__post_init__() in __post_init_original__, Python will find A.__post_init__ and pass instance b. Currently A.__post_init__ is also a pydantic.dataclasses._pydantic_post_init. So it will get __post_init_original__ by instance b, we fall into a infinite recursion.

Copy link

@codecov codecov bot commented Jun 20, 2019

Codecov Report

Merging #606 into master will not change coverage.
The diff coverage is 100%.

@@          Coverage Diff          @@
##           master   #606   +/-   ##
  Coverage     100%   100%           
  Files          15     15           
  Lines        2584   2582    -2     
  Branches      510    510           
- Hits         2584   2582    -2

Copy link

@samuelcolvin samuelcolvin left a comment

otherwise this looks great.

HISTORY.rst Outdated Show resolved Hide resolved
@@ -75,15 +63,23 @@ def _process_class(
post_init_post_parse = getattr(_cls, '__post_init_post_parse__', None)
if post_init_original and post_init_original.__name__ == '_pydantic_post_init':
post_init_original = None

def _pydantic_post_init(instance: 'DataclassType', *initvars: Any) -> None:
Copy link

@samuelcolvin samuelcolvin Jun 20, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any reason we can't call the first argument self?

Hanaasagi and others added 2 commits Jun 21, 2019
Co-Authored-By: Samuel Colvin <>
@samuelcolvin samuelcolvin merged commit 6233554 into samuelcolvin:master Jun 21, 2019
9 checks passed
Copy link

@samuelcolvin samuelcolvin commented Jun 21, 2019

great, thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
None yet
None yet

Successfully merging this pull request may close these issues.

2 participants