For teams with a large Python codebase

Four things Python can't do. Here's what U does instead.

Exact fractional arithmetic. Vectorized loops that use every core. Lock-free shared state with no interpreter lock. Native machine code with no runtime to ship.

Every claim below carries a marker saying whether it has been run and checked, partly works, or doesn't work yet. The markers are the point of the page — a language project is only worth your time if it tells you where the holes are.

01 — The problem

The four costs of staying in Python

None of these are news to anyone who has shipped Python at scale. They're listed because they're what U is actually aimed at — not at replacing Python everywhere.

cost 01

Binary floats lie about decimals

0.1 + 0.2 == 0.3 is False. Decimal and Fraction fix it and cost roughly 67× a float add in our measurement. Money, billing, and accounting code pays that tax on every line.

cost 02

One core per process

CPU-bound threads serialize on the interpreter lock. The workaround is multiprocessing: separate heaps, pickled messages, and memory multiplied by core count.

cost 03

No ahead-of-time compilation

Type annotations are documentation. Nothing checks them at runtime and nothing compiles against them, so a whole class of errors survives to production and every hot loop stays interpreted.

cost 04

The C-extension cliff

When a loop is too slow the answer is to leave Python — write C, learn the C-API or ctypes, and maintain a build. The performance escape hatch is a different language and a different toolchain.

02 — Exact arithmetic

Q — exact rational arithmetic

verified

U has a first-class rational type. Q holds a numerator and a denominator, reduces to canonical form on every operation, and compares without ever dividing — so no rounding is introduced anywhere in the chain.

The representation

Q is a pair of N64 doubles used strictly as integers. IEEE-754 doubles represent every integer with absolute value up to 253 exactly, so ordinary hardware floating point gives exact integer arithmetic across that whole range — no bignum library, no allocation, two machine words.

// representation Q = (n, d) n, d ∈ ℤ, d ≠ 0, gcd(n, d) = 1, d > 0 // operations — every result renormalised by gcd a/b + c/d = (a·d + c·b) / (b·d) a/b − c/d = (a·d − c·b) / (b·d) a/b × c/d = (a·c) / (b·d) a/b ÷ c/d = (a·d) / (b·c) // comparison — cross-multiplied, never divided a/b == c/d ⟺ a·d == c·b a/b < c/d ⟺ a·d < c·b // exactness condition exact ⟺ |a·d|, |c·b|, |b·d| ≤ 2⁵³ = 9 007 199 254 740 992

That last line is the honest bound. Q is exact while the cross-products stay under 253. For currency, tax splits, ratios, and unit conversion that ceiling is far away. For number theory on large integers it isn't, and you want a bignum rational instead.

What it looks like

python
>>> 0.1 + 0.2 == 0.3
False
>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 * 3 == 0.3
False

# the fix, and its price
>>> from fractions import Fraction as F
>>> F(1,10) + F(2,10) == F(3,10)
True
# ~67x slower than a float add
# (200k iterations, measured)
u
f main() -> I
	xx: Q = Q(1,10)
	yy: Q = Q(2,10)
	zz: Q = xx + yy
	ww: Q = Q(3,10)
	zz == ww ? r => 1
	r => 0

// exit 1 — exact, no library,
// two doubles, inlined C
ran:exit 1 0.1 + 0.2 == 0.3 is true in U, False in Python

Reduction and ordering come free

auto-reduction
f main() -> I
	xx: Q = Q(6,8)
	r => xx.den        // 6/8 → 3/4
division · ordering
f main() -> I
	xx: Q = Q(1,2)
	yy: Q = Q(1,4)
	zz = xx / yy
	r => zz.num        // (1/2)/(1/4) = 2
ran:exit 4andexit 2 reduction, division, and < ordering all exact
Fixed while writing this page. Q(n,d) was emitting a bare Q(1, 3) into the C — not a real function — so every Q program died at gcc with invalid initializer. Separately, the Q comparison table was nested inside the arithmetic-operator branch, making its == and < entries unreachable. Both are now wired, with the compiler's 639 codegen tests still passing.

