No braces. No boilerplate. No ceremony. And no ability to read your files, reach the network or start a process — until you grant it on the command line.
# everything you need to know, in nine lines
let name = ask "What is your name?"
say "Hello, {name}!"
let numbers = [4, 8, 15, 16, 23, 42]
say numbers
|> filter(fun(n) -> n % 2 is 0)
|> map(fun(n) -> n / 2)
|> sum()
→ 45
Why another language
Readable or capable. Friendly or safe. A toy you outgrow in a week, or a professional tool with a month-long ramp. Those were never real trade-offs.
Blocks end with end. Comparisons use is. Loops say
for each item in items. Someone who has never programmed can follow
a SHE file — and it still fits on one screen.
A program cannot read a file, open a socket or start a process until you say so on the command line. Not a linter rule — the runtime enforces it, every call.
Every message points at the exact source, explains the problem in plain words, and suggests the fix. Misspell a name and it tells you which one you meant.
The part that matters
Every other scripting language hands a downloaded script the full authority of whoever ran it. SHE hands it nothing. You grant exactly what the job needs, and the runtime refuses everything else — with a message naming the flag that would have allowed it.
No install required
This is the real interpreter, compiled to WebAssembly and running in your browser.
Nothing is sent anywhere. Press Run, or Ctrl+Enter.
Press Run to start. The first run downloads the interpreter — about ten seconds, once.
A tour
Pattern matching, gradual types, closures, destructuring, pipelines, async, modules and a test runner. None of it optional extras — all of it in the box.
let pi = 3.14159 # named once, never changes
var count = 0 # this one can
count += 1
if age >= 18
say "you can vote"
else if age >= 16
say "nearly there"
else
say "not yet"
end
for each n in 1..10 by 2
say "{n} squared is {n ^ 2}"
end
repeat
tries += 1
until tries >= 3
let [first, second, ...rest] = numbers # destructuring
let nickname = user?.profile?.name ?? "friend"
fun greet(who = "world") -> "Hello, {who}!"
fun classify(n)
if n < 0 then return "negative"
if n is 0 then return "zero"
return "positive"
end
# as many values as you like
fun total(...numbers) -> sum(numbers)
# name your arguments when it reads better
say rectangle(width: 4, height: 3)
# functions are values, and they close over what they see
let double = fun(n) -> n * 2
# gradual types — optional, checked when present
fun area(w: number, h: number): number -> w * h
type Point has x, y
"A place on a flat grid."
fun length(self) -> math.sqrt(self.x ^ 2 + self.y ^ 2)
fun plus(self, other) -> Point(self.x + other.x, self.y + other.y)
fun to_text(self) -> "({self.x}, {self.y})"
end
say Point(3, 4).length() → 5
# fields can declare a kind and a default
type Account has owner: text, balance: number = 0
fun deposit(self, amount)
if amount <= 0 then throw "a deposit has to be positive"
self.balance += amount
return self.balance
end
end
# one type can build on another
type Dog from Animal
fun speak(self) -> "woof"
end
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 [] -> "an empty list"
case [only] -> "just {only}"
case [first, ...rest] -> "{first}, {rest.length} more"
case {name: n} -> "something called {n}"
case Point(0, 0) -> "the origin"
case Point(x, y) -> "the point {x},{y}"
case text(t) -> "some words: {t}"
case _ -> "something else"
end
# forget a case and SHE tells you which value was not handled
try
let answer = 1 / 0
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 text, or a kind you name yourself
throw "the amount has to be positive"
throw error("NotEnough", "you only have {balance}")
# state your assumptions up front
assert numbers.length > 0, "average needs a number"
# an async function runs on its own thread
async fun fetch_price(symbol)
return http.json("https://api.example.com/{symbol}").price
end
# calling it hands back a task, straight away
let tasks = ["AAPL", "MSFT", "GOOG"] |> map(fetch_price)
# await one, or a whole list at once
for each price in await tasks
say price
end
# three requests in the time of the slowest, not the sum
# tests live next to the code they check
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
test "dividing by zero is refused"
try
safe_divide(1, 0)
expect false, "it should have thrown"
catch e
expect e.kind is "MathError"
end
end
# $ she test
# ok converts freezing and boiling
# ok dividing by zero is refused
# 2 passed, 0 failed, 2 total
The whole surface
| values | numbers, text, booleans, nothing, lists, maps, ranges, functions, your own types |
| text | interpolation "hi {name}", triple-quoted blocks, raw r"...", forty text functions |
| control flow | if / else if / else, while, repeat until, for each, break, skip |
| functions | defaults, named arguments, rest ...args, spread, closures, lambdas, recursion |
| types | type X has a, b, methods, inheritance, setup and to_text hooks |
| pattern matching | literals, ranges, or-patterns, guards, list and map destructuring, type patterns |
| errors | try / catch / finally, throw, catch by kind, assert |
| gradual typing | let n: number, parameter and return types, unions — checked at runtime, never required |
| concurrency | async fun, await, awaiting a whole list of tasks |
| modules | import math, from math import sqrt, use "./helpers.she" as helpers |
| modern sugar | pipelines |>, safe navigation ?., defaults ??, methods on every value |
| standard library | text, list, maps, math, json, re, time, random, csv, crypto, fs, http, os, web |
| security | capability sandbox, step and time budgets, hashing, HMAC, password storage, tokens |
| tooling | REPL, formatter, test runner, project scaffolder, doc browser, language server |
Built in
Not bindings you install and wire up. Ordinary SHE modules, documented alongside the rest.
Encryption whose key is a moment in time.
import crypto
let sealed = crypto.seal("meet at the bridge", "14:35:22")
say crypto.open(sealed, "14:35:22")
# envelopes carry sender, recipient and a checksum
let packet = crypto.envelope(msg, "ada", "bob")
say crypto.open_envelope(packet, "bob")
hash, hmac,
password_hash and token over vetted primitives.
Turn a live app, a repository or a document into a deterministic graph.
import web
let graph = web.extract("https://example.com", "web")
say "{web.nodes(graph).length} nodes"
say "{web.edges(graph).length} connections"
# the same input always gives the same identity
say web.fingerprint(graph)
Who it is for
The errors are written for you. Nothing you run can damage anything. Start at example one and you are writing real programs the same afternoon.
Recursion, closures, pattern matching, types, concurrency — every idea you will be taught, with no build system standing in the way.
Pipelines, destructuring, gradual types, a formatter, a test runner and an LSP. One pip install and the toolchain is there.
Run untrusted code with a step budget and no authority. Ship scripts whose reach is declared on the command line and enforced by the runtime.
Get started
No toolchain, no compiler, no configuration file. If you have Python 3.9 or newer, you already have everything SHE needs.
Apache 2.0, no telemetry, no paid tier, no catch. If it saved you time, taught you something, or you just want to see it keep going — a coffee helps more than you would think. Starring the repo costs nothing and helps just as much.