Lifecycle
An interpreter has a beginning, a middle, and an end. The
beginning reads configuration from the environment and the command
line, builds the runtime, registers builtins, and loads the
bootstrap importlib. The middle runs user code: a script, a
module, the REPL, an -c string. The end runs atexit
handlers, flushes streams, releases interpreter state, and exits
the process.
The CPython reference is Python/initconfig.c (config),
Python/pylifecycle.c (init/finalize), and Modules/main.c
(main entry). The gopy ports are split across initconfig/,
lifecycle/, and pythonrun/.
Where the code lives
| Package | Files | Role | CPython counterpart |
|---|---|---|---|
initconfig/ | preconfig.go | The pre-configuration: locale, hash randomisation flag, ... | Python/initconfig.c PyPreConfig |
initconfig/ | config.go, config_cli.go, config_env.go, config_read.go, config_copy.go | Full config: parses argv, environment, defaults. | Python/initconfig.c PyConfig |
initconfig/ | env.go, getopt.go | Environment helpers and the option parser. | Python/initconfig.c argv parsing |
initconfig/ | status.go | The Status return type (success / exit / error). | Python/initconfig.c PyStatus |
lifecycle/ | init.go | Builds the runtime, the interpreter, and the thread. | Python/pylifecycle.c Py_InitializeFromConfig |
lifecycle/ | finalize.go | Shuts the interpreter down, runs atexit. | Python/pylifecycle.c Py_Finalize |
lifecycle/ | main.go | The Main entry: parse, init, run, finalize. | Modules/main.c Py_Main |
pythonrun/ | repl.go | The interactive REPL. | Python/pythonrun.c PyRun_InteractiveLoop |
pythonrun/ | runfile.go | Runs a Python source file. | Python/pythonrun.c PyRun_SimpleFileExFlags |
pythonrun/ | runstring.go | Runs a Python source string (-c). | Python/pythonrun.c PyRun_SimpleStringFlags |
Preconfig
The preconfig is the small subset of configuration that has to be known before anything else. The most important fields:
- Allocator. Which malloc backend to use. (Informational on gopy; the Go GC handles allocation.)
- Configure locale. Whether to call
setlocale(LC_ALL, "")at startup. - Coerce C locale. PEP 538: if the locale is
CorPOSIX, attempt to coerce to a UTF-8 locale. - UTF-8 mode. PEP 540: force UTF-8 for filesystem and stream encodings regardless of the locale.
- Hash seed. The
PYTHONHASHSEEDvalue.
// initconfig/preconfig.go PreConfig
type PreConfig struct {
ParseArgv int
UseEnvironment int
ConfigureLocale int
CoerceCLocale int
Utf8Mode int
DevMode int
Allocator int
HashSeed uint64
UseHashSeed int
}
The fields are tri-state: -1 means "compute from defaults",
0 means false, 1 means true. The CPython convention is
preserved so that command-line flag handling matches.
Config
The full config has dozens of fields. The interesting ones:
Argv: the list of command-line arguments, after option parsing.ExecutableName: the path to the interpreter binary.PythonPath,PrefixDir,ExecPrefixDir: the paths used to derivesys.path.ModuleSearchPaths:sys.pathitself.ProgramName: the program name (typicallypythonorpython3).InteractiveMode,InspectMode: REPL flags.Optimization: -O / -OO levels.WarningOptions: -W filters.XOptions: -X options.
The config is read in stages:
- Defaults. Hardcoded values for unset fields.
- Environment.
PYTHONPATH,PYTHONHOME,PYTHONIOENCODING,PYTHONWARNINGS,PYTHONHASHSEED, ... - CLI. The arguments to
gopy(or whatever the binary is named). The-and--conventions are honoured; anything after a non-option arg becomes the script's argv.
// initconfig/config.go Config
type Config struct {
Preconfig PreConfig
Argv []string
ProgramName string
ExecutableName string
PythonPath string
PrefixDir string
ExecPrefixDir string
ModuleSearchPaths []string
InteractiveMode bool
InspectMode bool
Optimization int
WarningOptions []string
XOptions []string
// ...
}
// initconfig/config_read.go Read
func Read(argv []string, env map[string]string) (*Config, Status)
Read returns either a complete Config and a success status, or
a partial Config and an error/exit status (when the command line
contained --help or --version, or an invalid argument).
Status
Status is a tagged union used as a return value across the init
path. Three kinds:
StatusOkis success.StatusExitcarries an exit code; the caller should exit with that code without further work.StatusErrorcarries an error message and a function name; the caller should report and exit nonzero.
// initconfig/status.go Status
type Status struct {
Kind StatusKind
Func string
ErrorMsg string
ExitCode int
}
The shape mirrors CPython's PyStatus. The caller pattern is
"check status; on non-OK, exit with the appropriate code and
message."
Initialization
lifecycle.Init builds the runtime from a Config.
// lifecycle/init.go Init
func Init(cfg *Config) (*state.Runtime, Status)
The sequence:
- Create the
Runtimesingleton. - Create the main
Interpreter. - Create the main
Thread. - Install the small-int cache, the singletons (
None,NotImplemented,Ellipsis), and the basic type hierarchy. - Initialise the
builtinsmodule. - Initialise
sys. Wiresys.argv,sys.path,sys.executable,sys.platform,sys.flags,sys.version,sys.implementation. - Register the builtin importer and the frozen importer with
sys.meta_path. - Run the importlib bootstrap (frozen importlib loads itself, then loads the path-based finder).
- Install warnings filters from
-WandPYTHONWARNINGS. - Initialise codecs and the codec registry. Register the built-in codecs.
- Initialise the
signalmodule if signals are wanted. - Initialise
gc,weakref, and the other "always on" modules. - Set up the
__main__module. - Return the populated runtime.
If anything raises during init, the status is StatusError with
the failing step named.
Main
lifecycle.Main is the entry point a cmd/gopy binary calls.
// lifecycle/main.go Main
func Main(argv []string) int
The sequence:
- Pre-configure: read env, read preconfig CLI flags.
- Read the full config.
- On
StatusExit, return the exit code. - On
StatusError, print the error message and return 1. - Initialise the runtime.
- Dispatch to the chosen run mode:
-c "code":pythonrun.RunString.-m module:pythonrun.RunModule.script.py:pythonrun.RunFile.- No script:
pythonrun.RunRepl.
- Capture the dispatcher's exit code.
- Finalize the runtime.
- Return the exit code.
Errors raised during the run propagate as SystemExit to set the
exit code. Uncaught exceptions in the run print a traceback and
return 1.
RunFile, RunString, RunModule, RunRepl
pythonrun/runfile.go, pythonrun/runstring.go, pythonrun/repl.go
each implement one entry. They share a pattern:
- Compile the source (string, file, or REPL line) to a code object via the Compile pipeline.
- Build a
__main__module if one does not exist yet. - Execute the code object with
__main__.__dict__as globals. - Return the result (typically
None) or propagate the exception.
The REPL has additional concerns: read a line, decide whether it is complete (a single-line expression, the start of a compound statement, an in-progress multi-line input), accumulate input until complete, then compile and run.
The REPL uses the standard library's code.InteractiveConsole
behaviour, dispatching readline through the platform's GNU
readline or a fallback line editor.
Finalization
lifecycle.Finalize runs at the end of Main (or in response to
an explicit Py_Finalize C API call).
// lifecycle/finalize.go Finalize
func Finalize(rt *state.Runtime) error
The sequence:
- Run
atexitcallbacks in registration order. - Close any open stdio streams (
sys.stdout,sys.stderr,sys.stdin). - Run finalisers for any pending
__del__callbacks queued on the breaker. - Drain the async-generator finaliser queue.
- Run the
__atexit__of each loaded module that has one. - Clear
sys.modules. - Tear down the interpreter state.
- Tear down the runtime state.
Finalisation is cooperative: a misbehaving atexit callback (one
that hangs or raises) can prevent the rest of the sequence from
running. The implementation logs and continues on raise; on
hangs, there is no special timeout.
Finalisation does not try to free every object; the Go GC will handle that on its own when the process exits. The job is to flush, persist, and report.
atexit
atexit.register(func) adds a callback to a per-runtime queue.
The queue runs in reverse registration order during
Finalize. The implementation lives in module/atexit/ and is
a thin wrapper around a list maintained on the runtime.
Subinterpreters
PEP 684 introduces a per-interpreter GIL and clean isolation between sub-interpreters. CPython 3.14 lays the groundwork.
The gopy port:
state.Runtimemay host multiplestate.Interpreterinstances.- Each interpreter has its own
__main__, its ownsys.modules, its own builtins. lifecycle.Initcreates the main interpreter; further interpreters are created on demand.- The GIL (when strict GIL mode is enabled) is per-interpreter.
The subinterpreter surface is wired but the user-facing API
(interpreters.create(), interpreters.run_string()) lives in the
interpreters module, which is partial in gopy at the time of
writing.
Status
The whole lifecycle is end-to-end: read config, init runtime, run
script or REPL or -c, finalize, exit with the right code. CLI
parsing matches CPython for all standard options. Environment
variables are read. sys.argv, sys.path, sys.flags,
sys.implementation, sys.version are populated. The REPL is
functional. atexit runs.
Some edge cases continue to land: the -X frozen_modules=off flag,
some interactions between PYTHONSTARTUP and the REPL, and parts
of the subinterpreters surface.
Reference
- Port source:
initconfig/,lifecycle/,pythonrun/. - CPython source:
Python/initconfig.c,Python/pylifecycle.c,Modules/main.c,Python/pythonrun.c. - PEP 540, Add a new UTF-8 Mode.
- PEP 587, Python Initialization Configuration.
- PEP 538, Coercing the legacy C locale to a UTF-8 based locale.
- PEP 684, A Per-Interpreter GIL.