03 — Parallelism

Vectorization with one annotation

verified

+V on a list is a claim about the data: these elements are independent, so a map or reduce over them has no cross-lane dependency. The compiler is then free to emit SIMD and, above a threshold, fork across cores.

python + numpy
import numpy as np

vals = np.array([1,2,3,4,5,6,7,8,9,10])
sq   = vals ** 2
total = sq.sum()

# Fast — but only because the work
# left Python entirely. Step outside
# what NumPy vectorizes and you are
# back to an interpreted loop, or
# writing C.
u
f main() -> I
	vals: [I] +V = [1,2,3,4,5,6,7,8,9,10]
	sq = vals.map(v => v * v)
	r => sq.sum()

// +V is the whole change.
// The lambda is ordinary U — it is
// compiled, not dispatched into a
// separate array runtime.
ran:385— correct serially and with -DU_VEC_ENABLE -pthread(fork-join across cores)

What the annotation buys you

ConcernPythonU
Expressing intentImplicit — rewrite the loop as an array op and hope it stays on the fast path+V at the declaration site
Escaping the fast pathSilent: a non-vectorized op drops you to interpreted speed with no warningThe linter tells you a list needs +V
Threadingmultiprocessing: separate heaps, picklingFork-join over one heap above U_VEC_THRESHOLD (1024 elements)
DependencyNumPy plus its wheelsGenerated C; SIMD helpers compile with no pthread at all

04 — Concurrency

Shared state without a GIL

verified

Python's interpreter lock exists partly to make reference counting safe. U reference-counts per object, so there's no global lock to remove — and for shared mutable state it offers +M(MVCC), which compiles to a lock-free compare-and-swap retry loop.

python
import threading

class Counter:
    def __init__(self):
        self.value = 0
        self._lock = threading.Lock()

    def bump(self):
        with self._lock:
            self.value += 1

# The lock is manual, and correctness
# depends on every call site taking it.
# Threads still serialize on the GIL
# for the CPU-bound part anyway.
u
d Counter
	value: I +M = 0

f main() -> I
	cc: Counter +M(MVCC) = Counter({ value: 0 })
	cc << { value: 42 }
	r => cc.value

// +M(MVCC) at the declaration.
// Readers never block. Writers CAS
// and retry. No lock to forget.
ran:exit 42— emits _Atomic(Counter_Version*) headandu_mvcc_cas_retry

The << operator is deliberate. You cannot assign a field on an MVCC object directly — the linter rejects it — because a bare field write can't be made atomic with respect to the rest of the update. You submit a patch, and the runtime builds a new version and swaps it in.

Scope of the check. The MVCC path is verified single-threaded: the correct C is emitted, it compiles with -pthread, and it produces the right answer. A contended multi-writer benchmark has not been run, so treat throughput claims as unproven.

05 — Side by side

Interview problems, side by side

verified

An LRU cache with eviction — the standard systems screen. Below is the Python you'd write and the U that compiles to C and runs. Both are complete; neither is a sketch.

python
from collections import OrderedDict

class LRU:
    def __init__(self, cap):
        self.cap = cap
        self.store = OrderedDict()

    def get(self, key):
        if key not in self.store:
            return -1
        self.store.move_to_end(key)
        return self.store[key]

    def put(self, key, val):
        if key in self.store:
            self.store.move_to_end(key)
        elif len(self.store) >= self.cap:
            self.store.popitem(last=False)
        self.store[key] = val
u
d Node
	key: S +M = ""
	val: I +M = 0
	hits: I +M = 0

