SHE

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
Nothing else is required. The rest of the standard library — text, lists, maps, maths, JSON, patterns, time, files, HTTP, hashing — is built on Python's own standard library and works out of the box.

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

CommandWhat it does
sheInteractive prompt. Values print themselves, so typing 1 + 1 shows 2.
she run file.sheRun 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.sheLook for problems without running.
she new nameScaffold a project.
she doc [module]What a module provides, in the terminal.
she lspLanguage server, for editors.

At the interactive prompt

CommandWhat it does
:helpThe list of commands
:envWhat you have defined so far
:type exprWhat kind of value something is
:time exprRun something and report how long it took
:load file.sheRun a file into the current session
:perms / :grant readSee and change what this session may do
:quitLeave

Values

SHE has seven kinds of value, plus whatever types you define.

KindExamples
number42, 3.14, 1_000_000, 0xff, 0b1010, 1.5e3
text"hello", 'also hello', """a block""", r"raw\n"
booltrue, false
nothingnothing
list[1, 2, 3], [], ["a", [1, 2], {x: 1}]
map{name: "Ada", age: 36}, {}
range1..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

GroupOperators
arithmetic+ - * / // (whole division) % ^ (power)
comparisonis is not == != < > <= >=
logicand or not
membershipin not in
ranges1..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)
Lists are never copied behind your back. 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.

FeatureHow
Defaultsfun greet(who = "world")
Named argumentsrectangle(width: 4, height: 3)
Any number of valuesfun total(...numbers)
Spreading a list inadd(...pair)
Anonymouslet double = fun(n) -> n * 2
ClosuresAn 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

NameWhen 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
PatternMatches
42, "hi", trueExactly that value
nAnything, and names it n
_Anything, without naming it
1 | 2 | 3Any one of several
4..99A 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 > 0Add a condition to any pattern
If nothing matches, SHE stops and tells you which value went unhandled — so a 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

KindWhen
SyntaxErrorThe program could not be read
NameErrorA name has not been defined
TypeErrorA value was the wrong kind for what you asked
ValueErrorThe right kind, but not a usable value
IndexError / KeyErrorNothing at that position or key
MathErrorDividing by zero, the root of a negative number
ImportErrorNo such module or file
PermissionErrorSomething the program was not granted
LimitErrorA step or time budget ran out
AssertionErrorAn 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
How it works, plainly. An 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.

FlagAllows
--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-timeSleeping.
--allow-all, -AEverything. 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.
Working things out is always free. Arithmetic, text, lists, maps, your own functions and types never need a permission. Only reaching outside the program does.

Budgets and limits

FlagEffect
--max-steps=NStop after N steps. Catches loops that never finish.
--timeout=NStop after N seconds.
--max-depth=NHow 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))
What this is and is not. The sandbox governs what the SHE program can ask the runtime to do. It is a strong boundary for scripts, marking student work and plugin code. It is not a replacement for an OS-level sandbox or a container when you are running code from people you have reason to distrust.

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.

ModuleWhat it is forNeeds
coreAlways there — no import at all
textWords and characters
listOrdered collections
mapsKey/value collectionsimport
mathNumbers, rounding, trigonometry, statistics
jsonReading and writing JSON
reFinding and replacing with patterns
timeClocks and dates--allow-time to sleep
randomDice, shuffles, choices
csvComma-separated tablesimport
cryptoHashing, tokens, Kaalkaimport
fsFiles and folders--allow-read/write
httpTalking to the web--allow-net
osThe machine SHE is running on--allow-env/run
webWebWeaveX 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.

Said plainly. Kaalka is a novel construction that has not been through public cryptanalysis. SHE ships it for time-keyed handoff, puzzles and teaching. For secrets that matter, use 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:

v1v2
VAR x = 5let x = 5 (or var if it changes)
PRINT(x)say x
IF a THEN b ELSE cif a then b else c
FOR i = 1 TO 5 THEN ...for each i in 1..5 ... end
WHILE c THEN ... ENDwhile c ... end
FUN add(a, b) -> a + bfun 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/falsetrue / false
Beyond the syntax, three things changed on purpose: booleans are real values rather than 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.