--- title: Modules and Host Callbacks icon: dot order: 8 --- # Modules and host callbacks A module holds Python globals. Native modules created with `py_newmodule()` can be imported by scripts in the current VM, just like source modules. ## Create or look up a module This fragment runs after initialization: ```c py_GlobalRef settings = py_newmodule("settings"); py_newint(py_emplacedict(settings, py_name("screen_width")), 1280); bool ok = py_exec("import settings\nprint(settings.screen_width)", "app.py", EXEC_MODE, NULL); if(!ok) py_printexc(); ``` Create a module only once per VM: recreating an existing name is an error at the native API level. `py_getmodule("settings")` looks up an already registered module and returns `NULL` if it is absent; it does not perform an import. `py_import(name)` attempts to load a module and returns: | Return | Meaning | | --- | --- | | `1` | Success; the module is in `py_retval()`. | | `0` | The module was not found; the caller decides how to report this. | | `-1` | Loading failed with a Python exception. | Do not treat `py_import()` as a boolean: `-1` is true in a C condition. ## Where imports come from The importer first checks registered modules, then the host's optional `lazyimport` callback, then bundled Python modules. For a file-based module such as `game.level`, it asks `importfile` for these paths in order: 1. `game/level.py` 2. `game/level.pyc` 3. `game/level/__init__.py` 4. `game/level/__init__.pyc` Path separators follow the platform. Parent packages are loaded first. Supported desktop builds may then try a native dynamic module if enabled. The default file callback opens paths relative to the **process working directory**. Running `main scripts/app.py` does not automatically add `scripts/` to an import path. Start the host in the intended script directory or provide your own loader. pocketpy does not provide CPython's `sys.path` search mechanism. When both source and bytecode exist, source is preferred. See [bytecode deployment](../features/deploy.md) for the distribution format. ## Load a module from memory Replace `py_callbacks()->importfile` to load scripts from a game asset bundle, a virtual filesystem, or embedded strings. This complete example provides one module without reading a file: ```c #include "pocketpy.h" #include static char* import_source(const char* path, int* data_size) { if(strcmp(path, "settings.py") != 0) return NULL; const char* source = "screen_width = 1280\n"; size_t size = strlen(source); char* buffer = py_malloc(size + 1); memcpy(buffer, source, size + 1); if(data_size != NULL) *data_size = (int)size; return buffer; } int main(void) { py_initialize(); py_callbacks()->importfile = import_source; bool ok = py_exec("import settings\nassert settings.screen_width == 1280", "app.py", EXEC_MODE, NULL); if(!ok) py_printexc(); py_finalize(); return ok ? 0 : 1; } ``` The loader contract matters: - Return `NULL` when the requested path is unavailable. - Allocate a fresh buffer with `py_malloc()`; the interpreter owns and frees it. Do not return a string literal, stack buffer, or shared asset pointer. - NUL-terminate source text. For bytecode, report the exact byte length. - Check whether `data_size` is `NULL`: source reloads can omit this output. - Handle every path variant that your application supports. Callbacks belong to the current VM. Configure each VM that needs a custom loader, and configure it again after a reset. ## Redirect script output `py_callbacks()->print` receives NUL-terminated UTF-8 text. A single Python `print()` can call it more than once; do not assume each callback contains a whole line. Set `flush` if your output backend buffers data. `py_printexc()` also uses this output callback. The `getchr` callback supplies input for `input()`. More specialized hooks, including `gc_mark` and `displayhook`, are declared in [the public header](https://github.com/pocketpy/pocketpy/blob/main/include/pocketpy/pocketpy.h).