A summary of the problem from a bot below:
On Python 3.14, a class pickled by value before its annotations have ever been read unpickles with annotations == {}. This happens on cloudpickle 3.1.2, on #603's head (0f652a6), and on master (4af5936).
import subprocess, sys
import cloudpickle
ns = {"__name__": "not_importable"} # not importable, so the class is pickled by value
exec("class C:\n x: int\n", ns)
unread = cloudpickle.dumps(ns["C"])
ns["C"].__annotations__ # first read fills __annotations_cache__
read = cloudpickle.dumps(ns["C"])
load = "import pickle, sys; print(pickle.loads(sys.stdin.buffer.read()).__annotations__)"
for tag, blob in [("unread", unread), ("read", read)]:
out = subprocess.run([sys.executable, "-c", load], input=blob, capture_output=True, check=True)
print(tag, out.stdout.decode().strip())
Cause: _class_getstate (https://github.com/cloudpipe/cloudpickle/blob/4af5936/cloudpickle/cloudpickle.py#L792-L797) does clsdict.pop("annotate_func", None), with the comment "Python will recreate it when annotations is accessed at unpickling time". Python does not recreate it. Before the first read, the class dict holds only annotate_func, so the pickled state has no annotations at all. After a read, annotations_cache exists and survives. The note in _class_setstate (L1213 (https://github.com/cloudpipe/cloudpickle/blob/4af5936/cloudpickle/cloudpickle.py#L1213-L1215)) makes the same assumption.
#603 (crash report in #585) snapshots annotations for by-value annotate functions. The class path needs the same snapshot: in _class_getstate, evaluate the annotations (for example annotationlib.get_annotations(obj)) into annotations_cache before popping annotate_func. Dataclasses are not affected, because the decorator reads the annotations at class creation. Plain classes whose annotations are first read after pickling (typing introspection, pydantic-style consumers on the worker) lose them silently.
A summary of the problem from a bot below:
On Python 3.14, a class pickled by value before its annotations have ever been read unpickles with annotations == {}. This happens on cloudpickle 3.1.2, on #603's head (0f652a6), and on master (4af5936).
Cause: _class_getstate (https://github.com/cloudpipe/cloudpickle/blob/4af5936/cloudpickle/cloudpickle.py#L792-L797) does clsdict.pop("annotate_func", None), with the comment "Python will recreate it when annotations is accessed at unpickling time". Python does not recreate it. Before the first read, the class dict holds only annotate_func, so the pickled state has no annotations at all. After a read, annotations_cache exists and survives. The note in _class_setstate (L1213 (https://github.com/cloudpipe/cloudpickle/blob/4af5936/cloudpickle/cloudpickle.py#L1213-L1215)) makes the same assumption.
#603 (crash report in #585) snapshots annotations for by-value annotate functions. The class path needs the same snapshot: in _class_getstate, evaluate the annotations (for example annotationlib.get_annotations(obj)) into annotations_cache before popping annotate_func. Dataclasses are not affected, because the decorator reads the annotations at class creation. Plain classes whose annotations are first read after pickling (typing introspection, pydantic-style consumers on the worker) lose them silently.