Documentation
Everything SHE does, in the order you are likely to need it. Every snippet on this page runs as written — paste any of them into the playground.
Install
pip install she-lang
That is the whole installation. SHE has no dependencies of its own — if you have Python 3.9 or newer, you have everything it needs.
Two optional extras add modules that wrap outside libraries:
pip install "she-lang[crypto]" # Kaalka, for the crypto module
pip install "she-lang[web]" # WebWeaveX, for the web module
pip install "she-lang[all]" # both
Your first program
Put this in a file called hello.she:
let name = ask "What is your name?"
say "Hello, {name}!"
Then run it:
she run hello.she
Or start a new project with a layout, a test and a readme already in place:
she new my-project
cd my-project
she run main.she
The command line
| Command | What it does |
|---|---|
she | Interactive prompt. Values print themselves, so typing 1 + 1 shows 2. |
she run file.she | Run a program. she file.she works too. |
she test [path] | Run every test "..." block found. |
she fmt [path] | Format code. --check reports without changing anything. |
she check file.she | Look for problems without running. |
she new name | Scaffold a project. |
she doc [module] | What a module provides, in the terminal. |
she lsp | Language server, for editors. |
At the interactive prompt
| Command | What it does |
|---|---|
:help | The list of commands |
:env | What you have defined so far |
:type expr | What kind of value something is |
:time expr | Run something and report how long it took |
:load file.she | Run a file into the current session |
:perms / :grant read | See and change what this session may do |
:quit | Leave |
Values
SHE has seven kinds of value, plus whatever types you define.
| Kind | Examples |
|---|---|
number | 42, 3.14, 1_000_000, 0xff, 0b1010, 1.5e3 |
text | "hello", 'also hello', """a block""", r"raw\n" |
bool | true, false |
nothing | nothing |
list | [1, 2, 3], [], ["a", [1, 2], {x: 1}] |
map | {name: "Ada", age: 36}, {} |
range | 1..10 (both ends), 1..<10 (stops before 10) |
type_of(value) tells you which one you have.
Truthiness
In an if or a while, these count as false: false, nothing, 0, empty text, an empty list, an empty map. Everything else is true.
true is not 1. Booleans are their own kind of value, so
true is 1 is false and nothing is 0 is false.
If you want a number, ask for one: to_number(true).
Whole numbers stay whole
10 / 2 prints 5, not 5.0. When a division comes out even, you get a whole number back.
Text
Any value inside {braces} is worked out and dropped into the text.
let name = "Ada"
say "Hello, {name}! You have {2 + 3} messages."
say "nested is fine: {names |> text.join(", ")}"
Braces that are just braces
A { that does not hold a valid expression is left exactly as you typed it, so JSON, CSS and patterns need no escaping:
say '{"name": "Ada", "age": 36}' # prints the JSON as written
say "a pattern like {2,3} is fine"
say "body {{ color: red }}" # or double them to be explicit
Escapes and raw text
say "a tab\there and a newline\nthere"
say r"C:\Users\raw\path" # r"..." takes no escapes
say """
a block of text
across several lines
"""
Names
let pi = 3.14159 # named once, never changes
var count = 0 # this one can change
count += 1
let is the default because most things never need to change, and a value that cannot change is one fewer thing to keep track of. Try to reassign one and SHE says so:
TypeError: `pi` was declared with `let`, so it cannot be changed
--> circle.she:4:1
help: use `var pi = ...` if it needs to change.
Names may end in ? when they answer a question — empty?, prime?, valid?. It is only a convention, but it reads well.
Operators
| Group | Operators |
|---|---|
| arithmetic | + - * / // (whole division) % ^ (power) |
| comparison | is is not == != < > <= >= |
| logic | and or not |
| membership | in not in |
| ranges | 1..10 1..<10 1..10 by 2 |
| access | . ?. (skip when nothing) [] [a:b] |
| defaults | ?? — use the right side when the left is nothing |
| pipeline | |> — send the left value into the function on the right |
| assignment | = += -= *= /= //= %= ^= ??= |
The pipeline
x |> f(a) means f(x, a). It turns a nest of calls into a list of steps, read top to bottom:
say numbers
|> filter(fun(n) -> n % 2 is 0)
|> map(fun(n) -> n / 2)
|> sum()
A pipeline or a method chain may be split across lines — a line starting with |> or . continues the one above.
Control flow
Deciding
if age >= 18
say "you can vote"
else if age >= 16
say "nearly there"
else
say "not yet"
end
# one line, when it fits on one line
if ready then start()
# as a value — then and else are both required
let price = if member then 20 else 40
Repeating
for each item in shopping
say item
end
for each i, item in shopping # position and value
for each key, value in settings # over a map
for each n in 1..10 by 2 # over a range
while countdown > 0
countdown -= 1
end
repeat # always runs at least once
tries += 1
until tries >= 3
break leaves the loop. skip moves straight to the next turn.
Lists and maps
let numbers = [4, 8, 15, 16]
say numbers[0] → 4
say numbers[-1] → 16 (counting from the end)
say numbers[1:3] → [8, 15]
say numbers.length → 4
let person = {name: "Ada", born: 1815}
say person.name # the usual way
say person["born"] # when the key is in a variable
say person.get("email", "not given")
Methods on every value
"hi".upper() and text.upper("hi") are the same call — use whichever reads better where you are.
Spreading and taking apart
let [first, second, ...rest] = numbers
let combined = [...a, ...b, 99]
let updated = {...settings, theme: "dark"}
add(...arguments)
a + [3] gives you a new list
and leaves a alone. Only push, pop and friends change
a list in place, and their names say so.
Functions
# one expression — the arrow form
fun greet(who = "world") -> "Hello, {who}!"
# several steps — the block form
fun classify(n)
"Say whether a number is negative, zero or positive."
if n < 0 then return "negative"
if n is 0 then return "zero"
return "positive"
end
A bare piece of text at the top of a function body is its documentation, and help(classify) shows it.
| Feature | How |
|---|---|
| Defaults | fun greet(who = "world") |
| Named arguments | rectangle(width: 4, height: 3) |
| Any number of values | fun total(...numbers) |
| Spreading a list in | add(...pair) |
| Anonymous | let double = fun(n) -> n * 2 |
| Closures | An inner function keeps whatever it can see |
Types
type Point has x, y
"A place on a flat grid."
fun length(self) -> math.sqrt(self.x ^ 2 + self.y ^ 2)
fun to_text(self) -> "({self.x}, {self.y})"
end
let p = Point(3, 4)
say p → (3, 4)
say p.length() → 5
Two hooks
| Name | When it runs |
|---|---|
setup(self) | Just after a value is built — for working out extra fields |
to_text(self) | Whenever the value is printed or put inside text |
Fields and inheritance
# fields can declare a kind and a default
type Account has owner: text, balance: number = 0
# one type can build on another
type Animal has name
fun speak(self) -> "..."
fun introduce(self) -> "{self.name} says {self.speak()}"
end
type Dog from Animal
fun speak(self) -> "woof"
end
say Dog("Rex").introduce() → Rex says woof
Pattern matching
match picks a branch by the shape of a value, not just its value.
match value
case 0 -> "nothing at all"
case 1 | 2 | 3 -> "a small number"
case number(n) if n < 0 -> "below zero"
case 4..99 -> "a middling number"
case [first, ...rest] -> "a list, {first} first"
case {name: n} -> "something called {n}"
case Point(0, 0) -> "the origin"
case Point(x, y) -> "at {x},{y}"
case _ -> "something else"
end
| Pattern | Matches |
|---|---|
42, "hi", true | Exactly that value |
n | Anything, and names it n |
_ | Anything, without naming it |
1 | 2 | 3 | Any one of several |
4..99 | A number in that range |
[a, b] | A list of exactly two, naming both |
[first, ...rest] | A list of one or more |
{name: n} | A map holding at least name |
Point(x, y) | A Point, naming its fields in order |
number(n), text(t) | Any value of that kind |
case p if p > 0 | Add a condition to any pattern |
match that forgets a case fails loudly rather than quietly doing nothing.
Errors
try
risky()
catch e: MathError
say "a maths problem: {e.message}"
catch e: IndexError | KeyError
say "looked for something that was not there"
catch e
say "anything else: {e.kind}"
finally
say "cleaned up either way"
end
throw "the amount has to be positive" # kind is "Error"
throw error("NotEnough", "you only have {balance}")
assert numbers.length > 0, "average needs a number"
The kinds SHE raises
| Kind | When |
|---|---|
SyntaxError | The program could not be read |
NameError | A name has not been defined |
TypeError | A value was the wrong kind for what you asked |
ValueError | The right kind, but not a usable value |
IndexError / KeyError | Nothing at that position or key |
MathError | Dividing by zero, the root of a negative number |
ImportError | No such module or file |
PermissionError | Something the program was not granted |
LimitError | A step or time budget ran out |
AssertionError | An assert or expect did not hold |
Gradual typing
Types are optional everywhere. Where you write one, SHE checks it as the program runs — so you can add them to the parts that matter and leave the rest alone.
let count: number = 0
fun area(w: number, h: number): number -> w * h
fun show(v: number|text) -> "{v}"
type Account has owner: text, balance: number = 0
Names you can use: number, text, bool, list, map, range, function, nothing, error, any, or any type you define. Join them with |.
Doing things at once
async fun fetch_price(symbol)
return http.json("https://api.example.com/{symbol}").price
end
let tasks = ["AAPL", "MSFT"] |> map(fetch_price)
for each price in await tasks
say price
end
async fun runs on a worker thread; calling
it hands back a task straight away, and await waits for the answer. That makes
waiting on files and the network genuinely parallel. It is not a green-threaded event
loop, and SHE does not pretend otherwise.
Modules
import math # math.sqrt(16)
import math as m # m.sqrt(16)
from math import sqrt, pi # sqrt(16)
use "./helpers.she" as helpers # another file of yours
from "./helpers.she" import double
text, list, math, json, re, time and random are always there. Everything that can reach outside the program — fs, http, os, crypto, web, csv, maps — has to be imported on purpose.
Testing
fun celsius_to_fahrenheit(c) -> c * 9 / 5 + 32
test "converts freezing and boiling"
expect celsius_to_fahrenheit(0) is 32
expect celsius_to_fahrenheit(100) is 212
end
she test
ok converts freezing and boiling
1 passed, 0 failed, 1 total
expect takes is, is not, <, >, <=, >=, in, or nothing at all (meaning "this should be true"). Add a message with a comma. assert is the same check, for use outside tests.
Permissions
A SHE program begins with no authority whatsoever. It cannot read a file, write one, open a network connection, start another program or read an environment variable until you grant that capability when you run it.
| Flag | Allows |
|---|---|
--allow-read[=path] | Reading files. Narrow it to a folder or a single file. |
--allow-write[=path] | Writing or deleting files. |
--allow-net[=host] | Network connections. Hosts may use *. |
--allow-run[=program] | Starting other programs. |
--allow-env[=name] | Reading environment variables. |
--allow-time | Sleeping. |
--allow-all, -A | Everything. Handy while developing. |
she run report.she
she run report.she --allow-read=./data
she run report.she --allow-read=./data --allow-net=api.stripe.com
she run report.she --allow-net='*.github.com'
Scopes are checked properly: --allow-read=./data permits ./data/sales.csv and refuses ../secrets.txt, after resolving the path — so ./data/../../secrets.txt is refused too.
PermissionError: report.she tried to read files from disk (~/.ssh/id_rsa),
but was not given permission
--> report.she:14:13
|
14 | let key = fs.read("~/.ssh/id_rsa")
| ^^^^^^^
help: run it with `--allow-read=~/.ssh/id_rsa` to permit this,
or `--allow-all` while you are developing.
Budgets and limits
| Flag | Effect |
|---|---|
--max-steps=N | Stop after N steps. Catches loops that never finish. |
--timeout=N | Stop after N seconds. |
--max-depth=N | How deep calls may nest. Default 200. |
she run untrusted.she --max-steps=1000000 --timeout=5
Running untrusted code
The combination of no permissions and a step budget is what makes it reasonable to run a SHE program you did not write:
she run submission.she --max-steps=5000000 --timeout=10
From Python, the same thing:
from she import run
from she.sandbox import Sandbox
result, error = run(source, sandbox=Sandbox.locked(max_steps=5_000_000, timeout=10))
Standard library
she doc lists every module in your terminal, and she doc math shows one in full. help(math) does the same from inside a program.
| Module | What it is for | Needs |
|---|---|---|
core | Always there — no import at all | — |
text | Words and characters | — |
list | Ordered collections | — |
maps | Key/value collections | import |
math | Numbers, rounding, trigonometry, statistics | — |
json | Reading and writing JSON | — |
re | Finding and replacing with patterns | — |
time | Clocks and dates | --allow-time to sleep |
random | Dice, shuffles, choices | — |
csv | Comma-separated tables | import |
crypto | Hashing, tokens, Kaalka | import |
fs | Files and folders | --allow-read/write |
http | Talking to the web | --allow-net |
os | The machine SHE is running on | --allow-env/run |
web | WebWeaveX graphs | --allow-net |
core
Available in every program with no import.
say ask print show help exit modules
to_text to_number to_bool to_whole to_list to_map
len length type_of range error empty?
keys values items first last contains count
min max sum abs round sorted reversed
map filter reduce each any all enumerate zip
text
Every one of these is also a method: text.upper(s) and s.upper() are the same call.
upper lower title capitalise reverse slug
trim trim_start trim_end pad_start pad_end centre
split lines join replace remove repeat wrap indent
contains starts_with ends_with find find_last between
slice at chars count code from_code
empty? number? digits? letters?
list
push pop insert remove remove_at clear copy
map filter reduce each find find_index partition
sort reverse unique flatten chunk group_by shuffle
take drop slice first last index_of contains
sum average count zip concat join empty?
say people |> list.sort(by: fun(p) -> p.age, descending: true)
maps
Usually written as methods — m.get("key", fallback). The module is there when you want the function form.
get set has remove default clear copy
keys values items merge invert pick omit
map_values filter empty?
math
pi e tau infinity nan
sqrt root power exp log log2 log10
floor ceil round truncate absolute sign clamp between
factorial gcd lcm prime? even? odd?
mean median mode stdev variance
sin cos tan asin acos atan degrees radians hypotenuse
to_base from_base nan? finite?
json & csv
let data = json.parse(text)
say json.pretty(data)
say json.stringify(data)
say json.valid?(text)
import csv
let rows = csv.parse(text) # a list of maps
say csv.stringify(rows)
re
say re.matches?("hello", "^h")
say re.find_all("a1b2", "[0-9]")
say re.replace("a-b", "-", "+")
say re.named("2026-08-27", r"(?P<year>\d{4})-(?P<month>\d{2})")
Also find, groups, split, escape, count.
time & random
say time.now() time.today() time.clock() time.timestamp()
say time.format(nothing, "%A %d %B %Y")
say time.parts(time.timestamp()).weekday
random.seed(7) # repeatable runs, for tests
say random.whole(1, 6) random.dice(6, 2) random.uuid()
say random.choice(names) random.shuffle(deck)
fs — needs --allow-read / --allow-write
read read_lines read_bytes write append
exists? file? folder? list walk size modified
make_folder remove remove_folder copy move
join name folder extension absolute
http & os — need --allow-net / --allow-env / --allow-run
let answer = http.get("https://api.example.com/things")
say answer.status, answer.ok
let data = http.json("https://api.example.com/things")
http.post(url, {name: "Ada"}) # maps are sent as JSON
say os.platform() os.cpu_count() os.version()
say os.env("HOME", "unset")
say os.run("git", ["status"]).output
say os.args() # whatever followed the script name
crypto
Two halves, and it matters which you reach for.
Vetted primitives
say crypto.hash("she") # sha256 by default
say crypto.hmac("payload", "key")
say crypto.token(32)
let stored = crypto.password_hash(password) # PBKDF2-SHA256
say crypto.password_check(attempt, stored)
say crypto.compare(a, b) # timing-safe
Kaalka — encryption keyed by a moment in time
let sealed = crypto.seal("meet at the bridge", "14:35:22")
say crypto.open(sealed, "14:35:22")
let packet = crypto.envelope(message, "ada", "bob")
say crypto.open_envelope(packet, "bob")
seal and open armour Kaalka's output as base64. Raw kaalka_encrypt output holds characters that do not survive a file, a URL or a JSON field, so use the sealed pair whenever the ciphertext has to travel.
hash, hmac, password_hash and
token above, which wrap primitives that have been.
web — WebWeaveX, needs --allow-net
let graph = web.extract("https://example.com", "web")
say web.nodes(graph).length
say web.edges(graph).length
say web.fingerprint(graph) # same input, same identity
say web.kaalka_hash(graph)
web.repo(".") web.docs(path) web.crawl(url) web.query(graph, name)
Grammar
program = statement*
statement = "let" | "var" target [":" type] "=" expression
| target ("=" | "+=" | "-=" | ...) expression
| "say" [expression ("," expression)*]
| "if" expression ("then" statement | block ("else" ...)? "end")
| "while" expression block "end"
| "repeat" block "until" expression
| "for" ["each"] targets "in" expression ["by" expression] block "end"
| "fun" NAME params ["->" expression | block "end"]
| "type" NAME ["has" fields] ["from" NAME] [methods "end"]
| "try" block ("catch" [NAME [":" kinds]] block)* ["finally" block] "end"
| "throw" expression
| "import" NAME ["as" NAME] | "from" NAME "import" names
| "use" TEXT ["as" NAME]
| "test" TEXT block "end"
| ("expect" | "assert") expression [op expression] ["," expression]
| "return" [expression] | "break" | "skip"
| expression
expression = pipeline
pipeline = coalesce ("|>" coalesce)*
coalesce = or ("??" or)*
or = and ("or" and)*
and = compare ("and" compare)*
compare = range (("is"|"is not"|"=="|"!="|"<"|">"|"<="|">="|"in") range)*
range = sum ((".." | "..<") sum)*
sum = product (("+" | "-") product)*
product = power (("*" | "/" | "//" | "%") power)*
power = unary ("^" power)*
unary = ("not" | "-" | "+" | "await") unary | postfix
postfix = primary (call | "." NAME | "?." NAME | "[" index "]")*
primary = NUMBER | TEXT | "true" | "false" | "nothing" | NAME
| "(" expression ")" | list | map | lambda
| "if" e "then" e "else" e | "match" e case+ "end" | "ask" [e]
Embedding SHE
SHE is an ordinary Python package, so you can run SHE from Python with whatever authority you choose.
from she import run, Interpreter
from she.sandbox import Sandbox, Grant
# the simple way
result, error = run('say "Hello!"')
if error:
print(error.render())
# with exactly the authority you want
box = Sandbox([Grant("read", ["./data"])], max_steps=1_000_000, timeout=5)
result, error = run(source, sandbox=box)
# or drive the interpreter yourself, and add your own builtins
from she.stdlib import wrap
engine = Interpreter(sandbox=Sandbox.locked())
engine.globals.declare("double", wrap(lambda n: n * 2, "double"), mutable=False)
engine.run("say double(21)")
Editor support
SHE ships a language server. Any editor that speaks LSP can point at she lsp and get live diagnostics, completion, hover documentation and formatting.
cd editors/vscode
npm install && npm run package
code --install-extension she-lang-2.0.0.vsix
Coming from SHE v1
SHE 2.0 is a rewrite and does not run v1 programs. The old interpreter lives on the v1.0.0 branch. The translation is mechanical:
| v1 | v2 |
|---|---|
VAR x = 5 | let x = 5 (or var if it changes) |
PRINT(x) | say x |
IF a THEN b ELSE c | if a then b else c |
FOR i = 1 TO 5 THEN ... | for each i in 1..5 ... end |
WHILE c THEN ... END | while c ... end |
FUN add(a, b) -> a + b | fun add(a, b) -> a + b |
APPEND(xs, v) | xs.push(v) |
LEN(xs) | xs.length |
list / 0 (index) | list[0] |
KAALKA_ENCRYPT(m, t) | crypto.seal(m, t) after import crypto |
1 / 0 for true/false | true / false |
1 and 0; lists are no longer quietly shared between variables;
and a program has no permissions until you grant them.
Found this useful?
SHE is Apache 2.0 and built in the open. A coffee keeps it moving; a star helps others find it.