PEP 432: Simplifying the CPython startup sequence

After helping Brett with the migration to importlib in 3.3, and looking at some of the ideas kicking around for additional CPython features that would affect the startup sequence, I've come to the conclusion that what we have now simply isn't sustainable long term. It's already the case that if you use certain options (specifically -W or -X), the interpreter will start accessing the C API before it has called Py_Initialize(). It's not cool when other people do that (we'd never accept code that behaved that way as a valid reproducer for a bug report), and it's *definitely* not cool that we're doing it (even though we seem to be getting away with it for the moment, and have been for a long time). The attached PEP is a first attempt at a plan for doing something about it. (My notes at http://wiki.python.org/moin/CPythonInterpreterInitialization provide additional context - let me know if you think there's more material on that page that should be in the PEP itself) The PEP is also available online at http://www.python.org/dev/peps/pep-0432/ Cheers, Nick. PEP: 432 Title: Simplifying the CPython startup sequence Version: $Revision$ Last-Modified: $Date$ Author: Nick Coghlan <ncoghlan@gmail.com> Status: Draft Type: Standards Track Content-Type: text/x-rst Created: 28-Dec-2012 Python-Version: 3.4 Post-History: 28-Dec-2012 Abstract ======== This PEP proposes a mechanism for simplifying the startup sequence for CPython, making it easier to modify the initialisation behaviour of the reference interpreter executable, as well as making it easier to control CPython's startup behaviour when creating an alternate executable or embedding it as a Python execution engine inside a larger application. Proposal Summary ================ This PEP proposes that CPython move to an explicit 2-phase initialisation process, where a preliminary interpreter is put in place with limited OS interaction capabilities early in the startup sequence. This essential core remains in place while all of the configuration settings are determined, until a final configuration call takes those settings and finishes bootstrapping the interpreter immediately before executing the main module. As a concrete use case to help guide any design changes, and to solve a known problem where the appropriate defaults for system utilities differ from those for running user scripts, this PEP also proposes the creation and distribution of a separate system Python (``spython``) executable which, by default, ignores user site directories and environment variables, and does not implicitly set ``sys.path[0]`` based on the current directory or the script being executed. Background ========== Over time, CPython's initialisation sequence has become progressively more complicated, offering more options, as well as performing more complex tasks (such as configuring the Unicode settings for OS interfaces in Python 3 as well as bootstrapping a pure Python implementation of the import system). Much of this complexity is accessible only through the ``Py_Main`` and ``Py_Initialize`` APIs, offering embedding applications little opportunity for customisation. This creeping complexity also makes life difficult for maintainers, as much of the configuration needs to take place prior to the ``Py_Initialize`` call, meaning much of the Python C API cannot be used safely. A number of proposals are on the table for even *more* sophisticated startup behaviour, such as better control over ``sys.path`` initialisation (easily adding additional directories on the command line in a cross-platform fashion, as well as controlling the configuration of ``sys.path[0]``), easier configuration of utilities like coverage tracing when launching Python subprocesses, and easier control of the encoding used for the standard IO streams when embedding CPython in a larger application. Rather than attempting to bolt such behaviour onto an already complicated system, this PEP proposes to instead simplify the status quo *first*, with the aim of making these further feature requests easier to implement. Key Concerns ============ There are a couple of key concerns that any change to the startup sequence needs to take into account. Maintainability --------------- The current CPython startup sequence is difficult to understand, and even more difficult to modify. It is not clear what state the interpreter is in while much of the initialisation code executes, leading to behaviour such as lists, dictionaries and Unicode values being created prior to the call to ``Py_Initialize`` when the ``-X`` or ``-W`` options are used [1_]. By moving to a 2-phase startup sequence, developers should only need to understand which features are not available in the core bootstrapping state, as the vast majority of the configuration process will now take place in that state. By basing the new design on a combination of C structures and Python dictionaries, it should also be easier to modify the system in the future to add new configuration options. Performance ----------- CPython is used heavily to run short scripts where the runtime is dominated by the interpreter initialisation time. Any changes to the startup sequence should minimise their impact on the startup overhead. (Given that the overhead is dominated by IO operations, this is not currently expected to cause any significant problems). The Status Quo ============== Much of the configuration of CPython is currently handled through C level global variables:: Py_IgnoreEnvironmentFlag Py_HashRandomizationFlag _Py_HashSecretInitialized _Py_HashSecret Py_BytesWarningFlag Py_DebugFlag Py_InspectFlag Py_InteractiveFlag Py_OptimizeFlag Py_DontWriteBytecodeFlag Py_NoUserSiteDirectory Py_NoSiteFlag Py_UnbufferedStdioFlag Py_VerboseFlag For the above variables, the conversion of command line options and environment variables to C global variables is handled by ``Py_Main``, so each embedding application must set those appropriately in order to change them from their defaults. Some configuration can only be provided as OS level environment variables:: PYTHONHASHSEED PYTHONSTARTUP PYTHONPATH PYTHONHOME PYTHONCASEOK PYTHONIOENCODING Additional configuration is handled via separate API calls:: Py_SetProgramName() (call before Py_Initialize()) Py_SetPath() (optional, call before Py_Initialize()) Py_SetPythonHome() (optional, call before Py_Initialize()???) Py_SetArgv[Ex]() (call after Py_Initialize()) The ``Py_InitializeEx()`` API also accepts a boolean flag to indicate whether or not CPython's signal handlers should be installed. Finally, some interactive behaviour (such as printing the introductory banner) is triggered only when standard input is reported as a terminal connection by the operating system. Also see more detailed notes at [1_] Proposal ======== (Note: details here are still very much in flux, but preliminary feedback is appreciated anyway) Core Interpreter Initialisation ------------------------------- The only configuration that currently absolutely needs to be in place before even the interpreter core can be initialised is the seed for the randomised hash algorithm. However, there are a couple of settings needed there: whether or not hash randomisation is enabled at all, and if it's enabled, whether or not to use a specific seed value. The proposed API for this step in the startup sequence is:: void Py_BeginInitialization(Py_CoreConfig *config); Like Py_Initialize, this part of the new API treats initialisation failures as fatal errors. While that's still not particularly embedding friendly, the operations in this step *really* shouldn't be failing, and changing them to return error codes instead of aborting would be an even larger task than the one already being proposed. The new Py_CoreConfig struct holds the settings required for preliminary configuration:: typedef struct { int use_hash_seed; size_t hash_seed; } Py_CoreConfig; To "disable" hash randomisation, set "use_hash_seed" and pass a hash seed of zero. (This seems reasonable to me, but there may be security implications I'm overlooking. If so, adding a separate flag or switching to a 3-valued "no randomisation", "fixed hash seed" and "randomised hash" option is easy) The core configuration settings pointer may be NULL, in which case the default behaviour of randomised hashes with a random seed will be used. A new query API will allow code to determine if the interpreter is in the bootstrapping state between core initialisation and the completion of the initialisation process:: int Py_IsInitializing(); While in the initialising state, the interpreter should be fully functional except that: * compilation is not allowed (as the parser is not yet configured properly) * The following attributes in the ``sys`` module are all either missing or ``None``: * ``sys.path`` * ``sys.argv`` * ``sys.executable`` * ``sys.base_exec_prefix`` * ``sys.base_prefix`` * ``sys.exec_prefix`` * ``sys.prefix`` * ``sys.warnoptions`` * ``sys.flags`` * ``sys.dont_write_bytecode`` * ``sys.stdin`` * ``sys.stdout`` * The filesystem encoding is not yet defined * The IO encoding is not yet defined * CPython signal handlers are not yet installed * only builtin and frozen modules may be imported (due to above limitations) * ``sys.stderr`` is set to a temporary IO object using unbuffered binary mode * The ``warnings`` module is not yet initialised * The ``__main__`` module does not yet exist <TBD: identify any other notable missing functionality> The main things made available by this step will be the core Python datatypes, in particular dictionaries, lists and strings. This allows them to be used safely for all of the remaining configuration steps (unlike the status quo). In addition, the current thread will possess a valid Python thread state, allow any further configuration data to be stored on the interpreter object rather than in C process globals. Any call to Py_BeginInitialization() must have a matching call to Py_Finalize(). It is acceptable to skip calling Py_EndInitialization() in between (e.g. if attempting to read the configuration settings fails) Determining the remaining configuration settings ------------------------------------------------ The next step in the initialisation sequence is to determine the full settings needed to complete the process. No changes are made to the interpreter state at this point. The core API for this step is:: int Py_ReadConfiguration(PyObject *config); The config argument should be a pointer to a Python dictionary. For any supported configuration setting already in the dictionary, CPython will sanity check the supplied value, but otherwise accept it as correct. Unlike Py_Initialize and Py_BeginInitialization, this call will raise an exception and report an error return rather than exhibiting fatal errors if a problem is found with the config data. Any supported configuration setting which is not already set will be populated appropriately. The default configuration can be overridden entirely by setting the value *before* calling Py_ReadConfiguration. The provided value will then also be used in calculating any settings derived from that value. Alternatively, settings may be overridden *after* the Py_ReadConfiguration call (this can be useful if an embedding application wants to adjust a setting rather than replace it completely, such as removing ``sys.path[0]``). Supported configuration settings -------------------------------- At least the following configuration settings will be supported:: raw_argv (list of str, default = retrieved from OS APIs) argv (list of str, default = derived from raw_argv) warnoptions (list of str, default = derived from raw_argv and environment) xoptions (list of str, default = derived from raw_argv and environment) program_name (str, default = retrieved from OS APIs) executable (str, default = derived from program_name) home (str, default = complicated!) prefix (str, default = complicated!) exec_prefix (str, default = complicated!) base_prefix (str, default = complicated!) base_exec_prefix (str, default = complicated!) path (list of str, default = complicated!) io_encoding (str, default = derived from environment or OS APIs) fs_encoding (str, default = derived from OS APIs) skip_signal_handlers (boolean, default = derived from environment or False) ignore_environment (boolean, default = derived from environment or False) dont_write_bytecode (boolean, default = derived from environment or False) no_site (boolean, default = derived from environment or False) no_user_site (boolean, default = derived from environment or False) <TBD: at least more from sys.flags need to go here> Completing the interpreter initialisation ----------------------------------------- The final step in the process is to actually put the configuration settings into effect and finish bootstrapping the interpreter up to full operation:: int Py_EndInitialization(PyObject *config); Like Py_ReadConfiguration, this call will raise an exception and report an error return rather than exhibiting fatal errors if a problem is found with the config data. After a successful call, Py_IsInitializing() will be false, while Py_IsInitialized() will become true. The caveats described above for the interpreter during the initialisation phase will no longer hold. Stable ABI ---------- All of the APIs proposed in this PEP are excluded from the stable ABI, as embedding a Python interpreter involves a much higher degree of coupling than merely writing an extension. Backwards Compatibility ----------------------- Backwards compatibility will be preserved primarily by ensuring that Py_ReadConfiguration() interrogates all the previously defined configuration settings stored in global variables and environment variables. One acknowledged incompatiblity is that some environment variables which are currently read lazily may instead be read once during interpreter initialisation. As the PEP matures, these will be discussed in more detail on a case by case basis. The Py_Initialize() style of initialisation will continue to be supported. It will use the new API internally, but will continue to exhibit the same behaviour as it does today, ensuring that sys.argv is not set until a subsequent PySys_SetArgv call. A System Python Executable ========================== When executing system utilities with administrative access to a system, many of the default behaviours of CPython are undesirable, as they may allow untrusted code to execute with elevated privileges. The most problematic aspects are the fact that user site directories are enabled, environment variables are trusted and that the directory containing the executed file is placed at the beginning of the import path. Currently, providing a separate executable with different default behaviour would be prohibitively hard to maintain. One of the goals of this PEP is to make it possible to replace much of the hard to maintain bootstrapping code with more normal CPython code, as well as making it easier for a separate application to make use of key components of ``Py_Main``. Including this change in the PEP is designed to help avoid acceptance of a design that sounds good in theory but proves to be problematic in practice. One final aspect not addressed by the general embedding changes above is the current inaccessibility of the core logic for deciding between the different execution modes supported by CPython: * script execution * directory/zipfile execution * command execution ("-c" switch) * module or package execution ("-m" switch) * execution from stdin (non-interactive) * interactive stdin <TBD: concrete proposal for better exposing the __main__ execution step> Implementation ============== None as yet. Once I have a reasonably solid plan of attack, I intend to work on a reference implementation as a feature branch in my BitBucket sandbox [2_] References ========== .. [1] CPython interpreter initialization notes (http://wiki.python.org/moin/CPythonInterpreterInitialization) .. [2] BitBucket Sandbox (https://bitbucket.org/ncoghlan/cpython_sandbox) Copyright =========== This document has been placed in the public domain. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

