TurboPython: a Python-to-C++ compiler. Typed Python to native binaries with no Python runtime, no GC, no GIL.
See the codeTurboPython (TPy, package tpy-lang) compiles statically typed Python
through C++ to a native binary. The result has no interpreter, no garbage
collector, no automatic reference counting and no GIL; an ownership model
provides deterministic, compile-checked memory management. Source files
stay valid Python, so existing editors and linters read them, but
TurboPython is not a drop-in replacement for CPython.
Website | Documentation | Examples | Release notes
Status: early development. The core language compiles and runs real programs, but some ordinary Python constructs are still rejected, the standard library is a subset, and known bugs can produce wrong results silently. See what works before relying on it.
The source is Python with type annotations and two TurboPython types,
int32 and Own:
from dataclasses import dataclass
from tpy import Own, int32
@dataclass
class Stats:
count: int32
total: int32
longest: int32
def measure(words: list[str]) -> Own[Stats]:
total, longest = 0, 0
for w in words:
total += len(w)
longest = max(longest, len(w))
return Stats(len(words), total, longest)
s = measure("the quick brown fox jumps over the lazy dog".split())
print(s.count, s.total, s.longest)
tpy compiles and runs it; tpyc -b leaves a standalone binary:
$ tpy stats.py
9 35 5
$ tpyc -b stats.py
$ ./__tpyc__/stats.d/release/stats
9 35 5
int32 is a fixed-width integer. Integer literals and len() are int32
by default, so total and longest need no annotation, and arithmetic on
them is checked: an overflow stops the program with an error instead of
wrapping. Plain int stays arbitrary-precision, as in Python. Stats
becomes a plain struct of three 32-bit integers and the loop runs over the
list's storage directly: nothing is heap-allocated per object,
reference-counted or garbage-collected. Own[Stats] says the function
hands its result to the caller; see
Ownership.
Requirements: Python 3.12+ and a C++23 compiler on the PATH (g++ 13+ or clang++ 19+). Linux and macOS are supported; on Windows, WSL is the recommended setup.
$ pip install tpy-lang
$ pip install "tpy-lang[bundled]" # also installs zig as a bundled C++ compiler
Or as an isolated tool: uv tool install tpy-lang or pipx install tpy-lang.
Two commands are installed: tpy runs programs and drops to a REPL with no
arguments; tpyc compiles only and emits .hpp/.cpp files or a binary.
$ tpy # interactive REPL
$ tpy -c "print(1 + 2)" # run one line
$ tpy --debug app.py # unoptimized build with debug info (-g -O0)
$ tpy --dump-code app.py # print the generated C++
$ tpyc -o out/ app.py # emit .hpp/.cpp into out/
Building & running covers every command and flag, and the getting-started guide walks through a first program.
-> None), and class fields are declared with
annotations; local variables are inferred.len() are int32, with checked overflow; int
stays arbitrary-precision.Own[T]
moves a value instead.spawn runs real OS threads and checks Send/Sync at compile
time. A task that is not Send, or state shared through Arc that is not
Sync, is rejected; shared mutable state goes through Arc[Mutex[T]],
RwLock, atomics or channels.tplib that ship with the compiler, or others
written for it.tpy imports, needs
the tpy stub package from the source tree (lib/cpy) on the path; it is
not yet part of the installed package.The ownership rule is the one deep difference. A list owns its elements, so appending an object that is still in use afterwards stores a copy, and the compiler says so:
from dataclasses import dataclass
@dataclass
class Reading:
sensor: str
def main() -> None:
log: list[Reading] = []
r = Reading("boiler-3")
log.append(r) # r is used below, so the list gets a copy
r.sensor = "renamed"
print(log[0].sensor) # boiler-3 -- not renamed
main()
warning: copies Reading into owned storage; use copy() to make this explicit
Under CPython this program prints renamed. The program still compiles, and
the warning makes the difference visible at compile time; writing copy(r)
(from tpy import copy) states the intent and silences it. Unlike Cython, mypyc or Nuitka, the
output does not use the CPython runtime; unlike Codon and Shed Skin, memory
is managed by ownership instead of a garbage collector.
How TPy differs lists every
difference with the compiler's actual diagnostics, and
Ownership teaches the model.
TurboPython is pre-1.0 and changes between releases; read the release notes before upgrading.
BUGS.md. It
is long. Most entries are valid Python the compiler still rejects with
an error; the ones that matter most are the silent miscompiles, and
those are fixed first.BUGS.md.@noalloc
for hot paths. Where efficiency and CPython compatibility conflict,
efficiency wins.@native, and native code calls exported TurboPython
functions.tpy --dump-code prints the C++ the compiler writes, and tpyc emits it
as .hpp/.cpp files for use in a C++ project. The Stats class and the
measure function from the example above come out as:
namespace tpyapp::stats {
struct Stats {
int32_t count;
int32_t total;
int32_t longest;
Stats() = default;
explicit Stats(int32_t count, int32_t total, int32_t longest);
// ... __eq__, __repr__, operator== and a class-name constant
};
Stats measure(const std::vector<std::string>& words) {
int32_t total = 0;
int32_t longest = 0;
auto& __obj_0 = words;
auto __beg_0 = __obj_0.begin();
auto __end_0 = __obj_0.end();
for (; __beg_0 != __end_0; ++__beg_0) {
std::string_view w = *__beg_0;
total = ::tpy::add_check<int32_t>(total, ::tpy::__len__(w));
longest = ::std::max(longest, ::tpy::__len__(w));
}
return Stats(::tpy::__len__(words), total, longest);
}
} // namespace tpyapp::stats
add_check is the overflow-checked add; words is borrowed as a const&
and each w is a std::string_view into it, with no copies.
With an explicit output directory (tpyc -o out/ myapp.py), a
sources.cmake and the runtime headers and sources are written next to the
generated code, so out/ is self-contained and can be committed or copied
to another machine:
include(path/to/out/sources.cmake)
add_executable(myapp ${TPYC_SOURCES})
target_include_directories(myapp PRIVATE ${TPYC_INCLUDE_DIRS})
target_link_libraries(myapp PRIVATE ${TPYC_LIBRARIES})
target_compile_definitions(myapp PRIVATE ${TPYC_COMPILE_DEFINITIONS})
set_target_properties(myapp PROPERTIES CXX_STANDARD ${TPYC_CXX_STANDARD})
TPYC_COMPILE_DEFINITIONS is empty unless a build option fills it:
--no-signals puts TPY_NO_SIGNALS there, which compiles the Ctrl-C layer
out of the runtime for code embedded in a host (no SIGINT handler, no
KeyboardInterrupt from a signal, no check points; the generated code is
the same).
The generated code needs C++23 and the GCC statement-expression extension:
g++ 13+, clang++ 19+ or another LLVM-based compiler. MSVC is not supported.
Existing C and C++ code is reached through @native declarations, a
TurboPython function gets C linkage with @export(binding="C"), and a
module marked # tpy: ext_module builds into a regular CPython extension;
see Emit and integrate C++
and Native interop.
TurboPython source is valid Python, so coding agents work without a plugin.
What they lack is where TurboPython departs from Python.
tpy --install-agent-docs docs/ writes four reference files into a project
(a Python-to-TPy bootstrap, the language reference, standard-library
coverage and the exact API of the installed version) and prints a snippet
to add to AGENTS.md or CLAUDE.md. Re-running it after an upgrade
refreshes them.
Details.
From a source checkout: uv sync, then uv run tpy app.py and
uv run pytest. docs/ARCHITECTURE.md describes the compiler.
Apache License 2.0 with the LLVM Exceptions -- see
LICENSE. The
exception covers the runtime and library code that is compiled into every
program: a binary built with tpy can be distributed without carrying the
license text or a notice. The bundled third-party libraries keep their own
licenses, listed in
NOTICE.
TurboPython: a Python-to-C++ compiler. Typed Python to native binaries with no Python runtime, no GC, no GIL.
See the codeTurboPython (TPy, package tpy-lang) compiles statically typed Python
through C++ to a native binary. The result has no interpreter, no garbage
collector, no automatic reference counting and no GIL; an ownership model
provides deterministic, compile-checked memory management. Source files
stay valid Python, so existing editors and linters read them, but
TurboPython is not a drop-in replacement for CPython.
Website | Documentation | Examples | Release notes
Status: early development. The core language compiles and runs real programs, but some ordinary Python constructs are still rejected, the standard library is a subset, and known bugs can produce wrong results silently. See what works before relying on it.
The source is Python with type annotations and two TurboPython types,
int32 and Own:
from dataclasses import dataclass
from tpy import Own, int32
@dataclass
class Stats:
count: int32
total: int32
longest: int32
def measure(words: list[str]) -> Own[Stats]:
total, longest = 0, 0
for w in words:
total += len(w)
longest = max(longest, len(w))
return Stats(len(words), total, longest)
s = measure("the quick brown fox jumps over the lazy dog".split())
print(s.count, s.total, s.longest)
tpy compiles and runs it; tpyc -b leaves a standalone binary:
$ tpy stats.py
9 35 5
$ tpyc -b stats.py
$ ./__tpyc__/stats.d/release/stats
9 35 5
int32 is a fixed-width integer. Integer literals and len() are int32
by default, so total and longest need no annotation, and arithmetic on
them is checked: an overflow stops the program with an error instead of
wrapping. Plain int stays arbitrary-precision, as in Python. Stats
becomes a plain struct of three 32-bit integers and the loop runs over the
list's storage directly: nothing is heap-allocated per object,
reference-counted or garbage-collected. Own[Stats] says the function
hands its result to the caller; see
Ownership.
Requirements: Python 3.12+ and a C++23 compiler on the PATH (g++ 13+ or clang++ 19+). Linux and macOS are supported; on Windows, WSL is the recommended setup.
$ pip install tpy-lang
$ pip install "tpy-lang[bundled]" # also installs zig as a bundled C++ compiler
Or as an isolated tool: uv tool install tpy-lang or pipx install tpy-lang.
Two commands are installed: tpy runs programs and drops to a REPL with no
arguments; tpyc compiles only and emits .hpp/.cpp files or a binary.
$ tpy # interactive REPL
$ tpy -c "print(1 + 2)" # run one line
$ tpy --debug app.py # unoptimized build with debug info (-g -O0)
$ tpy --dump-code app.py # print the generated C++
$ tpyc -o out/ app.py # emit .hpp/.cpp into out/
Building & running covers every command and flag, and the getting-started guide walks through a first program.
-> None), and class fields are declared with
annotations; local variables are inferred.len() are int32, with checked overflow; int
stays arbitrary-precision.Own[T]
moves a value instead.spawn runs real OS threads and checks Send/Sync at compile
time. A task that is not Send, or state shared through Arc that is not
Sync, is rejected; shared mutable state goes through Arc[Mutex[T]],
RwLock, atomics or channels.tplib that ship with the compiler, or others
written for it.tpy imports, needs
the tpy stub package from the source tree (lib/cpy) on the path; it is
not yet part of the installed package.The ownership rule is the one deep difference. A list owns its elements, so appending an object that is still in use afterwards stores a copy, and the compiler says so:
from dataclasses import dataclass
@dataclass
class Reading:
sensor: str
def main() -> None:
log: list[Reading] = []
r = Reading("boiler-3")
log.append(r) # r is used below, so the list gets a copy
r.sensor = "renamed"
print(log[0].sensor) # boiler-3 -- not renamed
main()
warning: copies Reading into owned storage; use copy() to make this explicit
Under CPython this program prints renamed. The program still compiles, and
the warning makes the difference visible at compile time; writing copy(r)
(from tpy import copy) states the intent and silences it. Unlike Cython, mypyc or Nuitka, the
output does not use the CPython runtime; unlike Codon and Shed Skin, memory
is managed by ownership instead of a garbage collector.
How TPy differs lists every
difference with the compiler's actual diagnostics, and
Ownership teaches the model.
TurboPython is pre-1.0 and changes between releases; read the release notes before upgrading.
BUGS.md. It
is long. Most entries are valid Python the compiler still rejects with
an error; the ones that matter most are the silent miscompiles, and
those are fixed first.BUGS.md.@noalloc
for hot paths. Where efficiency and CPython compatibility conflict,
efficiency wins.@native, and native code calls exported TurboPython
functions.tpy --dump-code prints the C++ the compiler writes, and tpyc emits it
as .hpp/.cpp files for use in a C++ project. The Stats class and the
measure function from the example above come out as:
namespace tpyapp::stats {
struct Stats {
int32_t count;
int32_t total;
int32_t longest;
Stats() = default;
explicit Stats(int32_t count, int32_t total, int32_t longest);
// ... __eq__, __repr__, operator== and a class-name constant
};
Stats measure(const std::vector<std::string>& words) {
int32_t total = 0;
int32_t longest = 0;
auto& __obj_0 = words;
auto __beg_0 = __obj_0.begin();
auto __end_0 = __obj_0.end();
for (; __beg_0 != __end_0; ++__beg_0) {
std::string_view w = *__beg_0;
total = ::tpy::add_check<int32_t>(total, ::tpy::__len__(w));
longest = ::std::max(longest, ::tpy::__len__(w));
}
return Stats(::tpy::__len__(words), total, longest);
}
} // namespace tpyapp::stats
add_check is the overflow-checked add; words is borrowed as a const&
and each w is a std::string_view into it, with no copies.
With an explicit output directory (tpyc -o out/ myapp.py), a
sources.cmake and the runtime headers and sources are written next to the
generated code, so out/ is self-contained and can be committed or copied
to another machine:
include(path/to/out/sources.cmake)
add_executable(myapp ${TPYC_SOURCES})
target_include_directories(myapp PRIVATE ${TPYC_INCLUDE_DIRS})
target_link_libraries(myapp PRIVATE ${TPYC_LIBRARIES})
target_compile_definitions(myapp PRIVATE ${TPYC_COMPILE_DEFINITIONS})
set_target_properties(myapp PROPERTIES CXX_STANDARD ${TPYC_CXX_STANDARD})
TPYC_COMPILE_DEFINITIONS is empty unless a build option fills it:
--no-signals puts TPY_NO_SIGNALS there, which compiles the Ctrl-C layer
out of the runtime for code embedded in a host (no SIGINT handler, no
KeyboardInterrupt from a signal, no check points; the generated code is
the same).
The generated code needs C++23 and the GCC statement-expression extension:
g++ 13+, clang++ 19+ or another LLVM-based compiler. MSVC is not supported.
Existing C and C++ code is reached through @native declarations, a
TurboPython function gets C linkage with @export(binding="C"), and a
module marked # tpy: ext_module builds into a regular CPython extension;
see Emit and integrate C++
and Native interop.
TurboPython source is valid Python, so coding agents work without a plugin.
What they lack is where TurboPython departs from Python.
tpy --install-agent-docs docs/ writes four reference files into a project
(a Python-to-TPy bootstrap, the language reference, standard-library
coverage and the exact API of the installed version) and prints a snippet
to add to AGENTS.md or CLAUDE.md. Re-running it after an upgrade
refreshes them.
Details.
From a source checkout: uv sync, then uv run tpy app.py and
uv run pytest. docs/ARCHITECTURE.md describes the compiler.
Apache License 2.0 with the LLVM Exceptions -- see
LICENSE. The
exception covers the runtime and library code that is compiled into every
program: a binary built with tpy can be distributed without carrying the
license text or a notice. The bundled third-party libraries keep their own
licenses, listed in
NOTICE.