d LRU
	cap: I +M = 2
	keys: [S] +M = []
	store: {S: Node} +M = {}

	f get(kk: S) -> I
		t.store.has(kk) == false ? r => 0 - 1
		nd = t.store[kk]
		nd.hits = nd.hits + 1
		t.touch(kk)
		r => nd.val

	f touch(kk: S) -> none
		fresh: [S] +M = []
		keep = kk
		t.keys.on(ex => ex != keep ? fresh.push(ex) ! none)
		fresh.push(keep)
		t.keys = fresh
		none

	f evict() -> none
		t.keys.len == 0 ? r => none
		victim = t.keys[1]
		rest: [S] +M = []
		gone = victim
		t.store.delete(gone)
		t.keys.on(ex => ex != gone ? rest.push(ex) ! none)
		t.keys = rest
		none

	f put(kk: S, vv: I) -> none
		t.store.has(kk) == false & t.keys.len >= t.cap ? t.evict()
		t.store = t.store.set(kk, Node({ key: kk, val: vv, hits: 0 }))
		t.touch(kk)
		none
ran:0 lint errors → gcc → exit 40 put a1, put b2, get a1, put c3 ⟹ b2 evicted, a1(10) + c3(30) = 40

What the U version tells you that the Python version doesn't

The Python is shorter, and for an interview whiteboard that's the right answer. What the U buys is that every field carries its contract: keys: [S] +M says this list is mutable and holds strings, store: {S: Node} +M pins both key and value type. Change +M to +M(MVCC) on the declaration and the same cache becomes safe for concurrent readers without touching a method body. That's the argument for annotations carrying semantics rather than documentation.

Also fixed while writing this page. This example initially failed to link. A class field declared keys: [S] +M = [] emitted u_list_new_int32_t(0) for the empty default regardless of the declared element type, handing a list-of-int to a list-of-string parameter. Local variables already routed through the typed path; class construction did not. Fixed, 639 codegen tests still green.

06 — The hard part

Why compiling Python is hard

Worth stating plainly, because it's the reason no one has "just done it" and the reason our own transpiler is limited.

FeatureWhy it defeats static compilation
Duck typingdef area(s): return s.area() — the callee's type is unknown until runtime, so no call can be resolved statically
RebindingA name can hold an int, then a string, then a list. There is no single machine representation to pick
__getattr__Attribute access is arbitrary user code, not a struct offset
Monkey-patchingClasses and modules are mutable at runtime, so no layout or method table can be frozen
eval / execThe program isn't fully known until it runs
MetaclassesClass construction itself is programmable
Decorators@app.route("/x") rewrites the function at import; dropping it silently deletes behaviour
Generatorsyield needs a suspendable frame, not a C stack frame
globals(), inspectThe program reads and edits its own symbol table

Every serious project in this space handles the list the same way: by defining a subset of Python that excludes most of it, and being rigorous about the boundary. The interesting engineering isn't translating the easy 80% — it's refusing the other 20% loudly instead of emitting something that looks fine and isn't.

07 — Current state

Our transpiler: what it does and doesn't

refuses what it can't translate

This section corrects a number we published earlier. It's here rather than buried because an inflated benchmark is worse than a low one.

The number that was wrong, and the number now

We reported 24/24 Flask 3.1 files clean. Here is what "clean" was measuring, verbatim from the old batch checker:

the old test, in full
var hasRawPy = u.split('\n').some(function(l){
  var lt = l.trim();
  return !lt.startsWith('//') && !l.startsWith('\t\t\t')
      && (/\bdef\b/.test(lt) || /\bself\./.test(lt));
});

It greps the output for the strings def and self.. No lint, no compile, no execution. "Clean" meant the text no longer looked like Python. The transpiler now refuses what it cannot translate, so the number means something:

RungFlask 3.1, 24 files
Doesn't contain def24 / 24the old number — meaningless
Accepted (no refusal)5 / 2419 refused, each with a code and a line
Accepted and lints0 / 51, 1, 9, 34, 86 errors — mostly unresolved cross-module names
Lints and compiles0blocked by the rung above
Passes Flask's own tests0not attempted

