A dynamically-typed, garbage-collected scripting language.

Version 0.17.0

A Tour of Pyro

Pyro begs, borrows, and steals from every language I've ever used, but it drags Python down a back alley and leaves it naked, bleeding, and penniless. If you've used Python you'll find many aspects of Pyro's design philosophy familiar.


Comments begin with a # symbol and run to the end of the line:

# Full line comment.
var foo = 123; # Partial line comment.


Variables are declared using the var keyword:

var foo = 123;

Variables must be declared before use. If an initial value isn't specified, the variable gets the default value null, e.g.

var foo;
assert foo == null;

You can declare multiple variables in a single statement, e.g.

var foo, bar;
var baz = 123, bam = 456;

Variables have lexical scope and declarations inside blocks will shadow outer declarations, e.g.

var foo = 123;

    var foo = 456;
    assert foo == 456;

assert foo == 123;

Identifier names (i.e. variables, constants, functions, and classes) should begin with an ASCII letter or an underscore and contain only ASCII letters, numbers, and underscores.

Names beginning with a $ symbol are special — this namespace is reserved by the language for builtin variables and functions.


Use the let keyword to declare a constant, e.g.

let value = 123;

Constants work just like variables, but can only be assigned a value in their declaration statement, e.g.

let value = 123;
value = 456; # syntax error

You can learn more about constants here.


Pyro has two arithmetic number types: i64 for 64-bit signed two's-complement integers, and f64 for 64-bit IEEE 754 floats. Both behave as you'd expect and support the usual range of arithmetic operations.

+ Addition. Returns an integer if both operands are integers or a float if either or both are floats.
- Subtraction (binary) or negation (unary). Subtraction returns an integer if both operands are integers or a float if either or both are floats.
* Multiplication. Returns an integer if both operands are integers or a float if either or both are floats.
/ Floating-point division. Both operands will be converted to floats and the result will be a float.
// Truncating division. Returns an integer if both operands are integers or a float if either or both are floats.
% Modulo/remainder operator. Returns an integer if both operands are integers or a float if either or both are floats.
** Power operator. Both operands are converted to floats and the result is a float.

Integer literals can use binary, octal, decimal, or hex notation:

var a = 0b101;      # a == 5
var b = 0o101;      # b == 65
var c = 101;        # c == 101
var d = 0x101;      # d == 257

Integer literals can use underscores to improve readability:

var foo = 123_456;
var bar = 0b1010_0101;

Float literals require a decimal point and can also use underscores to improve readability:

var foo = 1.0;
var pi = 3.141_592_654;


A string, str, is an immutable array of bytes.

var string = "hello pyro";

Pyro strings have methods that let you manipulate them as ASCII or as UTF-8 but the string type itself is agnostic about its encoding — a string can contain any sequence of byte values including null bytes or invalid UTF-8.

You can find a full description of the string type here.


A rune, rune, is an unsigned 32-bit integer representing a Unicode code point.

Rune literals use single quotes:

var r1 = 'a';
var r2 = '€';
var r3 = '🔥';

You can find a full description of the rune type here.


A boolean value is either true or false. You can convert any value to a boolean using the $bool() function.

assert $bool(123) == true;
assert $bool(null) == false;

Values which convert to true are truthy, values which convert to false are falsey.


Uninitialized variables have the default value null.

var value;
assert value == null;

You can use the null-coalescing operator ?? to supply a default value in place of a null:

var result = maybe_null() ?? "default";

Functions which don't explicitly specify a return value have the default return value null.


The equality operator is ==. Its inverse is !=.

Numerical types (i.e. i64, f64, and rune) compare as equal using == if their values are numerically equal, e.g.

assert 1 == 1.0;
assert 'a' == 97;
assert 'b' == 98.0;

Strings compare as equal if they have the same content, e.g.

var foo = "foobar";
var bar = "foobar";
assert foo == bar;

Tuples compare as equal if they have the same length and their elements are equal, e.g.

var foo = ("foo", 123);
var bar = ("foo", 123);
assert foo == bar;

Sets compare as equal if they are set-equivalent, i.e. if they contain the same items in any order, e.g.

var foo = {1, 2, 3};
var bar = {3, 2, 1};
assert foo == bar;

By default, all other objects compare as equal only if they are the same object, e.g.

