Python 3.13 cannot pickle code objects, so a distributed job reports the wrong failure
An interpreter upgrade changes what pickle will serialise, and traceback objects stop qualifying. Any machinery that ships an exception between ranks then fails while packing it, so the error the cluster reports is the packing failure and the original fault is never printed.
Python 3.13 will not pickle code objects, and an extracted traceback contains them. The error you are reading is the reporting path failing, not the job. Find the real error in the per-rank logs, and format tracebacks to text before sending them.
What this failure is
An interpreter-upgrade failure in which traceback objects can no longer be pickled, so any code path that serialises an exception to move it between processes fails and replaces the original diagnosis with its own.
Why it happens (the mechanism)
Serialising compiled bytecode was never well defined across interpreter versions, and the newer release stops pretending otherwise. Distributed frameworks had come to rely on it because moving an exception whole is the most convenient way to re-raise it on another rank. The reliance is invisible until something fails, at which point the convenience becomes the failure.
What you'll observe
- The reported error is about pickling and has nothing to do with what actually went wrong
- The real exception is destroyed by the reporting path, so there is nothing to search for
- It only appears on the newer interpreter, so it reads as a framework bug rather than an environment change
- It surfaces on the error path, which by definition is the least exercised code in the system
Common symptoms and what they mean
| Symptom | Why it happens |
|---|---|
| TypeError: cannot pickle code objects raised while an exception was being propagated between ranks | An extracted traceback is not a lightweight record. Its frames reference code objects, and code objects are compiled bytecode with no stable serialised representation, so pickling them was always questionable. The newer interpreter stops permitting it, which is a correctness decision rather than a regression. |
| The failure appearing inside object gathering or an object-to-tensor conversion rather than in training code | The failure lands on the reporting path specifically because that is the only place a traceback is deliberately carried around. Normal operation never pickles one, so an upgrade can pass every test and every training step and still break the moment something else fails. |
| A test asserting on an error message finding the pickling message instead of the one it expected | The consequence is a lost diagnosis rather than a lost job. Whatever genuinely failed raised first; the attempt to describe it to the other ranks raised second; and only the second one reaches the log. |
| Identical code succeeding on the previous interpreter minor version | An extracted traceback is not a lightweight record. Its frames reference code objects, and code objects are compiled bytecode with no stable serialised representation, so pickling them was always questionable. The newer interpreter stops permitting it, which is a correctness decision rather than a regression. |
Which systems are affected
- Distributed error propagation, where a rank's exception is gathered to rank zero to be re-raised
- Asynchronous checkpoint save, which reports a worker's failure back to the caller
- Process-pool executors and any launcher that returns exceptions across a process boundary
- Anything that stores an extracted traceback for later inspection or logging
How to confirm this is the problem
Apply the following checklist to a small reproduction: each box below is a positive signal that you are looking at this exact failure rather than a sibling in the same taxonomy.
- ✓Extract a traceback and attempt to pickle it in a bare interpreter session. Failing on the new version and succeeding on the old one confirms the interpreter, not the framework.
- ✓Read the per-rank log files rather than the aggregated output; the original exception was recorded there before the propagation attempt.
- ✓Check whether the failing frame is inside object gathering or a pool executor's result handling, which is where a traceback would be serialised.
Example training logs (fingerprint)
TypeError: cannot pickle code objects
pickle.Pickler(io.BytesIO()).dump(trace)
[<FrameSummary file /tmp/pickly.py, line 2 in <module>>]
AssertionError: 'fail_once policy triggered failure' not found in 'cannot pickle code objects'Timestamps and exact values vary across runs, but the pattern. An info-level start, an early WARN, an ERROR carrying the symptom. Is the actual fingerprint you should alert on. The Denpex platform flags this combination automatically.
Root cause, fix & prevention, signed in
Sign up free to see why this failure really happens, the exact remediation steps, and the production-grade prevention pattern. You also get 3 free full diagnoses for your own training logs.
Sign up free. Unlock the full analysisNo credit card · 3 free diagnoses · Instant access
Why the recommended fix works
Formatting the traceback at capture time preserves everything a person reads from it — the frames, the lines, the final message — in a form that has no version sensitivity at all. Carrying the type name separately keeps whatever programmatic branching depended on the exception class. Nothing of diagnostic value is lost, and the transport stops depending on an interpreter implementation detail.
Code examples
// Typical pattern:
import torch.optim as optim
optimizer = optim.AdamW(model.parameters(), lr=3e-4)
scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=num_training_steps)
for step in range(num_training_steps):
optimizer.zero_grad()
loss = model(batch)
loss.backward()
optimizer.step()
scheduler.step()Adapt the snippet to your framework. The same pattern holds for PyTorch Lightning, Hugging Face Trainer, DeepSpeed, Megatron-LM, and vLLM training wrappers. Where the wrapper exposes a config flag (for examplelr_scheduler_type in Trainer), prefer the flag over the imperative API to keep the schedule declarative and reproducible.
Best practices by model family
| Model / Stack | Recommendation | Notes |
|---|---|---|
| You own the propagation code | Format the traceback to text at capture | Strings pickle on every interpreter version. |
| Framework owns it | Pin the interpreter minor version | Buys time without editing library internals. |
| Debugging right now | Read the per-rank logs | The original exception was written before propagation. |
| Writing new distributed code | Never send frames or code objects | Carry type name, message and formatted text. |
With the fix vs without the fix
| Dimension | With the fix | Without the fix |
|---|---|---|
| What the error describes | The reporting path | Read as the job's actual failure |
| Where the real cause is | In the per-rank log | Assumed lost |
| What to send between ranks | Formatted text and a type name | The live exception object |
Real engineering notes
“The expensive part of this is not the fix, it is the hours spent searching for a pickling bug that does not exist while the actual failure sits unread in a per-rank log. If you take one habit from it, take this: when the reported error is about the machinery of reporting, stop reading the aggregated output and go to the individual ranks. The same reflex pays off for launcher timeouts and for exit-code summaries that name no cause.”
Visual fingerprint
rank 3 real failure raised -> written to rank-3 log ✓
|
v
wrap exception + traceback
|
v
gather_object -> pickle -> TypeError: cannot pickle code objects
|
v
aggregated output shows ONLY the pickling errorRoot cause, fix & prevention
Frequently asked questions
Twelve targeted questions that engineers and on-call staff most commonly ask about this failure.
Did my training job fail because of pickle?
Why did this appear only after an upgrade?
Is downgrading the only option?
Will I lose detail by sending text?
Don't just read the fix, diagnose your run
The encyclopedia tells you what went wrong. Denpex tells you what went wrong in YOUR training run. With your logs, your config, and your stack.