Python flavor for AI agents
For AI coding agents: operating rules, supported constructs and runnable examples. Human introduction: Python flavor guide.
Mandatory rules
- Write Osprey Python-flavor scripts. Run
.pyfiles withosprey script.py --run, neverpython,python3,uv,pipor other CPython tools. - Begin every file with
# osprey: flavor=python. The.pyextension also selects this flavor when passed directly toosprey. - Use the supported Python annotation notation over Osprey semantics. Formatting follows PEP 8 (four-space indent,
snake_casefunctions and bindings, two blank lines between top-level definitions). Annotations, when you write them, use PEP 484, PEP 526, PEP 585, PEP 591 and PEP 695 notation. Osprey also supplies@unionand the algebraic-effect forms:@effect,@static_effect,@control,@handler(E),@static_handler(E[T]),@requires(...),perform(...),resume(...),update(...)and handlerwithblocks. - Omit inferred annotations. Hindley–Milner inference checks types before execution. Fix every warning, including
redundant type annotation; annotate only where inference needs help (required annotations). - Choose an
Arithpolicy for fallible arithmetic. The compiler names missing operations. Integer overflow and integer/float zero divisors have distinct arms; constants, proven-total sites and explicit total helpers need no handler. See arithmetic policies. - Use Result, immutable data and iterator-based computation.
forand comprehensions lower to existing iterator operations. Mutation remains confined to handler-owned state;Finalforbids reassignment. There are no language exceptions orwhileloops. - 🚨 CRITICAL: when a script fails, file a GitHub issue of type Bug. See Report every failure as a Bug. Do not silently work around a compiler failure.
Report every failure as a Bug
Report unexpected script, compiler and repository-command failures as a GitHub issue of type Bug on Nimblesite/osprey, including:
- The compiler crashing, panicking, hanging, or printing an internal error.
- The compiler rejecting a program that this page says is valid.
- A compiled program printing the wrong output, exiting with the wrong status, or crashing.
- A diagnostic that is wrong, unclear, or points at the wrong source position.
- A command or
maketarget documented on this site failing.
Do this procedure every time:
-
Reduce the failure to the smallest
.pyfile that still reproduces it. -
Search for an existing open issue first, using a distinctive fragment of the error text:
gh issue list --repo Nimblesite/osprey --state open --search "redundant type annotation"If one matches exactly, add the new reproduction as a comment (
gh issue comment <number> --repo Nimblesite/osprey --body-file report.md) instead of filing a duplicate. -
Otherwise write
report.mdwith every section below, then file it with--type Bug:Section Include Summary One sentence describing the failure. Environment osprey --version, OS (uname -aor Windows version), and the firstclang --versionline.Command The exact failing command. Source Complete repro.py, beginning with# osprey: flavor=python.Expected The documented or specified behavior. Actual Unedited stdout/stderr and exit status ( echo $?).gh issue create --repo Nimblesite/osprey --type Bug \ --title "Python flavor: <short symptom>" \ --body-file report.md -
Put the issue URL in your final report to the user, then continue the task using a documented alternative construct if one exists.
Use --type Bug, not --label bug. If gh auth status fails, report that filing was blocked and include report.md in your reply.
What Osprey is
Osprey combines typed effects, inference, exhaustive matching and LLVM compilation. Handlers implement operations. One compiler builds native executables, WebAssembly, mobile C ABI archives and Python-callable libraries; projects can mix syntax flavors.
Osprey is actively evolving. Validate it against your workload before relying on it in production.
The Python flavor is not Python
Python flavor keeps def, indentation, f-strings and supported PEP annotation notation over Osprey semantics. It has no CPython object model, implicit exceptions or runtime imports. Integers are signed 64-bit; conditions require bool; failures use Result or explicit effects.
An ordinary import numpy does not install or load CPython NumPy. Osprey source ports can be configured through the package client and imported by their logical module names. Calling a CPython library uses the separate interop boundary.
Install the toolchain
Native compilation needs compatible LLVM/clang 15 or newer, found on PATH or selected with OSPREY_CC. Follow Installing Osprey for platform-specific dependencies, Homebrew and Scoop.
Inside this checkout, build and select the local compiler:
make build
export PATH="$PWD/target/release:$PATH"
osprey --version
clang --version
make build creates the runtime archives, release Rust workspace and VS Code extension. After the archives exist, cargo build --release --manifest-path src/toolchain/Cargo.toml -p osprey-cli rebuilds the compiler alone.
Run a script
# osprey: flavor=python
"""Prints a greeting."""
def main():
print("hello from the Python flavor")
osprey hello.py --run
hello from the Python flavor
main() is the entry point; it takes no parameters. The if __name__ == "__main__": idiom does not exist and is rejected. A leading triple-quoted string is a documentation comment, not a value.
CLI reference
osprey <file-or-project> [mode] [options]. The mode defaults to --check for a file. Without --run nothing executes.
| Command | Effect |
|---|---|
osprey f.py |
Type-check only (same as --check). Prints f.py: ok (N statements) and exits 0 on success |
osprey f.py --run |
Compile to a temporary native binary and run it; the exit status is the program's |
osprey f.py --compile -o app |
Compile a native executable to ./app |
osprey f.py --ast |
Print the canonical AST |
osprey f.py --llvm |
Print the generated LLVM IR |
osprey f.py --symbols |
Print the file's symbols as JSON |
osprey f.py --run --memory=arc |
Choose managed memory: native gc (default tracing collector) or arc (reference counting); on Windows, pass --memory=arc because the tracing GC does not support Windows |
osprey f.py --compile --target=wasm32 --memory=arc |
WebAssembly. Other targets: native (default), ios, ios-sim, macos, android-arm64, android-x64, linux-x64, linux-arm64, windows-x64, python |
osprey f.py --run --sandbox |
Deny HTTP, WebSocket, file system, process and FFI capabilities. Individual subset flags: --no-http, --no-websocket, --no-fs, --no-ffi |
osprey f.py --run --quiet |
Suppress compiler progress output; warnings and errors still print |
osprey f.py --run --no-warnings |
Skip the warning pass entirely; errors still print |
osprey f --flavor python |
Force the Python flavor regardless of extension |
osprey build [dir] |
Build the project whose osprey.toml is in dir (default .) |
osprey test [path] [--filter name] |
Run Osprey tests |
osprey fmt [--check | --stdout] f.py |
Format in place, check formatting, or print formatted source |
osprey --version |
Print the compiler version |
Compile errors are printed to stderr prefixed with the file name, plus line:column when the error has a source position, and the exit status is non-zero. Standard input reaches the program (input() reads one line), so echo 42 | osprey f.py --run works.
Types
| Annotation (PEP spelling) | Osprey type | Notes |
|---|---|---|
int |
int |
signed 64-bit; overflow goes to the Arith handler |
float |
float |
IEEE 754 binary64 |
bool |
bool |
True, False; the only type allowed as a condition |
str |
string |
UTF-8, immutable |
None |
Unit |
the "no value" result; not a nullable value |
list[T] (or List[T] from typing) |
List<T> |
immutable, homogeneous |
dict[str, V] (or Dict[str, V]) |
Map<string, V> |
immutable, string keys |
Callable[[A, B], R] |
(A, B) -> R |
import from typing or collections.abc; a type alias can name a callback signature |
Result[T, E] |
Result<T, E> |
Success(value=...) or Error(message=...) |
Any |
any |
must be pattern-matched before use; avoid |
class Name: field: T |
record | closed, immutable |
type Name = T |
type alias | PEP 695 |
class Box[T]: |
generic record | PEP 695 type parameters |
from typing import Callable, Final, List, Dict, Any and from collections.abc import Callable are compile-time annotation imports. They load nothing at run time. Importing the same name from both modules is rejected with binding `Callable` cannot be reassigned.
Annotations you must write
The compiler tells you when it needs one. Write an annotation only when:
- An empty literal has no other context:
empty: list[int] = []. - A function's
Resulttype is not fixed by its body, for example a function whose onlyreturnsits inside awithhandler block (def safely(work) -> Result[int, str]:in control operations). - An
@externdeclaration: every parameter and the return type are mandatory. - An effect operation signature (inside
@effector@static_effect): every parameter and the return type are mandatory. A named callback alias such astype Kernel = Callable[[int], float]can keep operation signatures readable. Callback effect provenance must still be provable. - A function-valued record field (
def discount(amount: int) -> int: ...inside a plainclass): fully annotated.
Everywhere else, remove the annotation. The redundant type annotation warning names each one.
Bindings and functions
The functions example shows inferred parameters, named calls and an arithmetic handler.
name = valuestarts a lexical binding. Prefer immutable bindings; reassignment is checked under the existing handler-owned state rules. Ordinary function-body assignment is rejected.Final[T]forbids reassignment. Augmented, field/index assignment and tuple unpacking are rejected.defdeclares a flat function; a nesteddefis rejected. Bind a lambda instead. There are no default arguments,*args,**kwargs, argument unpacking, currying or implicitself.- Calls are fully positional or fully named.
lambda params: expris a single expression. returnis allowed at the tail of a function or every branch of a finalif/match. A non-tail conditional must produceNonein every branch. A function without a tail return, or an emptypassbody, producesUnit.len,take,dropandsubstringcount UTF-8 bytes:len("“ok”")is8.takeanddropnever split a character, so a count inside one covers all of it.substringis the byte-level operation and can.and,or,nottake booleans and short-circuit. There is no truthiness; writenot isEmpty(items).- Compare operands of the same type. Chained comparisons and
isare rejected; writea < b and b < c. /producesfloat. Integer//and%truncate toward zero:-7 // 2is-3;-7 % 3is-1. Their fallible sites need an arithmetic policy.- Mixed integer/float arithmetic promotes to
float.toFloat(n)converts explicitly.
Strings
- Literals:
"..."or'...', Python escapes (\n,\t,\",\\). - f-strings interpolate any printable value:
f"{name} has {count} items". Format specifiers ({x:.2f}), conversions ({x!r}) and the debug form ({x=}) are not supported. +concatenates two strings:"a" + toString(1).- Use one interpolated
printinstead of consecutiveprintcalls. - Every string function is listed in Built-in functions. Functions that can fail (
substring,split,replace,indexOf,parseInt,parseFloat) returnResult.
Records, Result and match
The record example shows construction, immutable updates and exhaustive Result handling.
- A plain
classwith annotated fields declares a closed immutable record. There is no inheritance,__init__,self, method dispatch, field default or class variable. - Construct every field by keyword:
Account(owner="Alice", balance=100). Missing, extra or wrongly typed fields fail checking. update(record, field=value)creates a copy;record.fieldreads a field.- A fully typed signature-only
def discount(amount: int) -> int: ...inside a record declares a function-valued field supplied at construction, not a method. Success(value=...)andError(message=...)constructResult. Match both cases or supplyunwrap_or(result, fallback), whose fallback is evaluated only on Error.matchis exhaustive. Literal, wildcard and constructor patterns are supported. Keyword fields can bind a different name:case Success(value=answer):andcase Circle(radius=r):work.@unionon a class declares a sum type; each immediately nested record class supplies one variant. Construct a variant with its fields by keyword. See union syntax.
Lists, maps and iteration
# osprey: flavor=python
def main():
squares = [wrapMul(n, n) for n in range(1, 4) if n > 0]
text = join(toList(map(range(len(squares)), lambda i: toString(unwrap_or(listGet(squares, i), 0)))), ",")
for i in range(2):
print(f"{i}: {text}")
This prints 0: 1,4,9 and 1: 1,4,9 on separate lines. wrapMul chooses wrapping explicitly, so this example needs no arithmetic handler.
- Lists are immutable.
listAppend,listPrepend,listConcatandlistReversereturn new lists.items[i]andlistGet(items, i)return Result, including Error for negative/out-of-range indexes. Slices are rejected. range(stop)andrange(start, stop)produceIterator[int].map,filter,fold,forEachandfor name in iterator:consume iterators, not lists. UseforEachListor iterate list indexes as above. A loop variable stays inside its callback scope.- List comprehensions accept the iterator surface, including filters and nested clauses; they produce host lists. They do not add arbitrary Python iterables or GPU placement.
- A list cannot be interpolated into an f-string. Convert elements with
toStringandjointhem.lenhandles strings, lists and maps. mapGetreturns Result;mapContainsreturns bool;mapKeys/mapValuesreturn lists.mapSet,mapRemoveandmapMergeproduce maps.while, loopelse,break,continue, tuple unpacking, generator/set/dictionary comprehensions,zip,sum,sortedandenumerateare rejected. Use folds and records.
Algebraic effects
Concepts
An effect declares typed operations. perform(E.op(...)) requests one; the caller installs its handler. Value operations return to the caller. A control arm can resume or answer for the whole handled block. Arms run outside their own activation and can forward to an outer handler. Requirements propagate through helpers, callbacks and fibers; missing handlers are compile errors.
Declare an effect
@effect above a class declares an effect. Each operation is a fully typed def signature ending in ..., with no self. Generic effects use PEP 695 type parameters.
Perform an operation
perform(Log.write("loading")) names an operation syntactically; it does not evaluate a Python method call first.
Define a handler
@handler(Log) declares a callable handler. Methods are operation arms with inferred parameters/results and no self. Creating it runs no work. An omitted arm remains an obligation for an outer handler.
Install a handler
with console: runs its suite under the handler; console(lambda: work()) runs a zero-argument callback. A tail with may supply the function result; a statement-position suite must produce Unit. Nest suites for several handlers. Arbitrary context managers, as and multiple context managers in one with are rejected.
Complete example
The Python effects example includes the declaration, request, console/audit handlers, both installation forms and output. Use that complete program rather than combining partial declarations.
Control operations and resume
# osprey: flavor=python
@effect
class Ask:
@control
def number(prompt: str) -> int: ...
@handler(Ask)
class answer_42:
def number(_prompt):
return resume(42)
def main():
with answer_42:
print(f"asked: {perform(Ask.number("pick"))}")
asked: 42
@controlabove an operation makes it a control operation.resume(value)continues the computation that performed the operation, withvalueas the operation's result. It is allowed only inside a control arm. Resumption is single-shot: callresumeat most once per arm invocation.- An arm parameter you do not use must start with
_(_prompt); otherwise the compiler warnsunused handler parameter. - A control arm that returns without calling
resumeends the whole handled block; its return value becomes the block's result. This is how to exit early with an error:
# osprey: flavor=python
@effect
class Fault:
@control
def halt(message: str) -> int: ...
@handler(Fault)
class halting:
def halt(message):
return Error(message=message)
@handler(Arith)
class arithmetic:
def overflow(op, l, r, _):
return perform(Fault.halt(f"integer overflow: {l} {op} {r}"))
def divideByZero(op, l):
return toFloat(perform(Fault.halt(f"division by zero: {l} {op} 0")))
def remainderByZero(l):
return perform(Fault.halt(f"remainder by zero: {l} % 0"))
def checked_div(a, b):
return perform(Fault.halt(f"{a} / 0")) if b == 0 else intDiv(a, b)
def safely(work) -> Result[int, str]:
with halting:
with arithmetic:
return Success(value=work())
def show(result):
match result:
case Success(value=value):
return f"ok {value}"
case Error(message=message):
return f"fault {message}"
def main():
print(f"{show(safely(lambda: checked_div(10, 2)))} {show(safely(lambda: checked_div(1, 0)))}")
ok 5 fault 1 / 0
halting turns any Fault.halt into an Error result for the whole with halting: block. safely installs it outside arithmetic, so an arithmetic fault is forwarded to Fault too. The annotation -> Result[int, str] is required here, because the body alone does not fix the error type.
Arithmetic needs a policy
The compiler requires handlers for sites it cannot prove total. Constants and other proven-total sites need none. Host execution invokes the policy; current Metal/OpenCL kernels abort on faults instead of recovering through it. See the device recovery limit.
| Operation | Fallible sites | Arm result |
|---|---|---|
overflow(op, left, right, wrapped) |
integer +, -, *, unary -, abs, and minimum-integer divided by -1 through ///intDiv |
int |
divideByZero(op, left) |
/ with any operands; % with a float operand |
float |
remainderByZero(left) |
integer ///intDiv or % with a zero divisor |
int |
A policy answering all three:
# osprey: flavor=python
@handler(Arith)
class total:
def overflow(_op, _left, _right, wrapped):
return wrapped
def divideByZero(_op, _left):
return 0.0
def remainderByZero(_left):
return 0
Use with total: around the computation. A smaller wrapping handler can provide only overflow with the same arm. A diagnostic names every missing operation. To stop at a fault, use the Fault forwarding example above.
wrapAdd/wrapSub/wrapMul and satAdd/satSub/satMul are total; checkedAdd/checkedSub/checkedMul return Result. Float +, -, *, unary minus and abs need no policy; float / and % still require zero-divisor handling unless proven safe. See arithmetic effects.
Declare required effects
@requires(Log, Arith) above a def states exactly which effects the function may perform; the compiler checks it. @requires() declares a pure function. Inference is the default, so add @requires only to pin a public API.
Static effects and static handlers
@static_effect declares operations resolved through a statically selected @static_handler(E[T]). Every static operation needs an answer; static operations cannot be control operations. Specialization removes runtime effect dispatch, not every function call or allocation.
GPT-2's backends.py declares Tensor[B]; cpu.py answers with lists and gpu.py with GPU buffers. model.py performs the same operations under either handler. Static selection adds no automatic offloading.
Built-in functions
Builtins need no import. The shared function reference and builtin specification cover strings, numbers, files, processes, collections, JSON, networking, terminal control and GPU operations. Their camelCase names remain available in Python flavor.
len, one-argument range, update, unwrap_or and the fiber intrinsics have Python-flavor lowering. perform, resume, update, unwrap_or, spawn, await_fiber, yield_fiber, send and recv are reserved and cannot be rebound.
Handle fallible results explicitly: match readFile(path) as Success(value=text) or Error(message=reason). File writes, parsing and indexing also return Result. Networking has a mixed interface: legacy HTTP/WebSocket handle and status builtins return integers, with negative values indicating errors; HTTP response acquisition, body/header access and release return Result. Check each function's reference signature.
Fibers
spawn(work) starts a fiber running the zero-argument function work and returns a handle; await_fiber(handle) waits for its result. Fibers share no memory: pass data in through the closure and out through the result or channels (send, recv). async def, await, yield and threads are rejected. See fibers and concurrency.
Modules and projects
A single file needs no project. For several files, write osprey.toml:
[project]
name = "tool"
source_roots = ["src"]
python_sources = ["src/main.py", "src/pricing.py"]
default_namespace = "tool"
entry = "src/main.py"
-
python_sourceslists every Python-flavor file explicitly. Unlisted.pyfiles under a source root are ignored, so real Python scripts can live alongside. -
Each module file starts with directives after the flavor marker:
# osprey: flavor=python # osprey: namespace=tool # osprey: module=Pricing -
__all__ = ["discounted"](a literal list of names) or@exporton a declaration makes it public. Everything else is module-private. -
Import with
from tool.Pricing import discounted,from tool import Pricingorimport tool.Pricing as Pricing. Imports name Osprey modules, in any flavor; wildcard and relative imports are rejected. -
Build and run with
osprey buildfrom the directory holdingosprey.toml, orosprey . --run.
Rejected Python constructs and their replacements
| Python construct | Status | Write instead |
|---|---|---|
import os, import json, any package |
rejected (unknown import target `os` ) |
built-in functions |
while, break, continue, loop else |
rejected | fold, forEach, recursion |
try/except/finally, raise |
rejected | return Result; @control effect for early exit |
async def, await, yield |
rejected | fibers |
class methods, __init__, self, inheritance, dunders |
rejected | free functions over records; function-valued fields |
| ordinary function-body reassignment; augmented, field/index assignment | rejected | a new immutable binding, update, folds; handler-owned state where permitted |
early return |
rejected | conditional expression or a final if/match |
if xs: (truthiness) |
type error | explicit bool test such as not isEmpty(xs) |
a < b < c, is, is not |
rejected | a < b and b < c, == |
tuples, a, b = pair |
rejected | records |
slices xs[1:3] |
rejected | take, drop, substring |
xs[-1] |
returns Error |
listGet(xs, len(xs) - 1) |
default, *args, **kwargs parameters |
rejected | explicit parameters, records |
f"{x:.2f}", {x!r}, {x=} |
rejected | plain {x} |
arbitrary decorators (@staticmethod, @dataclass, …) |
rejected | @union, effect decorators, @extern, @export, @requires |
if __name__ == "__main__": |
rejected | def main(): |
None as a nullable value |
None is Unit, never null |
Result |
Supported for loops, comprehensions, unions and renamed pattern bindings are documented above. The Python flavor specification and delivery plan record remaining limits; rejection of an explicitly unsupported construct is expected.
Diagnostics you will see
| Diagnostic fragment | Cause | Fix |
|---|---|---|
redundant type annotation on ... |
an annotation inference already derives | delete the annotation |
unhandled effect operations at program entry: Arith.overflow |
integer arithmetic with no Arith handler |
wrap the work in with wrapping: or another policy |
unhandled effect operations at program entry: E.op; add a matching `handle` |
an operation performed with no handler installed | install a handler for E around the call |
unhandled effect operations at program entry: Arith.divideByZero or Arith.remainderByZero |
/, // or % with a run-time divisor and no matching arm |
add the named arm (see the total policy) |
Python `…` has no supported Osprey lowering |
a Python construct outside the supported surface | use the replacement in the table above |
state mutation is only allowed inside an effect handler arm |
assignment outside a permitted handler arm | bind a new name or use a fold |
call arity mismatch: expected 2 argument(s), got 1 |
positional and keyword arguments mixed in one call | pass all positionally or all by keyword |
type mismatch: cannot unify Iterator<…> with List<…> |
a list passed to map, filter, fold or forEach |
iterate range(len(xs)) or use forEachList |
type mismatch: cannot unify List<int> with bool |
truthiness test on a list | not isEmpty(xs) |
cannot convert value for interpolation: List<…> |
a list inside an f-string | join the toString of each element |
unused handler parameter |
an arm parameter that is never read | prefix it with _ |
a declaration signature must have exactly an ... body |
a method body in a plain class |
move the logic into a free function |
error: could not invoke clang: No such file or directory |
no LLVM toolchain on PATH, or a wrong OSPREY_CC |
install clang or set OSPREY_CC |
Anything else that blocks a program this page says is valid: file a Bug.
Checklist before you finish
- [ ] Line 1 is
# osprey: flavor=python. - [ ]
osprey script.pyprintsokwith zero warnings. - [ ]
osprey script.py --runproduces the expected output and exit status. - [ ]
osprey fmt --check script.pypasses (it normalises indentation). - [ ] No redundant annotations, no consecutive
printcalls, no earlyreturn. - [ ] Every fallible arithmetic site, including float zero divisors, has the required policy; selected-device recovery limits are respected.
- [ ] Every failure you hit has a GitHub issue of type
Bug, and its URL is in your report.
Reference
- Python flavor guide, the human-oriented introduction.
- Python flavor specification, the full contract.
- Algebraic effects guide and effects specification.
- GPT-2 in the Python flavor, the largest working program.