Nick Coghlan <ncoghlan@...> writes:
PEP: 432 Title: Simplifying the CPython startup sequence
b In general, it looks quite nice. While you're creating new initialization APIs, it would be nice if they could support (or at least be future compatible with) a "interpreter context". If we ever get around to killing at the c-level global state in the interpreter, such a struct would hold the state. For example, it would be nice if instead of those Py_* option variables, members of a structure on PyInterpreter were used.

On Thu, Dec 27, 2012 at 6:39 PM, Yuval Greenfield <ubershmekel@gmail.com>wrote:
Here's the test I used https://gist.github.com/4389657

Am 27.12.2012 16:10, schrieb Nick Coghlan:
[...]
Hello Nick, we could use the opportunity and move more settings to Py_CoreConfig. At the moment several settings are stored in static variables: Python/pythonrun.c static wchar_t *progname static wchar_t *default_home static wchar_t env_home[PATH_MAX+1] Modules/getpath.c static wchar_t prefix[MAXPATHLEN+1] static wchar_t exec_prefix[MAXPATHLEN+1] static wchar_t progpath[MAXPATHLEN+1] static wchar_t *module_search_path static int module_search_path_malloced static wchar_t *lib_python = L"lib/python" VERSION; PC/getpath.c static wchar_t dllpath[MAXPATHLEN+1] These settings could be added to the Py_CoreConfig struct and unify the configuration schema for embedders. Functions like Py_SetProgramName() would set the members of a global Py_CoreConfig struct. Christian

