A personal fork of CPython. Nothing
interesting lives on master; it carries only this guide. The work is on the
branches, in three lineages (Lazy Imports, allocation accounting, and
out-of-process runtime monitoring), plus a few historical working branches I
keep as they actually happened.
Two branch styles:
- Clean single-feature branches. A release tag plus exactly one commit, so
the patch is the diff. Named
<version>-<feature>. - Historical branches. Real work as it happened, thousands of commits and rebases and dead ends, preserved as-is. I don't rewrite these.
The main branch is the vanilla 3.12 base these historical branches were cut
from (stock CPython 3.12.0+, frozen in 2023). It carries none of the work below.
The per-feature notes (What, Why, ABI, Remove when) come from the rationale each patch carries in its own header.
AGENTS.mdis the entry point for working here: the ABI rule that shapes every patch, how to verify a branch, and what not to do. Start here if you are an agent.docs/methodology.mdis the ABI and performance testing discipline, and why it is the way it is.- Per-feature deep dives:
allocated-bytes,gc-stats,current-loops,lazy-imports. tools/carriesabi-check.py,abi-witness.pyandperf-compare.py, so you can check a change yourself. They depend on nothing outside this repository.
Two implementations live here, and the split is worth knowing:
- The original lazy-imports work (the
lazy_imports*branches), the implementation that predates and motivated the standard. - The standard itself, PEP 810 (Explicit Lazy Imports), as it landed
upstream, backported on
3.14-lazy-imports.
Full notes: docs/lazy-imports.md.
The core (Objects/lazyimportobject.c, Include/lazyimportobject.h,
Include/internal/pycore_lazyimport.h) is byte-identical across the 3.12
branches below. They differ only in the CPython base they sit on.
| Branch | Base | What it is |
|---|---|---|
lazy_imports |
3.12.0 | A squashed single-commit share of the full feature, one clean commit to read without the thousands-of-commits churn. |
lazy_imports_3_12 |
3.12.11 | The full working branch (~2358 commits): vanilla 3.12 built up to full lazy imports, kept rebased to current 3.12. Same feature code as lazy_imports. |
lazy_imports_3_10 |
3.10 | Ported to 3.10. |
lazy_imports_3_8 |
3.8.13 | The earliest port, from before lazyimportobject.c existed. |
lazy_imports-frame_flag |
3.10-era (2022) | An experiment: a lazy_imports flag on _PyInterpreterFrame, the PyImport_SetLazyImports API, set_lazy_imports(excluding=...). |
lazy_imports-pyperformance |
3.10-era (2022) | The feature plus committed pyperformance before and after results. |
lazy_imports and lazy_imports_3_12 are the same feature code on a different
base. The share branch was squashed at 3.12.0; the working branch was carried
forward to 3.12.11. The large tree diff between them is entirely the upstream
3.12.0 to 3.12.11 maintenance delta, not the feature.
3.14-lazy-imports, base v3.14.8.
- What. The full upstream implementation: the
lazy importgrammar and AST, theLAZY_IMPORT_NAMEandLAZY_IMPORT_FROMopcodes and their specializations, lazy dict values indictobject.candmoduleobject.c,sys.set_lazy_imports(),sys.lazy_modules,-X lazy_imports,ForwardRef.evaluate()reification, plus the docs and tests. - Why. Import time dominates startup for larger Python services, and lazy imports cut it without every service hand-rolling module-level deferral.
- Provenance. A faithful backport of upstream's PEP 810 (
gh-142349) and its 3.15 resolver, tracking the 3.15 branch atd625ecbb8d. The feature isGH-142351and the resolver simplificationGH-158282; this carries the follow-upsGH-157714(sys.lazy_modules),GH-158521(sibling submodules),GH-149739(tests),GH-156924(ForwardRef.evaluate), and the doc fixesGH-155547/GH-156936. The resolver and tests match the 3.15 tip except for the ABI adaptations below. - ABI. No member offset moves on either the default or free-threaded build.
The lazy-import state is heap-allocated behind the existing unused
PyInterpreterState._malloced, andPyConfig.lazy_importsis omitted, so-X lazy_importsandPYTHON_LAZY_IMPORTSstill work. The per-thread resolving set is a new_PyThreadStateImpl.lazy_importsfield: on free-threaded builds it is carved from the gh-144438 cache-line padding ([64]to[56]), leaving the size unchanged; on the default build it is appended at the struct tail (+8 bytes, no offset moves). The exported_PyDict_LoadGlobalsymbol is kept as a raw compatibility shim so prebuilt 3.14 extensions that link it keep working. - Remove when. This line reaches 3.15, which ships PEP 810.
Python-visible byte accounting for the allocator, in two generations. The second is a redesign of the first. Full notes: docs/allocated-bytes.md.
sys__getallocatedbytes, the original (3.12, historical)
sys.getallocatedbytes() backed by one per-interpreter atomic counter
(raw_allocated_bytes): an atomic read-modify-write on a single shared counter
on every allocation and free. Simple, but that one contended counter is the cost
the per-thread redesign below exists to avoid. One reader, live bytes.
3.14-allocated-bytes, the optimized descendant (v3.14.7, single-feature)
- What. Three readers,
getallocatedbytes()(live),gettotalallocatedbytes()(cumulative), and_current_allocated_bytes()(per-thread), backed by per-thread allocated and freed byte counters in the private_PyThreadStateImpl, each charged once from the pymalloc, mimalloc and raw paths inobmalloc.c. Blocks aren't counted: once the raw domain is charged, a counted block isn't onegetallocatedblocks()'s walk can see. - Threads. One writer per counter, the owning thread. The exception is the
no-thread-state accumulator that detached threads share, charged with a relaxed
fetch-add, because a plain add on a counter with more than one writer loses
increments. The summed total is a sample, not a snapshot, unsynchronized on
purpose, exactly as upstream's shared
raw_allocated_blocksalready is. - ABI. No field offset moves. Three struct sizes grow by 16 bytes at the tail
(
_PyThreadStateImpl936 to 952,PyInterpreterState225912 to 225928,_PyRuntimeState316056 to 316072 on 3.14.7); a free-threaded build grows only_PyThreadStateImpl. Nothing shifts. - Cost. Within noise: -0.12% median over ten interleaved benchmark rounds against a build identical but for this patch, where the control itself moved +0.41%. (An unoptimized build of an earlier line showed a few percent. That figure predates this per-thread, single-write version.)
- Remove when. CPython grows equivalent counters upstream. Nothing is proposed today.
Each is a release tag plus one commit.
GC statistics: 3.12-gc-stats, 3.13-gc-stats, 3.14-gc-stats
Bases v3.12.13, v3.13.15, v3.14.7. Full notes: docs/gc-stats.md.
- What. A preallocated ring of recent collections (generation, start and stop
timestamps, objects examined and collected, live heap size),
candidatesanddurationkeys ongc.get_stats(), and a reader to pull the ring out of process. - Provenance. A backport of upstream's gcmon work (
gh-146527, first ships in 3.15), tracked throughgh-151646(the free-threadedgc.get_stats()lock). - ABI. Identical to a stock build. The ring pointer reuses 3.14's spare slot
in
_gc_runtime_staterather than appending, which would move every member after it, and the free-threaded lock lands in the struct's trailing padding. No member moves, even under a free-threaded build. - Remove when. This line reaches 3.15, which ships gcmon.
asyncio loops: 3.14-current-loops
Base v3.14.7. Full notes: docs/current-loops.md.
- What.
sys._current_loops(), a dict mapping each thread id to the asyncio loop running on it, read from_PyThreadStateImpl.asyncio_running_loop(where 3.13+ keeps the running loop), with the thread-state walk it needs. - Why. An event-loop monitor has to know which loop each thread runs. Keying
by thread id, the way
sys._current_frames()keys its own, lets the two snapshots join: one says where a thread is, the other says which loop it belongs to. - Provenance. Original to this fork's downstream line.
- Remove when. CPython grows a supported way to enumerate running loops. Nothing is proposed today.
I pruned two branches that never carried the feature their name promised, and one empty predecessor. Recorded here so the deletions have provenance too.
| Branch | Why removed | When |
|---|---|---|
lazy_imports_3_13, lazy_imports_3_14 |
Never carried lazy imports. Stock CPython trees; the only "lazy" content was unrelated upstream code. | 2026-09-06 |
getallocatedbytes |
An empty predecessor of sys__getallocatedbytes: no byte-counter work ever landed (only stock raw_allocated_blocks), and no sys reader. Restore point af6e5fa718. |
2026-09-06 |
<version>-<feature>branches are a release tag plus one commit.- The historical
lazy_imports*andsys__getallocatedbytesbranches stay as-is. Don't rebase or squash them; their history is the point. - Upstream issue numbers (
gh-...) and PEP 810 are the public anchors for provenance. Nothing here leans on downstream-internal references.