Pluggable cyclic GC strategy hook for CPython experiments and embedders

I would like to propose adding a narrow, interpreter-level hook that allows CPython’s cyclic GC strategy to be replaced or wrapped by a custom implementation, similar in spirit to the PEP 523 frame evaluation hook (_PyInterpreterState_SetEvalFrameFunc).

Proposal:

Today CPython’s cyclic GC implementation is tightly coupled to Python/gc.c, PyGC_Head, generation lists, and the existing tp_traverse / tp_clear protocol. Projects that want to experiment with alternative collection strategies, such as parallel collection, different scheduling policies, more detailed GC instrumentation, or platform-specific tuning, generally need to carry invasive patches against CPython.

The goal is not to change the default GC behavior, object model, or public Python semantics. The proposed direction is to keep CPython’s current GC as the default implementation, but expose an internal/unstable C-level strategy interface that can be installed per interpreter. For example, CPython could provide a default strategy backed by the existing implementation, while embedders or experimental builds could install a custom strategy:

typedef struct _PyGCStrategy {
    Py_ssize_t (*collect)(PyThreadState *tstate, int generation,
                          _PyGC_Reason reason);
    /* Additional hooks could be considered only if needed, such as
       initialization/finalization, collection scheduling, statistics,
       or delegation back to the default implementation. */
} _PyGCStrategy;

int _PyInterpreterState_SetGCStrategy(PyInterpreterState *interp,
                                      const _PyGCStrategy *strategy);
const _PyGCStrategy *_PyInterpreterState_GetGCStrategy(PyInterpreterState *interp);

An initial version could deliberately be limited to replacing or wrapping collection entry points such as _PyGC_Collect(), while keeping object tracking, PyGC_Head, tp_traverse, tp_clear, finalization behavior, weakref handling, and resurrection semantics under CPython’s existing rules. That would make the hook useful for experimentation without immediately requiring CPython to support arbitrary moving collectors or a stable GC plugin ABI.

Potential benefits:

  • Makes alternative GC strategies easier to prototype without maintaining large forks of Python/gc.c.
  • Helps embedders and experimental runtimes evaluate parallel, incremental, or workload-specific collection policies while preserving CPython object semantics.
  • Provides a cleaner boundary for GC instrumentation and performance experiments.
  • Mirrors the successful pattern of the frame evaluation hook: the default runtime remains unchanged, while advanced users can opt into an internal extension point.

Important constraints:

  • This should not be part of the Stable ABI.
  • The default CPython GC behavior should remain unchanged.
  • The first version should be explicitly private/unstable unless maintainers think a public API is appropriate later.
  • Custom strategies should be required to preserve CPython-visible GC semantics, including gc.collect(), gc.callbacks, finalization, weakrefs, resurrection behavior, and debug/statistics behavior as far as applicable.
  • The hook should be per-interpreter rather than global, matching the direction of CPython runtime isolation.

I am opening this issue to ask whether CPython maintainers would be open to such an extension point, and what minimal hook boundary would be acceptable for experimentation.

1 Like

It doesn’t look like something to set at runtime. Perhaps it should be initialization configuration; maybe even fully a build-time option at first if you don’t need it to be per-interpreter.
You can start with patching CPython – in the short term, that’s less work than building extension points. Once you build something useful, we can look at what kind of hooks would be necessary.

Thanks, that makes sense. I agree that a runtime-settable GC strategy is probably not the right first shape.

For context, one reason I raised this is that Cinder has already experimented with parallelizing parts of CPython’s cyclic GC. Sam Gross mentioned this in an earlier discussion: Does garbage collection run in a separate thread in free threaded python? - #4 by colesbury

Given that background, I’m mainly interested in whether CPython maintainers would be open to exposing some kind of narrow entry point for this class of experiments, whether that ends up being a build-time option, an initialization-time configuration, or something else.

Given that background, I’m mainly interested in whether CPython maintainers would be open to exposing some kind of narrow entry point for this class of experiments

No, not for initial experimentation. At this point, maintaining a hook in CPython is more work than maintaining a patch externally.

Once you run the experiment and have a result like “this patch shows a X% speedup on specialized workloads,” it’s time to start deciding what kind of hook you need.

1 Like