---
slug: 01a-python-to-synsema
title: Python → Synsema
description: The translation table — write Synsema by translating the Python you already know, with every semantic divergence flagged and doctested.
example_ids: [python-diff]
---

# 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)`).

Every semantic claim below is asserted by this passing doctest:

```synsema
-- Doc example: the Python → Synsema divergences that bite the hardest.
-- Every claim in the translation-table page is asserted here (doctested).
intent: "doc example: python-to-synsema translation table"

task boom()
    raise("kaput")

task reraise_error()
    try
        boom()
    recover err
        raise(err)

task bad_return()
    return 5

task give_none()
    give None

task iterate_map_directly()
    each k in {"a": 1}
        print(k)

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 "Python 'return'/'None'/'True' PARSE but are undefined names — use give/nothing/true"
    assert_error(bad_return)
    assert_error(give_none)
    assert_eq(nothing == nothing, true)

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 (engine v0.6.10+) 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 "append returns a NEW list (no mutating .append); 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])

test "f-string equivalent is the backtick string; quoted strings stay literal"
    assert_eq(`n={1 + 1}`, "n=2")
    assert(contains("n={1+1}", "{"))

test "each cannot iterate a map — go through keys()"
    assert_error(iterate_map_directly)
    let ks be []
    each k in keys({"x": 1, "y": 2})
        set ks to append(ks, k)
    assert_eq(ks, ["x", "y"])

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(slice([10, 20, 30, 40], 1, 3), [20, 30])
    assert_eq(slice([10, 20, 30, 40], -2, 4), [30, 40])

test "the `in` operator is contains(); on maps it checks KEYS"
    assert(contains([1, 2], 2))
    assert(contains("abc", "b"))
    assert(contains({"a": 1}, "a"))

test "text + number CONCATENATES (unlike Python); text * n does not exist"
    assert_eq("a" + 1, "a1")
    assert_error(() => "ab" * 2)

test "len/str/sorted are length/text/sort_by; d.get(k, default) does not exist"
    assert_eq(length("abc"), 3)
    assert_eq(text(42), "42")
    assert_eq(sort_by([3, 1, 2], (x) => x), [1, 2, 3])
    assert_error(() => get({"a": 1}, "a"))
    assert_error(() => {"a": 1}["b"])

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)
```

## Syntax reflexes

| In Python | In Synsema | ⚠️ Divergence |
|---|---|---|
| `x = 5` … `x = 6` | `let x be 5` … `set x to 6` | `x = 5` → parse error `Unexpected token: ASSIGN ('=')`. `=` exists ONLY in default params / named args: `task f(x, y = 1)`, `f(x, y = 2)` |
| `# comment` | `-- comment` | `#` → `Unexpected character: '#'` |
| `if / elif / else:` | `when / otherwise when / otherwise` (no colon) | a trailing `:` → parse error; `elif` is not a word |
| `x if c else y` | `when c then x otherwise y` | inline expression form, usable in `let`/args |
| `for x in xs:` | `each x in xs` | `for` → parse error |
| `for k in a_dict:` | `each k in keys(m)` | **`each` cannot iterate a map**: `Cannot iterate over map`. Go through `keys(m)`/`values(m)` |
| `for i, x in enumerate(xs):` | `each e in enumerate(xs)` … `e.index` / `e.item` | `enumerate(list)` → `[{index, item}, …]` |
| `while c:` | `while c` | same keyword, no colon; runaway loops hit `Loop exceeded maximum iterations` |
| `def f(x): return v` | `task f(x)` … `give v` | `def` → parse error. `return` PARSES as a plain name, then fails at runtime: `Undefined variable: 'return'` — the word is `give` |
| `lambda x: x + 1` | `(x) => x + 1` | — |
| `None` / `True` / `False` | `nothing` / `true` / `false` | capitalized forms parse, then fail: `Undefined variable: 'None'` (same for `True`/`False`) |
| `x is None` | `x == nothing` | no `is` operator for identity (`is` belongs to `match`) |
| `f"n={n}"` | `` `n={n}` `` (backtick string) | `f"..."` → parse error. **Quoted `"..."` strings do NOT interpolate** (`"{n}"` stays literal) and a literal newline inside them is `Unterminated string` — backticks do both |
| `"""multi-line"""` | `` `multi-line` `` | backticks allow real newlines + `{expr}` |
| `[f(x) for x in xs if p(x)]` | `apply(f, where(xs, p))` | comprehension syntax → parse error |
| `xs[1:3]`, `xs[-2:]` | `slice(xs, 1, 3)`, `slice(xs, -2, length(xs))` | `[1:3]` → parse error; `slice` takes Python-style negatives, works on lists/text/bytes |
| `x in xs` (operator) | `contains(xs, x)` | `in` is only valid inside `each`. On maps `contains` checks KEYS |
| `try/except E as e:` | `try` … `recover err` | `except` → parse error. `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; on engine ≤ v0.5.1 use the parens form |
| `import json`, `import requests` | nothing to import — builtins are global | `import x` parses as a name and fails: `Undefined variable: 'import'`. 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](/en/0.6.x/14-modules) |
| `class Person:` | `type Person` (fields) + plain tasks | no methods/inheritance/`self`; construct `Person("Alice", 30)`, access `p.name` / `name of p` / `p["name"]` |
| `match/case` | `match` … `is pattern` | arms use `is`, default is `otherwise` — [Syntax](/en/0.6.x/10-syntax) |

Also: the LLM words **`reason` / `decide` / `analyze` / `generate` are reserved
everywhere** (even as member/param names) — `let reason be 1` → `'reason' is a reserved
word in Synsema`. Name things `resolve`, `why`, etc.

## Builtin equivalents (methods are plain tasks)

| In Python | In Synsema |
|---|---|
| `len(x)` | `length(x)` (text/list/map/bytes/array) |
| `str(x)` / `int(s)` / `float(s)` | `text(x)` / `number(s)` (always float; `floor()` to get an integer) |
| `xs.append(x)` (mutates) | `append(xs, x)` → **returns a NEW list**; reassign: `set xs to append(xs, x)` |
| `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)` | `starts_with(s, p)` / `replace_text(s, a, b)` |
| `sorted(xs, key=f)` / `reverse=True` | `sort_by(xs, f)` / `sort_by(xs, (x) => 0 - x)` (no bare `sort`) |
| `sum(xs)` / `min(xs)` / `max(xs)` | `sum(xs)` / `min(xs)` / `max(xs)` (also variadic `max(a, b, c)`) |
| `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) / `v in xs` | `index_of(xs, v)` → **`nothing`** when absent (not -1, no error) |
| `d.get(k, default)` | does not exist — `when contains(m, "k")` then index (nested `when`, see traps) |
| `d.keys()` / `d.values()` / `d.items()` | `keys(m)` / `values(m)` / no `items` — iterate `keys(m)` and index |
| `json.dumps(x)` / `json.loads(s)` | `json_encode(x)` / `json_decode(s)` (pure, no import) — [JSON](/en/0.6.x/36-json) |
| `range(n)` | `range(n)` → a real list (also `range(a, b, step)`) |
| `print(...)` | `print(...)` (buffered under `run` until exit — `flush()` for live output) |
| `re.fullmatch` / `re.findall` | `matches(s, pat)` (FULL match) / `find_all(s, pat)` — [Builtins](/en/0.6.x/90-builtins) |
| `open(p).read()` / `requests.get(url)` | `read_file(p)` + `require file(...)` / `fetch(url)` + `require net(host)` — [Files](/en/0.6.x/30-io-files), [HTTP](/en/0.6.x/32-http-client) |

