Python Extension
The Python extension provides two things:
- Runtime support for
fn python ...blocks. - Child-LSP configuration so Shape LSP can delegate hover/completion/diagnostics inside Python bodies.
Install
Section titled “Install”shape ext install pythonThis downloads, compiles, and installs the extension to ~/.shape/extensions/.
Both the CLI and LSP will automatically detect it — no further configuration needed.
Build from source (alternative)
Section titled “Build from source (alternative)”If you need a development build from the workspace:
PYO3_PYTHON=$(which python3) cargo build -p shape-ext-python --releaseThis produces target/release/libshape_ext_python.so (Linux).
Load the Extension at Runtime
Section titled “Load the Extension at Runtime”If you installed via shape ext install, the extension is loaded automatically.
For manual loading, you can use any of the following methods.
1. CLI shorthand
Section titled “1. CLI shorthand”shape run script.shape --extension python2. Script frontmatter
Section titled “2. Script frontmatter”---[[extensions]]name = "python"---3. Project shape.toml
Section titled “3. Project shape.toml”[[extensions]]name = "python"autoload = trueExtensions installed via shape ext install are resolved by name from
~/.shape/extensions/. You can also specify an explicit path for
development builds.
Writing Python Foreign Functions
Section titled “Writing Python Foreign Functions”Declare a Python function with fn python:
fn python add(a: int, b: int) -> Result<int> { return a + b}
let result = add(1, 2)// result: Ok(3)Arguments are marshalled from Shape to Python, the Python body executes, and the return value is marshalled back. Both directions are type-checked against the declared signature.
Return Type Annotation
Section titled “Return Type Annotation”Every fn python block must have an explicit return type, and that return type must be Result<T> — Python is a dynamic-error-model runtime, so the compiler refuses bare return types to guarantee every call is forced to handle the failure case.
// Dynamic errors — Python exceptions become Err(...)fn python safe_divide(a: number, b: number) -> Result<number> { return a / b}(Earlier drafts of this chapter showed a “static error model” form without Result<T>. The compiler now rejects that form with Foreign function ...: return type must be Result<T> ....)
Type Marshalling Rules
Section titled “Type Marshalling Rules”Values crossing the Python/Shape boundary are converted according to these rules:
| Shape type | Python type | Direction | Notes |
|---|---|---|---|
int | int | both | Python float with integer value (e.g. 3.0) coerced to int |
number / float | float | both | Python int promoted to float |
string | str | both | |
bool | bool | both | Python bool is not coerced to int |
none | None | both | |
Vec<T> | list[T] | both | Each element validated recursively against T |
{f: T, ...} | dict | both | Declared fields validated; produces a TypedObject on return |
Coercion rules:
float → int: allowed only when the float has no fractional part (e.g.3.0→3, but3.5→ error)int → float/number: always allowedbool → int: rejected (Pythonboolis a subclass ofint, but Shape treats them as distinct types)- Type mismatch →
Err(MARSHAL_ERROR)in dynamic error model, orVMErrorin static model
Object Return Types
Section titled “Object Return Types”When a foreign function declares an inline object return type, Shape creates a schema at compile time and validates the Python dict on return:
fn python fetch_user(id: int) -> Result<{name: string, age: int}> { return {"name": "Alice", "age": 30}}
let user = fetch_user(1).unwrap()print(user) // {name: "Alice", age: 30}print(user.name) // "Alice"print(user.age) // 30The returned dict must contain all declared fields with the correct types. Missing fields or type mismatches produce a MARSHAL_ERROR.
Field Aliasing with @alias
Section titled “Field Aliasing with @alias”When the Python dict uses keys that aren’t valid Shape identifiers (spaces, hyphens, etc.), use @alias to map between the wire name and the Shape field name:
Named types
Section titled “Named types”type JokeResponse { setup: string, @alias("punch line") punch_line: string,}
async fn python fetch_joke(url: string) -> Result<JokeResponse> { import aiohttp async with aiohttp.ClientSession() as session: async with session.get(url) as response: data = await response.json() return data}
let joke = await fetch_joke("https://example.com/joke")?print(joke.punch_line) // works — mapped from wire key "punch line"Inline object types
Section titled “Inline object types”async fn python fetch(url: string) -> Result<{setup: string, @alias("punch line") punch_line: string}> { import aiohttp async with aiohttp.ClientSession() as session: async with session.get(url) as response: data = await response.json() return data}@alias also works for Arrow/DataTable column binding — use it anywhere the external name differs from the Shape field name. It replaces the older @column annotation.
Error Model
Section titled “Error Model”With Result<T> return types, two error codes distinguish failure sources:
| Error code | Meaning |
|---|---|
RUNTIME_ERROR | Python raised an exception |
MARSHAL_ERROR | Return value didn’t match declared type T |
fn python bad_return(x: int) -> Result<int> { return "not an int"}
let r = bad_return(1)// r: Err({message: "expected int, got str", code: "MARSHAL_ERROR", ...})Async Functions
Section titled “Async Functions”Add the async keyword for I/O-bound Python:
async fn python fetch_json(url: string) -> Result<{setup: string, punchline: string}> { import aiohttp async with aiohttp.ClientSession() as session: async with session.get(url) as response: data = await response.json() return data}
let values = await fetch_json("https://official-joke-api.appspot.com/random_joke")?
print(values)// {setup: "Why did the scarecrow win an award?", punchline: "He was outstanding in his field."}Async functions use Python’s asyncio event loop internally. The Shape VM blocks until the coroutine completes.
Enable It in LSP
Section titled “Enable It in LSP”Shape LSP can discover extension declarations from script frontmatter and shape.toml.
If you want Python always available in the editor (even before the current file declares it), configure always-load extensions in LSP initialization/settings.
Supported LSP Config Keys
Section titled “Supported LSP Config Keys”The same keys are accepted in:
- initialize
initializationOptions workspace/didChangeConfigurationsettings
Accepted key names:
alwaysLoadExtensions(camelCase)always_load_extensions(snake_case)
Location:
- top-level
- nested under
shape
Value Format
Section titled “Value Format”alwaysLoadExtensions is an array of entries. Each entry can be:
- string path:
{ "shape": { "alwaysLoadExtensions": [ "/absolute/path/to/libshape_ext_python.so" ] }}- object:
{ "shape": { "alwaysLoadExtensions": [ { "name": "python", "path": "/absolute/path/to/libshape_ext_python.so", "config": {} } ] }}Notes:
pathis required for object entries.- relative paths resolve against the workspace root.
configis optional; defaults to{}.- duplicate entries are de-duplicated.
Neovim Example
Section titled “Neovim Example”After installing the extension (shape ext install python), the LSP auto-detects
it from ~/.shape/extensions/. No manual path configuration needed:
return { { "neovim/nvim-lspconfig", opts = function() vim.filetype.add({ extension = { shape = "shape" } })
vim.lsp.config.shape = { cmd = { "shape-lsp" }, filetypes = { "shape" }, root_markers = { ".git", "shape.toml" }, }
vim.lsp.enable("shape") end, },}Troubleshooting
Section titled “Troubleshooting”No language runtime registered for 'python'
Section titled “No language runtime registered for 'python'”The extension was not loaded in runtime context. Verify frontmatter/shape.toml/--extension path and rebuild if needed.
Hover works but Python semantic highlighting is missing
Section titled “Hover works but Python semantic highlighting is missing”Shape delegates highlighting to the child Python LSP. If that server does not implement semantic-token methods, hover can work while semantic highlighting is unavailable.
Verify child server availability:
pyright-langserver --stdioThe current Python extension advertises pyright-langserver --stdio for child LSP startup.