Five out of twenty-four is a much worse headline than twenty-four out of twenty-four, and it is the first number here that can survive someone checking it.

What refusal looks like

A file that cannot be faithfully translated now says so in its own first line, with the construct, the source line, and what to do about it. There is no way to read the output and mistake it for a working translation.

transpiler output
// ═══════════════════════════════════════════════════════════════
// TRANSLATION INCOMPLETE — 1 construct could not be carried into U.
// The code below is NOT equivalent to the Python it came from.
// ═══════════════════════════════════════════════════════════════
// ✗ PY007  line 1  Decorator dropped — it rewrites the function at
//                  import time and that effect is not carried into U.
//     → Inline what the decorator does, or register it explicitly.

f handler(str: Tree)
	r => str.upper()

The refusal catalogue

Thirty codes, nineteen of them fatal. Every one names a Python construct whose meaning has no U equivalent, and every one carries a suggested rewrite. These are the counts across the Flask source:

CodeConstructHits in FlaskWhy it can't carry over
PY007Decorator dropped99Rewrites the function at import; @app.route is the routing table
PY030**kwargs53Keyword dict expanded at the call site; U has fixed arity
PY031*args43Variadic parameters; U parameters are fixed
PY040Monkey-patching32Rebinding a module or class attribute; U layouts are fixed at compile time
PY004getattr/setattr23Attribute name is a runtime value, not a struct offset
PY020yield16Needs a suspendable frame; U uses a C stack frame
PY013__enter__/__exit__6No with-statement lifecycle in U
PY010__getattr__4Attribute access as arbitrary user code
PY012Multiple inheritance3U has single inheritance, no MRO
PY034Type rebinding2One name holding two literal types; U bindings have one type
PY001 / PY002eval / exec1 / 1The program isn't fully known until it runs
PY025global/nonlocal1U closures capture by declared mutability, not scope escape

What got fixed on the accept side

Refusing more is only half of it. These were producing U that could not parse, and now translate correctly:

PythonWas emittedNow emitted
def area(s): return s.area()f area(str: Tree) over a body calling ss.area()f area(str: Tree) / str.area()
x in itemsxx in items — no such operatoritems.contains(xx)
x not in itemsxx ! in itemsitems.contains(xx) == false
0 < x < 100 < xx < 10 — parse error0 < xx & xx < 10
assert x > 0, "msg"passed through verbatim(xx > 0) == false ? x Error({ message: "msg" })
passpassnone
a, b = b, apassed through verbatimthree statements through a temp
if TYPE_CHECKING: blockimport inside a parenthesised blockdropped; it is type-only and never runs
function-local importimport inside a function bodyhoisted to file level

The first row was the worst of them. uName() mapped a single-letter parameter through a stock table (sstr) while the body renamer doubled the letter (sss), so every function with a one-letter parameter emitted a signature and a body that disagreed. Both paths now share one renamer.

The structural defect is fixed: the transpiler previously had no concept of "I cannot translate this," so it always succeeded — which is exactly why the metric read 24/24. It now returns ok: false and a list of coded diagnostics, and the batch checker reports acceptance rather than the absence of the word def.

08 — Prior art

Compared to Cython, mypyc, Nuitka, Codon

Static-subset compilation of Python is a real, established category. Placing U honestly inside it is more useful than claiming a breakthrough.

ProjectApproachAcceptsEscape hatch
Cython Python superset with C type declarations, compiled to a C extension All Python, but only annotated code gets fast Falls back to CPython objects
mypyc Compiles type-annotated Python using mypy's inference Typed subset; interops with untyped Python Falls back to CPython objects
Nuitka Whole-program compile to C, keeping CPython semantics Essentially all Python Embeds CPython; speedup is modest
Codon Ahead-of-time to native via LLVM with its own type system A deliberately restricted Python-like subset Explicit Python interop bridge
PyPy Tracing JIT, not ahead-of-time Nearly all Python n/a — it is a runtime
U Source-to-source into a separate language that compiles to portable C A named subset — 30 coded refusal rules, reported with construct and line None: a refused file is refused

