TurboPython - a Python-to-C++ compiler

TurboPython

4 min read Original article ↗

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.