@@ -568,20 +568,61 @@ _PyObject_GetXIData(PyThreadState *tstate,
568568
569569/* pickle C-API */
570570
571+ /* Per-interpreter cache for pickle.dumps and pickle.loads.
572+ *
573+ * Each interpreter has its own cache in _PyXI_state_t.pickle, preserving
574+ * interpreter isolation. The cache is populated lazily on first use and
575+ * cleared during interpreter finalization in _Py_xi_state_fini().
576+ *
577+ * Note: the cached references are captured at first use and not invalidated
578+ * on module reload. This matches the caching pattern used elsewhere in
579+ * CPython (e.g. arraymodule.c, _decimal.c). */
580+
581+ static PyObject *
582+ _get_pickle_dumps (PyThreadState * tstate )
583+ {
584+ _PyXI_state_t * state = _PyXI_GET_STATE (tstate -> interp );
585+ PyObject * dumps = state -> pickle .dumps ;
586+ if (dumps != NULL ) {
587+ return dumps ;
588+ }
589+ dumps = PyImport_ImportModuleAttrString ("pickle" , "dumps" );
590+ if (dumps == NULL ) {
591+ return NULL ;
592+ }
593+ state -> pickle .dumps = dumps ; // owns the reference
594+ return dumps ;
595+ }
596+
597+ static PyObject *
598+ _get_pickle_loads (PyThreadState * tstate )
599+ {
600+ _PyXI_state_t * state = _PyXI_GET_STATE (tstate -> interp );
601+ PyObject * loads = state -> pickle .loads ;
602+ if (loads != NULL ) {
603+ return loads ;
604+ }
605+ loads = PyImport_ImportModuleAttrString ("pickle" , "loads" );
606+ if (loads == NULL ) {
607+ return NULL ;
608+ }
609+ state -> pickle .loads = loads ; // owns the reference
610+ return loads ;
611+ }
612+
571613struct _pickle_context {
572614 PyThreadState * tstate ;
573615};
574616
575617static PyObject *
576618_PyPickle_Dumps (struct _pickle_context * ctx , PyObject * obj )
577619{
578- PyObject * dumps = PyImport_ImportModuleAttrString ( "pickle" , "dumps" );
620+ PyObject * dumps = _get_pickle_dumps ( ctx -> tstate );
579621 if (dumps == NULL ) {
580622 return NULL ;
581623 }
582- PyObject * bytes = PyObject_CallOneArg (dumps , obj );
583- Py_DECREF (dumps );
584- return bytes ;
624+ // dumps is a borrowed reference from the cache.
625+ return PyObject_CallOneArg (dumps , obj );
585626}
586627
587628
@@ -636,7 +677,8 @@ _PyPickle_Loads(struct _unpickle_context *ctx, PyObject *pickled)
636677 PyThreadState * tstate = ctx -> tstate ;
637678
638679 PyObject * exc = NULL ;
639- PyObject * loads = PyImport_ImportModuleAttrString ("pickle" , "loads" );
680+ // loads is a borrowed reference from the per-interpreter cache.
681+ PyObject * loads = _get_pickle_loads (tstate );
640682 if (loads == NULL ) {
641683 return NULL ;
642684 }
@@ -682,7 +724,6 @@ _PyPickle_Loads(struct _unpickle_context *ctx, PyObject *pickled)
682724 // It might make sense to chain it (__context__).
683725 _PyErr_SetRaisedException (tstate , exc );
684726 }
685- Py_DECREF (loads );
686727 return obj ;
687728}
688729
@@ -3094,6 +3135,10 @@ _Py_xi_state_init(_PyXI_state_t *state, PyInterpreterState *interp)
30943135 assert (state != NULL );
30953136 assert (interp == NULL || state == _PyXI_GET_STATE (interp ));
30963137
3138+ // Initialize pickle function cache (before any fallible ops).
3139+ state -> pickle .dumps = NULL ;
3140+ state -> pickle .loads = NULL ;
3141+
30973142 xid_lookup_init (& state -> data_lookup );
30983143
30993144 // Initialize exceptions.
@@ -3116,6 +3161,11 @@ _Py_xi_state_fini(_PyXI_state_t *state, PyInterpreterState *interp)
31163161 assert (state != NULL );
31173162 assert (interp == NULL || state == _PyXI_GET_STATE (interp ));
31183163
3164+ // Clear pickle function cache first: the cached functions may hold
3165+ // references to modules cleaned up by later finalization steps.
3166+ Py_CLEAR (state -> pickle .dumps );
3167+ Py_CLEAR (state -> pickle .loads );
3168+
31193169 fini_heap_exctypes (& state -> exceptions );
31203170 if (interp != NULL ) {
31213171 fini_static_exctypes (& state -> exceptions , interp );
0 commit comments