How it differs from Python
Python syntax with static types and a strict ownership model.
Type annotations are required on function parameters, return types, and class
fields; an unannotated integer literal defaults to a 32-bit int32. Beyond these
rules, most code reads like standard Python—minus Python's dynamic runtime features. Where a construct diverges from the static type and
ownership model, the compiler reports it, and its diagnostics name the required change.
No pip packages
Packages written for CPython, such as numpy or requests, cannot be
imported. Everything a program imports is compiled together with it. A dependency has to be written in
TurboPython or ported to it. What ships today is a subset of the standard library and TurboPython's own
tplib libraries; Compatibility lists them.
Ownership
Python reclaims objects automatically through reference counting, so a name is just a shared reference. TurboPython has no garbage collector: every value has exactly one owner — a function frame, a field, or a container — and the owner controls when it is freed. Locals and parameters still alias as they do in Python; persistent storage owns its values. Three cases follow, each shown with the compiler's actual output.
frame
A local is owned by the function that creates it.
Returning a local — won't compile
def make() -> Obj: o = Obj(1) return o # freed when make returns
error:Cannot return local or temporary as reference. Reference type 'Obj' is returned by reference. Use Own[Obj] to return by value.
Transfer ownership out — compiles
def make() -> Own[Obj]: o = Obj(1) return o # ownership moves to the caller
ok:x = make() now owns the object.
o lives in make's frame and is freed when the function returns;
returning it by reference would leave a dangling pointer. Own[Obj] hands the value itself to
the caller, who becomes its owner.
field
A field owns its value.
Storing a borrowed value — compiles with a warning
class Cache: item: Tag def __init__(self, t: Tag) -> None: self.item = t # Python would share a reference
warning:copies Tag into field; use copy() to make this explicit
Take ownership of the argument — compiles clean
class Cache: item: Tag def __init__(self, t: Own[Tag]) -> None: self.item = t # the field takes ownership
ok:the argument moves into the field; no copy.
A field outlives the call that sets it, so it must own its value. Declaring the parameter
Own[Tag] transfers the caller's object in; copy(t) is the alternative when the caller
keeps its own. Fields are declared as typed class attributes and set in __init__ —
there is no __dict__, so each one is a fixed, typed slot.
container
A collection owns its elements.
Keep using a value after storing it — compiles with a warning
l: list[Obj] = [] o = Obj(1) l.append(o) # implicit copy: o is used below o.x = 99
warning:copies Obj into owned storage; use copy() to make this explicit
Make the copy explicit — compiles clean
l: list[Obj] = [] o = Obj(1) l.append(copy(o)) o.x = 99 # the list holds its own copy
ok:the same copy happens either way; copy() just makes it explicit and clears the warning.
A list stores objects, not references, so the append copies either way — where Python
would share the same object. copy() doesn't introduce the copy; it makes the unavoidable one
explicit and clears the warning. If o weren't used afterward, it would be moved
into the list instead — no copy at all.
Other differences
Ownership is the conceptual shift. The rest are smaller, mechanical rules — the type system's requirements, and the constraints a compiled language places on Python's more dynamic features. They become familiar within the first few files.
🏷
Annotations required
Function parameters, returns, and class fields are annotated; local variables are inferred. Python's optional hints become the contract.
🔢
int32 by default
A bare integer is a 32-bit int32. int64/uint32/... pick a width; int is arbitrary-precision (heap-allocated for large values).
📦
Value vs reference
Primitives, tuples and views copy; classes, list, dict, set are reference types, passed and returned by reference.
🔤
str adapts to context
A str is an owned string or a borrowed view depending on context; String and StrView spell it out. Plain str is the right default when allocations are acceptable.
🔒
Static, not dynamic
No eval, monkey-patching, or runtime __dict__ mutation. Types and structure are fixed at compile time.
🧰
Core types in tpy
TurboPython's own types (Own, int32, Span, ...) live in the tpy module — a file can pull them all in with from tpy import *.
🤖
AI assistants are supported. tpy --install-agent-docs docs writes a
TurboPython primer alongside the code — the Python-to-TPy delta, the ownership rules, and the
idioms an assistant needs to generate correct TurboPython.