A dynamically-typed, garbage-collected scripting language.

Version 0.17.0


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;

The imported module's name becomes a variable in the importing scope:

assert math::abs(-1) == 1;

Use the member access operator :: to access a module's top-level members — i.e. its global variables, functions, and classes.

Public/Private Members

A module's top-level members (i.e. its global variables, functions, and classes) are private by default — use the pub keyword to make them public, e.g.

pub var number = 123;

pub def func() {
    return "hello world!";

pub class Object {
    var value;


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

import math::trig;

var value = trig::cos(1);

The top-level module math can live alongside the directory that contains its submodules:

|-- math.pyro
|-- math/
    |-- trig.pyro

Alternatively, it can live inside the math directory in a file called self.pyro:

|-- math/
    |-- self.pyro
    |-- trig.pyro

Finally, the top-level math module can be empty and simply function as a container for its submodules:

|-- math/
    |-- trig.pyro

Submodules can contain submodules of their own — there's no hard limit to how deep the nesting can go.

Note that only the final module name in the import chain is declared as a variable in the importing scope, e.g.

import foo::bar::baz;

In this case, only baz is declared as a variable in the importing scope.

Parents, Children, Siblings

Imagine we have the following module structure:

|-- math/
    |-- calc.pyro
    |-- self.pyro
    |-- trig.pyro

A parent module can import its child – e.g. in root/math/self.pyro:

import math::trig;

A child module can import its parent – e.g. in root/math/calc.pyro:

import math;

A child module can import its sibling – e.g. in root/math/calc.pyro:

import math::trig;

Module Caching

A module file is only executed the first time it's imported. The imported module is cached and any subsequent imports simply load the module object from the cache.

Circular Imports

In general, circular imports are fine — module foo can import module bar and module bar can import module foo.

Note that trying to use a member from an imported module that hasn't finished initializing can result in a panic.


You can import a module under an alias using an import ... as ... statement:

import math as alias;

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

Submodules can similarly be aliased:

import math::trig as alias;

var foo = alias::cos(1);

Importing Members

You can import a top-level member from a module by wrapping its name in braces, e.g.

import math::{abs};

assert abs(-1) == 1;

You can alias the imported member using an import ... as ... statement, e.g.

import math::{abs} as foo;

assert foo(-1) == 1;

You can import multiple members in a single import statement by separating their names with commas, e.g.

import math::{cos, sin};

You can alias the imported members using a comma-separated as list, e.g.

import math::{cos, sin} as foo, bar;

Import Roots

The global $roots vector contains the list of root directories that Pyro checks when attempting to import a module.

To customize Pyro's import behaviour:

Executing Module Directories

You can execute a module directory as a script if it contains a self.pyro file, e.g.

$ pyro path/to/module/directory

Pyro executes the self.pyro file, then the $main() function if it finds one.

Importing using $exec()

You can import a file as a module using the $exec() function, which executes a string of Pyro source code as a new module, e.g.

var code = $read_file("path/to/file.pyro");
var mod = $exec(code);

You can also use the $exec() function to create a new module directly from a string, e.g.

var mod = $exec(`pub def func() { return "foobar"; }`);
assert mod::func() == "foobar";

Importing using $import()

You can import a module using the $import() function, which takes the module's import path as a string, e.g.

var mod = $import("std::math");
assert mod::abs(-1) == 1;

You can wrap the $import() function in a try expression to check if a module exists, e.g.

var mod = try $import("foo::bar::baz");

if $is_err(mod) {
    echo "not found";

Note that the $import() function ignores the module cache — unlike an import statement, it will always reload and execute the latest version of the module's code.

Assigning to members

Pyro doesn't allow direct assignment to module members from outside the module, e.g.

# in module.pyro
pub var number = 123;
# in script.pyro
import module;
echo module::number;  # okay
module::number = 456; # syntax error

If you want to allow assignment to a module member from outside the module, use a member function, e.g.

# in module.pyro
pub var number = 123;

pub def set_number(new_value) {
    number = new_value;
# in script.pyro
import module;