Skip to main content

1622. gopy lifecycle

What we are porting

Five C files, ~11k lines total, that together drive the runtime from cold start to Py_Initialize() returning, and back down through Py_Finalize():

  • Python/pylifecycle.c (~3.6k lines). The Py_Initialize and Py_Finalize entry points plus the staged init phases: preinit, core, main. Sets up the runtime, builtins, sys, the initial thread state, and the import system.
  • Python/preconfig.c (~1k lines). Pre-init: locale, encoding detection, -X flags read before site.py runs.
  • Python/initconfig.c (~4.7k lines). The full PyConfig schema, env-var and CLI parsing, defaults, validation.
  • Python/pathconfig.c (~600 lines). sys.path defaults, prefix/exec_prefix discovery, PYTHONHOME handling.
  • Python/interpconfig.c (~300 lines). Per-sub-interpreter config knobs (mostly relevant in v0.13).

The companion Python/pythonrun.c lives in spec 1624 because its surface (REPL, PyRun_*) is consumer-facing rather than init-time.

Why this lands in v0.7

In v0.6 the cmd/gopy entry point hand-rolled a tiny boot sequence: state.NewThread, builtins.Init, then vm.EvalCode. v0.7 replaces that with the real CPython lifecycle so gopy -c and gopy script.py both flow through Py_Initialize / pythonrun / Py_Finalize. Once 1622 is in place, every later subsystem (import in v0.8, contextvars in v0.9, monitor in v0.11) can attach via the standard init hooks instead of bespoke wiring.

Package layout

gopy/
initconfig/ # 1622: preconfig + initconfig
preconfig.go
config.go
config_env.go # PYTHON* env-var parsing
config_cli.go # CLI -X / -c / -m parsing
config_validate.go
pathconfig/ # 1622: path discovery
pathconfig.go
pathconfig_unix.go
pathconfig_win.go
lifecycle/ # 1622: Py_Initialize / Py_Finalize
init.go
init_phases.go
finalize.go
interp.go
main.go # the Py_Main equivalent that cmd/gopy calls
pythonrun/ # 1624: separate spec, listed for context
runstring.go
runfile.go
repl.go
pyc.go

Existing v0.6 packages it touches:

  • gopy/state: Runtime / Interpreter / Thread already exist but were minimal. Lifecycle wires them through the real init.
  • gopy/builtins: builtins.Init keeps its signature but becomes one of the init phases instead of the entry point.
  • gopy/cmd/gopy: main.go shrinks to a thin lifecycle.Main(os.Args) call.

Init staging (the porting unit)

CPython splits init into ordered phases. We mirror them one-for-one because import, signals, codecs, and warnings each attach at a specific phase and changing the order would change when their callbacks fire.

PhaseCPython callgopy entry
pre-init: locale + UTF-8_PyPreConfig_Readinitconfig.PreConfig.Read
pre-init: allocators_PyPreConfig_InitFromPreConfiginitconfig.PreConfig.Apply
core init: runtime allocpyinit_core step 1lifecycle.initRuntime
core init: types_PyTypes_InitTypes(handled in objects, called here)
core init: builtins_PyBuiltin_Initbuiltins.Init (already exists)
core init: sys_PySys_Createsysmod.Init (1651)
core init: import systeminit_importlib_external(deferred to v0.8 spec 1623)
main init: site/userpathinit_set_builtins_open, pymain_run_*pythonrun.* (1624)
finalize: shutdown phasesPy_FinalizeExlifecycle.Finalize

Each row maps to one Go function with a CPython citation. The spec 1622 work order is top-to-bottom: a phase only lands when the phase above it lands.

v0.7 release blockers

Tracked as individual tasks; each one must close before tagging v0.7.0.

  • 1622-A Port PyPreConfig struct + defaults + PyPreConfig_InitPythonConfig. CPython: Python/preconfig.c:33 PyPreConfig_InitPythonConfig.
  • 1622-B Port the PYTHON* env-var reader subset gopy honours (PYTHONHOME, PYTHONPATH, PYTHONHASHSEED, PYTHONDONTWRITEBYTECODE, PYTHONUNBUFFERED, PYTHONUTF8, PYTHONDEBUG, PYTHONVERBOSE, PYTHONOPTIMIZE, PYTHONNOUSERSITE). CPython: Python/initconfig.c:_Py_str_to_int panel.
  • 1622-C Port the PyConfig struct (the v0.7 subset; full schema lands incrementally with later phases).
  • 1622-D Port PyConfig_Read (env then CLI then defaults). CPython: Python/initconfig.c:_PyConfig_Read.
  • 1622-E Port pyinit_core and pyinit_main skeletons. CPython: Python/pylifecycle.c:pyinit_core, Python/pylifecycle.c:pyinit_main.
  • 1622-F Port the path-config defaults for darwin and linux (Windows arrives later). CPython: Modules/getpath.py is the modern source: gopy reimplements the resolved logic in Go.
  • 1622-G Port Py_Finalize shutdown order (modules, sys, threads, types, runtime). CPython: Python/pylifecycle.c:Py_FinalizeEx.
  • 1622-H lifecycle.Main entry: parses args, runs init, hands off to pythonrun, runs finalize. CPython: Modules/main.c:Py_Main.

Test gates

  • lifecycle/init_test.go: Initialize, ask for state.GetThreadState(), Finalize, Initialize again. Must not panic and must not leak goroutines (counted via runtime.NumGoroutine).
  • lifecycle/main_test.go: feed []string{"-c", "print(1+2)"} through lifecycle.Main with a captured stdout. Output is "3\n" and exit code 0.
  • initconfig/config_test.go: env-var precedence: a flag set in env is overridden by a CLI value; CLI is overridden by an explicit PyConfig field set after Read.
  • pathconfig/pathconfig_unix_test.go: given a synthetic prefix layout (a temp dir with lib/python3.14/), the resolved sys.path matches the documented order from Doc/using/cmdline.rst.

Out of scope for v0.7

  • Py_NewInterpreter and the sub-interpreter init flow stays stubbed; spec 1622 lands a single-interpreter shape.
  • The pymain_run_module import-driven case (gopy -m foo) is gated on import (v0.8); v0.7 returns ErrNotImplemented.
  • Py_BytesMain (the wide-char Windows entry) is deferred.

CPython divergences worth pinning here

  • gopy never reads C-level allocator hooks (PyMemAllocatorEx) because Go owns allocation. The fields exist on PyPreConfig for shape parity but have no effect.
  • sys.implementation.cache_tag is "gopy-3140" (see 1690_quirks). The init code that builds sys.implementation must hard-code this string and pin it in tests.