I was planning to move most of those settings into the config dict. Both the core config struct and the config dict would then be stored in new slots in the interpreter struct. My preference is to push more settings into the config dictionary, since those can use the C API and frozen bytecode to do their calculations. -- Sent from my phone, thus the relative brevity :) On Dec 28, 2012 6:14 AM, "Christian Heimes" <christian@python.org> wrote:

On Fri, Dec 28, 2012 at 10:15 PM, Antoine Pitrou <solipsis@pitrou.net> wrote:
Yeah, you may be right. I'll add more on the internal storage of the configuration data and include that as an open question. I want the dict in the config API so we can distinguish between "please fill in the default value" and "don't fill this in at all", but there's nothing stopping us mapping that to a C struct internally. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On 27/12/12 15:10, Nick Coghlan wrote: Hi,
This PEP proposes that CPython move to an explicit 2-phase initialisation
Why only two phases? I was thinking about the initialisation sequence a while ago and thought that a three or four phase sequence might be appropriate. What matters is that the state in between phases is well defined and simple to understand. You might want to take a look at rubinius which implements most of its core components in Ruby, so needs a clearly defined startup sequence. http://rubini.us/doc/en/bootstrapping/ (Rubinius using 7 phases, but that would be overkill for CPython) Cheers, Mark.

