Thanks to visit codestin.com
Credit goes to github.com

Skip to content

FIX fix pickling for empty object with Python 3.11+ #25188

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

Merged

Conversation

BenjaminBossan
Copy link
Contributor

As discussed with @adrinjalali

Since Python 3.11, objects have a __getstate__ method by default:

python/cpython#70766

Therefore, the exception in BaseEstimator.__getstate__ will no longer be raised, thus not falling back on using the object's __dict__:

try:
state = super().__getstate__()
except AttributeError:
state = self.__dict__.copy()
if type(self).__module__.startswith("sklearn."):
return dict(state.items(), _sklearn_version=__version__)

If the instance dict of the object is empty, the return value will, however, be None. Therefore, the line below calling state.items() results in an error.

In this bugfix, it is checked if the state is None and if it is, the object's __dict__ is used (which should always be empty).

Not addressed in this PR is how to deal with slots (see also discussion in #10079). When there are __slots__, __getstate__ will actually return a tuple, as documented here.

The user would thus still get an indiscriptive error message.

Since Python 3.11, objects have a __getstate__ method by default:

python/cpython#70766

Therefore, the exception in BaseEstimator.__getstate__ will no longer be
raised, thus not falling back on using the object's __dict__:

https://github.com/scikit-learn/scikit-learn/blob/dc580a8ef5ee2a8aea80498388690e2213118efd/sklearn/base.py#L274-L280

If the instance dict of the object is empty, the return value will,
however, be None. Therefore, the line below calling state.items()
results in an error.

In this bugfix, it is checked if the state is None and if it is, the
object's __dict__ is used (which should always be empty).

Not addressed in this PR is how to deal with slots (see also discussion
in scikit-learn#10079). When there are __slots__, __getstate__ will actually return
a tuple, as documented here:

https://docs.python.org/3/library/pickle.html#object.__getstate__

The user would thus still get an indiscriptive error message.
Copy link
Member

@adrinjalali adrinjalali left a comment

Choose a reason for hiding this comment

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

It's interesting that now if a class has __slots__, it also has to implement __getstate__.

https://github.com/python/cpython/pull/2821/files#diff-3fd2e552779d3d96e7c1f2d80f4b8fd88752ef5437f42201037ed7c76bba2779R92

I think it makes sense to handle __slots__, now that it's influencing what's returned by __getstate__. But I'm not sure if it needs to be in this PR or a different one.

I recon this is somewhat a major change to warrant a @scikit-learn/core-devs ping.

Also needs a whatsnew entry.

sklearn/base.py Outdated
Comment on lines 279 to 280
# TODO: Remove once Python < 3.11 is dropped, as there will never be
# an AttributeError
Copy link
Member

Choose a reason for hiding this comment

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

this would still be raised for C extension types which don't inherit from PyObject though, right? One can have a C extension type w/o __reduce__ for instance. It doesn't make much sense, but one can do that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, right, I'll remove the comment.

@adrinjalali
Copy link
Member

For an end to end test, could you please add a test where pickle.loads(pickle.dumps(obj)) fails on main but passes here?

@BenjaminBossan
Copy link
Contributor Author

For an end to end test, could you please add a test where pickle.loads(pickle.dumps(obj)) fails on main but passes here?

Yeah, I had that and removed it again :) Added it back.

Copy link
Member

@adrinjalali adrinjalali left a comment

Choose a reason for hiding this comment

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

I'm happy with this PR, especially since we're not changing behavior from what we already have in main; but I think there are cases where we might be having a different behavior than python's default __getstate__, and it'd be nice to have them less diverged from one another.

Note to reviewers, the circleci job doesn't work for @BenjaminBossan 's PRs on this repo somehow.

@@ -273,6 +273,8 @@ def __repr__(self, N_CHAR_MAX=700):
def __getstate__(self):
try:
state = super().__getstate__()
if state is None:
state = self.__dict__.copy()
Copy link
Member

Choose a reason for hiding this comment

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

python seems to be making a distinction between a __dict__ attached to the instance, and a __dict__ attached to the class. I'm not sure if we should do the same:

>>> class A:
...     __dict__ = {"a": 10}
... 
>>> a = A()
>>> a.__dict__
{'a': 10}
>>> a.__getstate__()
>>> a.__dict__.update({"b": 3})
>>> a.__dict__
{'a': 10, 'b': 3}
>>> a.__getstate__()
>>> class B:
...     ...
... 
>>> b = B()
>>> b.b = 10
>>> b.__dict__
{'b': 10}
>>> b.__getstate__()
{'b': 10}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Having a __dict__ as class attribute results in some (for me) quite surprising behavior. Not sure if that should be supported and if yes, how to differentiate that here.

class F:
    __dict__ = {"x": 0}
    def __init__(self):
        self.y = 1

f = F()
print(f"{f.__getstate__()=}")  # {'y': 1}
print(f"{f.__dict__=}")  # {'x': 0}
print(f"{'x' in dir(f)=}")  # True
print(f"{'y' in dir(f)=}")  # False
f.y  # works
f.x  # AttributeError:  'F' object has no attribute 'x'

Copy link
Member

Choose a reason for hiding this comment

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

python seems to be making a distinction between a __dict__ attached to the instance, and a __dict__ attached to the class.

Class attributes and instance attributes are different, which is why you see what you are seeing. While this is maybe not a super useful answer, as it is just stating a fact about how Python works, I don't understand what you are trying to do that leads you down the path of poking around at this level. Can you give an example or use-case? Would help with giving a more helpful answer.

@adrinjalali adrinjalali added the Waiting for Second Reviewer First reviewer is done, need a second one! label Dec 14, 2022
@adrinjalali
Copy link
Member

adrinjalali commented Dec 14, 2022

Reading more about it here (https://wiki.python.org/moin/UsingSlots) it seems that __slots__ would prevent __dict__ to be populated. For instance, this would raise:

class C:
	__slots__ = ('a',)
	def __init__(self):
		setattr(self, 'b', 10)
C()
AttributeError: 'C' object has no attribute 'b'

Which also means it's going to be really tricky for a user to have a subclass with __slots__ and also handle the descriptors we've recently introduced in BaseEstimator. For this reason, I think we can simply ignore handling __slots__.

EDIT: __slots__ only affects instance attributes, so descriptors would still work since they're class attributes.

@betatim
Copy link
Member

betatim commented Dec 14, 2022

I was trying to find a case where you run into this problem. But I couldn't quickly find an estimator that is broken without this change. Do you know one or maybe elaborate when you hit this bug?

@BenjaminBossan
Copy link
Contributor Author

BenjaminBossan commented Dec 14, 2022

Do you know one or maybe elaborate when you hit this bug?

I stumbled upon this first with KernelCenterer, there may be others. Here is code to reproduce it:

>>> from sklearn.preprocessing import KernelCenterer
>>> import pickle
>>> pickle.dumps(KernelCenterer())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File ".../sklearn/base.py", line 280, in __getstate__
    return dict(state.items(), _sklearn_version=__version__)
                ^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'items'

@@ -273,6 +273,8 @@ def __repr__(self, N_CHAR_MAX=700):
def __getstate__(self):
try:
state = super().__getstate__()
if state is None:
state = self.__dict__.copy()
Copy link
Member

@betatim betatim Dec 14, 2022

Choose a reason for hiding this comment

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

One thing that puzzles me here is that the docs say that __getstate__() will return None if there is no instance __dict__. So how can we end up going down this if branch and then try to access self.__dict__? I've thought about this for a few minutes now but I can't work out when this would work, I'd expect that if we go down this path, then there is no self.__dict__, and so we can't access it here. So puzzled.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I have been thoroughly confused by the documentation as well. What I think it doesn't mention, which is relevant here, is that if the object does not define __getstate__ and if its instance dict is empty (but exists), it will also return None. This is the case we cover here.

>>> sys.version
3.11.0
>>> class A:
...  pass
>>> print(A().__getstate__())
None
>>> A().__dict__
{}

Perhaps that's what they mean by:

For a class that has no instance __dict__ and no __slots__, the default state is None.

but I'm not sure.

The other cases are indeed not covered, so this code can still break in very specific circumstances.

Copy link
Member

Choose a reason for hiding this comment

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

I'm glad I'm not the only one puzzled by this. For me "has no instance __dict__ sounds like "it doesn't exist" not "it exists but is empty". So yeah, confused.com

Could state = self.__dict__.copy() ever be anything than an empty dict? And if not, we could change it to state = {} no?

Copy link
Member

Choose a reason for hiding this comment

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

I'm wondering about C extension types which don't necessarily have to comply with python object conventions.

This part of our code seems to be quite buggy if we consider all sorts of things people could do with __dict__ and __slots__ both on the instance and on the class (I know they shouldn't lol). But I'm okay with not supporting all those odd usecases.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Could state = self.dict.copy() ever be anything than an empty dict? And if not, we could change it to state = {} no?

I wondered about this too. Probably, it would always be an empty dict. If there is ever a situation (maybe Python 3.12?) where it's not an empty dict, would it be better to return that non-empty dict or {}? I thought the former, so I chose this solution. Not sure what other considerations to make here, runtime performance shouldn't matter much.

But I'm okay with not supporting all those odd usecases.

At least as a first step, we could focus on making the existing sklearn estimators run again. I used the skops persistence tests, which should cover __getstate__ of all estimators, and in addition to KernelCenterer, I also found LabelEncoder, which is a big one. Both of these don't have any C extensions, right?

There could be more estimators that I'm missing because they're masked by other errors in the test suite that occur due to Python 3.11.

@adrinjalali
Copy link
Member

Do you know one or maybe elaborate when you hit this bug?

@betatim this is in the context of skops's persistence model, which makes use of __getstate__/__setstate__.

@adrinjalali adrinjalali added this to the 1.2.1 milestone Dec 14, 2022
@adrinjalali
Copy link
Member

@BenjaminBossan might make sense to run our skops test suit against this branch to see if it all works. WDYT?

@BenjaminBossan
Copy link
Contributor Author

might make sense to run our skops test suit against this branch to see if it all works. WDYT?

Yes, in theory that would be a good test. However, we have a few other errors in the same tests when using Python 3.11, which could mask errors stemming from this issue. So it would take a little bit of work on the skops side to clean up the tests (but we will have to do it eventually anyway).

Maybe it would also be good to check that pickle works for all estimators in sklearn test_common.py (though not necessarily in this PR).

@glemaitre glemaitre changed the title Bugfix: BaseEstimator __getstate__ in Python 3.11 FIX update BaseEstimator __getstate__ in Python 3.11 Dec 15, 2022
@glemaitre glemaitre self-requested a review December 15, 2022 13:39
@lesteve
Copy link
Member

lesteve commented Dec 15, 2022

Maybe it would also be good to check that pickle works for all estimators in sklearn test_common.py (though not necessarily in this PR).

There is already such a test, I would have been surprised of the contrary, see

def check_estimators_pickle(name, estimator_orig):

Maybe some estimators are not tested for some reason?

@glemaitre glemaitre changed the title FIX update BaseEstimator __getstate__ in Python 3.11 FIX fix pickling for empty object with Python 3.11+ Dec 15, 2022
@BenjaminBossan
Copy link
Contributor Author

There is already such a test, I would have been surprised of the contrary, see

Ah, I was not looking there

Maybe some estimators are not tested for some reason?

I think the reason why this passes is because the estimators are always fitted there. If fitted, there is always an instance dict for all tested estimators, therefore the edge case is not triggered and the tests pass.

The question is: Does sklearn want unfitted estimators to also be pickleable? And should we rely on the fact that all estimators, after fitting, have an instance dict? Theoretically, there could be estimators that don't need instance variables.

@lesteve
Copy link
Member

lesteve commented Dec 15, 2022

Maybe some estimators are not tested for some reason?

Actually, I checked and KernelCenterer is tested. The thing is that we try to pickle the estimator after calling .fit. I am guessing after .fit we are not in the problematic situation in Python 3.11, maybe because we set some attributes?

The following snippet works fine:

from sklearn.preprocessing import KernelCenterer
import pickle

est = KernelCenterer().fit([[1.]])

pickle.dumps(est)

I edited the test to pickle before fit, and KernelCenterer is the only one that fails. LabelEncoder is not tested in this test for some reason (I don't know why).

@adrinjalali
Copy link
Member

We certainly want unfitted estimators to be pickle-able, which I think might be used in some distributed settings. I don't see any reason why we'd intentionally not support pickling unfitted estimators.

Copy link
Member

@glemaitre glemaitre left a comment

Choose a reason for hiding this comment

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

I think that we also need an entry in 1.2.1 since this is a bug fix that solves an issue if people try to pickle in 1.2.0.

Otherwise, I only have some formatting changes. LGTM.

Comment on lines 680 to 701
def test_parent_object_empty_instance_dict():
# Since Python 3.11, Python objects have a __getstate__ method by default
# that returns None if the instance dict is empty. See #25188.
class Empty:
pass

class Estimator(Empty, BaseEstimator):
pass

state = Estimator().__getstate__()
expected = {"_sklearn_version": sklearn.__version__}
assert state == expected


def test_base_estimator_empty_instance_dict():
# Since Python 3.11, Python objects have a __getstate__ method by default
# that returns None if the instance dict is empty. See #25188.

# this should not raise
state = BaseEstimator().__getstate__()
expected = {"_sklearn_version": sklearn.__version__}
assert state == expected
Copy link
Member

Choose a reason for hiding this comment

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

Since we are testing the same behaviour here, I would prefer to parameterise the test and provide the object on which to __getstate__.

Also could you put the comment inside a docstring of the test

def test_base_estimator_empty_instance():
    """Check that `__getstate__` returns an empty `dict` with an empty
    instance.

    Python 3.11+ changed behaviour by returning `None` instead of raising
    an `AttributeError`.

    Non-regression test for gh-25188.
    """
    ...

I am wondering if we could make the round-trip pickling-unpickling directly in this test instead of having a new test.

@betatim
Copy link
Member

betatim commented Dec 15, 2022

This is a summary of what I've understood:

For Python before v3.11, __getstate__ could be missing. In which case the output of self.__dict__.copy() would be used.

For Python v3.11, __getstate__ always exists, but sometimes returns None. In that case we continue to use self.__dict__.copy(), which seems to always be an empty dict. The difference to the old state is that we expect self.__dict__.copy() to always return an empty dict, where as before it could contain things. Doesn't seem to be a problem.

Next __slots__. In Python before v3.11 instances of a class like the following:

class A:
  __slots__ = ["a"]
  def __init__(self, a):
    self.a = a

does not have a __getstate__ and also does not have a __dict__. I think this means the current code would break if it ever encountered an estimator like this.

In Python 3.11, the instances will have a __getstate__ (that returns (None, {'a': 42})). They still don't have a __dict__. This means if we adjust the code in this PR to handle the different return value of __getstate__ we'd not get exceptions. Or at least not get exceptions for things that used to create exceptions.

For completeness: Instances of this class

class A:
  __slots__ = ["a"]
  def __init__(self, a):
    pass

will not have a __dict__ and __getstate__ will return None (in 3.11).

@glemaitre
Copy link
Member

glemaitre commented Dec 15, 2022

The thing is that we try to pickle the estimator after calling .fit

Just a guess... what about stateless models that does not store anything during fit.

@glemaitre
Copy link
Member

We agree that regardless of the Python version, the __getstate__ would not work with __slots__ anyway. We don't introduce a regression since it would not work before as well (i.e. no __dict__ and thus failing copy).

@betatim
Copy link
Member

betatim commented Dec 15, 2022

We agree that regardless of the Python version, the __getstate__ would not work with __slots__ anyway. We don't introduce a regression since it would not work before as well (i.e. no __dict__ and thus failing copy).

Just to be super explicit, in Python 3.11 the default __getstate__ will succeed for instances that use __slots__. In older versions the scikit-learn __getstate__ would raise an exception for classes with __slots__ and without a __getstate__.

This means, we should handle the case where __getstate__ returns something in Python 3.11 for classes that use __slots__. It will return (None, {'x': 42}) in my example above, so if we don't adjust the code in this PR users will get some super weird exception.

@adrinjalali
Copy link
Member

I don't mind us handling __slots__ here. But we never really supported __slots__, and users were encountering silent bugs if the code were not raising an exception, and now we would raise, I'm okay with that.

I'm not sure if we should put code for something which we're really not supporting and there's no way we'll go down the road of supporting it.

@glemaitre
Copy link
Member

glemaitre commented Dec 15, 2022

This means, we should handle the case where getstate returns something in Python 3.11 for classes that use slots. It will return (None, {'x': 42}) in my example above, so if we don't adjust the code in this PR users will get some super weird exception.

Probably something like:

AttributeError: 'tuple' object has no attribute 'items'

If we want to handle it, my first guess would be something like:

        err_msg_slots = (
            "Pickling an estimator is not supported for classes using __slots__ "
            "instead of __dict__."
        )
        try:
            try:
                state = super().__getstate__()
                if state is None:
                    state = self.__dict__.copy()
            except AttributeError:
                state = self.__dict__.copy()
        except AttributeError as exc:
            # Fails if `__slots__` are used in an empty instance
            raise ValueError(err_msg_slots) from exc

        if not isinstance(state, Mapping):
            # Fails if `__slots__` are used with a non-empty instance
            raise ValueError(err_msg_slots)

and I hate nested try ... except .... A bit less nested:

        try:
            state = getattr(super, "__getstate__", self.__dict__.copy())
        except AttributeError as exc:
            # Fails if `__slots__` are used in an empty instance
            raise ValueError(err_msg_slots) from exc

        if state is None and hasattr(self, "__dict__"):
            state = self.__dict__.copy()
        else:
            raise ValueError(err_msg_slots)

@adrinjalali
Copy link
Member

Python's pickle code checks if the output of __getstate__ is a tuple, and if yes, it assumes __slots__ are available. We can do the same here and raise and say we don't support slots.

BenjaminBossan and others added 2 commits December 15, 2022 16:30
Co-authored-by: Guillaume Lemaitre <[email protected]>
Co-authored-by: Guillaume Lemaitre <[email protected]>
@BenjaminBossan
Copy link
Contributor Author

Python's pickle code checks if the output of __getstate__ is a tuple, and if yes, it assumes __slots__ are available. We can do the same here and raise and say we don't support slots.

I can add a check for slots if we agree on that. What would be an appropriate error to raise here?

@adrinjalali
Copy link
Member

We could simply do:

if getattr(self, "__slots__", None):
	TypeError("You cannot use `__slots__` in objects inheriting from `sklearn.base.BaseEstimator`")

similar to: https://github.com/python/cpython/pull/2821/files#diff-3fd2e552779d3d96e7c1f2d80f4b8fd88752ef5437f42201037ed7c76bba2779R92

- Add entry to changelog
- Check if slots are used, raise TypeError
- Refactor tests to use parametrize, test __getstate__ and pickle in
  same test
@BenjaminBossan
Copy link
Contributor Author

I think I made all changes as requested by reviewers:

  • Add entry to change log
  • Check if slots are used, raise TypeError
  • Refactor tests to use parametrize, test __getstate__ and pickle in same test

sklearn/base.py Outdated
@@ -271,9 +271,20 @@ def __repr__(self, N_CHAR_MAX=700):
return repr_

def __getstate__(self):
if hasattr(self, "__slots__"):
Copy link
Member

Choose a reason for hiding this comment

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

There's a difference between getattr and hasattr here. We should do getattr since if the attribute exists but it's none, we shouldn't fail.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

if the attribute exists but it's none, we shouldn't fail.

Isn't that what's happening with the existing code?

In [1]: class A:
   ...:     def __init__(self):
   ...:         self.x = None
   ...: 

In [2]: hasattr(A(), "x")
Out[2]: True

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also, it seems that you can't do __slots__ = None in the first place.

Copy link
Member

@glemaitre glemaitre Dec 15, 2022

Choose a reason for hiding this comment

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

@adrinjalali I think that __slots__ cannot be None:

Cell In[33], line 1
----> 1 class A:
      2     __slots__ = None

TypeError: 'NoneType' object is not iterable

Edit: arfff, GitHub latency :)

Copy link
Member

Choose a reason for hiding this comment

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

Sure, but if the code in cpython does getattr, I rather stay on that safer side. I don't know why they did that, and I'm not sure if I wanna know why lol

Copy link
Member

Choose a reason for hiding this comment

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

I think there can be a C extension type which exposes a __slots__=None, and that's why getattr(obj, '__slots__', None) is more correct than getattr(obj, '__slots__').

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But what about __slots__ = [], that should also trigger the error. We would have to additionally check for empty list and empty tuple.

Copy link
Member

Choose a reason for hiding this comment

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

I wouldn't try to be more correct than cpython, and I'm not sure what __slot__=[] would mean. I can understand __slots__=None would be equivalent to it not existing.

Copy link
Member

Choose a reason for hiding this comment

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

On having to understand the subtle differences between getattr and hasattr: I'm with Adrin on the "there must be a reason the CPython devs used that, even if we don't fully understand why".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay, I changed to if getattr(self, "__slots__", None)

Copy link
Member

@glemaitre glemaitre left a comment

Choose a reason for hiding this comment

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

LGTM

@adrinjalali adrinjalali enabled auto-merge (squash) December 16, 2022 10:46
@adrinjalali adrinjalali merged commit 9017c70 into scikit-learn:main Dec 16, 2022
@BenjaminBossan BenjaminBossan deleted the bugfix-getstate-python-3.11 branch December 16, 2022 12:00
jjerphan pushed a commit to jjerphan/scikit-learn that referenced this pull request Jan 3, 2023
Co-authored-by: Adrin Jalali <[email protected]>
Co-authored-by: Guillaume Lemaitre <[email protected]>

Python 3.11 introduces `__getstate__` on the `object` level, which breaks our existing `__getstate__` code for objects w/o any attributes. This fixes the issue.
jjerphan pushed a commit to jjerphan/scikit-learn that referenced this pull request Jan 20, 2023
Co-authored-by: Adrin Jalali <[email protected]>
Co-authored-by: Guillaume Lemaitre <[email protected]>

Python 3.11 introduces `__getstate__` on the `object` level, which breaks our existing `__getstate__` code for objects w/o any attributes. This fixes the issue.
jjerphan pushed a commit to jjerphan/scikit-learn that referenced this pull request Jan 20, 2023
Co-authored-by: Adrin Jalali <[email protected]>
Co-authored-by: Guillaume Lemaitre <[email protected]>

Python 3.11 introduces `__getstate__` on the `object` level, which breaks our existing `__getstate__` code for objects w/o any attributes. This fixes the issue.
jjerphan pushed a commit to jjerphan/scikit-learn that referenced this pull request Jan 23, 2023
Co-authored-by: Adrin Jalali <[email protected]>
Co-authored-by: Guillaume Lemaitre <[email protected]>

Python 3.11 introduces `__getstate__` on the `object` level, which breaks our existing `__getstate__` code for objects w/o any attributes. This fixes the issue.
adrinjalali pushed a commit that referenced this pull request Jan 24, 2023
Co-authored-by: Adrin Jalali <[email protected]>
Co-authored-by: Guillaume Lemaitre <[email protected]>

Python 3.11 introduces `__getstate__` on the `object` level, which breaks our existing `__getstate__` code for objects w/o any attributes. This fixes the issue.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Waiting for Second Reviewer First reviewer is done, need a second one!
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants