Getting Started
Python → Synsema — the translation table
You (an LLM, or a Python developer) already know Python. This page maps the Python reflex to the Synsema form and flags exactly where the semantics diverge. It is faster than learning from scratch, and it prevents the classic failure mode: writing Python with Synsema keywords.
The one rule that prevents most hallucinations: if you did not see it in these docs, it does not exist. There is no import, no Python stdlib, no classes, no comprehensions, no decorators, no with, no generators, no method-call syntax on values (xs.append(x) → builtins are plain tasks: append(xs, x)).
When a Python reflex slips through anyway, the engine answers in Python terms: the error names the Synsema form to write (return → give, len → length, d.get → get(m, key, default), x = 1 → let/set). The messages quoted below are the engine's own. Since engine v0.6.45 synsema check also catches a call to a name that exists nowhere before the line runs: len(xs) is the warning `there is no task, variable or builtin named 'len' in this file; it fails when that line runs — did you mean length(x)?` (a warning: the exit code does not change).
Every runtime claim below is asserted by this passing doctest. The parse-time hints (a Python keyword or operator that stops the program from loading) cannot run inside a test, so they are quoted from synsema check:
-- Doc example: the Python → Synsema divergences that bite the hardest.
-- Every runtime claim in the translation-table page is asserted here (doctested).
-- Parse-time hints (`x = 5`, `return v`, `elif`, `continue`, f"…"…) stop a program from
-- loading, so they cannot live in a test block: the page quotes them from `synsema check`.
intent: "doc example: python-to-synsema translation table"
require env("DOCS_UNSET_VAR")
-- The message of the error `f` raises, or nothing when it does not raise.
task error_of(f)
try
f()
recover err
give err
give nothing
task boom()
raise("kaput")
task reraise_error()
try
boom()
recover err
raise(err)
task add(a, b)
give a + b
-- With value semantics a task gets its own copy: the caller sees a change only if it is given back.
task mark(m)
set m["seen"] to true
give m
test "assignment is let/set, not = (x = 5 is a parse error)"
let x be 1
set x to 2
assert_eq(x, 2)
test "None/True/len/str are undefined names, and the error names the Synsema form"
assert(contains(error_of(() => None), "in Synsema: nothing"))
assert(contains(error_of(() => True), "in Synsema: true"))
assert(contains(error_of(() => len([1])), "in Synsema: length(x)"))
assert(contains(error_of(() => str(1)), "in Synsema: text(x)"))
assert(contains(error_of(() => dropna([])), "drop_missing(rows)"))
assert_eq(nothing == nothing, true)
assert(is_missing(nothing))
test "no methods: xs.append / d.get fail with the builtin to use"
let xs be [1]
let d be {"a": 1}
assert(contains(error_of(() => xs.append(2)), "set xs to append(xs, item)"))
assert(contains(error_of(() => d.get("a")), "get(m, key, default)"))
test "try/recover (not try/except); recover SWALLOWS unless you raise(err)"
let seen be ""
try
boom()
recover err
set seen to err
assert(contains(seen, "kaput"))
assert_error(reraise_error)
test "and/or short-circuit and always yield a bool"
let m be {"a": 1}
assert_eq(contains(m, "b") and m["b"] == 1, false)
assert_eq(contains(m, "a") and m["a"] == 1, true)
assert_eq(contains(m, "a") or m["zz"] == 1, true)
assert_eq(1 or 0, true)
test "truthiness: nothing, false, 0, empty text/list/map are falsy"
each v in [nothing, false, 0, "", [], {}]
assert_eq(when v then "t" otherwise "f", "f")
each v in [1, "a", [0], {"k": 0}]
assert_eq(when v then "t" otherwise "f", "t")
test "append/insert return a NEW list; reassign with set"
let xs be [1, 2]
let ys be append(xs, 3)
assert_eq(xs, [1, 2])
assert_eq(ys, [1, 2, 3])
assert_eq(insert([1, 2, 3], 1, 9), [1, 9, 2, 3])
assert_eq(insert([1], -1, 0), [0, 1])
assert(contains(error_of(() => insert([1], 5, 0)), "out of range"))
test "lists and maps are values: a copy never changes behind your back"
let xs be [1, 2]
let ys be xs
set ys[0] to 9
assert_eq(xs, [1, 2])
let orig be {"a": 1}
let changed be mark(orig)
assert_eq(orig, {"a": 1})
assert_eq(changed, {"a": 1, "seen": true})
let rows be [{"p": 1}, {"p": 2}]
each r in rows
set r["p"] to 0
assert_eq(rows, [{"p": 1}, {"p": 2}])
set rows[0]["p"] to 5
assert_eq(rows, [{"p": 5}, {"p": 2}])
let nested be {"a": {}}
set nested["a"]["b"] to 1
assert_eq(nested, {"a": {"b": 1}})
assert_eq(apply((r) => merge(r, {"x": 1}), [{"p": 1}]), [{"p": 1, "x": 1}])
test "strict arity: a missing or an extra argument is an error"
assert(contains(error_of(() => add(1)), "missing argument 'b'"))
assert(contains(error_of(() => add(1, 2, 3)), "takes 2 arguments, got 3"))
assert(contains(error_of(() => append([1], 2, 3)), "takes at most 2 arguments"))
assert(contains(error_of(() => round(2.567, 2)), "round_to(x, n)"))
assert_eq(round_to(2.567, 2), 2.57)
test "f-string equivalent is the backtick string; quoted strings stay literal"
let n be 1
assert_eq(`n={n + 1}`, "n=2")
assert_eq(`xs={[1, "a"]} v={nothing}`, "xs=[1, \"a\"] v=nothing")
assert(contains("n={1+1}", "{"))
assert(contains(error_of(() => "%d" % 5), "there is no %-formatting"))
test "fmt is strict like str.format: a {name} without a value is an error"
assert_eq(fmt("{a}-{b}", {"a": 1, "b": 2}), "1-2")
assert_eq(fmt("{{a}}", {}), "{a}")
assert(contains(error_of(() => fmt("{a}", {})), "no value for {a}"))
test "\u escapes like Python and JSON; \x is not an escape"
assert_eq("\u00e9", "é")
assert_eq("\u{1F600}", "😀")
assert_eq(length("\uD83D\uDE00"), 1)
assert_eq(length("\\u00e9"), 6)
assert_eq(length("C:\users"), 8)
assert_eq(length("C:\build\x64"), 12)
test "each over a map walks its keys; items gives key and value"
let ks be []
each k in {"x": 1, "y": 2}
set ks to append(ks, k)
assert_eq(ks, ["x", "y"])
assert_eq(items({"x": 1}), [{"key": "x", "value": 1}])
assert_eq(keys({"x": 1}), ["x"])
assert_eq(values({"x": 1}), [1])
let cs be []
each c in "ab"
set cs to append(cs, c)
assert_eq(cs, ["a", "b"])
let bs be []
each b in bytes([1, 255])
set bs to append(bs, b)
assert_eq(bs, [1, 255])
test "comprehension equivalent: apply + where; slicing is slice()"
assert_eq(apply((x) => x * 2, where([1, 2, 3], (x) => x > 1)), [4, 6])
assert_eq(apply([1, 2], (x) => x + 1), [2, 3])
assert_eq(slice([10, 20, 30, 40], 1, 3), [20, 30])
assert_eq(slice([10, 20, 30, 40], -2), [30, 40])
assert_eq(reverse("abc"), "cba")
assert_eq(split("ab", ""), ["a", "b"])
test "negative and text indexes work like Python; an index must be an integer"
assert_eq([1, 2, 3][-1], 3)
assert_eq("abc"[0], "a")
assert_eq("abc"[-1], "c")
assert_eq([1, 2][1.0], 2)
assert(contains(error_of(() => [1, 2][1.7]), "index must be an integer"))
test "`in` / `not in`: list item, map KEY, substring; text needs text"
assert(2 in [1, 2])
assert(3 not in [1, 2])
assert("a" in {"a": 1})
assert("b" in "abc")
assert(bytes([2, 3]) in bytes([1, 2, 3]))
assert(contains([1, 2], 2))
assert(contains(error_of(() => 1 in "a1"), "text(x) in s"))
test "chained comparisons like Python"
let x be 5
assert(1 < x <= 10)
assert_eq(1 < x < 3, false)
test "text + number CONCATENATES; text + nothing/list/map is an error; no text * n"
assert_eq("a" + 1, "a1")
assert_eq("a" + true, "atrue")
assert(contains(error_of(() => "a" + nothing), "Cannot add text and nothing"))
assert(contains(error_of(() => "a" + [1]), "Cannot add text and list"))
assert_error(() => "ab" * 2)
assert_error(() => 1 + true)
test "inside a list or map, text is shown quoted"
assert_eq(text(["1", 1]), "[\"1\", 1]")
assert_eq(text("x"), "x")
test "len/str/sorted are length/text/sort; d.get(k, default) is get(m, k, default)"
assert_eq(length("abc"), 3)
assert_eq(text(42), "42")
assert_eq(sort([3, 1, 2]), [1, 2, 3])
assert_eq(sort([3, 1, 2], desc = true), [3, 2, 1])
assert_eq(sort([3, nothing, 1]), [1, 3, nothing])
assert(contains(error_of(() => sort([1, "a"])), "cannot order number and text"))
assert_eq(sort_by([{"n": 1}, {"n": 3}], (r) => r.n, desc = true), [{"n": 3}, {"n": 1}])
assert_eq(get({"a": 1}, "a"), 1)
assert_eq(get({"a": 1}, "b"), nothing)
assert_eq(get({"a": 1}, "b", 0), 0)
assert_eq(get([1, 2, 3], -1), 3)
assert_eq(remove({"a": 1, "b": 2}, "a"), {"b": 2})
assert_eq(merge({"a": 1, "b": 2}, {"b": 3, "c": 4}), {"a": 1, "b": 3, "c": 4})
assert(contains(error_of(() => {"a": 1}["b"]), "Map has no key 'b'"))
test "sum/min/max, variadic max, reduce, index_of gives nothing when absent"
assert_eq(sum([1, 2, 3]), 6)
assert_eq(min([3, 1]), 1)
assert_eq(max(1, 5, 2), 5)
assert_eq(max([1, nothing, 3]), 3)
assert_eq(max(1, 2,), 2)
assert_eq(reduce([1, 2, 3], (a, b) => a + b, 0), 6)
assert_eq(index_of([1, 2], 9), nothing)
assert_eq(index_of("hello", "l"), 2)
assert_eq(range(0, 10, 3), [0, 3, 6, 9])
assert(contains(error_of(() => range(0, 2.5)), "must be an integer"))
test "starts_with / replace / regex: regex_capture is always a list"
assert(starts_with("hello", "he"))
assert_eq(replace("a-b", "-", "+"), "a+b")
assert(matches("abc", "[a-c]+"))
assert_eq(matches("abcd1", "[a-c]+"), false)
assert_eq(regex_find_all("a1b2", "[0-9]"), ["1", "2"])
assert_eq(regex_capture("a1", "([a-z])([0-9])"), ["a", "1"])
assert_eq(regex_capture("a1", "[0-9]"), ["1"])
assert_eq(regex_capture("ab", "[0-9]"), nothing)
assert_eq(regex_replace("a1b2", "[0-9]", "#"), "a#b#")
test "int() is exact; base and fallback go by name"
assert_eq(int("-42"), -42)
assert_eq(int("1_000"), 1000)
assert_eq(int("0x1f"), 31)
assert_eq(int(3.0), 3)
assert_eq(int("123456789012345678901"), 123456789012345678901)
assert_eq(int("ff", base = 16), 255)
assert_eq(int("abc", fallback = 0), 0)
assert(contains(error_of(() => int("ff", 16)), "int(x, base = 16)"))
assert(contains(error_of(() => int("ff", 2.0)), "int(x, base = 2)"))
assert(contains(error_of(() => int(1.5)), "not a whole number"))
assert_eq(number("42"), 42.0)
assert_eq(is_integer(number("42")), false)
test "hex, 0x/0o/0b literals, is_integer/is_text/is_list/is_map"
assert_eq(hex(7960), "0x1f18")
assert_eq(hex(bytes([0, 255])), "0x00ff")
assert_eq(0x1f18, 7960)
assert_eq(0o17, 15)
assert_eq(0b101, 5)
assert(is_integer(1))
assert_eq(is_integer(1.0), false)
assert(is_text("a") and is_list([]) and is_map({}))
assert_eq(type_of(1), "number")
test "// is floor division like Python, exact for big integers; / is always float"
assert_eq(7 // 2, 3)
assert_eq(-7 // 2, -4)
assert_eq(-7 % 3, 2)
assert_eq((10 ** 30 + 7) // 2, 500000000000000000000000000003)
assert_eq(7 // 0.1, 69.0)
assert_eq(10 / 2, 5.0)
assert_eq(1 / 2, 0.5)
test "-2 ** 2 is -4 and 1e18 is a float, as in Python; big integers compare exactly"
assert_eq(-2 ** 2, -4)
assert_eq((-2) ** 2, 4)
assert_eq(2 ** 3 ** 2, 512)
assert_eq(is_integer(1e18), false)
assert_eq(10 ** 18, 1000000000000000000)
assert_eq(2 ** 53 + 1 == 9007199254740992.0, false)
assert_eq(json_decode("12345678901234567890"), 12345678901234567890)
test "round is banker's rounding; floor gives an integer"
assert_eq(round(2.5), 2)
assert_eq(round(3.5), 4)
assert_eq(floor(3.7), 3)
assert(is_integer(floor(3.7)))
test "hmac gives bytes; hex() shows them"
let mac be hmac("msg", "key")
assert_eq(type_of(mac), "bytes")
assert(starts_with(hex(mac), "0x"))
test "enumerate gives {index, item} maps (Python's for i, x in enumerate)"
let e be enumerate(["a", "b"])
assert_eq(e[0].index, 0)
assert_eq(e[1].item, "b")
assert_eq(length(enumerate([])), 0)
test "pandas reflexes: a table is a list of maps"
let rows be [{"r": "n", "m": 1}, {"r": "s", "m": 2}, {"r": "n", "m": nothing}]
assert_eq(summarize(rows, "r", {"total": sum_of("m"), "n": count()}), [{"r": "n", "total": 1, "n": 2}, {"r": "s", "total": 2, "n": 1}])
assert_eq(group_by(rows, "r")[1], {"key": "s", "items": [{"r": "s", "m": 2}]})
assert_eq(count_by(rows, "r"), [{"key": "n", "count": 2}, {"key": "s", "count": 1}])
assert_eq(drop_missing(rows, "m"), [{"r": "n", "m": 1}, {"r": "s", "m": 2}])
assert_eq(fill_missing(rows, {"m": 0})[2], {"r": "n", "m": 0})
assert_eq(join([{"id": 1, "a": 1}], [{"id": 1, "b": 2}], "id", "left"), [{"id": 1, "a": 1, "b": 2}])
assert_eq(collect(rows, "m"), [1, 2, nothing])
assert_eq(mean([1, nothing, 3]), 2.0)
assert_eq(round_to(std([1, 2, 3, 4]), 3), 1.291)
assert_eq(round_to(std([1, 2, 3, 4], ddof = 0), 3), 1.118)
assert_eq(csv_parse("a,b\n1,\n", {"types": {"a": "int"}}), [{"a": 1, "b": nothing}])
assert_eq(random_int(rng(42), 1, 6), 1)
test "os.environ.get: env(name, nothing) is nothing when undefined (engine v0.6.45+)"
assert_eq(env("DOCS_UNSET_VAR", nothing), nothing)
assert_eq(env("DOCS_UNSET_VAR", true), "true") -- Synsema text, not Python's "True"
assert(contains(error_of(() => env("DOCS_UNSET_VAR")), "DOCS_UNSET_VAR"))Syntax reflexes§
| In Python | In Synsema | ⚠️ Divergence |
|---|---|---|
x = 5 … x = 6 | let x be 5 … set x to 6 | x = 5 → `declare with let x be …, change it with set x to … . = exists ONLY in default params / named args: task f(x, y = 1), f(x, y = 2)` |
x += 1 | set x to x + 1 | no compound assignment: `Synsema has no compound assignment (+=): write set x to x + 1` |
# comment / // comment | -- comment | # → Unexpected character: '#'. // is integer division, so a C-style comment fails with the hint `comments in Synsema start with -- ` |
if / elif / else: | when / otherwise when / otherwise (no colon) | a trailing : → Synsema blocks have no colon; if, elif, else → `is not a Synsema statement with the form to write (chain with otherwise when <condition> , the fallback branch is otherwise `) |
x if c else y / c ? x : y | when c then x otherwise y | inline expression form, usable in let/args; both Python and C forms → `the inline form is when c then a otherwise b ` |
a and b / a && b / a; b | a and b / one statement per line | &, | → `the logical operators are and / or ; ; → one statement per line (no ;)` |
for x in xs: | each x in xs | for → parse error |
break / continue / pass | stop / wrap the rest of the body in when / leave the branch out | there is no continue: `skip to the next item by wrapping the rest of the body in when ; break → leave a loop with stop ` |
d["k"] = v (add or overwrite a key) | set m["k"] to v | m["k"] reads it, "k" in m tests it. A set path starts from a variable (set m["a"]["b"] to v) |
for k in a_dict: / for k, v in d.items(): | each k in m / each e in items(m) … e.key / e.value | each over a map walks its keys in insertion order; over text, its characters; over bytes, ints 0–255 |
for i, x in enumerate(xs): | each e in enumerate(xs) … e.index / e.item | enumerate(list) → [{index, item}, …]; engine v0.6.46+ also over a numeric array (its first axis), which each does not walk |
while c: | while c | same keyword, no colon, no iteration cap |
def f(x): return v | task f(x) … give v | load-time errors that name the Synsema word: `return is not a Synsema statement: give <value> returns from a task`, def is not a Synsema statement: a function is task name(params) …`` |
f(1) on def f(a, b) | a missing or extra argument is an error | task 'f' is missing argument 'b' — pass it, or give the parameter a default, task 'f' takes 2 arguments, got 3; builtins too: append() takes at most 2 arguments, got 3 |
lambda x: x + 1 | (x) => x + 1 | lambda y: y → a lambda is (y) => y |
None / True / False | nothing / true / false | capitalized forms parse, then fail naming the Synsema form: `Undefined variable: 'None' — in Synsema: nothing` |
x is None | x == nothing / is_missing(x) | no is operator for identity (is belongs to match); the parse error says so |
os.environ.get("K") / os.getenv("K", "d") | env("K", nothing) / env("K", "d") (needs require env("K")) | an undefined variable with a nothing default is nothing (engine v0.6.45+; before it was the text "None"); another default comes back as Synsema text: env("K", true) → "true", not "True". Without a default, an undefined variable is an error |
f"n={n}" | ` n={n} ` (backtick string) | f"..." → `an f-string is a template with backticks here. A hole takes any value (lists, maps, nothing), like an f-string. Quoted "..." strings do NOT interpolate ("{n}" stays literal) and a literal newline inside them is Unterminated string` — backticks do both |
"%d" % x / "{}".format(x) | ` {x} / fmt("{x}", {"x": x})` | % on text → there is no %-formatting: use a template. fmt is strict like str.format: a {name} without a value is an error; {{/}} are literal braces |
"""multi-line""" | ` multi-line ` | backticks allow real newlines + {expr} |
"\u00e9", "\U0001F600" | "\u00e9", "\u{1F600}" | a surrogate pair "\uD83D\uDE00" is one character; for a literal backslash + u write "\\u". \x is not an escape, and an incomplete \u stays as written ("C:\users") |
[f(x) for x in xs if p(x)] | apply(f, where(xs, p)) | comprehension syntax → no comprehensions: apply(xs, (v) => …) to transform, where(xs, (v) => …) to filter |
xs[-1], s[0] | xs[-1], s[0] | same: negative indexes count from the end (lists, text, bytes); text is indexed by character. An index must be an integer (xs[1.7] → index must be an integer, got 1.7; xs[1.0] is fine) |
xs[1:3], xs[-2:] | slice(xs, 1, 3), slice(xs, -2) | [1:3] → no slice syntax: slice(xs, start, end?); slice takes Python-style negatives, works on lists/text/bytes |
x in xs / x not in xs | x in xs / x not in xs | same: list item, map key, substring, bytes subsequence. Text needs text: 1 in "a1" → an error (text(1) in "a1"). contains(xs, x) is the function form |
a < x <= b | a < x <= b | chained comparisons like Python; each operand is evaluated once |
try/except E as e: | try … recover err | except → `except is not Synsema: after the try block write recover err. err is the message TEXT (no exception types/hierarchy). recover SWALLOWS by default — re-propagate with raise(err)` |
raise ValueError("x") | raise("x") (or statement raise "x") | one error kind only |
import json, import requests | nothing to import — builtins are global | import x → `import is not a Synsema statement: import a local module with use "./file.syn" as name`. JSON/HTTP/etc. are builtins gated by capabilities (below) |
from mymodule import f | use "./mymodule.syn" as m … m.f() | only local .syn modules; exports need export — Modules |
class Person: | type Person (fields) + plain tasks | no methods/inheritance/self; construct Person("Alice", 30), access p.name / name of p / p["name"]. xs.append(2) → Synsema has no methods: set xs to append(xs, item) |
match/case | match … is pattern | arms use is, default is otherwise — Control flow |
Also: the LLM words reason / decide / analyze / generate (like every keyword — to, type, match…) are reserved as names you bind (variables, parameters, tasks): let reason be 1 → 'reason' is a reserved word in Synsema. After a . any word is fine (tx.to, ev.type, mod.decide(…)). Name things resolve, why, etc.
Builtin equivalents (methods are plain tasks)§
| In Python | In Synsema |
|---|---|
len(x) / type(x) | length(x) (text/list/map/bytes/array) / type_of(x) → "number", "text", … |
str(x) / int(s) / float(s) | text(x) / int(s) (exact integer: "-42", "1_000", "0x1f", a whole float; int(1.5) errors — floor/round/trunc on purpose) / number(s) (always float) |
int(s, 16) | int(s, base = 16). The base and the fallback go by name: int(s, fallback = 0) returns 0 instead of raising. int("ff", 16) (any whole number 2..36 in second place, 2.0 included) is an error that names both forms |
hex(n) / b.hex() | hex(n) → "0x1f18", hex(b) → "0x00ff" (always 0x-prefixed) |
isinstance(x, int) / str / list / dict | is_integer(x) / is_text(x) / is_list(x) / is_map(x) — is_integer(1.0) is false |
a // b / a % b | a // b / a % b — floor like Python (-7 // 2 → -4), exact for integers of any size; on floats exactly CPython's result (7 // 0.1 → 69.0) |
round(x, 2) | round_to(x, 2) — round takes one argument: round(x, 2) → round() takes at most 1 argument, got 2 — to round to n decimals: round_to(x, n) |
xs.append(x) (mutates) | append(xs, x) → returns a NEW list; reassign: set xs to append(xs, x) (constant time per step, fine in a loop) |
xs.insert(i, x) | set xs to insert(xs, i, x) — negatives like Python, but an out-of-range i is an error (Python clamps) |
s.upper() / s.lower() / s.strip() | upper(s) / lower(s) / trim(s) |
s.split(",") / ",".join(xs) | split(s, ",") / join(xs, ",") |
s.startswith(p) / s.replace(a, b) / s.find(p) | starts_with(s, p) / replace(s, a, b) / index_of(s, p) |
sorted(xs) / sorted(xs, key=f, reverse=True) | sort(xs) / sort_by(xs, f, desc = true) (stable; nothing and NaN go last; mixing numbers and text is an error) |
sum(xs) / min(xs) / max(xs) | sum(xs) / min(xs) / max(xs) (also variadic max(a, b, c); nothing values are skipped) |
map(f, xs) / filter(p, xs) | apply(f, xs) / where(xs, p) — both accept either argument order |
functools.reduce(f, xs, init) | reduce(xs, f, init) |
xs.index(v) (raises) | index_of(xs, v) → nothing when absent (not -1, no error) |
d.get(k) / d.get(k, default) | get(m, k) → nothing when missing / get(m, k, default); also lists: get(xs, -1) |
d.pop(k) / {a, b} | remove(m, k) / merge(a, b) — both return a NEW map (in merge the rightmost value wins) |
d.keys() / d.values() / d.items() | keys(m) / values(m) / items(m) → [{key, value}, …] |
json.dumps(x) / json.loads(s) | json_encode(x) / json_decode(s) (pure, no import; big integers stay exact; a secret inside is an error, engine v0.6.46+) — JSON |
pd.read_parquet(p, columns=["a"]) | parquet_scan(p, {"columns": ["a"]}) (engine v0.6.46+; only those pages are read from disk; require file.read(p)) — Data analysis |
range(n) | range(n) → a real list (also range(a, b, step); integers only) |
print(...) | print(...) — written immediately; text inside a list or map prints quoted: ["1", 1] |
re.fullmatch / re.findall / re.search(p, s).groups() / re.sub | matches(s, pat) (FULL match) / regex_find_all(s, pat) / regex_capture(s, pat) (always a list — the groups, or [match] without groups — or nothing) / regex_replace(s, pat, rep) — Builtins |
hmac.new(k, m, sha256).digest() | hmac(m, k) → bytes (hex(mac) to show it) |
open(p).read() / requests.get(url) | read_file(p) + require file(...) / fetch(url) + require net(host) — Files, HTTP |
requests.post(url, json=d) / r.json() / r.content | http_post(url, d) (a map → JSON + Content-Type) / json of r (nothing when not JSON) / http_bytes(...) → bytes of r — HTTP |
xs[::-1] / list(s) | reverse(xs) (also text) / split(s, "") |
os.getcwd() / os.path.expanduser("~/x") | cwd() + require file.read(".") / "~/x" works as-is in paths and scopes — Files |
os.remove(p) / shutil.rmtree(p) | delete_file(p) / delete_dir(p, {"recursive": true}) + file.write on every path |
zipfile / tarfile | zip_create/zip_extract / tar_create/tar_extract — zip-slip rejected, real-bytes ceiling (native) |
tomllib.loads(s) / xmltodict.parse(s) | toml_parse(s) (dates → ISO text) / xml_parse(s) (same shape) — JSON, XML & TOML |
Data analysis — pandas / numpy reflexes§
A table is a list of maps (no DataFrame, no index): every operation takes rows and gives rows. The pandas names themselves (dropna, fillna, groupby, value_counts, read_csv…) fail naming the Synsema builtin (`Undefined variable: 'dropna' — in Synsema: drop_missing(rows)`).
| In Python | In Synsema |
|---|---|
df.groupby("r").agg(total=("m", "sum"), n=("m", "size")) | summarize(rows, "r", {"total": sum_of("m"), "n": count()}) — groups in first-appearance order |
for k, g in df.groupby("r"): | each g in group_by(rows, "r") … g.key / g.items — a list of {key, items}, not a dict |
df["c"].value_counts() | count_by(rows, "c") → [{key, count}] |
df.dropna(subset=["m"]) / df.fillna({"m": 0}) | drop_missing(rows, "m") / fill_missing(rows, {"m": 0}) — missing is nothing |
df.merge(o, on="id", how="left") | join(rows, o, "id", "left") (join(xs, sep) with two arguments is still the text join) |
df["c"] / df[df.x > 2] | collect(rows, "c") / where(rows, (r) => r.x > 2) |
df.mean() (skips NaN) | mean(xs) skips nothing; NaN propagates |
df.std() / np.std(xs) | std(xs) is the sample figure, like pandas / std(xs, ddof = 0), like numpy |
pd.read_csv(p, dtype={"a": int}) | csv_parse(read_file(p), {"types": {"a": "int"}}) — untyped columns stay text; an empty cell is nothing |
np.random.default_rng(42).integers(1, 7) | random_int(rng(42), 1, 6) — the same numbers as numpy; the upper bound is inclusive |
The full pipeline (CSV, statistics, charts) is in CSV, stats & charts; every table builtin is in Builtins.
Semantic traps — looks like Python, behaves differently§
| It looks like | What actually happens (doctested above) |
|---|---|
a and b short-circuits and returns the operand (x or "default") | Short-circuits too but always returns a bool — x or "default" is true/false, never the default. Use when x == nothing … set x to "default", or get(m, k, default) for a map |
xs.append mutates in place | append (and friends) return new values; the original is untouched. Reassign with set |
| Two names for one list/dict see each other's changes | Lists and maps are values: let ys be xs + set ys[0] to 9 leaves xs alone, and a task that sets inside a map it received does NOT change the caller's — give it back and set it in the caller |
for r in rows: r["x"] = 1 updates the rows | each r in rows + set r["x"] to 1 changes the loop's copy (synsema check warns). Write set rows to apply((r) => merge(r, {"x": 1}), rows), or by index: set rows[i]["x"] to 1 |
d["missing"] → KeyError you catch by type | Map has no key 'missing' — catchable only as try/recover (message text). For an optional key, get(m, "missing", default) |
"a" + 1 → TypeError | It concatenates: "a" + 1 → "a1" (text + number/bool). But text + nothing/list/map/bytes IS an error (Cannot add text and nothing — convert it on purpose: text(x), or interpolate it), and so are "ab" * 2 and 1 + true — no repetition, no bool arithmetic |
except: keeps the program dying | recover swallows the error entirely (task ends normally). To fail upward, raise(err) inside recover |
More traps (also engine-verified): Counter your priors and Errors & exit codes.
Where Python intuition is SAFE (verified — trust it)§
- Division always returns float (
10 / 3→3.33…,10 / 2→5.0), like Python 3. Floor division is//. -2 2→-4,2 3 2→512, and1e18is a float — exactly like Python. Write an exact big integer as1018.- Integers have no size limit, and comparing an integer with a float is exact (
2 ** 53 + 1 == 9007199254740992.0→false);json_decodekeeps big integers exact. - Iterating a dict yields its keys (
each k in m);in,not in, negative indexes and chained comparisons work as in Python. - Calling with a missing or an extra argument is an error.
round()is banker's rounding, same as Python:round(2.5)→2,round(3.5)→4.- Truthiness:
nothing/false/0/""/[]/{}are falsy, everything else truthy. [1] + [2]→[1, 2](list concatenation),sliceaccepts negative indices.- Map literals
{"k": v}and list literals look and nest like dicts/lists; a trailing comma is fine in lists, maps and calls. - Indentation defines blocks (4 spaces), comments to end of line,
and/or/notare words.
No Python equivalent — read the topic page before using§
- Capabilities: I/O is deny-by-default; declare
require net("host")/file(...)/
db(...) / serve(PORT) / llm at the top or calls fail → Capabilities
- LLM ops as keywords:
decide between [...] given x,generate,analyze,reason→ LLM primitives - Agents/concurrency:
agent/spawn/share/observe/signal/wait_for,
parallel_map → Multi-agent
- HTTP server as syntax:
serve on 8080+route "GET /x"blocks → Serve - FastAPI reflexes:
@app.post("/x")+ a Pydantic model ↔route "POST /x"+expect body {…};/docsand/openapi.json↔ the same URLs, generated;app.openapi()↔synsema openapi app.syn→ Build an API - Secrets:
secret("KEY")values that never print/serialize → Secrets - Human-in-the-loop:
approve/confirm/ask/show→ Human-in-the-loop - Tests in-file:
test "name"blocks +assert_eq, run bysynsema test→ CLI