Cortex for Python Users
A working translation guide. Every Cortex example on this page is executed by
the documentation test suite and its // ➔ output verified, so nothing here
can drift from the implementation.
What carries over. The shape of a program: sequential statements,
lexically scoped functions, closures, first-class lambdas, Map/Filter, a
for x in collection loop, arbitrary-precision integers, % with Python's
sign convention, negative indices, chained comparisons, and ** for
exponentiation.
What to unlearn. Three things, in order of how much trouble they cause:
- Indexing is 1-based.
xs[1]is the first element. - Arithmetic is exact and symbolic by default.
1/3is the rational one third,Ln(2)staysln(2). Floats happen only when you ask, withN(…). //is a comment, not floor division, and=is assignment, never equality. Both fail quietly — see Traps.
There is no print. A program's value is the value of its last statement.
Variables and Functions
| Python | Cortex |
|---|---|
x = 5 | let x = 5 |
TAU = 6.28 (by convention) | const tau = 6.28 (enforced) |
x: int = 4 | let n: integer = 4 |
def f(x): return x**2 | f(x) = x^2 |
def f(x): with a body | function f(x) { … } — value is the last expression |
lambda x: x*2 | x |-> 2x |
lambda: 42 | () |-> 42 |
def f(x: float) -> float: | f(x: real) -> real = x^2 |
return | (no return) — the last expression is the value |
math.floor(x), np.mean(xs) | Floor(x), Mean(xs) — no modules, no imports |
Naming convention: Capitalized names are library operators, lowercase
names are yours. Calling an unknown function is not an error — the call stays
symbolic, with a did-you-mean warning when a close library name exists
(len suggests Length).
fact(n) = if n <= 1 { 1 } else { n * fact(n - 1) }
let double = x |-> 2x
(fact(5), double(21))
// ➔ (120, 42)
Collections
| Python | Cortex |
|---|---|
[1, 2, 3] | [1, 2, 3] |
{1, 2, 3} (set) | {1, 2, 3} |
(1, 2) (tuple) | (1, 2) |
{"a": 1} (dict) | {"a" -> 1}; empty dictionary is {->} |
d["a"] | d["a"] — never d.a |
xs[0] | xs[1] — 1-based |
xs[-1] | xs[-1] |
xs[1:3] | xs[2..3] — 1-based, inclusive on both ends |
range(1, 6) | 1..5 or Range(1, 5) — inclusive of the end |
len(xs) | Length(xs) |
sorted(xs) / sorted(xs, reverse=True) | Sort(xs) / Sort(xs, (a, b) |-> a > b) |
sum, min, max, any, all | Sum, Min, Max, Any, All |
reversed(xs) | Reverse(xs) |
zip(a, b) | Zip(a, b) |
enumerate(xs) | Zip(1..Length(xs), xs) |
xs.index(v) | IndexOf(xs, v) |
xs + ys, xs.append(v) | Join(xs, ys), Append(xs, v) — both return a new collection |
xs[2] = 9 | (no element assignment) — rebuild with Map/Join |
d.keys(), d.values() | Keys(d), Values(d) |
dict(zip(ks, vs)) | DictionaryFrom(Zip(ks, vs)) |
collections.Counter(xs) | Tally(xs) → a (values, counts) pair |
Collections are immutable values. There is no in-place mutation: build a new collection and rebind the name.
let counts = DictionaryFrom(Zip(["apples", "figs"], [3, 1]))
(counts["apples"], Keys(counts), counts["pears"])
// ➔ (3, ["apples","figs"], NaN)
A missing dictionary key yields NaN rather than raising KeyError — see
Traps.
Comprehensions
Cortex has no comprehension syntax. Use the pipeline operator |> with
Filter/Map; _ is the placeholder for the piped value.
sum(n**2 for n in range(1, 11) if n % 2 == 1)
1..10 |> Filter(_, n |-> n % 2 == 1) |> Map(_, n |-> n^2) |> Sum
// ➔ 165
Range, Map, Filter, Take, Drop and Join are generators, like
Python's — they enumerate only when materialized (indexed, aggregated, or
iterated). A deferred mapping function reads variables at materialization
time, so the same "late binding in a closure" surprise applies:
let n = 1
let m = Map(1..3, k |-> k * n)
n = 10
Sum(m)
// ➔ 60
Control Flow
| Python | Cortex |
|---|---|
if c: … elif d: … else: … | if c { … } else if d { … } else { … } |
a if c else b | if c { a } else { b } — if is an expression |
and, or, not | &&, ||, ! (the words are reserved but unimplemented) |
for x in xs: | for x in xs { … } |
for i in range(n): | for i in 1..n { … } |
while c: | while c { … } |
break, continue | (reserved, not implemented) — loop on a condition instead |
match … case (3.10+) | match … { pattern => body } |
try/except | (none) — errors are ordinary values |
# comment | // comment or /* … */ |
Loops run for effect: their value is Nothing. Accumulate into a variable
declared outside the loop, or use Map/Filter/Reduce/Fold when you want
a value.
let total = 0
for k in 1..100 { if k % 3 == 0 || k % 5 == 0 { total = total + k } }
total
// ➔ 2418
Pattern matching
Cortex match is close to Python 3.10's match/case, with three
differences: cases are written pattern => body (no case keyword and no
colon), a bare name always binds (it never compares), and you pin a value
to compare against with == expr.
match n:
case 0: "zero"
case k if k > 0: "positive"
case _: "negative"
classify(n) = match n {
0 => "zero"
k if k > 0 => "positive"
_ => "negative"
}
Map([-2, 0, 5], classify)
// ➔ ["negative", "zero", "positive"]
Because a bare name binds, match x { Pi => … } does not test for π — it
binds a fresh variable named Pi. Write match x { == Pi => … }. This is the
same rule as Python's (where a bare case FOO: is a capture pattern), but it
bites more often because Cortex's constants are ordinary names.
Math and Numerics
| Python | Cortex |
|---|---|
7 / 2 → 3.5 | 7 / 2 → the exact rational 7/2; N(7 / 2) → 3.5 |
7 // 2 → 3 | Floor(7 / 2) — // starts a comment in Cortex |
7 % 2, -7 % 3 → 2 | 7 % 2, -7 % 3 → 2 — same sign convention |
x ** 2, pow(x, 2) | x^2 or x**2 |
math.sqrt(x) | Sqrt(x) — exact: Sqrt(9) is 3, Sqrt(2) stays √2 |
math.pi, math.e | Pi, e |
math.log(x), math.log10(x) | Ln(x), Log(x); Log(x, b) for base b |
abs, round, math.floor, math.ceil | Abs, Round, Floor, Ceil (not Ceiling) |
float(expr) | N(expr), or N(expr, digits) for a precision |
10 ** 100 (bigint) | 10^100 — same unbounded integers |
complex(2, 3) | 2 + 3i |
statistics.mean/median | Mean, Median, Variance, StandardDeviation |
math.gcd, math.factorial | GCD, LCM, n! |
| (SymPy territory) | Simplify, Solve, D, Integrate, Limit, Series are built in |
Exactness is the default, and comparison is tolerant, so the classic floating-point gotcha does not appear:
let exact = 1/3 + 1/6
let approx = N(1/3 + 1/6)
(exact, approx, 0.1 + 0.2 == 0.3)
// ➔ (1/2, 0.5, True)
Round rounds halves away from zero; Python rounds halves to even. This
is the one numeric answer that differs on values you are likely to type:
(Round(0.5), Round(2.5), Round(-0.5))
// ➔ (1, 3, -1)
(Python gives 0, 2, 0.)
Because values are Compute Engine expressions, arithmetic over a list is elementwise without NumPy:
([1, 2, 3] + 1, [1, 2, 3] * [4, 5, 6], Sum(Map(1..4, k |-> k^2)))
// ➔ ([2,3,4], [4,10,18], 30)
Strings
| Python | Cortex |
|---|---|
f"x is {x}" | "x is \(x)" — works in any string literal |
"a" + "b" | StringJoin("a", "b") — + on strings is a type error |
len(s) | Length(Characters(s)) — strings are not collections |
s[0] | Characters(s)[1] |
s.split() / s.split(",") | StringSplit(s) / StringSplit(s, ",") |
"".join(parts) | StringJoin(…), or Fold over the parts |
str(x) | String(x) |
"""…""" | """…""" — multi-line strings, same delimiter |
r"raw\string" | #"raw\string"# — extended string literal |
let name = "world"
let parts = StringSplit("a b c")
("hello \(name)", StringJoin("a", "b"), Length(Characters(name)), parts[2])
// ➔ ("hello world", "ab", 5, "b")
There is no .upper(), .replace(), .find() or .strip(): the string
library today is Characters, GraphemeClusters, UnicodeScalars,
StringSplit, StringJoin, StringFrom and String. Decompose to a list of
characters or code points, work there, and rebuild.
Errors
There are no exceptions. A runtime problem becomes an ordinary
Error(…) value that flows through the computation, so a bad element does
not abort the rest of the work:
Map([16, -4, "banana", 81], x |-> Sqrt(x))
// ➔ [4, 2i, NaN, 9]
Note also Sqrt(-4) → 2i rather than a ValueError: the engine works over
the complex numbers. Malformed source is different — it produces
diagnostics with source positions, reported separately from the value.
Familiar
These transfer straight across — no translation needed:
let xs = [10, 20, 30]
(xs[-1], 20 in xs, 1 < 2 < 3, 2**10, -7 % 3)
// ➔ (30, True, True, 1024, 2)
- Negative indices count from the end;
intests membership. - Chained comparisons (
1 < x <= 4) mean the conjunction, as in Python. **is an accepted alias of^, right-associative (2^3^2is512).%is the remainder with Python's sign convention.- Integers are arbitrary precision, with no
int/longdistinction. true/falseare accepted spellings ofTrue/False.- Closures capture lexically, and functions are first-class values.
;separates statements on one line, exactly as in Python.
Traps
Reflexes that produce a wrong answer rather than an error. The parser emits
a warning diagnostic for the first three — visible on stderr from the CLI,
and in the diagnostics array when embedding — but the program still runs and
still returns a plausible-looking value.
| You write | What actually happens | Write instead |
|---|---|---|
7 // 2 | // starts a comment, so the statement is just 7 | Floor(7 / 2) |
xs[0] | Silently NaN — indexing is 1-based | xs[1] |
Solve(x^2 = 4, x) | Silently [] — = is assignment | Solve(x^2 == 4, x) |
d["missing"] | NaN, not a KeyError | Guard first: IndexOf(Keys(d), k) is 0 when the key is absent |
xs[1:3] | Python's half-open slice; xs[2..3] is 1-based and inclusive | check both ends |
x^1/2 | (x^1)/2 — ^ binds tighter than / | Sqrt(x) or x^(1/2) |
while c: … break | break is unimplemented; the loop runs to the iteration limit | make the condition do the work |
print(x) | Inert, nothing is printed | the program's value is its last statement |
Round(2.5) | 3 (half away from zero), not Python's 2 | (intentional) |
3!^2 | Diagnostic — the lexer reads !^ as one token | 3! ^ 2 |
a +b | Diagnostic — an infix operator needs spaces on both sides or neither | a + b or a+b |
"\(xs)" with a list xs | Broadcasts into a list of strings | interpolate scalars only |
x && y on fresh symbols | Types those symbols boolean for the engine's lifetime | use distinct names for boolean work |
One more, specific to a symbolic language: a Take(xs, 3) (or any lazy
operator) stored inside a tuple stays unevaluated, because a tuple does
not materialize its operands. Aggregate or index where you stand if you need
the work done now.