·9 min read·AlgoMindset Team

'NoneType' object is not subscriptable: The cloudpickle Bug That Eats Agent Deploys

The symptom

You deploy an agent to a managed runtime. The deploy reports success. The resource is active. You send a query and get back an empty response, or a traceback pointing at a line in the framework's internals rather than anything you wrote:

The instinct is to read your agent code. Do not — the bug is not there. The same object works perfectly when you run it locally, which is what makes this so disorienting.

text
TypeError: 'NoneType' object is not subscriptable
  File ".../google/adk/agents/llm_agent.py", line 643, in canonical_model
    resolved = self._resolved_model
  File ".../pydantic/main.py", line 1024, in __getattr__
    ...

# Or, just as often, no error at all - simply an empty response.

What actually happens at deploy time

Managed agent runtimes — Vertex AI Agent Engine among them — do not ship your source and import it. They serialise the live agent object with cloudpickle in whatever environment runs the deploy, upload the bytes, and reconstruct the object inside a container built from a requirements list you supply.

So there are two Python environments. The one where the object was pickled, and the one where it is unpickled. cloudpickle does not carry library code across that boundary; it carries the object's state plus references to the classes it belongs to. Reconstruction relies on the receiving environment having compatible versions of those classes.

If the versions differ, the unpickled object is built against a class definition that is not the one it was serialised from.

Why pydantic makes it silent instead of loud

A plain version mismatch usually raises something obvious. This one does not, and the reason is pydantic private attributes.

Modern agent frameworks are built on pydantic models, which keep internal state in private attributes — things like a resolved model client, a compiled schema, a cached callable. Those are deliberately excluded from the serialised representation, because they are derived state that gets rebuilt on construction.

The rebuild happens through the receiving version's `__init__` and validators. If that version initialises private attributes differently — or has renamed, added or removed one — you get an object that looks complete, passes every `isinstance` check, and has `None` where a resolved client should be. It fails at the first attribute access on a code path you never see, which is why the traceback lands inside the framework rather than in your code.

The general shape is worth remembering beyond this specific bug: derived state that is excluded from serialisation is exactly the state that breaks when the two sides disagree about how to derive it.

The fix

Pin every framework package to the version installed in the environment doing the deploying. Not a range, not the latest, not what the docs suggest — the exact version that produced the pickle.

Do it by reading the installed version at deploy time rather than hardcoding a string. A hardcoded pin drifts the moment someone upgrades a dependency locally, and it drifts silently.

python
import importlib.metadata as md

def pin(package: str, extras: str = "") -> str:
    """Pin to the version installed HERE.

    The agent is cloudpickled in this environment and unpickled in the
    runtime container. A different framework version on each side means
    pydantic private attributes do not survive the round trip, and every
    query dies inside the framework with an error that looks unrelated.
    """
    return f"{package}{extras}=={md.version(package)}"


REQUIREMENTS = [
    pin("google-adk"),
    pin("google-cloud-aiplatform", "[adk,agent-engines]"),
    # Your own dependencies do not cross the pickle boundary as class
    # definitions, so ranges are fine here.
    "httpx>=0.27.0",
    "google-cloud-secret-manager>=2.20.0",
]

Which packages need this

Anything whose classes appear in the object graph being pickled. In practice: the agent framework itself, the SDK that provides the deployment wrapper, and pydantic if you pin it explicitly anywhere.

Libraries your tools merely call — an HTTP client, a database driver, a cloud client — do not need pinning for this reason, because they are not part of the serialised object. Pin them if you want reproducible builds, but they are not what causes this failure.

A quick way to check: if the class appears in the type of the agent or anything it holds a reference to, pin it.

Diagnosing it when it happens to you

Read the deployed logs first. This is the step I skipped when I hit this, and it cost an hour of theorising about environment variables that turned out to be entirely irrelevant. The traceback is in the runtime logs and it points straight at the framework internals — which is itself the diagnostic signal.

Then compare the two environments directly. Print the installed versions locally, and print them from inside a deployed tool call. If they differ on a framework package, you have found it.

The tell that distinguishes this from an ordinary bug: your code appears nowhere in the traceback, and the failing attribute is something you never set.

python
# Add this as a temporary tool. It answers the question in one query.
def debug_environment() -> dict:
    """Report the runtime's installed versions. Remove before going live."""
    import importlib.metadata as md
    names = ["google-adk", "google-cloud-aiplatform", "pydantic", "cloudpickle"]
    return {n: md.version(n) for n in names if _installed(n)}

# Compare against the same list printed where you deploy from.
# Any framework package that differs is your bug.

The wider lesson

Serialising a live object and reconstructing it elsewhere is a convenient deployment model, and it hides a dependency you cannot see in your code: the receiving environment's library versions are part of your program's correctness.

Two habits follow. Pin what crosses the boundary, derived from the environment rather than typed by hand. And when something works locally but not deployed, check the boundary before you check your logic — the environment difference is the more likely cause, and it is faster to rule out.

This is also a good interview answer to "tell me about a difficult bug". It has a clear mechanism, a non-obvious symptom, a specific fix, and a generalisable lesson — which is exactly the shape interviewers are listening for.