class Object {}

var obj1 = Object();
var obj2 = Object();

assert obj1 == obj1;
assert obj1 != obj2;

You can overload the equality operator to customize its behaviour for your own types.


You can use the comparison operators (<, <=, >, >=) with any combination of numerical types, e.g.

assert 2.0 > 1;
assert 'a' > 42;

The comparison operators also work with strings, e.g.

assert "abc" < "def";

Strings are compared lexicographically by byte value, e.g.

assert "a" < "aa";
assert "aa" < "aaa";

You can use the comparison operators directly on tuples, e.g.

assert (1, 2, 3) < (1, 2, 4);

Tuples are compared lexicographically by element.

You can overload the comparison operators to customize their behaviour for your own types


The echo statement prints to the standard output stream. It's useful for simple printing and debugging, e.g.

echo "hello pyro";

You can echo any value — echo stringifies its argument before printing it. (It's equivalent to calling $str() on the argument first and printing the result.)

echo 123;

You can supply multiple arguments to echo, separated by commas, e.g.

echo foo, 123, "bar";

echo prints the stringified values separated by spaces.

Pyro also has a family of $print()/$println() functions with support for format strings, e.g.

$print("hello pyro");               # "hello pyro"
$print("{} and {}", "foo", "bar");  # "foo and bar"
$print("{} and {}", 123, 456);      # "123 and 456"

The only difference between $print() and $println() is that $println() adds a newline character to the output.

Pyro also has $eprint() and $eprintln() functions which print to the standard error stream.

See the string formatting documentation for a detailed look at the $fmt() function and format strings.


Pyro has support for three different looping constructs. The simplest is the infinite loop:

var i = 0;

loop {
    i += 1;
    if i == 5 {

assert i == 5;

The loop statement also supports C-style loops with an initializer, a condition, and an incrementing expression:

loop var i = 0; i < 10; i += 1 {
    echo i;

The for keyword in Pyro is used for looping over iterators:

var vec = ["foo", "bar", "baz"];

for item in vec {
    echo item;

Finally, Pyro supports while loops:

var i = 0;

while i < 10 {
    i += 1;

assert i == 10;

All the looping constructs support break and continue.


Conditional statements in Pyro look like this:

if money > 100 {
    echo "we have lots of money";

An if statement evaluates the truthiness of an expression. The syntax is:

if <expression> {

As you'd expect, if statements support optional else if and else clauses:

if money > 100 {
    echo "we have lots of money";
} else if money > 10 {
    echo "we have some money";
} else {
    echo "we're poor";

Pyro also supports conditional-scoped variables and conditional expressions.


Function definitions look like this:

def add(a, b) {
    return a + b;

If you don't explicitly return a value from a function, its return value is null.

Pyro has full support for closures. An inner function declared inside an outer function can capture the outer function's local variables, including its parameters, e.g.

def make_adder(increment) {
    def adder(num) {
        return num + increment;
    return adder;

var adds_one = make_adder(1);
assert adds_one(0) == 1;
assert adds_one(1) == 2;

var adds_two = make_adder(2);
assert adds_two(0) == 2;
assert adds_two(1) == 3;

Functions are first-class citizens in Pyro, meaning you can pass them around just like any other value. You can also declare and use functions anonymously, e.g.

var add = def(a, b) {
    return a + b;

assert add(1, 2) == 3;
assert add(3, 4) == 7;

You can find full documentation for Pyro's functions here.


Class definitions look like this:

class Person {
    pub var name;
    pub var role = "programmer";

    def $init(name) { = name;

    pub def info() {
        return "${} is a ${self.role}.";

Create an instance of a class by calling its name, e.g.

var person = Person("Dave");
assert == "Dave";

Arguments are passed to the (optional) initializer method, $init().

Call a method on an instance using the method access operator, :, e.g.

assert person:info() == "Dave is a programmer."

Get or set an instance's fields using the field access operator, ., e.g.

person.role = "pointy headed manager";
assert person:info() == "Dave is a pointy headed manager.";

Boo Dave.

Classes are a big enough topic to deserve a page of their own which you can find here.


Pyro has a builtin vector type, vec — a dynamic array similar to a list in Python. You can create a vec using a vector-literal, e.g.

var vec = ["foo", "bar"];

Vectors use zero-based indices, e.g.

assert vec[0] == "foo";
assert vec[1] == "bar";

You can index into a vector to get or set entries, e.g.

vec[0] = "baz";
assert vec[0] == "baz";

You can add items to a vector using the :append() method, e.g.

assert vec[2] == "bam";

You can find a full description of Pyro's vec type here.


Pyro has a builtin map type, map — an ordered hash-map similar to a dict in Python. You can create a map using a map-literal, e.g.

var map = {
    "foo" = 123,
    "bar" = 456,

You can index into a map to get or set entries, e.g.

assert map["foo"] == 123;
assert map["bar"] == 456;

map["baz"] = 789;
assert map["baz"] == 789;

You can find a full description of Pyro's map type here.


Pyro has a dedicated error type, err, which a function can return to indicate failure, e.g.

def str_len(arg: str) -> i64|err {
    if $is_str(arg) {
        return arg:byte_count();
    return $err("argument is not a string");

You can check if a value is an err using the $is_err() function, e.g.

var result = str_len("foo");

if $is_err(result) {
    $exit("error: ${result}");

echo result;

Alternatively, you can provide a default value for a function call that might fail using the error-coalescing operator !!, e.g.

var result = str_len("foo") !! 0;

You can find a full description of the err type here.


A panic in Pyro is similar to an exception in other languages — it indicates that the program has attempted to do something impossible like divide by zero or read from a file that doesn't exist.

An untrapped panic will cause the program to exit with an error message, a stack trace, and a non-zero status code.

You can trap a panic using a try expression, e.g.

var result = try might_panic();

A try expression returns the value of its operand if it evaluates without panicking or an err if it panics.

try is a unary operator with the same precedence as ! or - so you should wrap lower-precedence expressions in brackets, e.g.

var foo = try (1 / 2);  # 0.5
var bar = try (1 / 0);  # err

You can use the error-coalescing operator !! to provide a default value for a panicking expression, e.g.

var foo = try (1 / 2) !! 0;  # 0.5
var bar = try (1 / 0) !! 0;  # 0

You can raise a panic from your own code by calling the $panic() function with an error message, e.g.

$panic("oh no!");

If the panic is not trapped, the error message will be printed to the standard error stream and the program will exit with a non-zero status code.


A module is a Pyro file loaded as a library. Modules are loaded using the import keyword.

Assume we have a file called math.pyro containing math functions:

import math;

var foo = math::abs(-1);

Modules can contain submodules. Assume we have a directory called math containing a file called trig.pyro:

import math::trig;

var foo = trig::cos(1);

Modules are a big enough topic to deserve a page of their own which you can find here.

The Main Function

When you run a script file, Pyro first executes the script then looks for a function called $main(). If it finds a $main() function it runs it automatically.

def $main() {
    echo "hello from $main()";

Note that the $main() function won't be run if you import the same file as a module.


You can unpack the values of a tuple or vector in a var declaration using (), e.g.

var (foo, bar, baz) = $tup(123, 456, 789);

assert foo == 123;
assert bar == 456;
assert baz == 789;

You can discard unwanted values by assigning them to the unnamed variable _, e.g.

var (foo, _, baz) = $tup(123, 456, 789);

assert foo == 123;
assert baz == 789;

You can partially unpack the leading values, ignoring the remainder, e.g.

var (foo, bar) = $tup(123, 456, 789);

assert foo == 123;
assert bar == 456;

Similarly, you can unpack the loop variable of a for loop if it's a tuple or vector, e.g.

var map = {
    "foo" = 123,
    "bar" = 456,

for (key, value) in map {
    echo key;
    echo value;

This works because iterating over a map returns (key, value) tuples.

Source Code

Source code outside of string literals is assumed to be UTF-8 encoded. String literals can contain arbitrary byte sequences including null bytes and invalid UTF-8.


Pyro has builtin support for unit tests.

Use the assert statement to test your code, e.g.

def add(a, b) {
    return a + b;

assert add(1, 2) == 3;
assert add(3, 4) == 7;

An assert statement will panic if its operand expression evaluates as falsey. The syntax is:

assert <expression>;

You can use the test command to run test functions, e.g.

def $test_addition() {
    assert add(1, 2) == 3;
    assert add(3, 4) == 7;

You can find documentation for the test command here.