The column that matters is the last one. Every mature project either falls back to CPython or names its subset and rejects the rest. U now does the second: 30 coded rules, 19 of them fatal, each naming the construct and the line. What it still lacks is the fallback — there is no CPython to hand the hard cases to, so a refused file is simply refused.

Where U differs on purpose: the target isn't "Python, faster." It's a language whose type annotations decide memory residence, mutability, concurrency strategy, and vectorization — things Python annotations can't express at all, because they aren't checked and aren't compiled against. Q, +V, and +M(MVCC) have no Python equivalent to translate from.

09 — Recommendation

When writing U beats transpiling

Transpiling is the wrong instinct for most of what U is good at, and it's worth saying so on a page about Python.

reason 01

The best features have nothing to translate from

There is no Python source construct that means +V, +M(MVCC), or +R. A transpiler can only produce the defaults, so you get U's syntax without U's actual advantages.

reason 02

Translated code carries Python's shape

A Python class translated into a U class inherits decisions made for an interpreter: dictionary-backed attributes, per-object allocation, dynamic dispatch. Rewriting the hot module directly is usually less work than fighting that.

reason 03

A hot path is small

The loop that actually needs to be fast is rarely more than a few hundred lines. Writing it in U and calling it is the same integration story as a C extension, without the C-API.

reason 04

Exactness is a rewrite anyway

Moving money code from float to Q means revisiting every arithmetic site regardless of language. If you're touching it all, translate the intent, not the syntax.

The realistic shape today: keep Python for orchestration, glue, and everything served by its library ecosystem. Write the numerically exact parts, the vectorized parts, and the shared-state parts in U, compile them to C, and call across. That's a claim about a boundary, not a migration.

10 — The ledger

Full verification ledger

Everything on this page, and what backs it. If a row says not yet, it means we ran it and it didn't work — not that we haven't looked.

ClaimStateEvidence
Q exact addition and equalityverified0.1+0.2==0.3 → exit 1; Python → False
Q multiply, divide, reduce, orderverifiedQ(1,3)×Q(3,1)=1; (1/2)÷(1/4)=2; Q(6,8)→3/4; 1/3<1/2
+V map and reduceverifiedSum of squares 1..10 = 385, serial and -DU_VEC_ENABLE
+M(MVCC) lock-free updateverifiedexit 42; emits _Atomic head + u_mvcc_cas_retry
MVCC under write contentionnot measuredNo multi-writer benchmark has been run
LRU cache in Uverified0 lint errors → gcc → exit 40, correct eviction
U compiler test suiteverified1,127 passing: 120 parser, 368 linter, 639 codegen
Single- and multi-file U → C → binaryverifiedreturn, recursion, closures, classes all run correctly
Python → U transpiler: refusalverified30 coded rules; 19 of 24 Flask files refused with construct + line
Python → U transpiler: output qualitynot ready0 of the 5 accepted Flask files lint clean
Reference webserverpartial16 modules, 0 lint errors, 254 KB C — 116 gcc errors, no binary
Standard librarypartial23 of 75 modules compile
Self-hosted compilernot ready0 of 14 transpiled modules compile; 5 crash the compiler

The honest summary: the language and its compiler are real and tested, and the four capabilities this page is about — exact rationals, vectorization, lock-free shared state, native output — all work today. The Python on-ramp now refuses what it cannot translate instead of emitting broken U silently, which turns an unfalsifiable 24/24 into a measurable 5/24 with named reasons. Getting those five to lint, and then raising the acceptance rate, is the work. The reference webserver and self-hosting are blocked on a different gap: the code generator emits void* where it should propagate a known type.