A scripting language for people who enjoy the simpler things in life.

Version 0.7.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 has the value null.

var foo;
assert foo == null;

You can declare multiple variables in a single statement:

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

Variables have lexical scope and declarations inside blocks will shadow outer declarations:

var foo = 123;

    var foo = 456;
    assert foo == 456;

assert foo == 123;

Identifier names (this includes variables, 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 $pecial — this namespace is reserved by the language for builtin variables and functions.


Pyro has two numeric 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 numeric 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 character, char, is an unsigned 32-bit integer representing a Unicode code point.

Character literals use single quotes:

var c1 = 'a';
var c2 = '€';
var c3 = '🔥';

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


A boolean value is either true or false. You can convert any value to a boolean using the $bool(arg) 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 foo;
assert foo == null;

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

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

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


Numbers are equal if their values are numerically equal:

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

Strings are equal if they have the same content:

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

Tuples are equal if they have the same length and their elements are equal:

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

By default, other objects are equal only if they are the same object:

class Foo {}

var foo1 = Foo();
var foo2 = Foo();

assert foo1 == foo1;    # True.
assert foo1 == foo2;    # False.

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:

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

The comparison operators also work with strings:

assert "abc" < "def"

Strings are compared lexicographically by byte value.

You can use the comparison operators directly on tuples:

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

Tuples are compared lexicographically by element.


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

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.)

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

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:

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

Calling the $print() function with a single argument is equivalent to calling $str() on that argument first and printing the resulting string. Calling it with more than one argument is equivalent to calling $fmt() on those arguments first and printing the resulting string.

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

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


Pyro has support for several 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 reserved 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";

Conditional statements evaluate the truthiness of their conditions. As you'd expect, they 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 has support for conditional expressions using the ternary operator ? :|:

var status = money > 100 ? "rich" :| "poor";


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.

Inner functions declared inside outer functions can capture the outer function's local variables, including its parameters:

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

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

var adds_two = make_adder(2);
adds_two(0);  # 2;
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:

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

add(1, 2);  # 3
add(3, 4);  # 7


Class definitions look like this:

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

    def $init(name) { = name;

    def info() {
        $println("{} is a {}.",, self.role);

Create an instance of a class by calling its name:

>>> var dave = Person("Dave");

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

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

>>> dave:info();
Dave is a programmer.

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

>>> dave.role = "pointy headed manager";
>>> dave: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 dedicated error type, err, which a function can return to indicate failure.

var foo = $err("oops!");

You can check if a value is an err using the $is_err(arg) function. Alternatively, you can provide a default value for a function call that might fail using the error-coalescing operator !!:

var foo = might_fail() !! "default";


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 unhandled panic will result in the program exiting with an error message and a non-zero status code.

You can trap a panic using a try expression:

var foo = 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:

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

Note that you can use the error-coalescing operator !! to provide a default value for a panicking expression:

var foo = try (1 / 1) !! 0;  # 1
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:

$panic("oh no!");

If the panic is unhandled, 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, error, or vector in a var declaration using the following syntax:

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

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

Similarly, you can unpack the loop variable of a for loop:

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

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

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;

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.