## 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 (engine v0.6.10+) but **always returns a bool** — `x or "default"` is `true`/`false`, never the default. Use `when x == nothing` … `set x to "default"` |
| `xs.append` mutates in place | `append` (and friends) return new values; the original is untouched. Reassign with `set` |
| `d["missing"]` → KeyError you catch by type | `Map has no key 'missing'` — catchable only as `try/recover` (message text) |
| `"a" + 1` → TypeError | **It concatenates**: `"a" + 1` → `"a1"` (text + number coerces). But `"ab" * 2` and `1 + true` ARE errors — 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` |
| iterating a dict yields keys | `each` over a map is an ERROR — use `keys(m)` |

More traps (also engine-verified): [Counter your priors](/en/0.6.x/01-counter-your-priors)
and [Errors & exit codes](/en/0.6.x/92-errors).

## Where Python intuition is SAFE (verified — trust it)

- Division always returns float (`10 / 3` → `3.33…`), like Python 3. Floor-div: `floor(a / b)`.
- `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), `slice` accepts negative indices.
- Map literals `{"k": v}` and list literals look and nest like dicts/lists.
- Indentation defines blocks (4 spaces), comments to end of line, `and`/`or`/`not` are 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](/en/0.6.x/20-capabilities)
- **LLM ops as keywords**: `decide between [...] given x`, `generate`, `analyze`, `reason` → [LLM primitives](/en/0.6.x/50-llm-primitives)
- **Agents/concurrency**: `agent` / `spawn` / `share` / `observe` / `signal` / `wait_for`,
  `parallel_map` → [Multi-agent](/en/0.6.x/60-agents)
- **HTTP server as syntax**: `serve on 8080` + `route "GET /x"` blocks → [Serve](/en/0.6.x/40-serve)
- **FastAPI reflexes**: `@app.post("/x")` + a Pydantic model ↔ `route "POST /x"` + `expect body {…}`; `/docs` and `/openapi.json` ↔ the same URLs, generated; `app.openapi()` ↔ `synsema openapi app.syn` → [Build an API](/en/0.6.x/43-build-api)
- **Secrets**: `secret("KEY")` values that never print/serialize → [Secrets](/en/0.6.x/21-secrets)
- **Human-in-the-loop**: `approve` / `confirm` / `ask` / `show` → [Human-in-the-loop](/en/0.6.x/62-human)
- **Tests in-file**: `test "name"` blocks + `assert_eq`, run by `synsema test` → [CLI](/en/0.6.x/70-cli)
