NEOLearn

Python scripts

What NeoPy, NEO's Python for scripts, includes and leaves out, how its values behave, and which standard modules it provides.

Updated October 17, 2026

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#

door_puzzle.py
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 = DoorPuzzle
  • from neo import World, on, sleep, gather imports the World API. neo also has all, but importing it hides Python's built-in all; use gather instead.
  • The script's world class derives from World. default = DoorPuzzle names it; without that line, the last class in the file that derives from World is used.
  • @on takes 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#

FeatureNotes
Functions, lambda, default values, keyword arguments, *args, **kwargs
Classes, multiple inheritance (C3 method order), super(), class attributessuper().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 withOn 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 annotationsParsed and ignored.

Left out#

These are compile errors with a message saying why (or, where noted, errors when the code runs):

Left outWhy
eval, exec, compile, __import__, globals, locals, varsThey would escape the sandbox.
open, input, breakpoint, help, exit, quitNo host access.
id(), memoryviewAddress-based values aren't deterministic.
Imports other than neo, NEO's standard modules and the game's own modulesNo host access. See Modules.
from x import *
Interpreter internals: __class__, __bases__, __mro__, __subclasses__, __globals__, __code__, __closure__, __dict__, __getattribute__ and similarIntrospection 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 comprehensionWrite 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#

TypeHow it behaves
intUnbounded, as in Python: 2 ** 100 is exact. // and % round toward negative infinity. True + 1 is 2.
float64-bit. round() rounds halves to even: round(2.5) is 2. str(1.0) is "1.0". Dividing by zero raises ZeroDivisionError.
bool, NoneAs in Python.
strIndexed by code points: len("😀") is 1, "héllo"[1] is "é".
list, tuplesort is stable and compares values; mixed types raise TypeError.
dictInsertion order. Python key rules: 1, 1.0 and True are one key; lists, dicts and sets can't be keys.
set, frozensetIterate in insertion order (CPython's order depends on hashing; NeoPy's is deterministic).
bytesImmutable, 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.0 reads back as 1 (an int).
  • Tuples are stored as lists.
  • Dicts must have str keys.
  • Sets can't cross (convert them to a list first).
  • An int beyond 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:

Python
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:

ModuleWhat it has
mathpi, 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
jsonloads (integers stay int, objects become dicts) and dumps
functoolsreduce, partial
itertoolschain, 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.

See also#