On Sat, Dec 29, 2012 at 1:45 AM, Mark Shannon <mark@hotpy.org> wrote:
The "2-phase" term came from the fact that I'm trying to break Py_Initialize() into two separate phase changes that roughly correspond with the locations of the current calls to _Py_Random_Init() and Py_Initialize() in Py_Main(). There's also at least a 3rd phase (even in the current design), because there's a "get ready to start executing __main__" phase after Py_Initialise finishes that changes various attributes on __main__ and may also modify sys.path[0] and sys.argv[0]. This is the first phase where user code may execute (Package __init__ modules may run in this phase when the "-m" switch is used to execute a package or submodule) So yeah, I need to lose the "2-phase" term, because it's simply wrong. A more realistic description of the phases proposed in the PEP would be: PreInit Phase - No CPython infrastructure configured, only pure C code allowed Initializing Phase - After Py_BeginInitialization() is called. Limitations as described in the PEP. PreMain Phase - After Py_EndInitialization() is called. __main__ attributes, sys.path[0], sys.argv[0] may still be inaccurate Main Execution - Execution of the main module bytecode has started. Interpreter has been fully configured.
Thanks for the reference. However, it looks like most of those seven stages will still be handled in our preinit phase. It sounds like we do a *lot* more in C than Rubinius does, so most of that code really doesn't need much in the way of infrastructure. It's definitely not *easy* to understand, but we also don't mess with it very often, and it's the kind of code where having access to more of the Python C API wouldn't really help all that much. The key piece I think we're currently missing is the clearly phase change between "PreInit" (can't safely use the Python C API) and "Initializing" (can use most of the C API, with some restrictions). Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

Nick Coghlan <ncoghlan@...> writes:
PEP: 432 Title: Simplifying the CPython startup sequence
b In general, it looks quite nice. While you're creating new initialization APIs, it would be nice if they could support (or at least be future compatible with) a "interpreter context". If we ever get around to killing at the c-level global state in the interpreter, such a struct would hold the state. For example, it would be nice if instead of those Py_* option variables, members of a structure on PyInterpreter were used.

On Thu, Dec 27, 2012 at 6:39 PM, Yuval Greenfield <ubershmekel@gmail.com>wrote:
Here's the test I used https://gist.github.com/4389657

Am 27.12.2012 16:10, schrieb Nick Coghlan:
[...]
Hello Nick, we could use the opportunity and move more settings to Py_CoreConfig. At the moment several settings are stored in static variables: Python/pythonrun.c static wchar_t *progname static wchar_t *default_home static wchar_t env_home[PATH_MAX+1] Modules/getpath.c static wchar_t prefix[MAXPATHLEN+1] static wchar_t exec_prefix[MAXPATHLEN+1] static wchar_t progpath[MAXPATHLEN+1] static wchar_t *module_search_path static int module_search_path_malloced static wchar_t *lib_python = L"lib/python" VERSION; PC/getpath.c static wchar_t dllpath[MAXPATHLEN+1] These settings could be added to the Py_CoreConfig struct and unify the configuration schema for embedders. Functions like Py_SetProgramName() would set the members of a global Py_CoreConfig struct. Christian

I was planning to move most of those settings into the config dict. Both the core config struct and the config dict would then be stored in new slots in the interpreter struct. My preference is to push more settings into the config dictionary, since those can use the C API and frozen bytecode to do their calculations. -- Sent from my phone, thus the relative brevity :) On Dec 28, 2012 6:14 AM, "Christian Heimes" <christian@python.org> wrote:

