Skip to content

[BUG] Local Model Cloudpickle Parent Namespace Issue #221

@NeejWeej

Description

@NeejWeej

Summary

This is an issue with pydantic, but affects ccflow.

Local Pydantic model classes capture their defining function's local variables in
__pydantic_parent_namespace__. When cloudpickle serializes an instance of
that local model class, it can follow that namespace and fail on unrelated,
unpickleable locals.

This affects ccflow because ccflow supports local-scope BaseModel /
ContextBase classes via local persistence.

Minimal Pydantic Repro

import threading
import cloudpickle
import pydantic
from pydantic import BaseModel

print(f"pydantic={pydantic.__version__}")

class Poison:
    def __init__(self):
        self.lock = threading.RLock()

def repro():
    poison = Poison()

    class LocalModel(BaseModel):
        x: int

    print(sorted(LocalModel.__pydantic_parent_namespace__))
    print("captures_poison=", "poison" in LocalModel.__pydantic_parent_namespace__)
    cloudpickle.dumps(LocalModel(x=1))

repro()

Typical failure:

pydantic=2.13.4
['poison']
captures_poison=True
TypeError: cannot pickle '_thread.RLock' object

Minimal ccflow Repro

import threading
from importlib.metadata import version

import cloudpickle
import pydantic
from ccflow import ContextBase

print(f"ccflow={version('ccflow')}")
print(f"pydantic={pydantic.__version__}")

class Poison:
    def __init__(self):
        self.lock = threading.RLock()

def repro():
    poison = Poison()

    class LocalContext(ContextBase):
        x: int

    print(sorted(LocalContext.__pydantic_parent_namespace__))
    print("captures_poison=", "poison" in LocalContext.__pydantic_parent_namespace__)
    cloudpickle.dumps(LocalContext(x=1))

repro()

Typical failure:

ccflow=0.8.3
pydantic=2.13.4
['poison']
captures_poison=True
TypeError: cannot pickle '_thread.RLock' object

Why It Appeared Now

PR #215 changed ccflow BaseModel on Pydantic >= 2.13 to use raw Pydantic
ModelMetaclass:

_BASE_MODEL_METACLASS = ModelMetaclass if _USE_RUNTIME_POLYMORPHIC_SERIALIZATION else _SerializeAsAnyMeta

Before that, ccflow always routed class construction through
_SerializeAsAnyMeta. Pydantic still created __pydantic_parent_namespace__,
but it captured ccflow metaclass locals rather than user function locals.

After the change, local ccflow models use raw ModelMetaclass, so Pydantic
captures the user's defining function frame. Any unrelated local in that frame
can become part of the pickle graph.

What It Pertains To

This is about local-scope model classes:

def f():
    class LocalContext(ContextBase):
        x: int

Module-scope model classes are not affected in normal use because they do not
get this function parent namespace and are importable by module path.

Metadata

Metadata

Assignees

No one assigned

    Labels

    type: bugConcrete, reproducible bugs
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions