Loft and Python share a similar surface syntax — assignment without type annotations, brace-free expressions, and names that just work. But loft is statically typed and compiled to bytecode, while Python is dynamically typed and interpreted. This page documents the most important differences so that Python programmers know exactly what they gain and what they give up.
1. Variables — static types inferred at first assignment
Loft
x = 42 // integer — fixed at first assignment
x += 1
name = "Bob" // text — a different variable, not a rebind
x = "oops" // compile error: cannot assign text to integer
Python
x = 42 # int inferred; can be rebound to any type
x += 1
name = "Bob"
x = "oops" # fine — x is now a str
Upside Type errors are caught before the program runs. Renaming or reshaping data structures surfaces mismatches immediately. There is no equivalent of Python's runtime TypeError, AttributeError, or "NoneType has no attribute X" crash — those classes of bug are detected at compile time.
Downside Types are fixed at first assignment and cannot change. Python's dynamic typing genuinely speeds up prototyping: a function that accepts anything, a list that holds mixed types, or a variable that starts as an integer and becomes a float later are all natural in Python and impossible in loft. Adding a type annotation layer (mypy, pyright) gives Python static safety without giving up its flexibility.
2. Null — structured absence, not a universal wildcard
Loft
struct User {
email: text, // nullable by default
age: integer not null, // can never be null
}
u = User { age: 30 };
if u.email == null { print("no email"); }
u.age = null; // compile error: age is not null
Python
from dataclasses import dataclass
from typing import Optional
@dataclass
class User:
age: int
email: Optional[str] = None # explicit Optional
u = User(age=30)
if u.email is None:
print("no email")
u.age = None # allowed at runtime; mypy catches this
Upside Nullability is part of the struct definition — readable at a glance. not null fields are enforced by the compiler and additionally locked at runtime in debug builds, catching accidental null writes early in development. No Optional[T] wrapping needed; the comparison v == null is natural.
Downside The default is nullable, not non-nullable — the safe default is backwards from what static analysis advocates recommend. Python with mypy and Optional[T] annotation gives static guarantees that loft's runtime checks do not. Python's None also participates in truthiness testing (if not email:), pattern matching, and or chaining in ways that loft's null does not support.
3. Structs vs classes and dicts
Loft
struct Point { x: float, y: float }
p = Point { x: 1.0, y: 2.0 };
p.x += 0.5;
p.z; // compile error: no field z on Point
p.x = "hi"; // compile error: cannot assign text to float
fn distance(self: const Point) -> float {
sqrt(self.x * self.x + self.y * self.y)
}
Python
from dataclasses import dataclass
import math
@dataclass
class Point:
x: float
y: float
def distance(self) -> float:
return math.sqrt(self.x**2 + self.y**2)
p = Point(x=1.0, y=2.0)
p.z # AttributeError at runtime (or dict: KeyError)
p.x = "hi" # allowed at runtime; mypy catches this
Upside Field access is checked at compile time — typos in field names are caught before any code runs. Struct memory layout is fixed and unboxed; integer and float fields live directly in memory with no heap allocation overhead. Methods are ordinary named functions — they can be added from any file, at any time, without modifying the struct definition.
Downside No inheritance, no __repr__, no operator overloading (__add__, __eq__, etc.), no properties or descriptors. Python's @dataclass generates __init__, __repr__, and __eq__ automatically. Loft structs are data holders; all display and comparison logic must be written by hand. Python also supports plain dicts as lightweight records, which is often more convenient for ad-hoc data.
4. No while loop
Loft
// Condition-driven poll loop — use a large upper bound:
for _ in 0..2147483647 {
if ready() { break; }
step();
}
// Draining a collection while it has elements:
for _ in 0..2147483647 {
if length(queue) == 0 { break; }
process(queue[0]);
queue#remove;
}
Python
while not ready():
step()
while queue:
process(queue.pop(0))
Upside Every loop has an iteration variable, making it easy to add a cycle limit or index tracking without restructuring. The break fires long before the range limit in practice. Filtered loops (for x in v if pred(x)) and loop attributes (x#first, x#count) are only available on for, so a single loop construct covers all cases.
Downside while condition: is instantly understood by every programmer and reads exactly as its semantics. Its absence is surprising and the workaround is verbose. Python also has while ... else (executes when the condition first becomes false without a break), a pattern with no clean equivalent in loft.
5. Polymorphic enum dispatch vs isinstance
Loft
enum Shape {
Circle { r: float },
Rect { w: float, h: float }
}
fn area(self: Circle) -> float { PI * pow(self.r, 2.0) }
fn area(self: Rect) -> float { self.w * self.h }
s.area() // dispatches on the runtime variant
Python
import math
from dataclasses import dataclass
@dataclass
class Circle: r: float
@dataclass
class Rect: w: float; h: float
def area(s):
if isinstance(s, Circle): return math.pi * s.r ** 2
if isinstance(s, Rect): return s.w * s.h
# Python 3.10+ structural pattern matching:
match s:
case Circle(r=r): return math.pi * r ** 2
case Rect(w=w, h=h): return w * h
Upside Each variant's behaviour lives in its own small, named function — easy to read, easy to navigate in an editor. Adding a new shape only requires a new fn area(self: NewShape) with no changes to existing code or a central dispatch function. The compiler warns when a variant has no implementation for a called method.
Downside Unlike Rust's match, loft does not enforce exhaustiveness — a missing variant implementation produces only a warning, not a compile error. Python 3.10+ structural pattern matching (match/case) fully destructs the matched value and is exhaustive when case _: is present. Python also supports inheritance-based polymorphism (class Circle(Shape)) and abstract base classes, giving far richer dispatch options.
6. String formatting — embedded expressions
Loft
msg = "Hi {name}, score: {score:+8.2}"
hex = "{n:#x}"
list = "{for x in 1..4 {x*2}}" // "2,4,6"
pad = "{value:08}" // zero-padded
Python
msg = f"Hi {name}, score: {score:+8.2f}"
hex = f"{n:#x}"
# no loop in f-string; use a join:
lst = ",".join(str(x*2) for x in range(1, 4)) # "2,4,6"
pad = f"{value:08}"
Upside All loft strings are implicitly format strings — no f prefix needed. Inline for loops inside {} produce comma-separated output without a separate join. Format specifiers mirror Python's f-string mini-language: width, precision, sign, alignment, zero-padding, and radix (#x, #o, b) all work.
Downside Python f-strings accept arbitrary expressions: method calls ({obj.method():.2f}), ternary expressions ({"yes" if ok else "no"}), and join operations inline. Loft restricts what can appear inside {}. Python also supports conversion flags (!r for repr, !s for str, !a for ASCII) and the = debug specifier ({x=} prints x=42); loft has none of these.
7. Collections — typed and built-in
Loft
nums: vector<integer> = [1, 2, 3];
lookup: hash<text> = {};
lookup["key"] = "value";
scores: sorted<integer> = {};
scores[user] = 95; // O(log n) keyed insert
// Element type is enforced at compile time:
nums += ["x"]; // compile error: expected integer
Python
nums = [1, 2, 3] # list — any element type
lookup = {} # dict — any key/value type
lookup["key"] = "value"
# sorted dict requires an external package:
from sortedcontainers import SortedDict
scores = SortedDict()
scores[user] = 95
nums.append("x") # allowed at runtime
Upside All collection types — vector, hash, and sorted map — are built in; no extra import or install needed. Element types are checked at compile time. The same [] indexing syntax works on all three. for x in v if pred(x) { v#remove; } safely removes the current element while iterating — something Python requires careful index management for.
Downside Python's built-in dict and list are among the most heavily optimised data structures in any scripting runtime. Python also has set and frozenset (no loft equivalent), ordered insertion semantics on dict (Python 3.7+), and an enormously expressive comprehension syntax ([x*2 for x in v if x > 0]). Loft's map() / filter() / reduce() higher-order functions allocate a new vector at each stage, while Python's generators are lazy and allocation-free until consumed.
8. No closures or lambdas — but compile-checked function references
Loft
fn double(x: integer) -> integer { x * 2 }
fn is_even(x: integer) -> boolean { x % 2 == 0 }
fn add(a: integer, b: integer) -> integer { a + b }
f = fn double; // compile-checked fn reference
assert(f(5) == 10);
nums = [1, 2, 3, 4, 5];
doubled = map(nums, fn double);
evens = filter(nums, fn is_even);
total = reduce(nums, 0, fn add);
Python
double = lambda x: x * 2
is_even = lambda x: x % 2 == 0
f = double
assert f(5) == 10
nums = [1, 2, 3, 4, 5]
doubled = list(map(lambda x: x * 2, nums))
evens = list(filter(lambda x: x % 2 == 0, nums))
total = sum(nums)
# capture context freely:
offset = 10
shifted = [x + offset for x in nums]
Upside Simpler mental model — no capture modes, no scope surprises. The fn <name> expression gives a compile-checked reference to any named function; the compiler verifies the name exists and has a matching signature before emitting code. Higher-order functions (map, filter, reduce) accept fn-refs directly.
Downside No closures means context cannot be captured — any extra data must be embedded in the element struct or passed as an explicit parameter. Python's lambda and nested def close over surrounding variables naturally, making short callbacks, key functions, and event handlers concise. List comprehensions ([f(x) for x in v]) are shorter and more Pythonic than map(v, fn f). Indirect calls are supported, but fn-refs cannot be compared or inspected at runtime.
9. No exception handling
Loft
// Preconditions are assertions; failures abort the program:
fn divide(a: float, b: float) -> float {
assert(b != 0.0, "division by zero");
a / b
}
// I/O errors surface as null:
f = open("data.txt");
if f == null { print("file not found"); }
Python
def divide(a: float, b: float) -> float:
if b == 0:
raise ValueError("division by zero")
return a / b
try:
result = divide(x, y)
except ValueError as e:
print(f"error: {e}")
try:
with open("data.txt") as f:
data = f.read()
except FileNotFoundError:
print("file not found")
Upside No exception hierarchy to learn, no accidental exception swallowing, no overhead from unwinding the stack. File and I/O operations signal failure by returning null, which is explicit and cheap to check. The control flow of a loft function is always straightforward — there are no hidden exit paths.
Downside There is no structured way to recover from errors in user code. An assertion failure aborts the entire program. Python's try/except/finally/else and user-defined exception hierarchies allow fine-grained error handling, retry logic, cleanup on failure, and graceful degradation — patterns that are impossible to express in loft today.
10. Built-in parallel for-loops — par(...)
Loft
fn score(item: const Record) -> integer { item.value * 2 }
total = 0;
for item in records par(s=score(item), 4) {
total += s; // results arrive in original order
} // 4 worker threads, no GIL
Python
from concurrent.futures import ProcessPoolExecutor
def score(item): return item.value * 2
# ProcessPoolExecutor: bypasses GIL via separate processes
with ProcessPoolExecutor(max_workers=4) as ex:
results = list(ex.map(score, records))
total = sum(results)
# ThreadPoolExecutor: simpler but GIL limits CPU-bound work
Upside Built into the language — no import, no boilerplate. The GIL does not apply; worker threads run on separate OS threads inside the same process. Results arrive in the original vector order. The compiler validates the worker function signature at the call site. The thread count is set per call, making it easy to tune for the hardware.
Downside Workers must return a primitive (integer, long, float, or boolean) — returning text or a struct reference is not yet supported. Context must be embedded as fields in the element struct; workers cannot capture local variables. Python's ProcessPoolExecutor works with any picklable object and gives full control over timeouts, cancellation, and error propagation.
11. No generic functions
Loft
// Must write a version per type — no type parameters:
fn max_int(a: integer, b: integer) -> integer {
if a > b { a } else { b }
}
fn max_float(a: float, b: float) -> float {
if a > b { a } else { b }
}
Python
# Duck typing: works for any T that supports >
def max_val(a, b):
return a if a > b else b
# With type hints (Python 3.12+):
from typing import TypeVar
T = TypeVar("T")
def max_val(a: T, b: T) -> T:
return a if a > b else b
Upside Nothing to learn about type parameters, bounds, variance, or protocols. Collections (vector<T>, hash<T>, sorted<T>) are generic at the engine level, covering the most common need without any user-visible type parameter syntax.
Downside Code cannot be written once and reused across types. Every generic algorithm must be duplicated per type or moved into the standard library. Python's duck typing means a function that calls len(x) automatically works on any type that implements __len__ — no explicit annotation required. Python 3.12 TypeVar syntax and structural subtyping (Protocol) give this generality with optional static checking.
12. Function signatures — no defaults, keyword args, or *args
Loft
// All arguments are positional and required:
fn connect(host: text, port: integer, timeout: integer) { ... }
connect("localhost", 8080, 30); // OK
connect("localhost", 8080); // compile error: wrong argument count
Python
def connect(host: str, port: int = 80, timeout: int = 30):
...
connect("localhost") # uses defaults
connect("localhost", 8080) # overrides port only
connect("localhost", timeout=5) # keyword arg — skips port
def log(*args, **kwargs): ... # variadic
Upside Every call site is explicit — there are no hidden default values to look up. The number and order of arguments is always visible at the call site, making code easier to follow without jumping to the function definition.
Downside No default parameter values means wrapper overloads must be written by hand. No keyword arguments means callers of functions with many parameters must remember the correct order. No *args or **kwargs means variadic dispatch must be handled with a vector parameter. Python's flexible argument syntax is one of its most ergonomic features, enabling clean APIs, decorator patterns, and configuration-driven code that is awkward to express in loft.
13. Ecosystem — minimal standard library
Loft
// import "mylib" loads a .loft file relative to the script.
// The full standard library ships inside the interpreter binary;
// there is no package manager or external dependency system.
// Built-in: text, math, file I/O, collections,
// logging, threading, image, lexer/parser
Python
import numpy as np # numerical arrays / SIMD
import pandas as pd # dataframes
import requests # HTTP
import flask # web framework
import sqlalchemy # database ORM
import scikit_learn # machine learning
# 500 000+ packages on PyPI, installable with:
# pip install <package>
Upside Zero external dependencies — the interpreter is a single self-contained binary. Deployment is copying one file. There is no requirements.txt, no virtual environment, no version conflict, and no supply-chain risk. The built-in library covers text manipulation, math, file I/O, typed collections, parallel execution, image handling, and a full lexer/parser framework.
Downside Python's ecosystem is its defining advantage. NumPy, pandas, scikit-learn, TensorFlow, requests, Flask, SQLAlchemy, pytest, and hundreds of thousands of other packages are not available to loft programs. Any data science, web development, database integration, or protocol implementation task will require reimplementing from scratch what Python solves with a single pip install. For these domains, loft is the wrong tool today.
14. Exponentiation uses pow(); ^ is XOR
Loft
area = PI * pow(r, 2.0) // exponentiation via pow()
bits = a | b // bitwise OR
xor = a ^ b // bitwise XOR — not exponentiation
cube = pow(x, 3.0) // cube root: pow(x, 1.0/3.0)
Python
import math
area = math.pi * r ** 2 # ** is exponentiation
bits = a | b # bitwise OR
xor = a ^ b # bitwise XOR
cube = x ** 3 # integer cube — exact
cube = x ** (1/3) # cube root as float
Upside ^ behaves as bitwise XOR in both loft and Python — no mismatch. Bitwise operator precedence matches: | (loose) → ^ → & → shifts → arithmetic (tight). The pow() function is explicit, avoiding any ambiguity about whether ^ means XOR or exponentiation.
Downside Python's ** operator works on integers, floats, and complex numbers and produces the exact type the operands imply. Loft's pow() operates on float and single only — integer exponentiation has no built-in; compute it with a loop or cast to float first. Python's pow(base, exp, mod) three-argument form computes modular exponentiation efficiently; loft offers no equivalent.