On Fri, Dec 28, 2012 at 10:15 PM, Antoine Pitrou <solipsis@pitrou.net> wrote:
Yeah, you may be right. I'll add more on the internal storage of the configuration data and include that as an open question. I want the dict in the config API so we can distinguish between "please fill in the default value" and "don't fill this in at all", but there's nothing stopping us mapping that to a C struct internally. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia

On 27/12/12 15:10, Nick Coghlan wrote: Hi,
This PEP proposes that CPython move to an explicit 2-phase initialisation
Why only two phases? I was thinking about the initialisation sequence a while ago and thought that a three or four phase sequence might be appropriate. What matters is that the state in between phases is well defined and simple to understand. You might want to take a look at rubinius which implements most of its core components in Ruby, so needs a clearly defined startup sequence. http://rubini.us/doc/en/bootstrapping/ (Rubinius using 7 phases, but that would be overkill for CPython) Cheers, Mark.

On Sat, Dec 29, 2012 at 1:45 AM, Mark Shannon <mark@hotpy.org> wrote:
The "2-phase" term came from the fact that I'm trying to break Py_Initialize() into two separate phase changes that roughly correspond with the locations of the current calls to _Py_Random_Init() and Py_Initialize() in Py_Main(). There's also at least a 3rd phase (even in the current design), because there's a "get ready to start executing __main__" phase after Py_Initialise finishes that changes various attributes on __main__ and may also modify sys.path[0] and sys.argv[0]. This is the first phase where user code may execute (Package __init__ modules may run in this phase when the "-m" switch is used to execute a package or submodule) So yeah, I need to lose the "2-phase" term, because it's simply wrong. A more realistic description of the phases proposed in the PEP would be: PreInit Phase - No CPython infrastructure configured, only pure C code allowed Initializing Phase - After Py_BeginInitialization() is called. Limitations as described in the PEP. PreMain Phase - After Py_EndInitialization() is called. __main__ attributes, sys.path[0], sys.argv[0] may still be inaccurate Main Execution - Execution of the main module bytecode has started. Interpreter has been fully configured.
Thanks for the reference. However, it looks like most of those seven stages will still be handled in our preinit phase. It sounds like we do a *lot* more in C than Rubinius does, so most of that code really doesn't need much in the way of infrastructure. It's definitely not *easy* to understand, but we also don't mess with it very often, and it's the kind of code where having access to more of the Python C API wouldn't really help all that much. The key piece I think we're currently missing is the clearly phase change between "PreInit" (can't safely use the Python C API) and "Initializing" (can use most of the C API, with some restrictions). Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia
participants (7)
-
Antoine Pitrou
-
Benjamin Peterson
-
Christian Heimes
-
Eric Snow
-
Mark Shannon
-
Nick Coghlan
-
Yuval Greenfield