vs Python

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. while loop — yes; no loop or while ... else

Loft

while !ready() { step(); }  // works

while length(queue) > 0 {
    process(queue[0]);
    queue#remove;
}

// No loop keyword — infinite loop needs a large bound:
for _ in 0..2147483647 {
    if should_exit() { break; }
    step();
}

Python

while not ready():
    step()

while queue:
    process(queue.pop(0))

while True:          # truly infinite
    if should_exit(): break
    step()

Upside while condition { } works exactly as expected. Filtered loops (for x in v if pred(x)) and loop attributes (x#first, x#count) add expressive power to for without needing a separate loop form.

Downside No while True: equivalent — infinite loops require a bounded for range as a workaround. No while ... else (executes when the condition becomes false without a break), a Python 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. Closures — same-scope capture works

Loft

// Capture works when called in the same scope:
offset = 10;
shift = fn(x: integer) -> integer { x + offset };
assert(shift(5) == 15, "shift");

// Non-capturing lambdas work with map/filter/reduce:
doubled = map([1, 2, 3], fn(x: integer) -> integer { x * 2 });
evens   = filter([1, 2, 3, 4], |x| { x % 2 == 0 });

// Capturing lambda with map:
offset = 10;
shifted = map(nums, fn(x: integer) -> integer { x + offset });

Python

offset = 10
shift = lambda x: x + offset  # capture works anywhere
assert shift(5) == 15

nums = [1, 2, 3, 4, 5]
doubled = list(map(lambda x: x * 2, nums))
evens   = list(filter(lambda x: x % 2 == 0, nums))

# capturing lambda passed to map:
shifted = [x + offset for x in nums]

Upside Same-scope capture works for integers, text, and mutable variables — no capture modes, no scope surprises. Both long-form (fn(x: integer) -> integer { x * 2 }) and short-form (|x| { x * 2 }) lambdas are supported. Named function references (fn name) are compile-checked. Higher-order functions (map, filter, reduce) accept both lambdas and fn-refs.

Downside Capture is by value at definition time — later mutations to the original variable do not affect the lambda (and vice versa). Python's lambda and nested def close over variables by reference, so mutations are shared. Python's list comprehensions ([x + offset for x in v]) are shorter than map(v, fn(x) { x + offset }).

9. No exception handling — file errors use FileResult

Loft

// Logic errors: assert aborts the program
fn divide(a: float, b: float) -> float {
    assert(b != 0.0, "division by zero");
    a / b
}

// Reading: check f#exists before using content
f = file("data.txt");
if f#exists {
    data = f.content();
} else {
    print("file not found");
}

// Mutating ops return a FileResult enum
result = delete("old.txt");
match result {
    FileResult.Ok             -> print("deleted");
    FileResult.NotFound       -> print("not found");
    FileResult.PermissionDenied -> print("permission denied");
    FileResult.IsDirectory    -> print("is a directory");
    _                         -> print("other error");
}

// Or just check success:
if !move("a.txt", "b.txt").ok() { print("rename failed"); }

Python

# Logic errors: raise an exception
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}")

# File errors: catch specific exception types
try:
    with open("data.txt") as f:
        data = f.read()
except FileNotFoundError:
    print("file not found")

try:
    os.remove("old.txt")
except FileNotFoundError:
    print("not found")
except PermissionError:
    print("permission denied")
except IsADirectoryError:
    print("is a directory")

Upside File errors are represented as a typed FileResult enum — Ok, NotFound, PermissionDenied, IsDirectory, NotDirectory, Other — so every failure case is named and exhaustively matchable. No accidental exception swallowing, no hidden exit paths, no stack-unwinding overhead. The control flow of every loft function is straightforward to read.

Downside Logic errors (assert, panic) abort the entire program — there is no try/except to catch them. Python's exception hierarchy supports retry logic, cleanup via finally, and graceful degradation from arbitrary errors anywhere in the call stack — patterns that are not expressible 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 can return primitives (integer, long, float, boolean), text, and inline enums — but not struct references. 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. Generic functions — pass-through only, no duck typing

Loft

// Pass-through generics work:
fn identity<T>(x: T) -> T { x }
identity(42)        // integer
identity("hello")  // text

// Algorithms requiring operations on T still need per-type versions:
fn max_int(a: integer, b: integer) -> integer {
    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 Basic generic functions (pass-through, wrapping, forwarding) work out of the box. 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 A generic function cannot perform operations on T — no comparison, no arithmetic, no display. Any algorithm that requires an operation must be written once per concrete type. Python's duck typing lets a single function work on any type that supports the needed operation (len, >, +) without explicit annotation. Python 3.12 TypeVar and Protocol add optional static checking on top.

12. Function signatures — defaults and named args, but no *args

Loft

fn connect(host: text, port: integer = 80, tls: boolean = true) -> text { ... }

connect("localhost");                // uses defaults
connect("localhost", tls: false);    // named arg — skips port
connect("localhost", 443, false);   // positional — all explicit

Python

def connect(host: str, port: int = 80, tls: bool = True):
    ...

connect("localhost")            # uses defaults
connect("localhost", tls=False) # keyword arg — skips port

def log(*args, **kwargs): ...   # variadic

Upside Default parameter values and named arguments work similarly to Python. connect("host", tls: false) skips defaulted middle parameters. Every call site is readable without jumping to the function definition.

Downside No *args or **kwargs — variadic dispatch must use a vector parameter. Python's flexible argument syntax also enables decorator patterns and configuration-driven code that is harder 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.