v0.7.0 - Lifecycle drop
Released Pre-release.
When you type python -c "print('hi')" at a shell, an enormous
amount of work happens before that one line ever runs. Python
reads environment variables to figure out what locale you're in.
It parses -c, -m, -W, and -X flags. It computes
sys.prefix and sys.exec_prefix from the executable's location.
It initializes the interpreter state. It builds the GIL. It
imports builtins. It imports sys. It imports _warnings. It
seeds the warnings filter list. It runs the source you handed it
through the compile / eval pipeline. And then, when the source
finishes, it walks an ordered shutdown: registered atexit
callbacks, module finalizers, sys teardown, GIL release. CPython's
Python/pylifecycle.c is north of two thousand lines for a
reason.
For most of gopy's life up through v0.6, we didn't have this. We
had a hand-rolled entry that read argv with a Go-style flag
package, called into an eval loop with a hard-coded module
namespace, and called os.Exit(0) when the user program finished.
The result was usable for our own smoke tests and almost nothing
else. gopy was a runner that ran code, but it wasn't a Python
that booted.
v0.7.0 is the lifecycle drop. After this release the runtime
boots through the real CPython init path: PyPreConfig and
PyConfig parse env and CLI, the init phases hand off to
pythonrun, and gopy -c, gopy script.py, and the bare REPL
all share one entry. The builtins, sys, and _warnings surface
those entry points need lands alongside.
Highlights
Three pieces of work define this release.
One entry, three arms
gopy -c "expr", gopy script.py, and gopy (no argv) all flow
through the same Py_Main shaped function. argv goes into a
PyConfig. The config drives init. Init hands off to a single
dispatch that picks the right PyRun_* arm. Finalize runs on the
way out.
# All three of these route through one entry.
gopy -c "import sys; print(sys.argv)"
gopy ./script.py arg1 arg2
gopy
The win is that everything past the dispatch sees the same world. sys.argv is stamped the same way for all three. sys.path resolves the same way. The warnings filter starts from the same seed. The finalize order is the same. Bugs that used to reproduce in one arm and not the others (because each arm had its own argv parser, its own sys init, its own teardown) just stop existing.
Real PyPreConfig and PyConfig
CPython splits initialization into two phases. PyPreConfig settles
the questions that have to be answered before you can read
anything else: which locale, whether to coerce C locale to UTF-8,
whether to honor PYTHONUTF8. PyConfig then carries the rest:
argv, executable path, prefix, exec_prefix, sys.path, warning
options, optimization level, the -X panel.
// Three named initializers, one for each profile CPython exposes.
pre := initconfig.NewPreConfig()
initconfig.PyPreConfig_InitPythonConfig(pre)
// or _InitIsolatedConfig for -I behavior
// or _InitCompatConfig for the deprecated compat profile
cfg := initconfig.NewConfig()
initconfig.PyConfig_InitPythonConfig(cfg)
initconfig.PyConfig_Read(cfg) // merges env, CLI, defaults
The named initializers exist because CPython treats "isolated"
(-I), "python" (default), and "compat" as three distinct
starting points with different defaults for use_environment,
user_site_directory, parse_argv, and a handful of others.
Picking the wrong baseline silently flips behavior for code that
relies on env vars or site packages. We kept the three apart.
Builtins, sys, and _warnings
You can't have a working -c without print. You can't have a
working print without sys.stdout. You can't have a working
sys.stdout until you've stamped the PyConfig-driven attributes.
You can't have a useful failure mode without sys.stderr
plus _PyErr_Print. And you can't honor python -W until you
have a working _warnings module.
The dependency tree got resolved in one release. Builtins ship
five panels (iteration, reflection, attrs, aggregate, numeric,
constructors). sys ships create plus the PyConfig stamp.
_warnings ships the action / category / filter model with the
default filter seed CPython uses.
# All of these work after v0.7.
import sys
print(sys.argv, sys.path, sys.flags.optimize)
import warnings
warnings.simplefilter('error', DeprecationWarning)
What's new
The full feature breakdown, grouped by where it landed.
initconfig/
The config panel. Six files port the C side of CPython's two-phase init.
preconfig.gofromPython/preconfig.c.PyPreConfigwith the three named initializers (PyPreConfig_InitPythonConfig,_InitIsolatedConfig,_InitCompatConfig) and the env-var merge. The locale, coerce, and utf8 fields are reserved for v0.8 alongside the locale glue. We didn't try to ship the full locale pipeline in this drop because it has its own transitive dependencies (the codecs registry, the filesystem encoding panel) that we knew were coming in v0.8.getenv.gofromPython/getenv.c._Py_GetEnv,_Py_str_to_int,_Py_get_env_flag. Every config helper that consults the environment reads through these three. We kept them in their own file because CPython does, and because they're the exact functions you want to grep for when you're debugging "why did the runtime pick upPYTHONOPTIMIZEhere but not there".config.gofromPython/initconfig.c.PyConfig(the v0.7 subset of the C struct), plusPyConfig_InitPythonConfig,PyConfig_InitIsolatedConfig, andPyConfig_InitCompatConfig. The struct is intentionally not the full CPython surface yet. Fields that depend on subsystems we haven't ported (the faulthandler config, thetracemallocconfig) are absent so we don't carry dead state through the runtime.status.gofromPython/initconfig.c.PyStatuserror, exit, and ok constructors plus the env-merge into PyConfig. CPython propagates init errors through a return value rather than exceptions because exceptions aren't initialized yet at the point preconfig runs. We kept the same shape.cmdline.gofromPython/getopt.cplusPython/initconfig.c config_parse_cmdline. The-c/-m/-W/-Xpanel and the long-option subset gopy honors today (--help,--version,--check-hash-based-pycs). The_PyOS_GetOptshape comes fromPython/getopt.cbyte for byte; we ported the table format rather than translate it because the table is the readable description of the CLI.read.gofromPython/initconfig.c _PyConfig_Read. Walks the precedence chain (compile-time defaults, env, command line, explicit writes) and resolves to the final config. The order matters: an explicitcfg.Argv = ...write should beat aPYTHONARGVenv var should beat the compile-time default.
pathconfig/
sys.prefix and sys.path aren't constants. CPython walks the
executable location plus a handful of marker files
(pyvenv.cfg, pybuilddir.txt, lib/pythonX.Y/os.py) to compute
them at runtime. We ported the Darwin and Linux versions in this
release.
pathconfig_darwin.goandpathconfig_unix.gofromPython/getpath.cplus the platform getpath modules. Resolvesprefix,exec_prefix, andsys.pathfrom the executable's location with the documented fallbacks. Windows lands in v0.8. The two-file split mirrors CPython'sModules/getpath.pygenerated tree; the resolution logic is identical between the two platforms but the marker files and the search root differ.
We considered hard-coding the paths against a build-time
configuration and shipping a single pathconfig.go. We didn't,
because the moment you do that, gopy stops working when you
move the binary, and every "but it works on my machine" Python
bug story gets a gopy chapter.
lifecycle/
The actual init / finalize sequence.
init.gofromPython/pylifecycle.c pyinit_coreandpyinit_main. Phase 1 (pyinit_core) brings the core runtime up: interpreter state, GIL, builtins, sys. Phase 2 (pyinit_main) imports the user-facing modules and runs the_PyRunMainequivalent setup. The split exists in CPython because embedders (thePy_InitializeFromConfigflow) sometimes want to inject work between the two phases. We preserved the split even though gopy doesn't have embedders yet, because retrofitting it later would have meant rewriting every consumer.finalize.gofromPython/pylifecycle.c Py_FinalizeEx. The shutdown order CPython documents: registered atexit callbacks, module finalizers, sys teardown, GIL release. We kept the order exact because real Python programs depend on it.atexitcallbacks fire beforesys.stderrcloses; module finalizers fire before the GIL releases. Inverting either of those breaks observable behavior.main.gofromModules/main.c Py_Main. The unified entry: read argv into PyConfig, init, dispatch through pythonrun, finalize. This is the functioncmd/gopy/main.gocalls.
pythonrun/
The dispatch arms. -c, file, REPL.
runstring.gofromPython/pythonrun.c PyRun_SimpleStringFlagsandPyRun_StringFlags. The-csmoke fixtures from v0.6 re-root here; their assertions stayed unchanged. That was a forcing function: if a test that used to pass against the hand-rolled runner failed against the new one, the new one was wrong.runfile.gofromPython/pythonrun.c PyRun_AnyFileExFlags. File open, source decode, parse, compile, eval. The file-positional caller incmd/gopyroutes through this. Source decode goes through the codecs layer that lands in v0.8 today, with a utf-8 fast path baked into the v0.7 cut.repl.gofromPython/pythonrun.c PyRun_InteractiveLoopFlags. Basic READ / EVAL / PRINT loop. Readline editing and PS1 / PS2 customization land with the readline port in v0.9. We didn't block the REPL on readline because the dumb loop is useful for testing the dispatch path, and readline brings in terminal handling that's its own multi-day project.dispatch.gocovers the three-arm matrix (-c, file, REPL) solifecycle.Maincalls a single switch rather than open-coding the choice. Every arm reads from the same PyConfig fields; every arm writes to the samesysattributes; every arm returns through the same finalize path.
errors/
We needed at least the printing path to fail usefully.
print.gofromPython/pythonrun.c _PyErr_Print. SystemExit short-circuits to exit-code translation; everything else renders through the traceback panel and writes tosys.stderr. The traceback panel itself is still the v0.6 placeholder (file:line:col without PEP 657 caret pinning); we marked that as pending and moved on rather than block the lifecycle drop on a caret renderer.
sys/
The sys module gets a proper bring-up.
create.gofromPython/sysmodule.c _PySys_Create. Builds the module dict and stamps the static attributes that don't depend on PyConfig (maxsize,byteorder,int_info,float_info,hash_info,thread_info).config_attrs.goandflags.gostamp the PyConfig-driven slice:sys.argv,sys.path,sys.path0,sys.executable, thesys.flagsnamed tuple,sys.warnoptions,sys.dont_write_bytecode. These are re-stamped if PyConfig changes between phase 1 and phase 2, matching CPython's late-binding shape.runtime.gofromPython/sysmodule.c.sys.exit,getrecursionlimit/setrecursionlimit,getrefcount(always 1 in gopy because we GC through Go),intern,gettrace/settrace,getfilesystemencoding.gettraceandsettraceare the hooks profilers and debuggers use; we shipped the surface but the tracer call hook on every line lands alongside the trace plumbing in a later release.implementation.gopinssys.implementationto the gopy-specific fields:name = "gopy",cache_tag = "gopy-3140",version,hexversion. We chosegopy-3140because the__pycache__directory format encodes the implementation, and we didn't want our.pycfiles to collide with CPython's against the same source tree.
builtins/
The builtin namespace lands in six panels, each ported from a
clean slice of Python/bltinmodule.c.
iter.go. The iteration panel:iter,next,enumerate,zip,range,reversed,map,filter. Each of these cooperates with the iterator protocol on the object side; the builtins are thin wrappers that walk intotp_iterandtp_iternext.reflect.go. The reflection panel:type,isinstance,issubclass,id,hash,repr,len,callable.idis the address of the underlying Go pointer rather than a refcount bump (gopy has no refcounts); we documented this in the runtime notes so embedders don't expect refcount semantics.attrs.go. The attribute panel:getattr,hasattr,setattr,delattr.varsanddirare parked behind frame-locals access in v0.8. We picked this split becausevars()with no arguments has to walk the calling frame's locals dict, and the locals-dict story was its own dependency chain.aggregate.go. The aggregation panel:sum,min,max,any,all,sorted. Thekey=anddefault=kwargs match CPython behavior;sorted(reverse=True)inverts the comparison rather than reversing the result, which produces stable behavior when the key function ties.numeric.go. The numeric and format panel:abs,divmod,pow,chr,ord,bin,oct,hex,ascii,format. Routes through theformat/package for the[[fill]align][sign][...][type]mini-language. We ported the mini-language parser as its own subsystem rather than inline it in the builtins becausestr.format, f-strings, andformat()all consume it.ctor.go. The constructor wrappers:int,float,bool,list,tuple,dict. PEP 515 underscore stripping inint(...)parsing is honored at the panel level rather than the int parser, because the underscore rule is a surface-language concession that the parser shouldn't carry.set/frozensetwait on the set type in objects (which arrives in v0.8 alongside imports).
warnings/
Warnings are part of the boot. python -W error::DeprecationWarning
has to take effect before the first import can fire a warning, so
we landed the full filter machinery here rather than wait.
filters.goandwarnings.gofromPython/_warnings.c. The Action / Category / Filter model, thedefaultFiltersseed (matches_PyWarnings_InitState), and_Py_Warndispatch. Categories use a Go struct hierarchy with a Parent pointer until the warning class machinery lands with the type port. We didn't block this drop on a Python-level warning class hierarchy because the C struct hierarchy with a Parent pointer is exactly what_Py_Warnwalks anyway.registry.goports the per-module__warningregistry__dedup.ActionOnceshares a synthetic__once__bucket so dedup is global across modules; the other narrowing actions key by(action, category, message, module, lineno)shape. Getting the dedup key right matters because a chattyDeprecationWarningon a tight loop can produce thousands of lines of stderr noise if the key is wrong.api.goports the user-facingsimplefilter,filterwarnings,resetwarningsfromLib/_py_warnings.py. Action validation, the lineno guard, and the_add_filterdedup-and-prepend semantics with optionalappend=True. The Python-side surface matches the docs verbatim.
Why we built it this way
A few decisions in this release deserve a callout.
The two-phase init is worth the cost. Folding phase 1 and
phase 2 into a single function would have saved a few hundred
lines and looked simpler at the call site. We didn't, because
real CPython embedders (Blender, GIMP, gdb, lldb) all rely on the
gap between the phases to inject host-specific setup. We don't
have an embedder API yet, but we will, and retrofitting the split
later means rewriting every consumer of pyinit_core. Shipping
the split now costs nothing; not shipping it costs a lot later.
We didn't unify the dispatch arms into one Run function.
You could argue runstring, runfile, and repl are three
spellings of the same thing: decode source, compile, eval. The
trouble is that each arm has its own error reporting expectations.
-c wants the error message to mention <string> as the
filename. The file arm wants the actual filename. The REPL wants
the input number plus a recompile-with-an-extra-newline retry
when the source ends mid-statement. CPython keeps the three
separate; we kept the three separate.
getrefcount returns 1 unconditionally. CPython programs
sometimes call sys.getrefcount(x) for debugging or to dispatch
on whether something is uniquely held. In gopy we GC through Go,
which means there's no refcount to read. We returned a constant
1 (rather than NotImplemented or an error) because the most
common idiom is if sys.getrefcount(x) == 2: (the 2 accounts for
the caller's argument binding), and any nonzero answer makes that
branch fall through harmlessly. The alternative behaviors broke
real code.
Where it lives
The new packages, with their entry points.
initconfig/.preconfig.go,getenv.go,config.go,status.go,cmdline.go,read.go.pathconfig/.pathconfig_darwin.go,pathconfig_unix.go.lifecycle/.init.go,finalize.go,main.go. Entry point islifecycle.Main.pythonrun/.runstring.go,runfile.go,repl.go,dispatch.go.errors/.print.go(joins the prior errors panel).sys/.create.go,config_attrs.go,flags.go,runtime.go,implementation.go.builtins/.iter.go,reflect.go,attrs.go,aggregate.go,numeric.go,ctor.go.warnings/.filters.go,warnings.go,registry.go,api.go.
Compatibility
A few user-visible changes are worth flagging if you were tracking gopy through v0.6.
gopyinvocation behaves likepythonnow. Flags before the script name are interpreted; flags after the script name go intosys.argv. v0.6 used a Go flag package that didn't honor that boundary.sys.argv[0]is the script path for the file arm,-cfor the-carm, and `` (empty string) for the REPL. v0.6 used the gopy binary path in all three cases.- Exit codes follow CPython.
SystemExit(2)exits with code 2.SystemExit('msg')writes 'msg' to stderr and exits 1. An uncaught exception exits 1. v0.6 always exited 0 unless the Go runtime itself crashed. PYTHONOPTIMIZE,PYTHONDONTWRITEBYTECODE,PYTHONIOENCODINGand the rest of the env-var panel actually work. They didn't in v0.6 because the runtime never read them.
What's next
The v0.8 release is the import drop. Highlights:
importand the source-encoding handshake.gopy -m fooreturnsErrNotImplementeduntil the import system lands. v0.8 closes that gap with marshal, codecs (utf-8, ascii, latin-1), and a full sys.modules / frozen / builtin lookup chain.- Sub-interpreters and
Py_NewInterpreter. The thread-state and interpreter-state machinery is in place but only one interpreter runs. compile/exec/evalbuiltins beyond the smoke wrappers. The pieces exist (the parser, the bytecode assembler, the eval loop) but the builtin surface for user-level metaprogramming isn't wired yet.- Windows
pathconfig. Darwin and Linux are covered; Windows lands with v0.8 to keep parity with the import work. - PEP 657 caret pinning in tracebacks. v0.7 still emits file:line:col only.
- The remaining bytecode handlers parked behind future spec blocks. LOAD_SPECIAL / LOAD_SUPER_ATTR, IMPORT_NAME / FROM / STAR, SETUP_FINALLY / WITH_EXCEPT_START / CLEANUP_THROW / BEFORE_WITH, CHECK_EG_MATCH / LOAD_ASSERTION_ERROR, YIELD_VALUE / SEND / GET_AWAITABLE / RETURN_GENERATOR, MATCH. Most of these land in v0.8 and v0.9.
The lifecycle is a foundation release. None of the work in this drop is a feature you'd put on a marketing page. It's the plumbing that lets every later feature have somewhere to plug in. With v0.7 shipped, every subsystem we add from here on out walks through real config, real init, real dispatch, real finalize, real sys, real builtins, real warnings. That's a much better starting point than the v0.6 ad-hoc runner.
Acknowledgments
This release lines up against Python/pylifecycle.c,
Python/initconfig.c, Python/preconfig.c, Python/getpath.c,
Python/pythonrun.c, Python/sysmodule.c,
Python/bltinmodule.c, Python/_warnings.c, Python/getenv.c,
and Modules/main.c in the CPython 3.14 tree. Each ported file
in gopy carries a citation to the originating function so a
future reader can compare line by line.