as
One Python, three syntaxes. as converts Python source between the indentation skin you know and two it can wear: C-style braces and Lisp-style S-expressions. Same semantics, different look — hence the name: as lisp, as brace, as py.
def quicksort(items): def quicksort(items) {
if len(items) <= 1: if len(items) <= 1 {
return items return items;
pivot, *rest = items; }
lo = [x for x in rest pivot, *rest = items;
if x < pivot] lo = [x for x in rest if x < pivot];
hi = [x for x in rest hi = [x for x in rest if x >= pivot];
if x >= pivot] return quicksort(lo) + [pivot] + quicksort(hi);
return quicksort(lo) + [pivot] }
+ quicksort(hi)
(defn quicksort (items)
(if (<= (len items) 1) (return items))
(set! '(pivot (* rest)) items)
(set! lo (list-comp x (for [x in rest]) (when (< x pivot))))
(set! hi (list-comp x (for [x in rest]) (when (>= x pivot))))
(return (+ (+ (quicksort lo) [pivot]) (quicksort hi))))
Why
“Braces or indentation” is one of the oldest holy wars in programming. as does not pick a side — it makes the question meaningless. Write in whichever skin you like, read in whichever skin you like, and argue about taste instead of syntax. The round trip is exact: converting py → skin → py yields an AST identical to the original (tested on the whole as codebase and 40 stdlib modules, see Tests).
Install & use
pip install like-a
as lisp foo.py # Python → S-expressions (stdout)
as brace foo.py -o foo.cpy
as py foo.lpy # S-expressions → Python
cat foo.py | as brace - --from py
Input style is detected from the extension: .py, .cpy (braces), .lpy (lisp). Use --from to override.
The importable package is aslang (from aslang import convert) because as is a Python keyword. The CLI keeps the two-letter name (as lisp, as brace) — same phrase as like a lisp; the distribution on PyPI is like-a, since PyPI forbids Python keywords as project names.
The three skins
| skin | extension | blocks | collections |
|---|---|---|---|
| py | .py |
indentation | (), [], {}, {k: v} |
| brace | .cpy |
if cond { ... } |
unchanged (Python expressions) |
| lisp | .lpy |
(if cond ...) |
[...] list, '(...) tuple, {(k v)} dict, #{...} set |
The lisp skin is more than a re-bracketing: operators go prefix ((+ a (* b c))), methods become calls ((len items), (items.append x)), comprehensions get for/when clauses, match cases carry their patterns as data. The brace skin stays deliberately close to Python — only the block structure changes.
lpy form reference (selected)
(defn name (params...) [-> ret] body...) (set! target value)
(for [x in iter] body... [(else ...)]) (while cond body...)
(if test then... [(elif ...)...] [(else ...)])
(with [(expr as name) ...] body...)
(try body... (except Type [as e] h...)...)
(match subject (case pattern [(when guard)] body...))
patterns: literal | name | _ | (as pat name) | (or p...) | [p *rest]
{(key pat) ... (** rest)} | (Cls p... (kws (attr pat)...))
(list-comp elt (for [x in xs]) (when cond))
(fstr "x = " (fv value) "!")
Correctness
- py ↔ brace converts text directly on the token stream: comments, formatting and even docstrings survive byte-for-byte outside the changed block lines.
- anything ↔ lisp goes through the
astmodule: the reader builds a real AST, the printer walks it. Comments are preserved best-effort (dropped by theast, re-attached by line numbers). - Round-trip proof:
ast.dump(ast.parse(src)) == ast.dump(parse(convert( convert(src, py, skin), skin, py)))for every corpus file.
Compared
| bython | Hy | Nim | as | |
|---|---|---|---|---|
| direction | → py | → py AST | own lang | any → any |
| braces skin | ✓ | — | ✓ | ✓ |
| S-expression skin | — | ✓ (own) | — | ✓ |
| round-trip guaranteed | — | — | — | ✓ (AST-equal) |
| Python semantics | ✓ | mostly | no | ✓ exactly |
Limitations
matchis supported in all skins;typing.TypeAliasstatements andtry*-era syntax are not yet in the lisp skin.- The lisp reader accepts a fixed set of special forms; arbitrary macro machinery is out of scope (this is a converter, not a language).
- A case body that begins with a call to a function literally named
whenneeds parentheses discipline (the(when ...)guard marker wins).
Possible next
An indentation skin for C (drop braces, blocks by indentation) is deliberately out of scope for now: in C, braces are load-bearing (enum {A, B}, aggregate initializers, GNU ({ ... }) statements), so separating block braces from literal braces needs a real parser (libclang/tree-sitter), not token rewriting - and the round trip would be AST-level, not byte-level. Demand cuts both ways: some like everything about Nim except its Python-style indentation, and Nim has no brace form (its braces are taken by set/table literals and pragmas)
- so skins could run in both directions. No slight to Nim: braces-for-data is a design choice, the canonical indentation form stays canonical - skins would add contributors, not churn. Interest-gated: if people ask for it, it may happen.
Note on the name
Yes, as shadows GNU as (the assembler) if ~/.local/bin precedes /usr/bin in your PATH. If you build C, keep your PATH straight or alias. The two-letter namespace was simply too good to leave unused — import json as j, as lisp foo.py.
Development
pip install -e .
pytest # 92 tests: unit + stdlib corpus + self-application
The tool digests its own source in the test suite: every module of aslang is converted to both skins, back, and the result must be AST-identical — and must still execute.
MIT license.