Python scripts
What NeoPy, NEO's Python for scripts, includes and leaves out, how its values behave, and which standard modules it provides.
On this page
NEO runs Python scripts with NeoPy: Python syntax and semantics, compiled onto the same script engine as JavaScript. A NeoPy script has the same World API, limits and permissions as a JavaScript one, and the same script written in both languages leaves the same result in the world. NeoPy isn't CPython: it has no access to the host, its standard modules are NEO's own, and a few features are left out on purpose.
To use Python, set language = "py" in the [script] section of neo.toml. Every script and module in the game is then Python. Python support is part of the engine build; if a build doesn't include it, Python scripts are refused and the script log says so.
The shape of a Python script#
from neo import World, on
class DoorPuzzle(World):
state = {"opened": 0, "last_opener": None, "solved": False}
@on("tick")
def tick(self, dt):
if self.state.opened >= 3 and not self.state.solved:
self.state.solved = True
self.emit("puzzle_solved", {"by": self.state.last_opener})
@on("interact", tag="Door")
def open(self, player, door):
door.open()
self.state.opened += 1
self.state.last_opener = player.id
default = DoorPuzzlefrom neo import World, on, sleep, gatherimports the World API.neoalso hasall, but importing it hides Python's built-inall; usegatherinstead.- The script's world class derives from
World.default = DoorPuzzlenames it; without that line, the last class in the file that derives fromWorldis used. @ontakes its options as keywords:@on("interact", tag="Door"),@on("tick", every=2).- State keys and entity fields are snake_case, and you reach them as attributes:
self.state.last_opener,platform.display_name.
Included#
| Feature | Notes |
|---|---|
Functions, lambda, default values, keyword arguments, *args, **kwargs | |
Classes, multiple inheritance (C3 method order), super(), class attributes | super().method(...) |
@staticmethod, @classmethod, @property with setters | |
__getattr__, __setattr__, __delattr__ | Python's way of writing a proxy. |
Special methods: __init__, __repr__, __str__, __eq__ and the other comparisons, __hash__, __len__, __iter__, __next__, __contains__, __getitem__, __setitem__, __call__, __bool__, arithmetic methods, __enter__/__exit__ | Operators use them. |
| Decorators on functions and methods, with arguments | |
List, dict and set comprehensions, generator expressions, unpacking, starred assignment, := | |
Generators (yield, yield from) | |
async def, await, async for, async with | On the simulation clock. See Time, sleep and async. |
try / except / else / finally, raise, assert, with | |
match statements | |
f-strings and str.format, with format specs (f"{x:.2f}", f"{n:,}", f"{p:.0%}") | |
| Type annotations | Parsed and ignored. |
Left out#
These are compile errors with a message saying why (or, where noted, errors when the code runs):
| Left out | Why |
|---|---|
eval, exec, compile, __import__, globals, locals, vars | They would escape the sandbox. |
open, input, breakpoint, help, exit, quit | No host access. |
id(), memoryview | Address-based values aren't deterministic. |
Imports other than neo, NEO's standard modules and the game's own modules | No host access. See Modules. |
from x import * | |
Interpreter internals: __class__, __bases__, __mro__, __subclasses__, __globals__, __code__, __closure__, __dict__, __getattribute__ and similar | Introspection could reach outside the script. Also refused at run time through getattr. |
Metaclasses (class A(B, metaclass=M)) | |
Complex numbers (1j) | |
The @ operator (matrix multiply) | |
except* (exception groups) | |
%-formatting of strings ("%d" % n) | Use f-strings or str.format. Raises at run time. |
async for inside a comprehension | Write the loop out. |
**rest in a mapping pattern |
print() exists but does nothing: a world has no console. Use self.emit(...) or state to see values.
Values#
| Type | How it behaves |
|---|---|
int | Unbounded, as in Python: 2 ** 100 is exact. // and % round toward negative infinity. True + 1 is 2. |
float | 64-bit. round() rounds halves to even: round(2.5) is 2. str(1.0) is "1.0". Dividing by zero raises ZeroDivisionError. |
bool, None | As in Python. |
str | Indexed by code points: len("😀") is 1, "héllo"[1] is "é". |
list, tuple | sort is stable and compares values; mixed types raise TypeError. |
dict | Insertion order. Python key rules: 1, 1.0 and True are one key; lists, dicts and sets can't be keys. |
set, frozenset | Iterate in insertion order (CPython's order depends on hashing; NeoPy's is deterministic). |
bytes | Immutable, sliceable; "text".encode() makes one. |
hash() gives the same value on every run and device. Values for objects without __hash__ come from creation order, never memory addresses.
Values that cross into the world#
When you store a value in state, emit it or pass it to an exposed method, it is converted to NEO's shared value types:
- A whole number within ±2^53 is stored as an integer, whatever its type. So
self.state.x = 1.0reads back as1(anint). - Tuples are stored as lists.
- Dicts must have
strkeys. - Sets can't cross (convert them to a list first).
- An
intbeyond 64 bits raises an error.
Values that come from the world arrive as Python values: integers as int, maps as dicts. A map from the world (an event payload, for example) can also be read with attributes: player.id.
Errors#
Errors are Python's exceptions: ZeroDivisionError, KeyError, IndexError, AttributeError, TypeError, ValueError, NameError, ImportError, RecursionError and the rest of the usual hierarchy, including Exception and BaseException.
A capability denial (reading or writing a field you may not use, calling a method you may not call) raises PermissionError:
try:
door.open = True
except PermissionError:
self.state.denied = [*self.state.denied, door.id]An exception that isn't caught ends the handler with a fault, the same as in JavaScript. See Events and handlers.
Built-in functions#
abs, all, any, bool, bytes, callable, chr, dict, divmod, enumerate, filter, float, format, frozenset, getattr, hasattr, hash, int, isinstance, len, list, map, max, min, object, ord, pow, print, range, repr, reversed, round, set, setattr, sorted, str, sum, tuple, type, zip.
The methods of str, list, dict and set cover the common ones: for example split, join, strip, replace, find, format, startswith, upper; append, extend, insert, pop, remove, sort; get, items, keys, values, setdefault, update; add, discard, union, intersection.
Standard modules#
NeoPy provides four standard modules. Each is NEO's own implementation, not CPython's:
| Module | What it has |
|---|---|
math | pi, e, tau, inf, nan, sqrt, sin, cos, tan, asin, acos, atan, atan2, exp, log, log2, log10, hypot, fabs, pow, floor, ceil, trunc, isnan, isinf, isfinite, copysign, radians, degrees, fmod, gcd |
json | loads (integers stay int, objects become dicts) and dumps |
functools | reduce, partial |
itertools | chain, accumulate |
Import them with import math or from functools import reduce. Any other standard module, such as random, time, os or re, is a compile error. For randomness use self.rng.random(); for time use self.time.