From 2234f957ca8315aa473dd4165d8d3d06846a1f99 Mon Sep 17 00:00:00 2001 From: Guillaume Lemaitre Date: Fri, 31 Oct 2025 17:16:56 +0100 Subject: [PATCH 1/6] FIX remove __annotate_func__ that return __dict__ within a closure in Python 3.14 --- cloudpickle/cloudpickle.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/cloudpickle/cloudpickle.py b/cloudpickle/cloudpickle.py index 4f4d857a..589eb152 100644 --- a/cloudpickle/cloudpickle.py +++ b/cloudpickle/cloudpickle.py @@ -751,6 +751,9 @@ def _class_getstate(obj): clsdict = _extract_class_dict(obj) clsdict.pop("__weakref__", None) + print(obj) + print(clsdict) + if issubclass(type(obj), abc.ABCMeta): # If obj is an instance of an ABCMeta subclass, don't pickle the # cache/negative caches populated during isinstance/issubclass @@ -783,6 +786,14 @@ def _class_getstate(obj): clsdict.pop("__dict__", None) # unpicklable property object + if sys.version_info >= (3, 14): + # PEP-649/749: __annotate_func_ contains a closure that references the class + # dict. We need to exclude it from pickling. Python will recreate it when + # __annotations__ is accessed at unpickling time. + clsdict.pop("__annotate_func__", None) + + print(clsdict) + return (clsdict, {}) @@ -1190,6 +1201,10 @@ def _class_setstate(obj, state): for subclass in registry: obj.register(subclass) + # PEP-649/749: During pickling, we excluded the __annotate_func__ attribute but it + # will be created by Python. Subsequently, annotations will be recreated when + # __annotations__ is accessed. + return obj From e46d132c5318e50b3345602d81e2635f092369e9 Mon Sep 17 00:00:00 2001 From: Guillaume Lemaitre Date: Fri, 31 Oct 2025 17:18:49 +0100 Subject: [PATCH 2/6] remove debug --- cloudpickle/cloudpickle.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/cloudpickle/cloudpickle.py b/cloudpickle/cloudpickle.py index 589eb152..79b3dba9 100644 --- a/cloudpickle/cloudpickle.py +++ b/cloudpickle/cloudpickle.py @@ -751,9 +751,6 @@ def _class_getstate(obj): clsdict = _extract_class_dict(obj) clsdict.pop("__weakref__", None) - print(obj) - print(clsdict) - if issubclass(type(obj), abc.ABCMeta): # If obj is an instance of an ABCMeta subclass, don't pickle the # cache/negative caches populated during isinstance/issubclass From 8c460bba62a32e5ef82d854300a15bb985e39113 Mon Sep 17 00:00:00 2001 From: Guillaume Lemaitre Date: Fri, 31 Oct 2025 17:19:12 +0100 Subject: [PATCH 3/6] removing more debug --- cloudpickle/cloudpickle.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/cloudpickle/cloudpickle.py b/cloudpickle/cloudpickle.py index 79b3dba9..b4db8f56 100644 --- a/cloudpickle/cloudpickle.py +++ b/cloudpickle/cloudpickle.py @@ -789,8 +789,6 @@ def _class_getstate(obj): # __annotations__ is accessed at unpickling time. clsdict.pop("__annotate_func__", None) - print(clsdict) - return (clsdict, {}) From e794563383fe3d980837251cb007b6935e0dc435 Mon Sep 17 00:00:00 2001 From: Guillaume Lemaitre Date: Fri, 31 Oct 2025 17:40:28 +0100 Subject: [PATCH 4/6] add non-regression tests --- tests/cloudpickle_test.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/cloudpickle_test.py b/tests/cloudpickle_test.py index 45e8c592..1463bd7b 100644 --- a/tests/cloudpickle_test.py +++ b/tests/cloudpickle_test.py @@ -2672,6 +2672,15 @@ class C: C1 = pickle_depickle(C, protocol=self.protocol) assert C1.__annotations__ == C.__annotations__ + def test_class_annotations_abstractclass(self): + # see https://github.com/cloudpipe/cloudpickle/issues/572 + + class C(abc.ABC): + a: int + + C1 = pickle_depickle(C, protocol=self.protocol) + assert C1.__annotations__ == C.__annotations__ + def test_function_annotations(self): def f(a: int) -> str: pass From e9e66706eb347025882f1ec490f7abdf0e43a8a7 Mon Sep 17 00:00:00 2001 From: Guillaume Lemaitre Date: Fri, 31 Oct 2025 18:02:42 +0100 Subject: [PATCH 5/6] address comment --- cloudpickle/cloudpickle.py | 2 +- tests/cloudpickle_test.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/cloudpickle/cloudpickle.py b/cloudpickle/cloudpickle.py index b4db8f56..e600b35f 100644 --- a/cloudpickle/cloudpickle.py +++ b/cloudpickle/cloudpickle.py @@ -784,7 +784,7 @@ def _class_getstate(obj): clsdict.pop("__dict__", None) # unpicklable property object if sys.version_info >= (3, 14): - # PEP-649/749: __annotate_func_ contains a closure that references the class + # PEP-649/749: __annotate_func__ contains a closure that references the class # dict. We need to exclude it from pickling. Python will recreate it when # __annotations__ is accessed at unpickling time. clsdict.pop("__annotate_func__", None) diff --git a/tests/cloudpickle_test.py b/tests/cloudpickle_test.py index 1463bd7b..7b9cb749 100644 --- a/tests/cloudpickle_test.py +++ b/tests/cloudpickle_test.py @@ -2680,6 +2680,13 @@ class C(abc.ABC): C1 = pickle_depickle(C, protocol=self.protocol) assert C1.__annotations__ == C.__annotations__ + C2 = pickle_depickle(C1, protocol=self.protocol) + if sys.version_info >= (3, 14): + # check that __annotate_func__ is created by Python + assert hasattr(C2, "__annotate_func__") + assert C2.__annotations__ == C1.__annotations__ + c2 = C2() + assert isinstance(c2, C2) def test_function_annotations(self): def f(a: int) -> str: From 0f776e371aa5d2d47ad4a879c8e954d3526f5e53 Mon Sep 17 00:00:00 2001 From: Guillaume Lemaitre Date: Fri, 31 Oct 2025 18:04:59 +0100 Subject: [PATCH 6/6] add entry in changelog --- CHANGES.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index b14f8a41..b20f1605 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,12 @@ In development ============== +3.1.2 +===== + +- Fix pickling of classes containing type annotations for Python 3.14 + ([PR#578](https://github.com/cloudpipe/cloudpickle/pull/578)) + 3.1.1 =====