Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The Zuri Standard Library

Every module Zuri ships with, and every public name in each one. The pages here are generated from the library’s own doc blocks, so what you read is what the code says.

This is a reference, not a tutorial. It answers “what does this take and what does it return”. For the narrative version, with worked examples and the reasoning behind the design, read The Zuri Programming Language, which cargo run-docs serves alongside this.

How to Read a Page

Every module page opens with the import that reaches it, then the module’s own description, then its members. A package also carries an index of its whole public surface, because a name you reach as os.get_env may well be declared in a file called something else.

Signatures are written the way the source declares them:

  • name: string is a parameter that must be a string, enforced at the call.
  • name: ?string accepts a string or nil, which is how an optional argument is spelled.
  • ...values takes any number of arguments and arrives as a list.
  • -> string is what the call gives back.

A name beginning with an underscore is private: the compiler refuses to import it across a module boundary, so none appear here.

The Modules

Text and Markup

Parsing, building and colouring text meant to be read.

ModuleWhat it is for
htmlA complete HTML5 parser, DOM and serializer, following the WHATWG HTML Living Standard.
wireWire, an HTML template engine.
urlThis module provides classes and functions for parsing, building, resolving and processing URLs (and URL-like identifiers, such…
mimeThis module provides functions that allow easy mime type detection from files.
colorsThis module provides functionalities for color conversion and manipulation.

Data Formats

Reading and writing the formats other programs speak.

ModuleWhat it is for
jsonProvides APIs for encoding and decoding JSON data.
yamlA complete, YAML 1.2.2-compliant library for parsing and emitting YAML (YAML Ain’t Markup Language) documents.
tomlTOML, read for its values or edited in place without disturbing the file around them.
csvA complete, RFC 4180-compliant library for reading and writing Comma-Separated Values (CSV) data.
base64This module provides interface for encoding binary data into strings and decoding such encoded strings back into binary data…
structThis module provides functions for converting between Zuri values and C/C++/Rust structs and vice-versa in the binary format.

Numbers and Collections

Arithmetic, and the containers the built-in types do not cover.

ModuleWhat it is for
mathThis module contains functions and constants to make trigonometric and non-trigonometric mathematics a breeze.
arrayThis module provides fixed-width typed array classes: twos- complement integers (Int8Array/UInt8Array through…
setThis module provides functionalities for working with mathematical sets.

Dates and Times

Calendar arithmetic, formatting, and the timezone database.

ModuleWhat it is for
dateThis modules provides Zuri’s implementation of date and time manipulation methods.

The Machine

The filesystem, other processes, the terminal, the command line, and the configuration around them.

ModuleWhat it is for
osThe os module is Zuri’s interface to the underlying operating system: environment variables, the filesystem, other processes,…
envConfiguration from a file, in the environment, in the type you wanted.
ioThis module provides interfaces for working with to I/O stream and TTYs as well as expose the operating system standard I/O for…
argsThis module provides a complete, batteries-included framework for building command-line interfaces.
logThis module implements a simple and flexible event logging system for all Zuri applications and modules.
statThe module provides constants and functions for interpreting results of file.stat().

Testing

Suites, matchers, test doubles, snapshots, and the reports they produce.

ModuleWhat it is for
testA testing framework: suites, assertions, mocks, snapshots, lifecycle hooks, and reports worth reading.

Networking

Sockets, addresses, and a complete HTTP stack and JSON-RPC on top of them.

ModuleWhat it is for
netThe net module provides networking primitives for Transmission Control Protocol and User Datagram Protocol communication as…
httpA complete HTTP stack: a client, a server, and the pieces both are built from.
rpcJSON-RPC 2.0: calling methods on another program, and answering its calls, over HTTP, WebSockets, sockets, pipes, or any stream…

Security and Identity

Hashing, signing, password storage, tokens and input validation.

ModuleWhat it is for
cryptoComprehensive cryptographic primitives for Zuri applications.
hashThis module provides a framework for cryptographic and non-cryptographic encryption.
bcryptGenerating and verifying bcrypt password hashes, and reading information back out of an existing hash (its cost factor and salt).
jwtThe jwt module provides a complete implementation of JSON Web Tokens (JWT) as defined in RFC 7519, with support for signing,…
uuidProvides RFC 9562 (and RFC 4122) compliant Universally Unique Identifier (UUID) generation, parsing, validation, and inspection.
validateSchema-based input validation for Zuri applications.

Compression and Archives

Every codec and container format the runtime ships with.

ModuleWhat it is for
compressThe compress library provides in-memory and streaming compression and decompression across several algorithms, plus…

Concurrency

Real OS threads, with separate heaps and message passing between them.

ModuleWhat it is for
isolateHigh-throughput parallel concurrency backed by a pool of operating-system isolate threads.

Images

Decoding, drawing, filtering and encoding raster images.

ModuleWhat it is for
imagineReading, writing, drawing and transforming raster images.

Databases

One contract every relational database answers, and the adapters that answer it.

ModuleWhat it is for
sqlOne way to talk to a relational database, whichever one it is.

Mail

Messages, the three protocols that move them, and the servers at the far end.

ModuleWhat it is for
mailMail, from the message to the socket.

Interoperability

Calling C and Rust libraries, being called back by them, and linking static ones.

ModuleWhat it is for
ffiCalling C and Rust libraries, and being called by them.

Types and Conversion

Checked coercion between types, named constants, and conversions between bases and encodings.

ModuleWhat it is for
typesProvides type validation and conversion capabilities
enumThis module provides Zuri’s implementation of enumerations as described in the language documentation: a set of unique values…
convertData conversion between hexadecimal, arbitrary numeric bases, bytes, and decimal numbers.

Metaprogramming

Lexing, parsing and compiling Zuri source, and reflecting on live functions, classes and modules.

ModuleWhat it is for
zuriExposes Zuri’s own compiler pipeline as a library: lexing, parsing, and compiling a Zuri source file, plus a reflect submodule…

html

import html

A complete HTML5 parser, DOM and serializer, following the WHATWG HTML Living Standard.

Parse a page, walk it, query it with CSS selectors, change it, and write it back out. Nothing here fetches anything over the network and nothing here executes a script; <script> content is inert text and always will be.


Quick start

import html

var doc = html.parse('
  <article>
    <h1 id="title">Zuri</h1>
    <ul class="links">
      <li><a href="/docs">Docs</a>
      <li><a href="/spec">Spec</a>
    </ul>
  </article>
')

echo doc.get_element_by_id('title').text_content()

for link in doc.query_selector_all('ul.links a') {
  echo '${link.text_content()} -> ${link.get_attribute("href")}'
}

Notice that neither <li> is closed and there is no <html>, <head> or <body> anywhere. That is fine: the standard defines a tree for every possible input, and this module builds it. Parsing never raises on malformed markup.


Finding things

MethodFinds
get_element_by_id(id)the first element with that id
get_elements_by_class_name(c)every element with those classes
get_elements_by_tag_name(t)every element with that tag
query_selector(sel)the first CSS match
query_selector_all(sel)every CSS match
element.matches(sel)whether this element matches
element.closest(sel)this element or its nearest matching ancestor

The selector syntax is a deliberately bounded subset: type, *, #id, .class, [attr], [attr="value"], the combinators >, + and ~, descendant combination by whitespace, :first-child, :last-child, :nth-child(), and comma-separated lists of all of those. Anything outside it raises a SelectorError rather than silently matching nothing.


Changing things

import html

var doc = html.parse('<div class="box"><p>old</p></div>')
var box = doc.query_selector('.box')

box.set_attribute('data-seen', 'yes')
box.class_list().add('active')
box.set_inner_html('<p>new</p>')

echo box.outer_html()
# <div class="box active" data-seen="yes"><p>new</p></div>

Zuri has no property setters, so every writable DOM property is a pair of methods: text_content() reads and set_text_content() writes, and likewise for inner_html and outer_html.


Writing it back out

outer_html() round-trips a node exactly. When you want the output shaped for a particular audience, minify() strips what a browser does not need and format() indents for a human:

A string is parsed as a whole document, so <html>, <head> and <body> show up in the output the way a browser would produce them. Pass { fragment: true } when the input is a fragment and you want just that fragment back:

%> import html
%> html.minify('<p>  a   b  </p>\\n<p>c</p>', { fragment: true })
'<p>a b</p><p>c</p>'
%> echo html.format('<ul><li>a<li>b</ul>', { fragment: true })
<ul>
  <li>a</li>
  <li>b</li>
</ul>

Escaping

encode() makes text safe to embed and decode() resolves character references. Decoding knows all 2231 named references, not a shortlist:

%> import html
%> html.encode('<b>Tom & Jerry</b>')
'&lt;b&gt;Tom &amp; Jerry&lt;/b&gt;'
%> html.decode('caf&eacute; &#8212; &#x2603;')
'café: ☃'

Encoding

Input is UTF-8 text that has already been decoded, which is what a Zuri string always is. There is no charset sniffing, no byte order mark handling and no <meta charset> processing: if you are reading bytes off a socket, decode them before they get here.


Parse errors

HTML has no fatal errors, so a parse always succeeds. Every conformance problem the standard defines is collected instead:

%> import html
%> var doc = html.parse('<p><b>x</p>')
%> for e in doc.errors { echo e.to_string() }
missing-doctype at 1:1
unexpected-end-tag at 1:8

Each entry carries the standard’s own error name, so you can look it up, plus the line and column where it was noticed.

The html API

Every public name in html, wherever it is declared. Each links to the page that documents it.

NameKindSummary
html.ClassListclassA live view of one element’s class attribute.
html.CommentclassAn HTML comment.
html.DocumentclassA whole parsed document.
html.DocumentFragmentclassA parentless container for a run of nodes.
html.DocumentTypeclassThe <!DOCTYPE ...> node at the top of a document.
html.ElementclassAn element in the tree.
html.HTML_NAMESPACEconstantNamespace URI of ordinary HTML elements.
html.HierarchyErrorclassRaised when a tree mutation would produce a structure that cannot exist, such as inserting a node before…
html.MATHML_NAMESPACEconstantNamespace URI of elements inside a <math> subtree.
html.NODE_COMMENTconstantNode type of a Comment.
html.NODE_DOCUMENTconstantNode type of a Document.
html.NODE_DOCUMENT_FRAGMENTconstantNode type of a DocumentFragment, including the fragment that holds a <template> element’s content.
html.NODE_DOCUMENT_TYPEconstantNode type of a DocumentType (the <!DOCTYPE ...> node).
html.NODE_ELEMENTconstantNode type of an Element.
html.NODE_TEXTconstantNode type of a Text node.
html.NodeclassThe base class every node in a parsed document inherits from.
html.ParseErrorclassA parse error raised while tokenizing or building the tree.
html.RAW_TEXT_ELEMENTSconstantElements whose text children are markup, not content: their text is written out byte for byte and never…
html.SVG_NAMESPACEconstantNamespace URI of elements inside an <svg> subtree.
html.SelectorclassA compiled selector list: h1, h2 is one of these holding two ComplexSelector instances.
html.SelectorErrorclassRaised when a selector cannot be parsed, or uses syntax this module deliberately does not support.
html.TextclassA run of character data in the tree.
html.TokenclassOne token from the tokenizer.
html.TokenizerclassTurns markup into tokens, one call to next_token() at a time.
html.TreeBuilderclassBuilds a document tree from a token stream.
html.VOID_ELEMENTSconstantThe HTML elements that are written without a closing tag and can hold no content.
html.XLINK_NAMESPACEconstantNamespace URI used by the xlink: attribute prefix in SVG.
html.XMLNS_NAMESPACEconstantNamespace URI used by the xmlns and xmlns:xlink attributes.
html.XML_NAMESPACEconstantNamespace URI used by the xml: attribute prefix.
html.compilefunctionCompiles source into a reusable Selector.
html.decodefunctionDecodes every character reference in text and returns the result.
html.elements.FOREIGN_ATTRIBUTESconstantAttributes in foreign content that belong to a namespace, mapping the lowercase name the tokenizer produced…
html.elements.FOREIGN_BREAKOUT_TAGSconstantStart tags that are always a mistake inside foreign content and that break out of it, closing SVG or MathML…
html.elements.FORMATTING_ELEMENTSconstantThe formatting elements: the ones the adoption agency algorithm reopens across a badly nested boundary, so…
html.elements.HTML4_TRANSITIONAL_PREFIXESconstantPublic identifier prefixes whose mode depends on whether the doctype also carries a system identifier: quirks…
html.elements.IMPLIED_END_TAGSconstantElements whose end tag is implied by the start of a sibling, so that <li>a<li>b produces two list items…
html.elements.LIMITED_QUIRKS_PUBLIC_PREFIXESconstantPublic identifier prefixes that always mean limited-quirks mode.
html.elements.MATHML_ATTRIBUTESconstantThe one MathML attribute whose case the tokenizer destroys.
html.elements.MATHML_TEXT_INTEGRATION_POINTSconstantThe MathML elements whose children are parsed as HTML rather than as MathML.
html.elements.QUIRKS_PUBLIC_EXACTconstantThe two public identifiers that put a document in quirks mode on an exact match rather than a prefix match,…
html.elements.QUIRKS_PUBLIC_PREFIXESconstantPublic identifier prefixes that put a document in quirks mode, in lowercase for case-insensitive comparison.
html.elements.QUIRKS_SYSTEM_IDconstantThe system identifier that alone puts a document in quirks mode, in lowercase.
html.elements.SCOPE_HTMLconstantThe HTML elements that make up the “in scope” barrier every scope check shares.
html.elements.SPECIAL_HTMLconstantThe HTML elements the standard calls “special”: the ones a list item or definition term stops searching past,…
html.elements.SPECIAL_MATHMLconstantThe MathML elements that count as special.
html.elements.SPECIAL_SVGconstantThe SVG elements that count as special.
html.elements.SVG_ATTRIBUTESconstantSVG attribute names the tokenizer lowercased and that have to be put back, keyed by the lowercase form.
html.elements.SVG_HTML_INTEGRATION_POINTSconstantThe SVG elements whose children are parsed as HTML rather than as SVG.
html.elements.SVG_TAG_NAMESconstantSVG tag names the tokenizer lowercased and that have to be put back, keyed by the lowercase form.
html.elements.THOROUGH_IMPLIED_END_TAGSconstantEverything in IMPLIED_END_TAGS plus the elements only closed when the standard says to generate implied end…
html.elements.adjust_mathml_attributesfunctionRepairs the case of MathML attribute names in attributes and returns a new dictionary.
html.elements.adjust_svg_attributesfunctionRepairs the case of SVG attribute names in attributes and returns a new dictionary.
html.elements.adjust_svg_tag_namefunctionThe correctly cased SVG tag name for the lowercase name the tokenizer produced, or name itself when it…
html.elements.doctype_modefunctionWhich quirks mode a doctype puts a document in.
html.elements.is_html_integration_pointfunctionTrue when element is an HTML integration point.
html.elements.is_mathml_text_integration_pointfunctionTrue when element is a MathML text integration point, meaning its children are parsed as HTML.
html.elements.is_specialfunctionTrue when the element name in namespace namespace is one of the standard’s special elements.
html.encodefunctionEscapes text so that it can be embedded in an HTML document without being reinterpreted as markup.
html.entities.NO_BREAK_SPACEconstant
html.entities.REPLACEMENT_CHARACTERconstant
html.entities.code_point_to_stringfunctionApplies the standard’s “numeric character reference end state” rules to a raw code point and returns the…
html.entities.consume_referencefunctionConsumes a character reference from chars beginning at the ampersand at index start.
html.entities.is_ascii_alphanumericfunctionReturns true when c is one of 0-9, A-Z or a-z.
html.entities.is_ascii_digitfunctionReturns true when c is an ASCII decimal digit.
html.entities.is_ascii_hex_digitfunctionReturns true when c is an ASCII hexadecimal digit, in either case.
html.escape_attributefunctionEscapes value the way the HTML fragment serialization algorithm requires for a double-quoted attribute…
html.escape_textfunctionEscapes text the way the HTML fragment serialization algorithm requires for element content.
html.formatfunctionRenders source as indented, readable HTML.
html.minifyfunctionRemoves the markup a browser does not need and returns the result.
html.parsefunctionParses source as a complete HTML document.
html.parse_filefunctionReads the file at path and parses it as HTML.
html.parse_fragmentfunctionParses source as a fragment, as though it had been written inside context.
html.parser.FormattingEntryclassOne entry in the list of active formatting elements.
html.selector.AttributeTestclassOne name/=value test from a [...] block.
html.selector.ComplexSelectorclassOne selector from a comma-separated list: a chain of compounds joined by combinators, such as ul > li + li.
html.selector.CompoundSelectorclassA run of simple selectors with nothing between them, such as div#main.active[data-x]:first-child.
html.selector.PseudoTestclassOne :pseudo or :pseudo(...) test.
html.serialize.INLINE_ELEMENTSconstantElements that flow inside a line of text rather than starting a new block.
html.serialize.PRESERVE_WHITESPACEconstantElements whose text content is significant to the last character.
html.tokenizefunctionTokenizes source and returns every token, ending with the eof token.
html.tokenizer.RAWTEXT_ELEMENTSconstantTag names whose content is text with neither markup nor character references.
html.tokenizer.RCDATA_ELEMENTSconstantTag names whose content is text with character references but no markup.

Submodules

ModuleReached asSummary
html.elementshtml.elements.*The element tables the tree construction algorithm consults on almost every token: which elements are…
html.entitieshtml.entities.*Character reference handling for HTML: turning text into something safe to drop into a document (encode()),…
html.namespaceshtml.*The five namespace URIs the HTML parser deals in.
html.nodehtml.*The document tree that html.parse() produces, and everything you can do with it: walking it, querying it,…
html.parserhtml.parser.*The HTML tree construction stage: the half of the parser that takes the tokenizer’s stream and decides what…
html.selectorhtml.selector.*A small, deliberately bounded CSS selector engine: enough to find things in a parsed document, and no more.
html.serializehtml.serialize.*Writing a document back out, shaped for whoever has to read it next: minify() for a browser, format() for…
html.tokenizerhtml.tokenizer.*The HTML tokenizer from the WHATWG HTML Living Standard: the stage that turns a string of markup into a…

2026, Richard Ore and The Zuri Contributors

html.elements

import html

html exposes this as html.elements, so import html is enough and the names are called as html.elements.*. import html.elements reaches the same definitions directly.

The element tables the tree construction algorithm consults on almost every token: which elements are “special”, which are formatting elements, which close which, and how SVG and MathML tag and attribute names have to be repaired after the tokenizer has lowercased them.

These are data, lifted from the WHATWG HTML Living Standard. They live apart from html.parser so the algorithm there reads as the algorithm and not as a wall of lists.

Constants

SPECIAL_HTML

html.elements.SPECIAL_HTML = [...]

The HTML elements the standard calls “special”: the ones a list item or definition term stops searching past, and the ones the adoption agency algorithm treats as a block boundary when it looks for the furthest block.

Nothing to do with CSS. This is purely the tree construction algorithm’s own category.

SPECIAL_MATHML

html.elements.SPECIAL_MATHML = [...]

The MathML elements that count as special.

SPECIAL_SVG

html.elements.SPECIAL_SVG = [...]

The SVG elements that count as special.

FORMATTING_ELEMENTS

html.elements.FORMATTING_ELEMENTS = [...]

The formatting elements: the ones the adoption agency algorithm reopens across a badly nested boundary, so that <b>a<p>b</b>c puts a fresh <b> inside the paragraph.

SCOPE_HTML

html.elements.SCOPE_HTML = [...]

The HTML elements that make up the “in scope” barrier every scope check shares. The narrower checks (list item scope, button scope) add to this list rather than replacing it.

IMPLIED_END_TAGS

html.elements.IMPLIED_END_TAGS = [...]

Elements whose end tag is implied by the start of a sibling, so that <li>a<li>b produces two list items rather than nesting.

THOROUGH_IMPLIED_END_TAGS

html.elements.THOROUGH_IMPLIED_END_TAGS = [...]

Everything in IMPLIED_END_TAGS plus the elements only closed when the standard says to generate implied end tags thoroughly, which is what </template> and the table sections do.

MATHML_TEXT_INTEGRATION_POINTS

html.elements.MATHML_TEXT_INTEGRATION_POINTS = [...]

The MathML elements whose children are parsed as HTML rather than as MathML.

SVG_HTML_INTEGRATION_POINTS

html.elements.SVG_HTML_INTEGRATION_POINTS = [...]

The SVG elements whose children are parsed as HTML rather than as SVG.

FOREIGN_BREAKOUT_TAGS

html.elements.FOREIGN_BREAKOUT_TAGS = [...]

Start tags that are always a mistake inside foreign content and that break out of it, closing SVG or MathML elements until an HTML insertion point is reached.

SVG_TAG_NAMES

html.elements.SVG_TAG_NAMES = {...}

SVG tag names the tokenizer lowercased and that have to be put back, keyed by the lowercase form. SVG is case-sensitive and <foreignobject> is not the same element as <foreignObject>.

SVG_ATTRIBUTES

html.elements.SVG_ATTRIBUTES = {...}

SVG attribute names the tokenizer lowercased and that have to be put back, keyed by the lowercase form.

MATHML_ATTRIBUTES

html.elements.MATHML_ATTRIBUTES = {...}

The one MathML attribute whose case the tokenizer destroys.

FOREIGN_ATTRIBUTES

html.elements.FOREIGN_ATTRIBUTES = {...}

Attributes in foreign content that belong to a namespace, mapping the lowercase name the tokenizer produced to the qualified name and the namespace URI it should carry.

QUIRKS_PUBLIC_PREFIXES

html.elements.QUIRKS_PUBLIC_PREFIXES = [...]

Public identifier prefixes that put a document in quirks mode, in lowercase for case-insensitive comparison.

QUIRKS_PUBLIC_EXACT

html.elements.QUIRKS_PUBLIC_EXACT = [...]

The two public identifiers that put a document in quirks mode on an exact match rather than a prefix match, in lowercase.

LIMITED_QUIRKS_PUBLIC_PREFIXES

html.elements.LIMITED_QUIRKS_PUBLIC_PREFIXES = [...]

Public identifier prefixes that always mean limited-quirks mode.

HTML4_TRANSITIONAL_PREFIXES

html.elements.HTML4_TRANSITIONAL_PREFIXES = [...]

Public identifier prefixes whose mode depends on whether the doctype also carries a system identifier: quirks without one, limited-quirks with one.

QUIRKS_SYSTEM_ID

html.elements.QUIRKS_SYSTEM_ID

The system identifier that alone puts a document in quirks mode, in lowercase.

Functions

is_special()

html.elements.is_special(namespace: string, name: string) -> bool

True when the element name in namespace namespace is one of the standard’s special elements.

Parameters

  • namespace (string)
  • name (string)

Returns bool

is_mathml_text_integration_point()

html.elements.is_mathml_text_integration_point(element) -> bool

True when element is a MathML text integration point, meaning its children are parsed as HTML.

Parameters

  • element (any)

Returns bool

is_html_integration_point()

html.elements.is_html_integration_point(element) -> bool

True when element is an HTML integration point.

An <annotation-xml> qualifies only when its encoding attribute names an HTML flavour, which is how MathML embeds real markup.

Parameters

  • element (any)

Returns bool

adjust_svg_tag_name()

html.elements.adjust_svg_tag_name(name: string) -> string

The correctly cased SVG tag name for the lowercase name the tokenizer produced, or name itself when it needs no repair.

Parameters

  • name (string)

Returns string

adjust_svg_attributes()

html.elements.adjust_svg_attributes(attributes: dict) -> dict

Repairs the case of SVG attribute names in attributes and returns a new dictionary. Attributes that need no repair are copied across untouched, in their original order.

Parameters

  • attributes (dict)

Returns dict

adjust_mathml_attributes()

html.elements.adjust_mathml_attributes(attributes: dict) -> dict

Repairs the case of MathML attribute names in attributes and returns a new dictionary.

Parameters

  • attributes (dict)

Returns dict

doctype_mode()

html.elements.doctype_mode(name: ?string, public_id: ?string, system_id: ?string, force_quirks: bool) -> string

Which quirks mode a doctype puts a document in.

name may be nil for a doctype with no name at all, and the two identifiers may be nil when the doctype omits them; all three cases mean something different to the algorithm, which is why they are not folded into empty strings.

Returns 'no-quirks', 'limited-quirks' or 'quirks'. Nothing in this module behaves differently between them, but knowing which one a browser would have picked is exactly the question you are asking when you audit a legacy document.

Parameters

  • name (?string)
  • public_id (?string)
  • system_id (?string)
  • force_quirks (bool)

Returns string


2026, Richard Ore and The Zuri Contributors

html.entities

import html.entities

html lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled html.entities.* needs import html.entities.

Character reference handling for HTML: turning text into something safe to drop into a document (encode()), and turning a document’s &amp;-style references back into the characters they stand for (decode()).

Both directions follow the WHATWG HTML Living Standard. Decoding understands all 2231 named references, decimal references (&#169;), hexadecimal references (&#xA9;), the Windows-1252 substitutions the standard mandates for code points in the C1 range, and the awkward legacy rule that lets a handful of names work without their trailing semicolon.

%> import html
%> html.decode('caf&eacute; &#8212; &#x2603;')
'café: ☃'
%> html.encode('<a href="x">Tom & Jerry</a>')
'&lt;a href=&quot;x&quot;&gt;Tom &amp; Jerry&lt;/a&gt;'

Constants

REPLACEMENT_CHARACTER

html.entities.REPLACEMENT_CHARACTER = '�'

NO_BREAK_SPACE

html.entities.NO_BREAK_SPACE = ' '

Functions

is_ascii_alphanumeric()

html.entities.is_ascii_alphanumeric(c: string) -> bool

Returns true when c is one of 0-9, A-Z or a-z.

Named references are made up entirely of these, so this is the test that bounds how far reference matching will scan.

Parameters

  • c (string)

Returns bool

is_ascii_digit()

html.entities.is_ascii_digit(c: string) -> bool

Returns true when c is an ASCII decimal digit.

Parameters

  • c (string)

Returns bool

is_ascii_hex_digit()

html.entities.is_ascii_hex_digit(c: string) -> bool

Returns true when c is an ASCII hexadecimal digit, in either case.

Parameters

  • c (string)

Returns bool

code_point_to_string()

html.entities.code_point_to_string(code: number) -> string

Applies the standard’s “numeric character reference end state” rules to a raw code point and returns the string it should decode to.

Null, out-of-range and surrogate code points become U+FFFD. Code points in the C1 range are remapped through Windows-1252. Noncharacters and control characters are parse errors but decode to themselves, which is what browsers do and what round-tripping malformed documents depends on.

Parameters

  • code (number)

Returns string

consume_reference()

html.entities.consume_reference(chars: list, start: number, in_attribute: bool) -> dict

Consumes a character reference from chars beginning at the ampersand at index start.

This is the standard’s “character reference state” as a single call, shared by decode() and by the tokenizer so both agree on every edge case. It always succeeds: when the text at start is not a valid reference the ampersand is returned as an ordinary character and next points just past it.

in_attribute selects the attribute-value flavour of the algorithm. Inside an attribute a semicolon-less legacy reference followed by = or by an alphanumeric is left alone, so that a query string like ?x=1&notit=2 keeps its literal &not rather than turning into ¬it.

The returned dictionary has:

  • text: the decoded text (never nil, never empty)
  • next: index of the first character not consumed
  • error: nil, or a short description of the parse error the standard raises for this input; the text is still produced

Parameters

  • chars (list)
  • start (number)
  • in_attribute (bool)

Returns dict

encode()

html.encode(text: string, options: ?dict) -> string

Escapes text so that it can be embedded in an HTML document without being reinterpreted as markup.

By default this escapes &, <, >, " and ', which is safe for both element content and quoted attribute values. Everything else is passed through unchanged, so the result stays readable and stays valid UTF-8.

options is an optional dictionary:

  • quotes (bool, default true): escape " and '. Turn this off only when the result is going into element content, never into an attribute.
  • non_ascii (bool, default false): also escape every character above U+007F. Useful when the output has to survive a transport that is not UTF-8 clean.
  • named (bool, default true): when a character being escaped has a named reference, use it. With false, non-syntactic characters use hexadecimal numeric references instead. The five syntax characters above always use their fixed forms.
%> import html
%> html.encode('5 > 3 & "quoted"')
'5 &gt; 3 &amp; &quot;quoted&quot;'
%> html.encode("it's fine", { quotes: false })
"it's fine"
%> html.encode('café', { non_ascii: true })
'caf&eacute;'
%> html.encode('café', { non_ascii: true, named: false })
'caf&#xE9;'

Surrogate code points cannot appear in a Zuri string, so no unpaired-surrogate handling is needed here.

Parameters

  • text (string)
  • options (?dict)

Returns string

decode()

html.decode(text: string, in_attribute: ?bool) -> string

Decodes every character reference in text and returns the result.

Named, decimal and hexadecimal references are all understood. Text that merely looks like a reference is left exactly as it was, so decode() is safe to run over arbitrary content: 'a & b' stays 'a & b' and '&nosuchthing;' stays '&nosuchthing;'.

Pass true for in_attribute when the text came from an attribute value. That switches on the standard’s attribute rule, under which a legacy reference written without its semicolon is not decoded if the next character is = or alphanumeric. It is what keeps ?a=1&copy=2 from becoming ?a=1©=2. The default is false, the element-content behaviour.

%> import html
%> html.decode('&lt;b&gt; &amp; &#169; &#xA9; &nbsp;')
'<b> & © ©  '
%> html.decode('?a=1&copy=2')
'?a=1©=2'
%> html.decode('?a=1&copy=2', true)
'?a=1&copy=2'

Parameters

  • text (string)
  • in_attribute (?bool)

Returns string

escape_text()

html.escape_text(text: string) -> string

Escapes text the way the HTML fragment serialization algorithm requires for element content.

Exactly four characters change: & becomes &amp;, U+00A0 becomes &nbsp;, < becomes &lt; and > becomes &gt;. Quotes are left alone because they carry no meaning in element content, and the no-break space is escaped because it is otherwise indistinguishable from a plain space in a source listing.

This is what outer_html() and the serializers use. For escaping text you are about to hand to something other than this module’s own serializer, prefer encode(), which is stricter.

Parameters

  • text (string)

Returns string

escape_attribute()

html.escape_attribute(value: string) -> string

Escapes value the way the HTML fragment serialization algorithm requires for a double-quoted attribute value.

Three characters change: & becomes &amp;, U+00A0 becomes &nbsp; and " becomes &quot;. < and > are deliberately left alone; they are ordinary characters inside a quoted attribute and escaping them would needlessly change the value’s serialized form.

Parameters

  • value (string)

Returns string


2026, Richard Ore and The Zuri Contributors

html.namespaces

import html

Everything here is re-exported by html, so import html is enough and the names are called as html.*. Importing html.namespaces on its own works too and reaches the same definitions.

The five namespace URIs the HTML parser deals in.

They live in a module of their own, importing nothing, because almost every other module in html needs them at load time and a shared leaf is the only arrangement that has no load order to get wrong.

html.node re-exports all of these, so node.HTML_NAMESPACE and html.HTML_NAMESPACE both work and you rarely need to name this module directly.

Constants

HTML_NAMESPACE

html.HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml'

Namespace URI of ordinary HTML elements.

SVG_NAMESPACE

html.SVG_NAMESPACE = 'http://www.w3.org/2000/svg'

Namespace URI of elements inside an <svg> subtree.

MATHML_NAMESPACE

html.MATHML_NAMESPACE = 'http://www.w3.org/1998/Math/MathML'

Namespace URI of elements inside a <math> subtree.

html.XLINK_NAMESPACE = 'http://www.w3.org/1999/xlink'

Namespace URI used by the xlink: attribute prefix in SVG.

XML_NAMESPACE

html.XML_NAMESPACE = 'http://www.w3.org/XML/1998/namespace'

Namespace URI used by the xml: attribute prefix.

XMLNS_NAMESPACE

html.XMLNS_NAMESPACE = 'http://www.w3.org/2000/xmlns/'

Namespace URI used by the xmlns and xmlns:xlink attributes.


2026, Richard Ore and The Zuri Contributors

html.node

import html

Everything here is re-exported by html, so import html is enough and the names are called as html.*. Importing html.node on its own works too and reaches the same definitions.

The document tree that html.parse() produces, and everything you can do with it: walking it, querying it, reading and changing attributes and content, and serializing any part of it back to markup.

The shape follows the DOM closely enough that the names are familiar, without pretending to be a browser. Where this module departs from the DOM it does so deliberately and says why in the relevant doc block; the two differences worth knowing up front are:

  • children holds every child node (elements, text and comments alike), which is what the DOM calls childNodes. Use child_elements() when you only want elements.
  • There are no property setters in Zuri, so every DOM property that can be written is a pair of methods: text_content() reads and set_text_content() writes.
import html

var doc = html.parse('<ul><li class="a">One<li class="a">Two</ul>')

for item in doc.query_selector_all('li.a') {
  echo item.text_content()
}

Constants

NODE_ELEMENT

html.NODE_ELEMENT = 1

Node type of an Element.

NODE_TEXT

html.NODE_TEXT = 3

Node type of a Text node.

NODE_COMMENT

html.NODE_COMMENT = 8

Node type of a Comment.

NODE_DOCUMENT

html.NODE_DOCUMENT = 9

Node type of a Document.

NODE_DOCUMENT_TYPE

html.NODE_DOCUMENT_TYPE = 10

Node type of a DocumentType (the <!DOCTYPE ...> node).

NODE_DOCUMENT_FRAGMENT

html.NODE_DOCUMENT_FRAGMENT = 11

Node type of a DocumentFragment, including the fragment that holds a <template> element’s content.

VOID_ELEMENTS

html.VOID_ELEMENTS = [...]

The HTML elements that are written without a closing tag and can hold no content. Serializing one of these emits <br> and nothing else, and any child it somehow acquired is not written out, because markup that cannot be read back is worse than markup that loses it.

This is the list the fragment serialization algorithm uses, which is wider than the standard’s modern “void elements” list: it also covers basefont, bgsound, frame, keygen and param, five legacy elements that were dropped from the authoring list but still take no end tag and still turn up in real documents.

RAW_TEXT_ELEMENTS

html.RAW_TEXT_ELEMENTS = [...]

Elements whose text children are markup, not content: their text is written out byte for byte and never escaped, because escaping it would change what a stylesheet or a script means.

The serialization algorithm also lists noscript here, but only when the scripting flag is enabled. This module parses with scripting disabled by default, and in that mode a <noscript> holds real elements rather than text, so leaving it out is right for the default and safe for the other case: the worst that happens to a document parsed with scripting: true is that its <noscript> text comes back escaped, whereas including it would let hand-built text inside a <noscript> serialize into live markup.

Classes

HierarchyError

class html.HierarchyError < Error

Raised when a tree mutation would produce a structure that cannot exist, such as inserting a node before something that is not a child of the target, or making a node its own ancestor.

Constructor

html.HierarchyError(message)

Parameters

  • message (string)

Node

class html.Node

The base class every node in a parsed document inherits from.

You never construct a Node directly; parsing produces Document, Element, Text, Comment and DocumentType instances, and this class is where the behaviour they share lives.

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
node_typeOne of the NODE_* constants in this module, identifying which subclass this node actually is.
parent_nodeThe node this one hangs off, or nil for a Document and for any node that has been detached.
childrenEvery child of this node in document order: elements, text nodes and comments together.

Constructor

html.Node(node_type: number)

Parameters

  • node_type (number)

Node.is_element()

html.Node.is_element() -> bool

True when this node is an Element.

Returns bool

Node.is_text()

html.Node.is_text() -> bool

True when this node is a Text node.

Returns bool

Node.is_comment()

html.Node.is_comment() -> bool

True when this node is a Comment.

Returns bool

Node.is_document()

html.Node.is_document() -> bool

True when this node is a Document.

Returns bool

Node.is_doctype()

html.Node.is_doctype() -> bool

True when this node is a DocumentType.

Returns bool

Node.is_fragment()

html.Node.is_fragment() -> bool

True when this node is a DocumentFragment.

Returns bool

Node.first_child()

html.Node.first_child() -> ?Node

The first child of this node, or nil when it has none.

Returns ?Node

Node.last_child()

html.Node.last_child() -> ?Node

The last child of this node, or nil when it has none.

Returns ?Node

Node.index_in_parent()

html.Node.index_in_parent() -> number

Position of this node among its parent’s children, or -1 when it has no parent.

Returns number

Node.next_sibling()

html.Node.next_sibling() -> ?Node

The node immediately after this one under the same parent, or nil when this is the last child or has no parent.

Returns ?Node

Node.previous_sibling()

html.Node.previous_sibling() -> ?Node

The node immediately before this one under the same parent, or nil when this is the first child or has no parent.

Returns ?Node

Node.next_element_sibling()

html.Node.next_element_sibling() -> ?Element

The next sibling that is an element, skipping over text and comments, or nil when there is none.

Returns ?Element

Node.previous_element_sibling()

html.Node.previous_element_sibling() -> ?Element

The previous sibling that is an element, skipping over text and comments, or nil when there is none.

Returns ?Element

Node.child_elements()

html.Node.child_elements() -> list

This node’s children that are elements, in document order.

A fresh list is returned on every call, so changing it does not change the tree.

Returns list

Node.first_element_child()

html.Node.first_element_child() -> ?Element

The first child that is an element, or nil.

Returns ?Element

Node.last_element_child()

html.Node.last_element_child() -> ?Element

The last child that is an element, or nil.

Returns ?Element

Node.root()

html.Node.root() -> Node

The topmost node reachable by following parent_node, which for a parsed document is the Document itself and for a detached subtree is that subtree’s own root.

Returns Node

Node.has_ancestor()

html.Node.has_ancestor(other: instance) -> bool

True when other is this node or one of its ancestors.

Parameters

  • other (Node)

Returns bool

Node.descendants()

html.Node.descendants() -> list

Every node beneath this one, in document order, not including this node itself.

Returns list

Node.walk()

html.Node.walk(callback: function)

Calls callback once for every node beneath this one, in document order. The node is passed as the only argument.

Returning false from the callback prunes that node’s subtree; any other return value (including nil) keeps walking. The walk reads children as it goes, so do not restructure the tree from inside the callback.

Parameters

  • callback (function)

Node.append_child()

html.Node.append_child(node: instance) -> Node

Adds node as this node’s last child, detaching it from wherever it currently lives first. Returns node.

Parameters

  • node (Node)

Returns Node

Raises HierarchyError when node is this node or one of its ancestors, which would make the tree cyclic.

Node.insert_before()

html.Node.insert_before(node: instance, reference: ?instance) -> Node

Inserts node immediately before reference, which must be a child of this node. Passing nil for reference appends. Returns node.

Parameters

  • node (Node)
  • reference (?Node)

Returns Node

Raises HierarchyError when reference is not a child of this node, or when the insertion would make the tree cyclic.

Node.remove_child()

html.Node.remove_child(node: instance) -> Node

Removes node from this node’s children and returns it. The removed node keeps its own children; only its link to this parent is broken.

Parameters

  • node (Node)

Returns Node

Raises HierarchyError when node is not a child of this node.

Node.replace_child()

html.Node.replace_child(replacement: instance, existing: instance) -> Node

Puts replacement where existing currently sits and returns existing, now detached.

Parameters

  • replacement (Node)
  • existing (Node)

Returns Node

Raises HierarchyError when existing is not a child of this node, or when the replacement would make the tree cyclic.

Node.detach()

html.Node.detach() -> Node

Removes this node from its parent, if it has one. Returns this node so calls can be chained.

Returns Node

Node.clear_children()

html.Node.clear_children() -> Node

Removes every child of this node. The children are detached but otherwise untouched.

Returns Node

Node.text_content()

html.Node.text_content() -> string

The concatenated text of every Text node beneath this one, in document order. Comments and doctypes contribute nothing.

Text and Comment override this to return their own data.

Unlike the browser DOM, a Document answers this the same way an element does rather than returning nothing; being told the text of a document you just parsed is far more useful than being told nil.

Returns string

Node.set_text_content()

html.Node.set_text_content(value: string) -> Node

Replaces every child of this node with a single Text node holding value. Passing an empty string just empties the node, matching the DOM.

Parameters

  • value (string)

Returns Node

Node.inner_html()

html.Node.inner_html() -> string

The markup of this node’s children, serialized the way the HTML fragment serialization algorithm specifies.

Returns string

Node.set_inner_html()

html.Node.set_inner_html(source: string) -> Node

Parses source as HTML in the context of this node and replaces all of its children with the result.

The parse runs the fragment parsing algorithm with this node as the context element, so source is interpreted exactly as it would be had it appeared inside this element in the original document. That matters: '<td>x' keeps its cell inside a <tr> and loses it anywhere else, which is what a browser does too.

Nothing is executed and nothing is fetched; <script> content becomes an inert text node.

Parameters

  • source (string)

Returns Node

Node.outer_html()

html.Node.outer_html() -> string

The markup of this node including its own tags.

For a Document this is the whole document; for a Text node it is the escaped text; for a Comment it is <!--...-->.

Returns string

Node.set_outer_html()

html.Node.set_outer_html(source: string) -> list

Parses source as HTML in the context of this node’s parent and puts the result where this node currently sits.

Returns the list of nodes that replaced this one, which may be empty when source produces nothing. This node is detached either way.

Parameters

  • source (string)

Returns list

Raises HierarchyError when this node has no parent, since there would be nowhere to put the result.

Node.serialize_into()

html.Node.serialize_into(out: list)

Appends this node’s markup to out, a list of string pieces the caller is expected to join().

outer_html() is the friendly form of this and is what you normally want. Reach for this one when you are writing a large document out in pieces and would rather not build the whole string in memory first.

Every node class overrides it; the base implementation writes nothing.

Parameters

  • out (list)

Node.clone_node()

html.Node.clone_node(deep: ?bool) -> Node

A deep or shallow copy of this node, detached from any parent.

The copy is shallow by default, matching the DOM: you get the node itself with its attributes but with no children. Pass true for deep to copy the whole subtree.

Parameters

  • deep (?bool)

Returns Node

Node.get_element_by_id()

html.Node.get_element_by_id(value: string) -> ?Element

The first element beneath this node whose id attribute is value, or nil when there is none.

Ids are compared exactly, including case: HTML does not fold the case of id values even though it folds tag names.

A document with duplicate ids is malformed but perfectly parseable, and this returns the first one in document order, which is what browsers settled on.

Parameters

  • value (string)

Returns ?Element

Node.get_elements_by_class_name()

html.Node.get_elements_by_class_name(names) -> list

Every element beneath this node carrying all of the classes in names, in document order.

names may be a single class name, several separated by whitespace, or a list of names; in every form an element must carry all of them to match. Class names are compared exactly, including case. An empty names matches nothing.

%> doc.get_elements_by_class_name('warning')
%> doc.get_elements_by_class_name('warning sticky')
%> doc.get_elements_by_class_name(['warning', 'sticky'])

Parameters

  • names (string|list)

Returns list

Node.get_elements_by_tag_name()

html.Node.get_elements_by_tag_name(name: string) -> list

Every element beneath this node whose tag name is name, in document order.

The comparison folds ASCII case, so 'DIV' and 'div' find the same elements. Passing '*' returns every element.

Parameters

  • name (string)

Returns list

Node.query_selector()

html.Node.query_selector(query: string) -> ?Element

The first element beneath this node matching the CSS selector query, in document order, or nil when nothing matches.

The supported selector syntax is described in html.selector.

Parameters

  • query (string)

Returns ?Element

Raises SelectorError when query is not valid.

Node.query_selector_all()

html.Node.query_selector_all(query: string) -> list

Every element beneath this node matching the CSS selector query, in document order.

Parameters

  • query (string)

Returns list

Raises SelectorError when query is not valid.

Node.to_string()

html.Node.to_string() -> string

Renders this node as markup, the same as outer_html().

Returns string

Text

class html.Text < Node

A run of character data in the tree.

The parser merges adjacent characters into as few Text nodes as the tree construction algorithm allows, so a paragraph of prose is normally one node rather than one per character.

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
dataThe characters this node holds, already decoded: character references were resolved during parsing, so data…

Constructor

html.Text(data: string)

Parameters

  • data (string)

Text.text_content()

html.Text.text_content() -> string

The characters this node holds.

Returns string

Text.set_text_content()

html.Text.set_text_content(value: string) -> Text

Replaces this node’s characters with value.

Parameters

  • value (string)

Returns Text

Text.append_data()

html.Text.append_data(value: string) -> Text

Appends value to this node’s characters.

Parameters

  • value (string)

Returns Text

Text.serialize_into()

html.Text.serialize_into(out: list)

Parameters

  • out (list)

Comment

class html.Comment < Node

An HTML comment.

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
dataThe text between <!-- and -->, exactly as it appeared.

Constructor

html.Comment(data: string)

Parameters

  • data (string)

Comment.text_content()

html.Comment.text_content() -> string

The comment’s text.

Returns string

Comment.set_text_content()

html.Comment.set_text_content(value: string) -> Comment

Replaces the comment’s text with value.

Parameters

  • value (string)

Returns Comment

Comment.serialize_into()

html.Comment.serialize_into(out: list)

Parameters

  • out (list)

DocumentType

class html.DocumentType < Node

The <!DOCTYPE ...> node at the top of a document.

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
nameThe doctype’s name, lowercased by the tokenizer.
public_idThe public identifier, or an empty string when the doctype has none.
system_idThe system identifier, or an empty string when the doctype has none.

Constructor

html.DocumentType(name: string, public_id: ?string, system_id: ?string)

Parameters

  • name (string)
  • public_id (?string)
  • system_id (?string)

DocumentType.text_content()

html.DocumentType.text_content() -> string

A doctype contains no text, so this is always an empty string.

Returns string

DocumentType.serialize_into()

html.DocumentType.serialize_into(out: list)

Parameters

  • out (list)

DocumentFragment

class html.DocumentFragment < Node

A parentless container for a run of nodes.

Fragment parsing produces one of these, and every <template> element owns one holding its content.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

html.DocumentFragment()

DocumentFragment.serialize_into()

html.DocumentFragment.serialize_into(out: list)

Parameters

  • out (list)

Document

class html.Document < Node

A whole parsed document.

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
modeWhich quirks mode the doctype (or the lack of one) put this document in: 'no-quirks', 'limited-quirks' or…
errorsEvery parse error the tokenizer and tree builder raised, in the order they happened.

Constructor

html.Document()

Document.document_element()

html.Document.document_element() -> ?Element

The document’s root element, normally <html>, or nil for an empty document.

Returns ?Element

Document.doctype()

html.Document.doctype() -> ?DocumentType

The document’s <!DOCTYPE> node, or nil when it has none.

Returns ?DocumentType

Document.head()

html.Document.head() -> ?Element

The document’s <head> element, or nil.

The tree construction algorithm always creates one when parsing a whole document, even for input with no <head> tag in it, so this only returns nil for a document that was built by hand.

Returns ?Element

Document.body()

html.Document.body() -> ?Element

The document’s <body> element, or the <frameset> that stands in for it in a frameset document, or nil when there is neither.

Returns ?Element

Document.title()

html.Document.title() -> string

The text of the document’s <title> element, with leading and trailing whitespace removed, or an empty string when there is no title.

Returns string

Document.serialize_into()

html.Document.serialize_into(out: list)

Parameters

  • out (list)

ClassList

class html.ClassList

A live view of one element’s class attribute.

Every method reads and writes the attribute itself, so a class_list never goes stale and two of them taken from the same element always agree.

%> var el = doc.query_selector('div')
%> el.class_list().add('active')
%> el.class_list().contains('active')
true
%> el.get_attribute('class')
'active'
  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
elementThe element this list reads from and writes to.

Constructor

html.ClassList(element)

Parameters

  • element (Element)

ClassList.to_list()

html.ClassList.to_list() -> list

The class names currently on the element, in source order, with duplicates removed.

Returns list

ClassList.length()

html.ClassList.length() -> number

How many distinct classes the element carries.

Returns number

ClassList.contains()

html.ClassList.contains(name: string) -> bool

True when the element carries name. The comparison is exact: HTML class names are case-sensitive in standards mode.

Parameters

  • name (string)

Returns bool

ClassList.add()

html.ClassList.add(...names: list) -> ClassList

Adds every name given to the element’s class attribute, ignoring any it already has. Returns the list so calls can be chained.

An empty name, or one containing whitespace, is rejected: those cannot be written to a class attribute without changing what it means.

Parameters

  • names (...string)

Returns ClassList

Raises ArgumentError

ClassList.remove()

html.ClassList.remove(...names: list) -> ClassList

Removes every name given from the element’s class attribute, ignoring any it does not have. Returns the list.

Parameters

  • names (...string)

Returns ClassList

Raises ArgumentError

ClassList.toggle()

html.ClassList.toggle(name: string, force: ?bool) -> bool

Adds name when the element does not have it and removes it when it does, then returns true when the class is present afterwards.

Passing force turns this into an unconditional add (true) or remove (false), which is handy when the desired state is already in a variable.

Parameters

  • name (string)
  • force (?bool)

Returns bool

Raises ArgumentError

ClassList.to_string()

html.ClassList.to_string() -> string

The class attribute’s value as it would be written out.

Returns string

Element

class html.Element < Node

An element in the tree.

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
tag_nameThe element’s tag name.
namespaceThe namespace this element lives in: one of HTML_NAMESPACE, SVG_NAMESPACE or MATHML_NAMESPACE.
attributesThe element’s attributes, in source order, as a dictionary of name to value.
attribute_namespacesNamespace URIs for the few attributes that have one, keyed by the same qualified name used in attributes.
source_lineThe line in the source where this element’s start tag began, counting from 1, or 0 for an element that was…
source_columnThe column in the source where this element’s start tag began, counting from 1, or 0 for an element that…
contentFor a <template> element, the DocumentFragment holding its content.

Constructor

html.Element(tag_name: string, namespace: ?string, attributes: ?dict)

Parameters

  • tag_name (string)
  • namespace (?string)
  • attributes (?dict)

Element.is_html()

html.Element.is_html() -> bool

True when this is an HTML element, as opposed to one inside an <svg> or <math> subtree.

Returns bool

Element.is_void()

html.Element.is_void() -> bool

True when this element is one of the HTML void elements, which have no closing tag and can hold no content.

Returns bool

Element.get_attribute()

html.Element.get_attribute(name: string) -> ?string

The value of the attribute name, or nil when the element does not carry it.

The lookup folds ASCII case for HTML elements, so get_attribute('HREF') and get_attribute('href') are the same question. For foreign elements the name is matched exactly, because SVG really does distinguish viewBox from viewbox.

Parameters

  • name (string)

Returns ?string

Element.has_attribute()

html.Element.has_attribute(name: string) -> bool

True when the element carries the attribute name, whatever its value.

Parameters

  • name (string)

Returns bool

Element.set_attribute()

html.Element.set_attribute(name: string, value: string) -> Element

Sets the attribute name to value, replacing any existing value and leaving the attribute’s position among the others unchanged. A new attribute is appended after the existing ones.

Pass an empty string for a valueless attribute such as disabled; there is no separate “no value” state.

Parameters

  • name (string)
  • value (string)

Returns Element

Raises ArgumentError when name is empty or contains a character that cannot appear in an attribute name.

Element.remove_attribute()

html.Element.remove_attribute(name: string) -> bool

Removes the attribute name and returns true when it was actually there.

Parameters

  • name (string)

Returns bool

Element.attribute_names()

html.Element.attribute_names() -> list

The attribute names this element carries, in source order.

Returns list

Element.attribute_namespace()

html.Element.attribute_namespace(name: string) -> ?string

The namespace URI of the attribute name, or nil when it has none. Only foreign content produces namespaced attributes.

Parameters

  • name (string)

Returns ?string

Element.set_attribute_namespace()

html.Element.set_attribute_namespace(name: string, uri: string) -> Element

Records that the attribute name belongs to uri. Called by the parser while adjusting foreign attributes; there is no reason to call it by hand.

Parameters

  • name (string)
  • uri (string)

Returns Element

Element.id()

html.Element.id() -> string

The element’s id, or an empty string when it has none.

Returns string

Element.class_name()

html.Element.class_name() -> string

The element’s class attribute verbatim, or an empty string.

Returns string

Element.class_names()

html.Element.class_names() -> list

The element’s classes as a list, in source order and without duplicates. An element with no class attribute gives an empty list.

Returns list

Element.class_list()

html.Element.class_list() -> ClassList

A ClassList view of this element’s class attribute, for adding, removing and toggling classes.

A fresh view is returned on every call; because it reads and writes the attribute directly there is no state to keep, and two views of the same element behave identically.

Returns ClassList

Element.matches()

html.Element.matches(query: string) -> bool

True when this element matches the CSS selector query.

Parameters

  • query (string)

Returns bool

Raises SelectorError when query is not valid.

Element.closest()

html.Element.closest(query: string) -> ?Element

This element, or its nearest ancestor, matching the CSS selector query: or nil when neither this element nor any ancestor does.

The search starts at the element itself, which is what makes el.closest('div') return el when el is a div.

Parameters

  • query (string)

Returns ?Element

Raises SelectorError when query is not valid.

Element.serialize_into()

html.Element.serialize_into(out: list)

Parameters

  • out (list)

Element.inner_html()

html.Element.inner_html() -> string

The markup of this element’s children.

A <template> answers with its content fragment rather than with its own children, which are always empty. That matches the DOM, and it is the only way the question has a useful answer for a template.

Returns string

Element.set_inner_html()

html.Element.set_inner_html(source: string) -> Element

Parses source and replaces this element’s children with the result.

For a <template> the result goes into the content fragment, matching where inner_html() reads from.

Parameters

  • source (string)

Returns Element

Element.clone_node()

html.Element.clone_node(deep: ?bool) -> Element

A deep or shallow copy of this element. Deep-cloning a <template> also copies its content fragment, which is otherwise not part of the element’s children.

Parameters

  • deep (?bool)

Returns Element


2026, Richard Ore and The Zuri Contributors

html.parser

import html.parser

html lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled html.parser.* needs import html.parser.

The HTML tree construction stage: the half of the parser that takes the tokenizer’s stream and decides what the document actually means.

This is where HTML’s famous forgiveness lives. <b>a<p>b</b>c, <table><td>x, an unclosed <li>, a </div> with nothing to close: none of them are errors you have to handle, because the standard defines exactly what tree each one produces, and this module implements those rules rather than inventing its own.

Most programs want html.parse() and never come here. TreeBuilder is public for the cases that need the machinery itself: reading errors to lint a document, or driving a parse whose tokenizer you control.

Functions

parse()

html.parse(source: string, options: ?dict) -> Document

Parses source as a complete HTML document.

The result is always a usable Document, however broken the input was: HTML defines a tree for every possible string, and this follows those rules rather than raising. Anything the standard calls a parse error is collected in document.errors, so a non-empty list there is how you find out the source was not conformant.

The document always has an <html> root with a <head> and a <body>, even when the source contains none of those tags, because the tree construction algorithm creates them.

options takes:

  • scripting (bool, default false): parse as though scripting were enabled. With the default, <noscript> content is parsed as real markup, which is what you want when reading a page rather than rendering it. Nothing is ever executed either way.

The source is treated as UTF-8 text that has already been decoded. No charset sniffing, byte order mark handling or <meta charset> processing happens; a Zuri string is already Unicode by the time it reaches here.

%> import html
%> var doc = html.parse('<p>Hello<p>World')
%> doc.query_selector_all('p').length()
2
%> doc.body().outer_html()
'<body><p>Hello</p><p>World</p></body>'

Parameters

  • source (string)
  • options (?dict)

Returns Document

parse_file()

html.parse_file(path: string, options: ?dict) -> Document

Reads the file at path and parses it as HTML.

The file is read as UTF-8. Bytes that are not valid UTF-8 are replaced with U+FFFD rather than raising, so a mislabelled file still parses into something you can inspect.

import html

var doc = html.parse_file('page.html')
echo doc.title()

Parameters

  • path (string)
  • options (?dict) — The same options parse() takes.

Returns Document

Raises Error when the file cannot be read.

parse_fragment()

html.parse_fragment(source: string, context, options: ?dict) -> DocumentFragment

Parses source as a fragment, as though it had been written inside context.

This is the algorithm behind set_inner_html(), and the context matters: '<td>x' keeps its cell when the context is a <tr> and loses it anywhere else, exactly as it would in a browser.

context may be an Element or a tag name. A tag name is treated as an HTML element with no attributes, which is the common case. Passing nil parses in a <body> context.

The returned DocumentFragment holds the parsed nodes. Its children are detached from any document, ready to be inserted wherever you want them.

%> import html
%> html.parse_fragment('<td>x', 'tr').inner_html()
'<td>x</td>'
%> html.parse_fragment('<td>x', 'div').inner_html()
'x'

Parameters

  • source (string)
  • context (?Element|string)
  • options (?dict) — The same options parse() takes.

Returns DocumentFragment

Classes

FormattingEntry

class html.parser.FormattingEntry

One entry in the list of active formatting elements.

The list holds the element and the token it was created from, because the adoption agency algorithm has to build fresh copies of an element from its original attributes long after the tag that opened it has gone.

Fields

FieldTypeDescription
elementThe element currently standing for this entry, or nil when the entry is a marker.
tokenThe start tag token the element was created from.

Constructor

html.parser.FormattingEntry(element, token)

Parameters

  • element (?Element) — nil makes this a marker.
  • token (?Token)

FormattingEntry.is_marker()

html.parser.FormattingEntry.is_marker() -> bool

True when this entry is a marker rather than an element.

Markers are pushed when a <table>, <template>, <caption>, <td> or an applet-like element opens, and they stop formatting elements from leaking across that boundary.

Returns bool

TreeBuilder

class html.TreeBuilder

Builds a document tree from a token stream.

The usual way in is parse(), which wires a tokenizer to a builder and runs it. Construct one directly when you want to watch the process: errors accumulates every conformance problem, and the stack of open elements is readable at any point.

Fields

FieldTypeDescription
documentThe document being built.
tokenizerThe tokenizer feeding this builder.
errorsParse errors from both stages, in the order they happened.
open_elementsThe stack of open elements.
formattingThe list of active formatting elements, holding FormattingEntry instances.
modeThe current insertion mode, named as the standard names it: 'initial', 'in body', 'in table text' and…
original_modeWhere to return to after a RAWTEXT or RCDATA element finishes.
template_modesThe stack of template insertion modes, one per open <template>.
head_elementThe <head> element, once it exists.
form_elementThe innermost open <form>, which is what makes a stray </form> close the right thing.
frameset_okWhether a <frameset> could still legally replace the body.
scriptingWhether to parse as though scripting were enabled.
fragment_contextThe context element when this builder is parsing a fragment, or nil for a whole document.
foster_parentingWhether insertions are currently being foster parented out of a table.
pending_charactersCharacter tokens collected by the “in table text” insertion mode before it decides whether they are legal…
doneSet once parsing has stopped, either at end of file or because a rule said to stop.

Constructor

html.TreeBuilder(source, options: ?dict)

Parameters

  • source (Tokenizer)
  • options (?dict) — scripting (default false).

TreeBuilder.run()

html.TreeBuilder.run() -> Document

Runs the parse to completion and returns the document.

Returns Document

TreeBuilder.current_node()

html.TreeBuilder.current_node() -> ?Element

The element the parser is currently inside, or nil when the stack is empty.

Returns ?Element

TreeBuilder.prepare_fragment()

html.TreeBuilder.prepare_fragment(context) -> Element

Puts this builder into the state the fragment parsing algorithm starts from, with context as the element the markup is being parsed inside, and returns the synthetic <html> root the parsed nodes will hang off.

parse_fragment() is the friendly form of this and is what you normally want; this is public so a caller driving the tokenizer by hand can set the same state up.

Parameters

  • context (Element)

Returns Element


2026, Richard Ore and The Zuri Contributors

html.selector

import html.selector

html lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled html.selector.* needs import html.selector.

A small, deliberately bounded CSS selector engine: enough to find things in a parsed document, and no more.

Supported syntax

FormExample
typediv
universal*
id#main
class.warning
attribute presence[disabled]
attribute equality[type="text"]
descendantarticle p
childul > li
next siblingh2 + p
subsequent siblingh2 ~ p
first childli:first-child
last childli:last-child
nth childli:nth-child(2n+1)
selector listh1, h2, h3

Compound selectors combine freely: ul.menu > li[data-id]:first-child is valid, as is any comma-separated list of those.

Deliberately not supported

Everything else, and the parser says so rather than quietly ignoring it. :not(), :has(), :nth-of-type(), ::before, namespace prefixes, and the substring attribute operators (^=, $=, *=, ~=, |=) all raise a SelectorError. A selector that silently matches nothing is far worse to debug than one that refuses to compile.

Case sensitivity

Type selectors and attribute names fold ASCII case, matching how HTML itself treats them. Ids, class names and attribute values are compared exactly, which is what standards-mode HTML does.

Functions

compile()

html.compile(source: string) -> Selector

Compiles source into a reusable Selector.

Compiled selectors are cached by source text, so calling this with the same string repeatedly costs a dictionary lookup rather than a parse. The cache holds at most 256 selectors and is dropped wholesale when it fills, so a program that builds selector strings dynamically cannot grow it without bound.

%> import html
%> var s = html.selector.compile('ul.menu > li:first-child')
%> s.matches(element)
true

Parameters

  • source (string)

Returns Selector

Raises SelectorError when source is empty, malformed, or uses syntax outside the supported subset.

Classes

SelectorError

class html.SelectorError < Error

Raised when a selector cannot be parsed, or uses syntax this module deliberately does not support. The message names the offending part of the selector.

Constructor

html.SelectorError(message)

Parameters

  • message (string)

AttributeTest

class html.selector.AttributeTest

One name/=value test from a [...] block.

Fields

FieldTypeDescription
nameThe attribute name, lowercased.
valueThe value the attribute must equal, or nil when the test is a bare presence check.

Constructor

html.selector.AttributeTest(name: string, value: ?string)

Parameters

  • name (string)
  • value (?string)

AttributeTest.matches()

html.selector.AttributeTest.matches(element) -> bool

True when element satisfies this test.

Parameters

  • element (any)

Returns bool

PseudoTest

class html.selector.PseudoTest

One :pseudo or :pseudo(...) test.

Fields

FieldTypeDescription
nameThe pseudo-class name, lowercased and without its colon.
stepThe a of the an+b argument, for nth-child.
offsetThe b of the an+b argument, for nth-child.

Constructor

html.selector.PseudoTest(name: string, step: ?number, offset: ?number)

Parameters

  • name (string)
  • step (?number)
  • offset (?number)

PseudoTest.matches()

html.selector.PseudoTest.matches(element) -> bool

True when element satisfies this test.

Parameters

  • element (any)

Returns bool

CompoundSelector

class html.selector.CompoundSelector

A run of simple selectors with nothing between them, such as div#main.active[data-x]:first-child. Every part has to match the same element.

Fields

FieldTypeDescription
tag_nameThe type selector’s tag name in lowercase, or nil when the compound has none or uses *.
idThe id the element must carry, or nil.
classesClass names the element must all carry.
attributesAttributeTest instances the element must all satisfy.
pseudosPseudoTest instances the element must all satisfy.

Constructor

html.selector.CompoundSelector()

CompoundSelector.matches()

html.selector.CompoundSelector.matches(element) -> bool

True when element satisfies every part of this compound.

Parameters

  • element (any)

Returns bool

ComplexSelector

class html.selector.ComplexSelector

One selector from a comma-separated list: a chain of compounds joined by combinators, such as ul > li + li.

Fields

FieldTypeDescription
compoundsThe compounds, leftmost first.
combinatorsHow each compound relates to the one before it.

Constructor

html.selector.ComplexSelector()

ComplexSelector.matches()

html.selector.ComplexSelector.matches(element) -> bool

True when element matches this selector.

Matching runs right to left, which is what makes a selector like body div span cheap: the rightmost compound rejects almost every element outright, and only survivors pay for the walk up the tree.

Parameters

  • element (any)

Returns bool

Selector

class html.Selector

A compiled selector list: h1, h2 is one of these holding two ComplexSelector instances.

Instances are immutable once compiled and safe to reuse, which is why compile() caches them.

Fields

FieldTypeDescription
sourceThe selector text this was compiled from, kept for error messages and for to_string().
alternativesThe alternatives, in the order they were written.

Constructor

html.Selector(source: string, alternatives: list)

Parameters

  • source (string)
  • alternatives (list)

Selector.matches()

html.Selector.matches(element) -> bool

True when element matches any alternative in this selector.

Parameters

  • element (any)

Returns bool

Selector.to_string()

html.Selector.to_string() -> string

The selector text this was compiled from.

Returns string


2026, Richard Ore and The Zuri Contributors

html.serialize

import html.serialize

html lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled html.serialize.* needs import html.serialize.

Writing a document back out, shaped for whoever has to read it next: minify() for a browser, format() for a person.

Neither of these is the same as outer_html(). That one is exact and round-trips a tree byte for byte; these two deliberately change whitespace, which is the whole point of both.

%> import html
%> html.minify('<p>  a   b  </p>\n<p>c</p>', { fragment: true })
'<p>a b</p><p>c</p>'
%> echo html.format('<ul><li>a<li>b</ul>', { fragment: true })
<ul>
  <li>a</li>
  <li>b</li>
</ul>

Constants

INLINE_ELEMENTS

html.serialize.INLINE_ELEMENTS = [...]

Elements that flow inside a line of text rather than starting a new block. Whitespace around these carries meaning, so neither serializer moves it.

This is the default rendering of each element. CSS can turn any element into any box type, and neither serializer reads CSS, so a stylesheet that makes a <div> inline is a case where format() can change how the page looks. minify() is safe either way: it only ever collapses runs of whitespace to a single space, which both box types render identically.

PRESERVE_WHITESPACE

html.serialize.PRESERVE_WHITESPACE = [...]

Elements whose text content is significant to the last character. Nothing inside one of these is ever reflowed, re-indented or collapsed.

Functions

minify()

html.minify(source, options: ?dict) -> string

Removes the markup a browser does not need and returns the result.

source is either a node you already have or a string of HTML. A string is parsed as a complete document, so <html>, <head> and <body> appear in the output even when the input had none of them, exactly as a browser would produce them. Pass { fragment: true } to parse and return just the markup you gave it instead.

What this changes:

  • Runs of whitespace in text become a single space.
  • Whitespace-only text between two block level elements is removed entirely, along with leading and trailing whitespace inside a block level element.
  • Comments are removed.

What this never touches:

  • The content of <pre>, <textarea>, <script>, <style>, <xmp>, <listing> and <plaintext>.
  • Whitespace next to an inline element, which is a word separator and cannot be removed without changing the text.
  • Attribute values, attribute order, tag names, or the doctype.

options:

  • fragment (bool, default false): parse a string source as a fragment rather than a whole document. Ignored when source is already a node.
  • comments (bool, default false): keep comments. Turn this on when the markup carries conditional comments or a licence header that has to survive.
  • collapse_whitespace (bool, default true): do the whitespace work at all. With false the only thing minifying does is drop comments.

The tree passed in is never modified; the work happens on a copy.

%> import html
%> html.minify('<div>  <p>a   b</p>  <!-- x --> </div>', { fragment: true })
'<div><p>a b</p></div>'
%> html.minify('<p>a <b> b </b> c</p>', { fragment: true })
'<p>a <b> b </b> c</p>'

Parameters

  • source (Node|string)
  • options (?dict)

Returns string

format()

html.format(source, options: ?dict) -> string

Renders source as indented, readable HTML.

This is a pretty printer, not a round-trip. It moves whitespace between elements to make the structure visible, so the output is equivalent for reading and reviewing but is not byte-identical to the input. Use outer_html() when the exact markup matters.

Whitespace inside inline content is left alone, because collapsing it would change the rendered text; only the gaps between block level elements are re-indented. The content of <pre>, <textarea>, <script>, <style>, <xmp>, <listing> and <plaintext> is copied through untouched.

options:

  • indent (number or string, default 2): a number is that many spaces per level; a string is used as the unit verbatim, so '\t' gives tab indentation.
  • fragment (bool, default false): parse a string source as a fragment rather than a whole document.
  • comments (bool, default true): keep comments.
%> import html
%> echo html.format('<div><p>Hi <b>there</b></p></div>', { fragment: true })
<div>
  <p>Hi <b>there</b></p>
</div>
%> echo html.format('<div><p>x</p></div>', { fragment: true, indent: '\t' })
<div>
	<p>x</p>
</div>

Parameters

  • source (Node|string)
  • options (?dict)

Returns string


2026, Richard Ore and The Zuri Contributors

html.tokenizer

import html.tokenizer

html lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled html.tokenizer.* needs import html.tokenizer.

The HTML tokenizer from the WHATWG HTML Living Standard: the stage that turns a string of markup into a stream of doctype, tag, comment and character tokens.

You rarely need this directly. html.parse() drives it to build a document tree, and that is what almost every program wants. Reach for the tokenizer when you care about the markup as written: linting a template, rewriting attributes in place, or checking which parse errors a document raises.

import html

for token in html.tokenize('<p class="x">hi</p>') {
  echo '${token.type} ${token.name}${token.data}'
}

# start-tag p
# character hi
# end-tag p
# eof

Two deliberate departures from a literal reading of the spec

The standard emits one character token per character. This tokenizer emits one per contiguous run, so a paragraph of prose is a single token rather than a few hundred. Nothing observable changes: the tree builder splits runs back apart wherever the algorithm is defined per character.

The standard also spells the “character reference” sub-machine as nine separate tokenizer states. Here that algorithm lives in html.entities.consume_reference() and is shared with html.decode(), so both agree on every edge case by construction rather than by careful duplication.

Constants

RCDATA_ELEMENTS

html.tokenizer.RCDATA_ELEMENTS = [...]

Tag names whose content is text with character references but no markup. A <title> may contain &amp; but never a nested element.

RAWTEXT_ELEMENTS

html.tokenizer.RAWTEXT_ELEMENTS = [...]

Tag names whose content is text with neither markup nor character references. <noscript> joins this list only when the tokenizer is created with scripting enabled.

Functions

tokenize()

html.tokenize(source: string, options: ?dict) -> list

Tokenizes source and returns every token, ending with the eof token.

This is the whole-document convenience over Tokenizer: it runs with auto_content_state on, so <script>, <style>, <title>, <textarea> and <plaintext> have their content tokenized as text exactly as a browser would, without a tree builder being involved.

%> import html
%> html.tokenize('<b>hi</b>')
[<start-tag b>, <character 'hi'>, <end-tag b>, <eof>]

options takes scripting (default false), which decides whether <noscript> content is text or markup.

Parameters

  • source (string)
  • options (?dict)

Returns list

Classes

Token

class html.Token

One token from the tokenizer.

A single class covers all five token types rather than five classes, because the tree builder dispatches on type on every token and an inheritance check would be the hottest thing in the parser. Which fields carry meaning depends on type:

typefields that matter
doctypename, public_id, system_id, force_quirks
start-tagname, attributes, self_closing
end-tagname, attributes, self_closing
commentdata
characterdata
eofnone
  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
typeOne of 'doctype', 'start-tag', 'end-tag', 'comment', 'character' or 'eof'.
nameThe tag or doctype name, lowercased.
dataCharacter data for a character token, or the text between <!-- and --> for a comment.
attributesA tag’s attributes in source order, as a dictionary of lowercased name to value.
self_closingTrue when the tag was written with a trailing slash, as in .
acknowledged_self_closingSet by the tree builder when it has taken self_closing into account, which only a void or foreign element…
public_idA doctype’s public identifier, or nil when it has none.
system_idA doctype’s system identifier, or nil when it has none.
force_quirksSet on a doctype the tokenizer could not read properly.
line1-based line the token started on.
column1-based column the token started at.

Constructor

html.Token(type: string)

Parameters

  • type (string)

Token.is_start()

html.Token.is_start(name: string) -> bool

True when this token is a start tag named name.

Parameters

  • name (string)

Returns bool

Token.is_end()

html.Token.is_end(name: string) -> bool

True when this token is an end tag named name.

Parameters

  • name (string)

Returns bool

Token.to_string()

html.Token.to_string() -> string

A short, readable rendering of the token, meant for debugging and for test output rather than for round-tripping to markup.

Returns string

ParseError

class html.ParseError

A parse error raised while tokenizing or building the tree.

HTML has no fatal errors: every one of these is recoverable and the parse always produces a usable result. They are collected so that a linter, or anyone auditing markup, can see what a browser silently forgave.

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
codeThe standard’s name for this error, such as 'unexpected-null-character' or 'eof-in-tag'.
line1-based line the error was noticed on.
column1-based column the error was noticed at.

Constructor

html.ParseError(code: string, line: number, column: number)

Parameters

  • code (string)
  • line (number)
  • column (number)

ParseError.to_string()

html.ParseError.to_string() -> string

Returns string

Tokenizer

class html.Tokenizer

Turns markup into tokens, one call to next_token() at a time.

The tokenizer’s state is not entirely its own: which state it should be in after a start tag depends on what the tree builder decided to do with that tag. Two ways to bridge that:

  • Driven by html.parse(), the tree builder calls set_state() itself, exactly as the standard describes.
  • Used on its own (which is what tokenize() does), the tokenizer switches itself on <script>, <style>, <title>, <textarea>, <plaintext> and friends, which is what the tree builder would have told it to do anyway.

The second behaviour is controlled by the auto_content_state option and is on by default, so a tokenizer you create by hand does the useful thing without extra ceremony.

Fields

FieldTypeDescription
inputThe source, split into characters, with newlines already normalized: CRLF and lone CR both become LF, as the…
posHow far into input the tokenizer has read.
stateThe state machine’s current state, named exactly as the standard names it but in snake case: 'data',…
errorsParse errors seen so far, as ParseError instances, in the order they happened.
scriptingWhether to enter RAWTEXT for <noscript>, and generally to behave as a browser with scripting turned on…
auto_content_stateWhether the tokenizer switches itself into RCDATA, RAWTEXT, script data or PLAINTEXT after the corresponding…
cdata_okSet by the tree builder when the insertion point is inside foreign content, where <![CDATA[ is meaningful.

Constructor

html.Tokenizer(source: string, options: ?dict)

Parameters

  • source (string)
  • options (?dict) — scripting (default false) and auto_content_state (default true).

Tokenizer.next_token()

html.Tokenizer.next_token() -> ?Token

The next token, or nil once the eof token has been returned.

The eof token is returned exactly once and is always the last one, so a loop can stop on nil or on token.type == 'eof' interchangeably.

Returns ?Token

Tokenizer.set_state()

html.Tokenizer.set_state(name: string) -> Tokenizer

Puts the tokenizer into name, one of the state names listed on the state field.

The tree builder calls this to enter RCDATA or RAWTEXT after a start tag, which is how the standard splits the work between the two stages.

Parameters

  • name (string)

Returns Tokenizer

Tokenizer.set_last_start_tag()

html.Tokenizer.set_last_start_tag(name: ?string) -> Tokenizer

Tells the tokenizer which start tag the tree builder most recently opened, so that RCDATA and RAWTEXT know which end tag closes them.

Parameters

  • name (?string)

Returns Tokenizer


2026, Richard Ore and The Zuri Contributors

wire

import wire

Wire, an HTML template engine.

A Wire template is an HTML document. Not a document with a templating language sprinkled through it, and not a string that happens to end up looking like HTML: the source is parsed by the same WHATWG parser the html module uses, and every valid HTML5 document is already a valid Wire template. What Wire adds are attributes on ordinary elements and values written between {{ and }}.

Parsing rather than substituting is what makes the rest possible. Wire knows that one interpolation sits in a paragraph, another in an href, another in a <script>, and it escapes each of them for where it actually is. A value can never turn into structure.

import wire

echo wire.render_string(
  '<p>Hello {{ name }}</p>',
  { name: '<b>Ada</b>' }
)
# <p>Hello &lt;b&gt;Ada&lt;/b&gt;</p>

Templates from files

render_string() is convenient for a one-off and for reading documentation. Real templates live in files under a root directory, which is templates beside the working directory unless you say otherwise:

import wire

var view = wire.wire()
view.set_root('./views')

echo view.render('pages/home', { user })

The .html extension is added when the path as written names no file, so 'pages/home' finds pages/home.html. set_extension() changes that.

Every path resolves inside the root and nowhere else. One that climbs out with .., or names an absolute path elsewhere, is refused. That matters the moment a path is built from anything a request supplied.


Values

Anything between {{ and }} is an expression. The full language is in wire.expression; in practice it reads the way you would guess:

<h1>{{ post.title }}</h1>
<p>{{ post.author.name|upper }}</p>
<p>{{ post.tags|join(', ') }}</p>
<p>{{ post.views > 1000 ? 'popular' : 'quiet' }}</p>
<p>{{ user.nickname ?? user.name }}</p>

A name that was never supplied is nil, and so is a key read off it, so {{ user.address.city }} on a request with no user renders as nothing rather than failing. Ask with x-if when the difference matters.

Writing {{ literally is %{{.

Filters

A value passed through | goes through a filter. They chain, and they take arguments:

{{ name|trim|title }}
{{ total|number_format(2) }}
{{ summary|truncate(80, '…') }}

wire.filters lists the forty-odd that ship with Wire, and register_filter() adds your own.


Escaping, and why there is no way to turn it off

Every interpolation is escaped for the place it lands in:

Where it isWhat happens
text between tags&, < and > become references
an attribute value& and " become references
href, src, actionthe URL’s scheme is checked as well
<script>, onclickthe value is encoded as JSON
<style>anything outside a CSS-safe set is dropped

A javascript: URL in an href becomes about:blank. set_url_schemes() changes the allowlist when an application really does need another scheme.

Inside a <script> the value carries its own quotes, so do not add any:

<script>
  var user = {{ name }};    // right, renders as "Ada"
  var user = '{{ name }}';  // wrong, renders as '"Ada"'
</script>

Markup you built yourself and want rendered as markup goes through the raw filter, or arrives already wrapped in wire.safe():

<div class="body">{{ article.rendered_html|raw }}</div>

raw is a promise that the value is safe where it lands. Applying it to anything a user supplied is how a template engine becomes a cross-site scripting hole.


Directives

Wire’s own attributes all begin with x-. Wire owns that prefix outright: an x- attribute it does not recognize is an error rather than something that quietly does nothing, so x-fi is caught the first time the template compiles.

A directive can go on any element, and the element it goes on is part of what it controls. Put it on <template> when you want the control without an element in the output; <template> is the one element HTML lets through untouched anywhere, including inside <head>, a table and a <select>, and Wire removes it.

Choosing

<p x-if="user.is_admin">Admin</p>
<p x-elif="user.is_staff">Staff</p>
<p x-else>Member</p>

x-elif and x-else attach to the element sibling in front of them, with only whitespace and comments allowed in between. x-not is the inverse of x-if and takes no chain.

Repeating

<li x-for="items" x-value="item" x-key="index">
  {{ index }}: {{ item.name }}
</li>

The element repeats, not just its children. Lists, dictionaries, strings and ranges all iterate; a list binds x-key to the position and a dictionary to the key.

Every iteration also publishes a loop variable:

FieldIs
loop.indexthe position counting from 1
loop.index0the position counting from 0
loop.firsttrue on the first pass
loop.lasttrue on the last
loop.lengthhow many entries there are
loop.even, .oddwhether the position is even or odd
loop.key, .valuethis entry, whether or not it is bound
loop.parentthe enclosing loop’s own loop

x-loop="row" renames it, which is how two nested loops can both be read without going through parent.

An x-if on a repeated element is asked once per iteration, so it can read the loop’s variables. It cannot be continued with x-elif there.

Content

<p x-text="summary"></p>
<div x-html="article.body|raw"></div>
<a x-attr="{ href: post.url, rel: post.external ? 'nofollow' : nil }">…</a>

x-text and x-html replace an element’s children. x-attr spreads a dictionary onto it, where true gives a valueless attribute and false or nil leaves the attribute off.


Composition

Including

<include path="partials/header.html" />
<template x-include="partials/header.html"></template>

Those two are the same thing. <include>, <extend>, <declare>, <define> and <super> are sugar, rewritten into the directive form while the source is being tokenized, which is why they work inside a <head> or a table where an unknown element would be moved or dropped.

A path is read as text, not as an expression, so it can be written plainly and computed where it needs to be:

<include path="themes/{{ theme }}/header.html" />

An include sees the variables around it. x-with adds to them and x-only withholds everything else, which is what turns a partial into a component with a real interface:

<include path="components/badge" x-with="{ label: 'New', tone: 'green' }" only />

Anything written inside an include is handed to it as the region called content, so a partial can wrap what it was given:

<!-- components/card.html -->
<div class="card">
  <h3>{{ title }}</h3>
  <declare name="content"></declare>
</div>

<!-- the page -->
<include path="components/card" x-with="{ title: 'Totals' }">
  <p>{{ orders|length }} orders</p>
</include>

Inheriting

A base template marks the regions a page may replace:

<!-- layouts/page.html -->
<!DOCTYPE html>
<html>
  <head>
    <title>{{ title }}</title>
    <declare name="head"></declare>
  </head>
  <body>
    <main>
      <declare name="content"></declare>
    </main>
    <footer>
      <declare name="footer">
        <p>&copy; {{ year }}</p>
      </declare>
    </footer>
  </body>
</html>

A page fills them in:

<!-- pages/home.html -->
<extend base="layouts/page.html">
  <define name="content">
    <h1>Hello {{ user.name }}</h1>
  </define>
  <define name="footer">
    <super />
    <p>All rights reserved.</p>
  </define>
</extend>

A region nobody defines renders whatever the base put inside it. <super /> renders the definition this one replaces, so a page can add to a footer instead of restating it. Inheritance nests as deep as you like, and a middle template can declare regions of its own.

One template extends one base, and everything in an extending template has to be inside a <define>, because the base is what decides the structure. Defining the same region twice needs override said out loud:

<define name="content" override>…</define>

Extending Wire from Zuri

import wire

var view = wire.wire()

# A filter: the value comes first, then whatever the template passed.
view.register_filter('excerpt', @(value, words) {
  var count = words == nil ? 25 : words
  return ' '.join(value.split('/\\s+/').take(count)) + '…'
})

# A value every template can see without being handed it.
view.register_global('site_name', 'Example')

# A function a template calls.
view.register_global('route', @(name) {
  return '/' + name
})

Used as:

<p>{{ post.body|excerpt(40) }}</p>
<a href="{{ route('about') }}">{{ site_name }}</a>

register_element() claims a tag name outright and hands every one of them to a function, which is the escape hatch for anything the directives cannot express.


Compiling and caching

A template is parsed once, not once per render. render() compiles on first use and keeps the result, checking the file’s modification time and size before reusing it.

That check is one stat per template per render, which is what you want while writing templates and not what you want under load. set_auto_reload(false) turns it off, and clear_cache() is how a long-running process picks up a deployment.


Comments

HTML comments are server-side notes and do not reach the page:

<!-- This never ships, and neither does {{ secret }}. -->

Nothing inside one is evaluated. set_comments(true) keeps them when a comment is genuinely meant for the browser, as a conditional comment or a build marker is.


When something is wrong

Everything Wire raises is a WireError and carries the template and the line and column of the tag at fault. TemplateSyntaxError comes from compiling, RenderError from rendering, and TemplateNotFoundError from a path that goes nowhere.

catch {
  echo view.render('pages/home', { user })
} as e {
  if instance_of(e, wire.WireError) {
    echo 'template ${e.location()}: ${e.reason}'
  }
}

The module is callable. wire(...) is the same call as wire.wire(...), documented below.

The wire API

Every public name in wire, wherever it is declared. Each links to the page that documents it.

NameKindSummary
wire.RenderErrorclassRaised while rendering, for anything that depends on the values a template was given: iterating something…
wire.SafeclassA string that is already markup and must be written out as it is.
wire.TemplateNotFoundErrorclassRaised when a template file cannot be found, or when it resolves to somewhere outside the root directory.
wire.TemplateSyntaxErrorclassRaised while compiling a template, for anything Wire can tell is wrong without rendering: an unknown…
wire.WireclassA template engine: a root directory, a set of filters, and a cache of everything compiled so far.
wire.WireErrorclassThe base class of every error Wire raises.
wire.compile.AttributeclassOne attribute of an element.
wire.compile.BranchclassOne arm of a conditional chain.
wire.compile.CommentclassAn HTML comment that survived into the output.
wire.compile.CompilerclassCompiles one template’s source.
wire.compile.ConditionalclassA chain of x-if, x-elif and x-else, or a lone x-if.
wire.compile.ElementclassAn element, with everything about it already worked out.
wire.compile.GroupclassA run of instructions with nothing of its own, which is what a <template> carrying a directive leaves…
wire.compile.IncludeclassAnother template rendered in this instruction’s place.
wire.compile.InstructionclassThe base of every instruction, carrying the kind the renderer switches on and the place in the source it came…
wire.compile.LoopclassA repetition, from x-for.
wire.compile.RawclassCharacters written out exactly as they are, used for the doctype.
wire.compile.SegmentclassOne piece of a run of text or of an attribute value: either literal characters or an expression to evaluate.
wire.compile.SlotclassA region an extending template may replace, from x-slot.
wire.compile.SuperclassThe definition this one replaces, from x-super.
wire.compile.TemplateclassA compiled template, ready to render as many times as you like.
wire.compile.TextclassA run of text, possibly with interpolations in it.
wire.compile.compilefunctionCompiles source into a Template.
wire.constants.ATTR_ATTRconstantSpreads a dictionary of name/value pairs onto the element as attributes.
wire.constants.CALL_CLOSEconstantCloses a template function call.
wire.constants.CALL_OPENconstantOpens a template function call.
wire.constants.CARRIER_TAGconstantThe element Wire uses to carry a directive without emitting anything of its own.
wire.constants.DEFAULT_EXTENSIONconstantThe extension render() appends when the path it was given does not name an existing file and carries no…
wire.constants.DEFAULT_LOOP_NAMEconstantThe variable an x-for publishes its loop metadata under.
wire.constants.DEFAULT_ROOTconstantThe directory render() resolves template paths against until set_root() says otherwise, which is a…
wire.constants.DEFINE_ATTRconstantReplaces the base template’s region of the same name.
wire.constants.DIRECTIVESconstantEvery directive attribute Wire understands.
wire.constants.DIRECTIVE_PREFIXconstantThe prefix every Wire directive attribute carries.
wire.constants.ELIF_ATTRconstantContinues an x-if chain on the next element sibling.
wire.constants.ELSE_ATTRconstantCloses an x-if chain on the next element sibling.
wire.constants.ESCAPE_CHARconstantThe character that escapes an interpolation, making Wire emit the braces literally instead of reading what is…
wire.constants.EXPRESSION_DIRECTIVESconstantThe directives whose value is a Wire expression.
wire.constants.EXTEND_ATTRconstantMarks this template as extending the base template named by the value.
wire.constants.FLAG_DIRECTIVESconstantThe directives that take no value at all.
wire.constants.FOR_ATTRconstantRepeats the element once per entry of the expression’s value.
wire.constants.HTML_ATTRconstantReplaces the element’s children with the expression’s value as markup, without escaping it.
wire.constants.IF_ATTRconstantRenders the element only when the expression is truthy.
wire.constants.INCLUDE_ATTRconstantRenders another template in this element’s place.
wire.constants.KEY_ATTRconstantNames the variable each x-for iteration binds its key or index to.
wire.constants.LOOP_ATTRconstantRenames the loop metadata variable an x-for publishes, which is called loop unless this says otherwise.
wire.constants.MAX_DEPTHconstantHow deep x-include and x-extend may nest before Wire calls it a cycle.
wire.constants.NAME_DIRECTIVESconstantThe directives whose value names a variable or a region, and is read as a bare identifier rather than…
wire.constants.NOT_ATTRconstantRenders the element only when the expression is falsy, the inverse of x-if.
wire.constants.ONLY_ATTRconstantWithholds the surrounding scope from an x-include or a component, leaving it only what x-with passes.
wire.constants.OVERRIDE_ATTRconstantPermits an x-define to replace a definition of the same name made earlier in the same template.
wire.constants.PATH_DIRECTIVESconstantThe directives whose value is a template path, read as literal text with {{ }} interpolation allowed inside…
wire.constants.PSEUDO_ELEMENTSconstantThe pseudo elements Wire accepts as sugar, each mapped to the directive it becomes and the attribute that…
wire.constants.PSEUDO_FLAGSconstantAttributes a pseudo element may carry that become valueless directives rather than the directive its name…
wire.constants.SLOT_ATTRconstantDeclares a region an extending template may replace.
wire.constants.SUPER_ATTRconstantRenders the definition this one replaces.
wire.constants.TEXT_ATTRconstantReplaces the element’s children with the expression’s value as text.
wire.constants.VALUE_ATTRconstantNames the variable each x-for iteration binds its value to.
wire.constants.VAR_CLOSEconstantCloses an interpolation.
wire.constants.VAR_OPENconstantOpens an interpolation.
wire.constants.WITH_ATTRconstantSupplies the variables an x-include or a component is rendered with.
wire.escape.BLOCKED_URLconstantWhat a blocked URL is replaced with.
wire.escape.CONTEXT_ATTRIBUTEconstantAn ordinary attribute value.
wire.escape.CONTEXT_SCRIPTconstantA place the browser reads as JavaScript: the body of a <script>, or the value of an on* handler attribute.
wire.escape.CONTEXT_STYLEconstantThe body of a <style> element.
wire.escape.CONTEXT_TEXTconstantText between tags.
wire.escape.CONTEXT_URLconstantAn attribute the browser resolves as a URL.
wire.escape.DEFAULT_URL_SCHEMESconstantThe URL schemes Wire lets through by default.
wire.escape.STYLE_SAFEconstantThe characters a value may contain inside a <style> body.
wire.escape.URL_ATTRIBUTESconstantThe attributes whose value a browser resolves as a URL, and which therefore have to be checked for a…
wire.escape.URL_LIST_ATTRIBUTESconstantURL attributes holding a list of URLs rather than a single one.
wire.escape.attributefunctionEscapes value for a double quoted attribute value, turning & and " into character references.
wire.escape.attribute_contextfunctionThe escaping context an attribute named name calls for.
wire.escape.escapefunctionEscapes value for the given context.
wire.escape.is_script_attributefunctionWhether name is an event handler attribute, whose value a browser reads as JavaScript.
wire.escape.is_url_attributefunctionWhether name is an attribute the browser resolves as a single URL.
wire.escape.is_url_list_attributefunctionWhether name is an attribute holding a list of URLs.
wire.escape.scriptfunctionEscapes value for a place the browser reads as JavaScript, by encoding it as JSON and then hiding the…
wire.escape.stylefunctionEscapes value for the body of a <style> element by dropping every character outside a conservative…
wire.escape.textfunctionEscapes value for text between tags, turning &, < and > into character references.
wire.escape.text_contextfunctionThe escaping context text inside an element named tag calls for.
wire.escape.urlfunctionEscapes value for an attribute the browser resolves as a URL, replacing it with about:blank when its…
wire.escape.url_listfunctionEscapes value for an attribute holding several URLs, checking each entry’s scheme on its own.
wire.escape.uses_descriptorsfunctionWhether a URL list attribute allows a descriptor after each URL, which srcset does and ping does not.
wire.expression.BinaryclassAn arithmetic or comparison operator.
wire.expression.CallclassA call, as in route('home') or user.display_name().
wire.expression.Conditionalclasscondition ? consequence : alternative.
wire.expression.DictLiteralclassA dictionary literal.
wire.expression.ExpressionclassThe base of every node in a parsed expression.
wire.expression.FilterclassA value passed through a filter, as in name|upper.
wire.expression.IndexclassA bracketed lookup, as in items[index], where the key is itself an expression.
wire.expression.KEYWORDSconstantThe words that are part of the language rather than names a template can bind.
wire.expression.LONG_OPERATORSconstantOperators made of more than one character, longest first so that <= is never read as < followed by =.
wire.expression.ListLiteralclassA list literal.
wire.expression.LiteralclassA number, a string, or one of true, false and nil.
wire.expression.Logicalclassand, or or ??, each of which decides whether to evaluate its right side after looking at its left.
wire.expression.MemberclassA dotted lookup, as in user.name.
wire.expression.ParserclassBuilds a syntax tree from an expression’s tokens.
wire.expression.SHORT_OPERATORSconstantOperators made of a single character.
wire.expression.TokenclassOne piece of an expression’s source: its kind, its value, and where in the expression it started.
wire.expression.Unaryclassnot x, !x or -x.
wire.expression.VariableclassA bare name, looked up in the variables the template was rendered with.
wire.expression.parsefunctionCompiles source into a syntax tree.
wire.expression.tokenizefunctionSplits source into tokens.
wire.filters.BUILTINconstantEvery filter Wire starts with, keyed by the name a template calls it by.
wire.filters.absfunctionThe value without its sign.
wire.filters.capitalizefunctionThe value with its first letter in upper case and the rest left alone.
wire.filters.ceilfunctionThe smallest whole number at or above the value.
wire.filters.datefunctionA date written out with the given format.
wire.filters.default_tofunctionfallback when the value is falsy, otherwise the value.
wire.filters.emptyfunctionWhether the value has nothing in it.
wire.filters.escape_valuefunctionEscapes a value for a context other than the one it is being written into, or escapes a value that was…
wire.filters.filesizefunctionA byte count written the way a person reads it.
wire.filters.firstfunctionThe first entry, or nil when there is none.
wire.filters.floorfunctionThe largest whole number at or below the value.
wire.filters.isfunctionWhether the value equals expected.
wire.filters.joinfunctionThe entries joined into one string with glue between them.
wire.filters.jsonfunctionThe value as JSON.
wire.filters.json_scriptfunctionThe value as JSON wrapped in a <script type="application/json"> element, ready to be read back by a script…
wire.filters.keysfunctionA dictionary’s keys, in insertion order.
wire.filters.lastfunctionThe last entry, or nil when there is none.
wire.filters.lengthfunctionHow many entries the value has.
wire.filters.lowerfunctionThe value in lower case.
wire.filters.lpadfunctionThe value padded on the left with fill until it is width characters long.
wire.filters.nl2brfunctionThe value with its line breaks turned into elements.
wire.filters.notfunctionWhether the value differs from expected.
wire.filters.number_formatfunctionThe value written out with thousands separated and a fixed number of decimal places.
wire.filters.rawfunctionMarks a value as markup so Wire writes it out without escaping it.
wire.filters.repeatfunctionThe value repeated count times.
wire.filters.replacefunctionThe value with every occurrence of search replaced by replacement.
wire.filters.reversefunctionThe entries in the opposite order, or a string backwards.
wire.filters.roundfunctionThe value rounded to places decimal places.
wire.filters.rpadfunctionThe value padded on the right with fill until it is width characters long.
wire.filters.slicefunctionThe entries from start up to but not including end.
wire.filters.slugfunctionThe value as a lowercase, hyphen separated slug.
wire.filters.sortfunctionThe entries in ascending order.
wire.filters.splitfunctionThe value split into a list on separator.
wire.filters.strip_tagsfunctionThe value with every HTML tag removed, leaving only its text.
wire.filters.sumfunctionThe entries added together.
wire.filters.titlefunctionThe value with the first letter of every word in upper case and the rest in lower case.
wire.filters.trimfunctionThe value with leading and trailing whitespace removed.
wire.filters.truncatefunctionThe value cut down to length characters, with suffix put on the end when anything was actually cut.
wire.filters.uniquefunctionThe entries with later duplicates removed, keeping the first of each.
wire.filters.upperfunctionThe value in upper case.
wire.filters.url_encodefunctionThe value percent encoded for use inside a URL.
wire.filters.valuesfunctionA dictionary’s values, in insertion order.
wire.is_safefunctionTrue when value is a Safe.
wire.loader.LoaderclassFinds template files under one root directory.
wire.normalize.NormalizerclassA tokenizer that rewrites Wire’s pseudo elements on the way past.
wire.normalize.parsefunctionParses source into a tree with Wire’s pseudo elements already rewritten.
wire.renderfunctionRenders the template at path using the shared Wire.
wire.render.DefinitionclassOne definition of a region, and what to render it against.
wire.render.FrameclassOne level of variables, pointing at the level around it.
wire.render.RendererclassRenders compiled templates.
wire.render_stringfunctionRenders source using the shared Wire.
wire.safefunctionMarks value as markup that Wire must not escape.
wire.sharedfunctionThe Wire behind the module-level render() and render_string().
wire.stringifyfunctionWhat value looks like once it reaches the page, before escaping.
wire.truthyfunctionWhether value counts as true in an x-if, an x-not, or a boolean operator inside an expression.
wire.values.MAX_ARGUMENTSconstantThe most arguments a filter or a template function can be called with.
wire.values.comparefunctionOrders a before, with or after b, returning -1, 0 or 1.
wire.values.invokefunctionCalls target with arguments spread into its parameters.
wire.values.is_blankfunctionWhether text is empty or is nothing but whitespace.
wire.values.is_emptyfunctionWhether value has nothing in it, for the empty filter and for anything else that wants the question asked…
wire.values.type_namefunctionWire’s name for value’s type, used in error messages so that a complaint reads “expected a list, got a…
wire.values.unwrapfunctionStrips the Safe wrapper off value, leaving anything else alone.
wire.wirefunctionA new Wire.

Submodules

ModuleReached asSummary
wire.compileimport wire.compileTurning a parsed template into the instruction tree the renderer walks.
wire.constantswire.constants.*The names Wire reserves: its directive attributes, the pseudo elements that are sugar for them, and the…
wire.errorswire.errors.*The errors Wire raises, and the location information they carry.
wire.escapewire.escape.*Turning a value into something safe to write at a particular place in a page.
wire.expressionwire.expression.*Wire’s expression language: the thing that sits between {{ and }}, and the thing an x-if or an x-for…
wire.filterswire.filters.*The filters every Wire template starts with.
wire.loaderimport wire.loaderTurning the path written in an x-include into a file on disk, and refusing to when it points somewhere it…
wire.normalizewire.normalize.*Rewriting Wire’s pseudo elements into something HTML’s own tree construction will not move, drop or reshape.
wire.renderimport wire.renderWalking a compiled template and writing the page out.
wire.valueswire.values.*How Wire reads the values a template is given: what counts as true, what a value looks like once it reaches…

Functions

wire()

wire.wire(options: ?dict) -> Wire

A new Wire.

options takes any of root, extension, compact, comments, auto_reload and url_schemes, each the same as the matching set_ method, so a whole configuration can be written in one place.

import wire

var view = wire.wire({
  root: './views',
  auto_reload: false,
})

Parameters

  • options (?dict)

Returns Wire

shared()

wire.shared() -> Wire

The Wire behind the module-level render() and render_string().

Configure this to use those without building your own, which is worth doing for a small program and not worth doing for anything that wants two different roots.

import wire

wire.shared().set_root('./views')
echo wire.render('pages/home', { user })

Returns Wire

render()

wire.render(path: string, variables: ?dict) -> string

Renders the template at path using the shared Wire.

Parameters

  • path (string)
  • variables (?dict)

Returns string

Raises WireError

render_string()

wire.render_string(source: string, variables: ?dict, path: ?string) -> string

Renders source using the shared Wire.

Parameters

  • source (string)
  • variables (?dict)
  • path (?string)

Returns string

Raises WireError

Classes

Wire

class wire.Wire

A template engine: a root directory, a set of filters, and a cache of everything compiled so far.

One of these is normally built at startup, configured once, and used for the life of the process. Rendering does not change it, so it is safe to render from several places at once.

import wire

var view = wire.wire()
view.set_root('./views')
view.register_global('site_name', 'Example')

echo view.render('pages/home', { user })

Fields

FieldTypeDescription
globalsValues every template can read without being handed them.
url_schemesThe URL schemes allowed in an href and its relatives.

Constructor

wire.Wire(options: ?dict)

Parameters

  • options (?dict) — Any of root, extension, compact, comments, auto_reload and url_schemes, each the same as the matching set_ method.

Raises ArgumentError when an option is not of the type it should be.

Wire.set_root()

wire.Wire.set_root(path: string) -> Wire

Sets the directory template paths resolve inside.

A relative path is taken from the working directory. The directory does not have to exist yet; a path under one that does not is reported as not found like any other.

Changing the root empties the cache, since the same path can now mean a different file.

Parameters

  • path (string)

Returns Wire

Raises ArgumentError

Wire.root()

wire.Wire.root() -> string

The directory template paths resolve inside, as an absolute path.

Returns string

Wire.create_root()

wire.Wire.create_root() -> bool

Creates the root directory when it does not exist, and reports whether it had to.

Wire never does this on its own. Creating directories is not a template engine’s business, and a typo in a root should read as a missing template rather than quietly produce an empty directory.

Returns bool

Raises Error when the directory cannot be created.

Wire.set_extension()

wire.Wire.set_extension(extension: string) -> Wire

Sets the extension tried when a path names no file as written.

It has to begin with a dot. render('home') then looks for home and then for home plus this.

Parameters

  • extension (string)

Returns Wire

Raises ArgumentError when it does not begin with a dot.

Wire.set_compact()

wire.Wire.set_compact(compact: bool) -> Wire

Drops whitespace-only text from the output when compact is true.

This is the cheap kind of minification: the indentation between tags goes, and nothing else is touched. Whitespace inside a <pre> is whitespace-only only when the whole run is, so this can still change how preformatted text reads; leave it off where that matters and reach for html.minify() instead, which knows the difference.

Parameters

  • compact (bool)

Returns Wire

Raises ArgumentError

Wire.set_comments()

wire.Wire.set_comments(comments: bool) -> Wire

Keeps HTML comments in the output when comments is true.

Comments are dropped by default, which is what makes an HTML comment a server-side note. Nothing inside one is ever evaluated either way.

Parameters

  • comments (bool)

Returns Wire

Raises ArgumentError

Wire.set_auto_reload()

wire.Wire.set_auto_reload(reload: bool) -> Wire

Checks a cached template against its file before reusing it when reload is true, which is the default.

The check is one stat per template per render. Leave it on while templates are being written and turn it off where throughput matters, remembering that clear_cache() is then the only way a running process notices a deployment.

Parameters

  • reload (bool)

Returns Wire

Raises ArgumentError

Wire.set_url_schemes()

wire.Wire.set_url_schemes(schemes: list) -> Wire

Replaces the URL schemes allowed in an href and its relatives.

A URL with no scheme is always allowed, so every relative link keeps working whatever this is set to. A URL whose scheme is not listed is replaced with about:blank.

The default is in escape.DEFAULT_URL_SCHEMES and deliberately leaves out javascript, vbscript, data and file.

view.set_url_schemes(escape.DEFAULT_URL_SCHEMES + ['app'])

Parameters

  • schemes (list) — Scheme names without the colon, in lower case.

Returns Wire

Raises ArgumentError

Wire.register_filter()

wire.Wire.register_filter(name: string, handler) -> Wire

Registers a filter templates can use after a |.

The value being filtered arrives as the first argument and whatever the template passed follows it. An argument the template left out arrives as nil, so give it a default rather than insisting.

Registering a name that already exists replaces it, which is how an application makes date mean its own thing.

view.register_filter('excerpt', @(value, words) {
  var count = words == nil ? 25 : words
  return ' '.join(value.split('/\\s+/').take(count)) + '…'
})

A filter returning a plain string has its result escaped like any other value. One that genuinely produces markup returns wire.safe(), and is then responsible for what is inside it.

Parameters

  • name (string)
  • handler (callable)

Returns Wire

Raises ArgumentError

Wire.register_element()

wire.Wire.register_element(name: string, handler) -> Wire

Claims a tag name and hands every element of that name to handler instead of writing it out.

The handler is called with the Wire and a dictionary describing the element:

KeyIs
namethe tag name
attributesits attributes, with every value already rendered and escaped
contentits children, already rendered
variablesthe scope it was written in

Return wire.safe(markup) to write markup, any other value to write it as escaped text, or nil to write nothing at all.

view.register_element('icon', @(view, element) {
  var name = element.attributes.get('name', 'dot')
  return wire.safe('<svg class="icon"><use href="#${name}"></use></svg>')
})

Directives still work on a claimed element, so <icon x-if="…" /> behaves the way it reads. Reach for this only where the directives genuinely cannot express something; a partial and x-include is easier to read and does not need Zuri code to follow it.

Parameters

  • name (string)
  • handler (callable)

Returns Wire

Raises ArgumentError

Wire.register_global()

wire.Wire.register_global(name: string, value) -> Wire

Makes value readable from every template by name.

A variable of the same name passed to render() wins, so a global is a default rather than an override. A callable global is a function a template can call.

Parameters

  • name (string)
  • value (any)

Returns Wire

Raises ArgumentError

Wire.render()

wire.Wire.render(path: string, variables: ?dict) -> string

Renders the template at path and returns the page.

path is relative to the root and may leave off the extension. variables is what the template can read, and defaults to nothing at all.

echo view.render('pages/home', { user, posts })

Parameters

  • path (string)
  • variables (?dict)

Returns string

Raises TemplateNotFoundError when the template does not exist or is outside the root.

Raises TemplateSyntaxError when it does not compile.

Raises RenderError when rendering it fails.

Wire.render_string()

wire.Wire.render_string(source: string, variables: ?dict, path: ?string) -> string

Renders source as a template and returns the page.

path names the source in any error raised and defaults to <source>. Includes and extends inside the source still resolve against the root.

The result is not cached, since there is no file to key it on. Use render() for anything rendered more than once.

echo view.render_string('<p>{{ greeting }}</p>', { greeting: 'Hi' })

Parameters

  • source (string)
  • variables (?dict)
  • path (?string)

Returns string

Raises TemplateSyntaxError

Raises RenderError

Wire.compile()

wire.Wire.compile(path: string) -> Template

Compiles the template at path without rendering it, and returns it.

Useful for checking a directory of templates at build or startup time, so that a syntax error is found then rather than on the request that reaches it.

for name in os.list_dir('./views') {
  view.compile(name)
}

Parameters

  • path (string)

Returns Template

Raises TemplateNotFoundError

Raises TemplateSyntaxError

Wire.clear_cache()

wire.Wire.clear_cache() -> Wire

Forgets every compiled template.

A long-running process with set_auto_reload(false) needs this to notice a deployment.

Returns Wire

Wire.load()

wire.Wire.load(path: string, context, from: ?string, line: ?number, column: ?number) -> Template

The compiled template at path, from the cache when it is there and still current.

context names the element the template will sit inside, so that a partial holding table rows is parsed knowing it is going into a table. from, line and column describe where the include was written, for the error when the path goes nowhere.

The renderer calls this; there is rarely a reason to call it directly.

Parameters

  • path (string)
  • context (?string)
  • from (?string)
  • line (?number)
  • column (?number)

Returns Template

Raises TemplateNotFoundError

Raises TemplateSyntaxError

Wire.filter()

wire.Wire.filter(name: string) -> ?callable

The filter called name, or nil.

Parameters

  • name (string)

Returns ?callable

Wire.element()

wire.Wire.element(name: string) -> ?callable

The handler registered for the tag name, or nil.

Parameters

  • name (string)

Returns ?callable


2026, Richard Ore and The Zuri Contributors

wire.compile

import wire.compile

wire does not re-export this module, so it is reached only by importing it directly.

Turning a parsed template into the instruction tree the renderer walks.

Wire compiles rather than substitutes. A template is parsed once, every expression in it is parsed once, every directive is resolved once, and what comes out is a tree of plain instructions that holds no reference back to the document it came from. Rendering that tree is then a walk with no parsing in it at all, which is what lets the same page be rendered thousands of times from one compile, and what lets the renderer know the exact escaping context of every interpolation instead of guessing at it.

Compiling is also where a template’s mistakes are found. An unknown directive, an expression that does not parse, an x-else with no x-if in front of it, the same region defined twice: all of them are reported here, once, rather than on the request that happens to reach them.

Functions

compile()

wire.compile.compile(source: string, path: string, context, options: ?dict) -> Template

Compiles source into a Template.

context names the element the result will sit inside, and should be nil for a template rendered on its own. options takes compact (drop whitespace-only text, default false) and comments (keep HTML comments, default false).

import wire

var template = wire.compile.compile(
  '<p x-if="user">Hello {{ user.name }}</p>', 'greeting.html', nil, nil
)

Parameters

  • source (string)
  • path (string)
  • context (?string)
  • options (?dict)

Returns Template

Raises TemplateSyntaxError

Classes

Segment

class wire.compile.Segment

One piece of a run of text or of an attribute value: either literal characters or an expression to evaluate.

Fields

FieldTypeDescription
textThe literal characters, or nil when this is an expression.
valueThe expression, or nil when this is literal text.

Constructor

wire.compile.Segment(text, value)

Parameters

  • text (?string)
  • value (?Expression)

Instruction

class wire.compile.Instruction

The base of every instruction, carrying the kind the renderer switches on and the place in the source it came from.

Fields

FieldTypeDescription
kindWhat kind of instruction this is.
lineThe line of the tag it came from, or 0.
columnThe column of that tag, or 0.

Constructor

wire.compile.Instruction(kind: string, line: number, column: number)

Parameters

  • kind (string)
  • line (number)
  • column (number)

Text

class wire.compile.Text < Instruction

A run of text, possibly with interpolations in it.

Fields

FieldTypeDescription
segmentsThe literal and interpolated pieces, in order.
contextThe escaping context every interpolation in this run needs.
raw_literalTrue when the literal pieces are written out unchanged, which is the case inside a <script> or a <style>…

Constructor

wire.compile.Text(segments: list, context: string, raw_literal: bool)

Parameters

  • segments (list)
  • context (string)
  • raw_literal (bool)

Raw

class wire.compile.Raw < Instruction

Characters written out exactly as they are, used for the doctype.

Fields

FieldTypeDescription
textThe characters.

Constructor

wire.compile.Raw(text: string)

Parameters

  • text (string)

Comment

class wire.compile.Comment < Instruction

An HTML comment that survived into the output.

Fields

FieldTypeDescription
dataThe text between the delimiters.

Constructor

wire.compile.Comment(data: string)

Parameters

  • data (string)

Attribute

class wire.compile.Attribute

One attribute of an element.

Fields

FieldTypeDescription
nameThe attribute’s name.
segmentsThe literal and interpolated pieces of its value.
contextThe escaping context its interpolations need, decided by the attribute’s name.
url_listTrue when the attribute holds several URLs rather than one, so each has to be checked on its own.
url_descriptorsTrue when each URL in the list may be followed by a descriptor, as srcset allows and ping does not.

Constructor

wire.compile.Attribute(name: string, segments: list)

Parameters

  • name (string)
  • segments (list)

Element

class wire.compile.Element < Instruction

An element, with everything about it already worked out.

Fields

FieldTypeDescription
tagThe tag name, as it will be written.
attributesThe attributes written literally on the element.
spreadsExpressions giving further attributes, from x-attr, applied after the literal ones so a computed value wins.
childrenThe instructions for the element’s children.
voidTrue for an element written without a closing tag.
contentAn expression replacing the element’s children, from x-text or x-html, or nil.
content_rawTrue when content came from x-html and must not be escaped.

Constructor

wire.compile.Element(tag: string, line: number, column: number)

Parameters

  • tag (string)
  • line (number)
  • column (number)

Group

class wire.compile.Group < Instruction

A run of instructions with nothing of its own, which is what a <template> carrying a directive leaves behind.

Fields

FieldTypeDescription
childrenThe instructions.

Constructor

wire.compile.Group(children: list)

Parameters

  • children (list)

Branch

class wire.compile.Branch

One arm of a conditional chain.

Fields

FieldTypeDescription
testThe expression being tested, or nil for the final x-else.
negateTrue when the arm renders on a falsy test, which is what x-not means.
bodyThe instruction to render when the arm is taken.

Constructor

wire.compile.Branch(test, negate: bool, body)

Parameters

  • test (?Expression)
  • negate (bool)
  • body (Instruction)

Conditional

class wire.compile.Conditional < Instruction

A chain of x-if, x-elif and x-else, or a lone x-if.

Fields

FieldTypeDescription
branchesThe arms, in the order they were written.

Constructor

wire.compile.Conditional(branches: list, line: number, column: number)

Parameters

  • branches (list)
  • line (number)
  • column (number)

Loop

class wire.compile.Loop < Instruction

A repetition, from x-for.

Fields

FieldTypeDescription
sequenceThe expression giving the thing to iterate.
value_nameThe name each value is bound to, or nil.
key_nameThe name each key or index is bound to, or nil.
loop_nameThe name the loop’s own metadata is published under.
bodyThe instruction rendered once per entry.

Constructor

wire.compile.Loop(sequence, line: number, column: number)

Parameters

  • sequence (Expression)
  • line (number)
  • column (number)

Include

class wire.compile.Include < Instruction

Another template rendered in this instruction’s place.

Fields

FieldTypeDescription
pathThe literal and interpolated pieces of the path.
contextThe element the include sits inside, so a partial holding table rows is parsed knowing that, or nil for a…
variablesAn expression giving further variables for the included template, from x-with, or nil.
onlyTrue when the included template sees only what x-with gave it.
bodyThe instructions for the include’s own children, which the included template can place with…

Constructor

wire.compile.Include(path: list, context, line: number, column: number)

Parameters

  • path (list)
  • context (?string)
  • line (number)
  • column (number)

Slot

class wire.compile.Slot < Instruction

A region an extending template may replace, from x-slot.

Fields

FieldTypeDescription
nameThe region’s name.
bodyThe instructions rendered when nothing has replaced it.

Constructor

wire.compile.Slot(name: string, body: list, line: number, column: number)

Parameters

  • name (string)
  • body (list)
  • line (number)
  • column (number)

Super

class wire.compile.Super < Instruction

The definition this one replaces, from x-super.

Constructor

wire.compile.Super(line: number, column: number)

Parameters

  • line (number)
  • column (number)

Template

class wire.compile.Template

A compiled template, ready to render as many times as you like.

Nothing here changes while a render is running, so one of these can be shared.

Fields

FieldTypeDescription
pathThe file it was compiled from, or the source name given for a template compiled from a string.
bodyThe instructions making up the template’s own output.
blocksThe regions this template defines, keyed by name, from x-define.
extendsThe literal and interpolated pieces of the base template’s path, or nil when this template extends nothing.
contextThe element this template was compiled to sit inside, or nil when it was compiled on its own.
fingerprintWhat the file looked like when it was read, so a cached template can tell whether the file has changed…
errorsThe parse errors the HTML parser found in the source.

Constructor

wire.compile.Template(path: string, context)

Parameters

  • path (string)
  • context (?string)

Compiler

class wire.compile.Compiler

Compiles one template’s source.

There is one of these per compile and it is thrown away afterwards; the Template it produces holds nothing that points back at it.

Fields

FieldTypeDescription
pathThe template being compiled, for error messages.
compactWhether whitespace-only text is dropped.
commentsWhether comments survive into the output.

Constructor

wire.compile.Compiler(path: string, options: ?dict)

Parameters

  • path (string)
  • options (?dict)

Compiler.compile()

wire.compile.Compiler.compile(source: string, context) -> Template

Compiles source as a template that will sit inside context, or on its own when context is nil.

Parameters

  • source (string)
  • context (?string)

Returns Template

Raises TemplateSyntaxError

Compiler.children()

wire.compile.Compiler.children(nodes: list) -> list

Compiles a list of sibling nodes, tying conditional chains together as it goes.

Parameters

  • nodes (list)

Returns list

Raises TemplateSyntaxError

Compiler.other()

wire.compile.Compiler.other(node) -> ?Instruction

Compiles a node that is not an element, or returns nil when it contributes nothing.

Parameters

  • node (Node)

Returns ?Instruction

Compiler.text()

wire.compile.Compiler.text(node) -> Text

Compiles a text node, working out its escaping context from the element it sits in.

Parameters

  • node (Text)

Returns Text

Compiler.element()

wire.compile.Compiler.element(node) -> Instruction

Compiles an element, wrapping it in whatever its directives ask for.

The wrapping runs outside in: a repetition holds a condition, a condition holds an include, and the element itself is innermost. That order is what lets an x-if on a repeated element see the loop’s own variables.

Parameters

  • node (Element)

Returns Instruction

Raises TemplateSyntaxError

Compiler.expression_of()

wire.compile.Compiler.expression_of(node, directive: string) -> Expression

Parses the expression a directive carries.

Parameters

  • node (Element)
  • directive (string)

Returns Expression

Raises TemplateSyntaxError

Compiler.name_of()

wire.compile.Compiler.name_of(node, directive: string) -> string

Reads the bare identifier a naming directive carries.

Parameters

  • node (Element)
  • directive (string)

Returns string

Raises TemplateSyntaxError when it is not a plain name.

Compiler.path_segments()

wire.compile.Compiler.path_segments(value: string, node) -> list

Splits a template path into its literal and interpolated pieces.

A path is read as text rather than as an expression, so x-include="header.html" names a file instead of reading a html key off a header variable. Use {{ }} where part of it is computed.

Parameters

  • value (string)
  • node (Element)

Returns list

Raises TemplateSyntaxError

Compiler.segments()

wire.compile.Compiler.segments(value: string, context: string, host) -> list

Splits text into its literal and interpolated pieces.

Parameters

  • value (string)
  • context (string)
  • host (?Node) — The node the text belongs to, for error positions.

Returns list

Raises TemplateSyntaxError


2026, Richard Ore and The Zuri Contributors

wire.constants

import wire

wire exposes this as wire.constants, so import wire is enough and the names are called as wire.constants.*. import wire.constants reaches the same definitions directly.

The names Wire reserves: its directive attributes, the pseudo elements that are sugar for them, and the handful of defaults the Wire class starts out with.

Everything a template author can type that Wire treats specially is named here rather than spelled inline, so the answer to “is this attribute mine?” only ever has one place to live.

Constants

DIRECTIVE_PREFIX

wire.constants.DIRECTIVE_PREFIX = 'x-'

The prefix every Wire directive attribute carries.

Wire owns this prefix outright: an attribute starting with x- that Wire does not recognize is a compile error rather than a passthrough, which is what turns a typo like x-fi into a message instead of a silently missing section. Markup that genuinely needs an x- prefixed attribute in the output can emit one through x-attr.

CARRIER_TAG

wire.constants.CARRIER_TAG = 'template'

The element Wire uses to carry a directive without emitting anything of its own.

<template> is the only HTML element the tree construction algorithm lets through untouched everywhere: inside <head>, inside a table, inside a <select>. Every other name would be relocated or dropped by the parser before Wire ever saw it, which is why Wire’s own pseudo elements are rewritten into this one while the source is being tokenized.

IF_ATTR

wire.constants.IF_ATTR = 'x-if'

Renders the element only when the expression is truthy.

ELIF_ATTR

wire.constants.ELIF_ATTR = 'x-elif'

Continues an x-if chain on the next element sibling.

ELSE_ATTR

wire.constants.ELSE_ATTR = 'x-else'

Closes an x-if chain on the next element sibling. Valueless.

NOT_ATTR

wire.constants.NOT_ATTR = 'x-not'

Renders the element only when the expression is falsy, the inverse of x-if.

FOR_ATTR

wire.constants.FOR_ATTR = 'x-for'

Repeats the element once per entry of the expression’s value.

VALUE_ATTR

wire.constants.VALUE_ATTR = 'x-value'

Names the variable each x-for iteration binds its value to.

KEY_ATTR

wire.constants.KEY_ATTR = 'x-key'

Names the variable each x-for iteration binds its key or index to.

LOOP_ATTR

wire.constants.LOOP_ATTR = 'x-loop'

Renames the loop metadata variable an x-for publishes, which is called loop unless this says otherwise.

TEXT_ATTR

wire.constants.TEXT_ATTR = 'x-text'

Replaces the element’s children with the expression’s value as text.

HTML_ATTR

wire.constants.HTML_ATTR = 'x-html'

Replaces the element’s children with the expression’s value as markup, without escaping it.

ATTR_ATTR

wire.constants.ATTR_ATTR = 'x-attr'

Spreads a dictionary of name/value pairs onto the element as attributes.

INCLUDE_ATTR

wire.constants.INCLUDE_ATTR = 'x-include'

Renders another template in this element’s place.

WITH_ATTR

wire.constants.WITH_ATTR = 'x-with'

Supplies the variables an x-include or a component is rendered with.

ONLY_ATTR

wire.constants.ONLY_ATTR = 'x-only'

Withholds the surrounding scope from an x-include or a component, leaving it only what x-with passes. Valueless.

EXTEND_ATTR

wire.constants.EXTEND_ATTR = 'x-extend'

Marks this template as extending the base template named by the value.

SLOT_ATTR

wire.constants.SLOT_ATTR = 'x-slot'

Declares a region an extending template may replace.

DEFINE_ATTR

wire.constants.DEFINE_ATTR = 'x-define'

Replaces the base template’s region of the same name.

OVERRIDE_ATTR

wire.constants.OVERRIDE_ATTR = 'x-override'

Permits an x-define to replace a definition of the same name made earlier in the same template. Valueless.

SUPER_ATTR

wire.constants.SUPER_ATTR = 'x-super'

Renders the definition this one replaces. Valueless.

DIRECTIVES

wire.constants.DIRECTIVES = [...]

Every directive attribute Wire understands.

A template carrying an x- prefixed attribute outside this list is rejected at compile time.

EXPRESSION_DIRECTIVES

wire.constants.EXPRESSION_DIRECTIVES = [...]

The directives whose value is a Wire expression.

NAME_DIRECTIVES

wire.constants.NAME_DIRECTIVES = [...]

The directives whose value names a variable or a region, and is read as a bare identifier rather than evaluated.

PATH_DIRECTIVES

wire.constants.PATH_DIRECTIVES = [...]

The directives whose value is a template path, read as literal text with {{ }} interpolation allowed inside it.

FLAG_DIRECTIVES

wire.constants.FLAG_DIRECTIVES = [...]

The directives that take no value at all.

PSEUDO_ELEMENTS

wire.constants.PSEUDO_ELEMENTS = {...}

The pseudo elements Wire accepts as sugar, each mapped to the directive it becomes and the attribute that carries the directive’s value.

These are rewritten into <template> while the source is being tokenized, so by the time a tree exists there is only one form left to compile. attribute is nil for a pseudo element that takes no value of its own.

PSEUDO_FLAGS

wire.constants.PSEUDO_FLAGS = {...}

Attributes a pseudo element may carry that become valueless directives rather than the directive its name maps to.

Only <define name="x" override> uses this today, which is why the mapping is a single entry rather than a per-element table.

VAR_OPEN

wire.constants.VAR_OPEN = '{{'

Opens an interpolation.

VAR_CLOSE

wire.constants.VAR_CLOSE = '}}'

Closes an interpolation.

CALL_OPEN

wire.constants.CALL_OPEN = '{!'

Opens a template function call.

CALL_CLOSE

wire.constants.CALL_CLOSE = '!}'

Closes a template function call.

ESCAPE_CHAR

wire.constants.ESCAPE_CHAR = '%'

The character that escapes an interpolation, making Wire emit the braces literally instead of reading what is between them.

DEFAULT_EXTENSION

wire.constants.DEFAULT_EXTENSION = '.html'

The extension render() appends when the path it was given does not name an existing file and carries no extension of its own.

DEFAULT_ROOT

wire.constants.DEFAULT_ROOT = 'templates'

The directory render() resolves template paths against until set_root() says otherwise, which is a templates directory beside the current working directory.

DEFAULT_LOOP_NAME

wire.constants.DEFAULT_LOOP_NAME = 'loop'

The variable an x-for publishes its loop metadata under.

MAX_DEPTH

wire.constants.MAX_DEPTH = 64

How deep x-include and x-extend may nest before Wire calls it a cycle.

A template that includes itself would otherwise recurse until the process died, and the depth a legitimate layout hierarchy reaches is nowhere near this.


2026, Richard Ore and The Zuri Contributors

wire.errors

import wire.errors

wire lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled wire.errors.* needs import wire.errors.

The errors Wire raises, and the location information they carry.

Every one of them knows which template it came from and, where the problem can be traced to a specific tag, which line and column that tag started at. A template engine that can only say “something is wrong” is not much use once a layout hierarchy is a few files deep, so the location travels with the error rather than being reassembled by whoever catches it.

Classes

WireError

class wire.WireError < Error

The base class of every error Wire raises.

Catching this catches all of them, which is what a web handler wanting to turn any template failure into a 500 page should do.

import wire

catch {
  echo wire.render('dashboard')
} as e {
  if instance_of(e, wire.WireError) {
    echo 'template problem in ${e.path}: ${e.reason}'
  }
}

Fields

FieldTypeDescription
pathThe template the problem was found in: a file path for a template read from disk, or whatever was passed as…
lineThe line the offending tag started on, counting from 1, or 0 when the problem cannot be tied to one place…
columnThe column the offending tag started at, counting from 1, or 0.
reasonThe problem on its own, without the location that message glues onto the end of it.

Constructor

wire.WireError(reason: string, path: ?string, line: ?number, column: ?number)

Parameters

  • reason (string) — What went wrong, phrased without any location; the location is appended for you.
  • path (?string) — The template the problem is in. Defaults to <source>.
  • line (?number) — The line, counting from 1. Omit or pass 0 when there is no meaningful line.
  • column (?number) — The column, counting from 1.

WireError.location()

wire.WireError.location() -> string

Where the problem is, as path[line,column], or just the path when no line was recorded.

Returns string

TemplateNotFoundError

class wire.TemplateNotFoundError < WireError

Raised when a template file cannot be found, or when it resolves to somewhere outside the root directory.

A path that climbs out of the root with .. is reported as not found rather than as a permission problem, so a template cannot be used to probe the filesystem for what does and does not exist.

Constructor

wire.TemplateNotFoundError(reason: string, path: ?string, line: ?number, column: ?number)

Parameters

  • reason (string)
  • path (?string)
  • line (?number)
  • column (?number)

TemplateSyntaxError

class wire.TemplateSyntaxError < WireError

Raised while compiling a template, for anything Wire can tell is wrong without rendering: an unknown directive, an expression that does not parse, an x-elif with no x-if in front of it, a region defined twice.

Compilation happens once per template rather than once per render, so these surface the first time a template is used and not on every request after that.

Constructor

wire.TemplateSyntaxError(reason: string, path: ?string, line: ?number, column: ?number)

Parameters

  • reason (string)
  • path (?string)
  • line (?number)
  • column (?number)

RenderError

class wire.RenderError < WireError

Raised while rendering, for anything that depends on the values a template was given: iterating something that cannot be iterated, a filter that rejects its input, an include hierarchy that never bottoms out.

A variable that was simply not supplied is not one of these. Missing variables render as empty and test as falsy, which is what makes optional sections readable.

Constructor

wire.RenderError(reason: string, path: ?string, line: ?number, column: ?number)

Parameters

  • reason (string)
  • path (?string)
  • line (?number)
  • column (?number)

2026, Richard Ore and The Zuri Contributors

wire.escape

import wire

wire exposes this as wire.escape, so import wire is enough and the names are called as wire.escape.*. import wire.escape reaches the same definitions directly.

Turning a value into something safe to write at a particular place in a page.

Escaping HTML is not one operation, it is five. The same string that is harmless between two tags will end a <script> early, or turn an href into a code execution, or break out of a stylesheet. Wire knows which of those places every interpolation sits in, because it compiles a real parsed tree rather than substituting into text, so it applies the escaping that place actually needs instead of one approximation everywhere.

Nothing here is optional or discoverable at render time. A template that wants unescaped output has to say so with the raw filter, or hand Wire a Safe value.

Constants

CONTEXT_TEXT

wire.escape.CONTEXT_TEXT = 'text'

Text between tags.

CONTEXT_ATTRIBUTE

wire.escape.CONTEXT_ATTRIBUTE = 'attribute'

An ordinary attribute value.

CONTEXT_URL

wire.escape.CONTEXT_URL = 'url'

An attribute the browser resolves as a URL.

CONTEXT_SCRIPT

wire.escape.CONTEXT_SCRIPT = 'script'

A place the browser reads as JavaScript: the body of a <script>, or the value of an on* handler attribute.

CONTEXT_STYLE

wire.escape.CONTEXT_STYLE = 'style'

The body of a <style> element.

URL_ATTRIBUTES

wire.escape.URL_ATTRIBUTES = [...]

The attributes whose value a browser resolves as a URL, and which therefore have to be checked for a dangerous scheme rather than merely escaped.

srcset and ping hold several URLs separated by commas or spaces and are checked entry by entry.

URL_LIST_ATTRIBUTES

wire.escape.URL_LIST_ATTRIBUTES = [...]

URL attributes holding a list of URLs rather than a single one.

DEFAULT_URL_SCHEMES

wire.escape.DEFAULT_URL_SCHEMES = [...]

The URL schemes Wire lets through by default.

A URL with no scheme at all is always allowed, which covers every relative link, every absolute path and every protocol-relative URL. A scheme outside this list is replaced with about:blank, because javascript: and vbscript: in an href are script execution dressed as a link, and a data: document inherits the origin of the page that opened it.

Wire.set_url_schemes() replaces this list when an application genuinely needs another one, app: or mailto+-style custom handlers being the usual reason.

BLOCKED_URL

wire.escape.BLOCKED_URL = 'about:blank'

What a blocked URL is replaced with. A blocked link stays a link so the page does not fall apart, but it goes nowhere.

STYLE_SAFE

wire.escape.STYLE_SAFE = '/[^-a-zA-Z0-9 \t\n\r#%.,()_\/:;+*!@]/'

The characters a value may contain inside a <style> body.

Everything else is dropped rather than escaped, because a stylesheet has no escape syntax that would make a < harmless: the tokenizer that finds </style> runs before CSS is ever parsed.

Functions

escape()

wire.escape.escape(value, context: string, schemes: ?list) -> string

Escapes value for the given context.

A Safe value is written out unchanged in every context. That is the whole point of it, and it is why raw must only ever be applied to markup you built yourself.

schemes is the URL scheme allowlist, and is only consulted for CONTEXT_URL. Passing nil uses DEFAULT_URL_SCHEMES.

Parameters

  • value (any)
  • context (string) — One of the CONTEXT_* constants.
  • schemes (?list)

Returns string

text()

wire.escape.text(value) -> string

Escapes value for text between tags, turning &, < and > into character references.

Parameters

  • value (any)

Returns string

attribute()

wire.escape.attribute(value) -> string

Escapes value for a double quoted attribute value, turning & and " into character references.

A < needs no escaping inside an attribute and is left alone, which is what the HTML serialization algorithm does too.

Parameters

  • value (any)

Returns string

script()

wire.escape.script(value) -> string

Escapes value for a place the browser reads as JavaScript, by encoding it as JSON and then hiding the characters that would end the surrounding element or start a comment.

The result carries its own quotes when the value is a string, so an interpolation in a script must not be quoted by hand:

<script>
  var user = {{ name }};    // right: renders as "Ada"
  var user = '{{ name }}';  // wrong: renders as '"Ada"'
</script>

U+2028 and U+2029 are escaped as well. They are legal inside a JSON string but end a line in JavaScript, and an unescaped one turns a valid script into a syntax error.

Parameters

  • value (any)

Returns string

style()

wire.escape.style(value) -> string

Escapes value for the body of a <style> element by dropping every character outside a conservative CSS-safe set.

Dropping rather than escaping is deliberate. There is no escape sequence that stops a < inside a stylesheet from being found by the HTML tokenizer looking for </style>, so the only safe answer is for the character not to be there.

Parameters

  • value (any)

Returns string

url()

wire.escape.url(value, schemes: ?list) -> string

Escapes value for an attribute the browser resolves as a URL, replacing it with about:blank when its scheme is not in schemes.

The scheme is read the way a browser reads it: leading and embedded whitespace and control characters are ignored, and the comparison folds case, so Java\tscript:alert(1) is caught along with the plain spelling.

Parameters

  • value (any)
  • schemes (?list) — The allowlist. nil means DEFAULT_URL_SCHEMES.

Returns string

url_list()

wire.escape.url_list(value, schemes: ?list, descriptors: bool) -> string

Escapes value for an attribute holding several URLs, checking each entry’s scheme on its own.

The two attributes that hold a list spell it differently. ping is whitespace separated and every token in it is a URL. srcset is comma separated and only the first token of each entry is a URL; what follows is a descriptor such as 2x or 640w and is left alone. descriptors picks between them.

Every separator is preserved exactly as it was written, since a srcset whose spaces went missing is no longer a srcset.

Parameters

  • value (any)
  • schemes (?list)
  • descriptors (bool) — True for srcset, false for ping.

Returns string

is_url_attribute()

wire.escape.is_url_attribute(name: string) -> bool

Whether name is an attribute the browser resolves as a single URL.

Parameters

  • name (string)

Returns bool

is_url_list_attribute()

wire.escape.is_url_list_attribute(name: string) -> bool

Whether name is an attribute holding a list of URLs.

Parameters

  • name (string)

Returns bool

uses_descriptors()

wire.escape.uses_descriptors(name: string) -> bool

Whether a URL list attribute allows a descriptor after each URL, which srcset does and ping does not.

Parameters

  • name (string)

Returns bool

is_script_attribute()

wire.escape.is_script_attribute(name: string) -> bool

Whether name is an event handler attribute, whose value a browser reads as JavaScript.

Every one of them starts with on, and treating the whole prefix as script rather than keeping a list means a handler HTML gains next year is covered the day it ships.

Parameters

  • name (string)

Returns bool

attribute_context()

wire.escape.attribute_context(name: string) -> string

The escaping context an attribute named name calls for.

Parameters

  • name (string)

Returns string

text_context()

wire.escape.text_context(tag: string) -> string

The escaping context text inside an element named tag calls for.

Parameters

  • tag (string)

Returns string


2026, Richard Ore and The Zuri Contributors

wire.expression

import wire

wire exposes this as wire.expression, so import wire is enough and the names are called as wire.expression.*. import wire.expression reaches the same definitions directly.

Wire’s expression language: the thing that sits between {{ and }}, and the thing an x-if or an x-for is given.

It is deliberately a small language rather than an embedded Zuri. There is no assignment, no statement, no way to define anything and no way to reach a global, because a template is a description of a page and everything else belongs in the code that renders it. What is here is what reading a value out of a dictionary and deciding whether to show a section actually needs:

KindWritten as
literals12, 1.5, 'text', "text", true, false, nil
collections[1, 2], { id: 1, name }
lookupuser.address.city, items.0, items[index]
callsroute('home'), user.display_name()
arithmetic+, -, *, /, %
comparison==, !=, <, <=, >, >=, in, not in
logicand, or, not (also &&, `
choicestock > 0 ? 'in stock' : 'sold out'
defaultnickname ?? name
ranges0..pages, 1..10
filters`name

Truthiness is Wire’s, not Zuri’s: an empty list is false and a negative number is true. wire.values explains why, and it matters for every and, or, not and ? here.

Missing values

Reading a name that was never supplied gives nil rather than raising, and so does reading a key off it. {{ user.address.city }} on a request with no user renders as nothing at all instead of failing three levels down, which is what makes an optional section readable. Ask with x-if when the difference matters.

Names beginning with an underscore cannot be read. Those are private in Zuri, and a template has no business reaching into them.

Constants

KEYWORDS

wire.expression.KEYWORDS = [...]

The words that are part of the language rather than names a template can bind.

LONG_OPERATORS

wire.expression.LONG_OPERATORS = [...]

Operators made of more than one character, longest first so that <= is never read as < followed by =.

SHORT_OPERATORS

wire.expression.SHORT_OPERATORS = [...]

Operators made of a single character.

Functions

tokenize()

wire.expression.tokenize(source: string, path: string, line: number, column: number) -> list

Splits source into tokens.

path, line and column are only used to place the error when the source does not lex, and name the tag the expression was written on rather than anything inside the expression.

Parameters

  • source (string)
  • path (string)
  • line (number)
  • column (number)

Returns list

Raises TemplateSyntaxError

parse()

wire.expression.parse(source: string, path: string, line: number, column: number) -> Expression

Compiles source into a syntax tree.

path, line and column describe the tag the expression was written on and are only used to place a syntax error.

import wire

var tree = wire.expression.parse('price * quantity', '<source>', 0, 0)

Parameters

  • source (string)
  • path (string)
  • line (number)
  • column (number)

Returns Expression

Raises TemplateSyntaxError when source is not a valid expression.

Classes

Expression

class wire.expression.Expression

The base of every node in a parsed expression.

Nodes are built once, when a template is compiled, and evaluated once per render. They hold no state of their own, so the same compiled template can be rendered from several places at once.

Expression.evaluate()

wire.expression.Expression.evaluate(scope) -> any

Works out this node’s value.

scope is supplied by the renderer and answers the three questions an expression cannot answer for itself: what a name is bound to, what a filter does, and how to report a failure with the position of the tag the expression came from.

Parameters

  • scope (Scope)

Returns any

Literal

class wire.expression.Literal < Expression

A number, a string, or one of true, false and nil.

Fields

FieldTypeDescription
valueThe value, ready to use.

Constructor

wire.expression.Literal(value)

Parameters

  • value (any)

Literal.evaluate()

wire.expression.Literal.evaluate(scope) -> any

Parameters

  • scope (Scope)

Returns any

Variable

class wire.expression.Variable < Expression

A bare name, looked up in the variables the template was rendered with.

Fields

FieldTypeDescription
nameThe name being read.

Constructor

wire.expression.Variable(name: string)

Parameters

  • name (string)

Variable.evaluate()

wire.expression.Variable.evaluate(scope) -> any

Parameters

  • scope (Scope)

Returns any

Member

class wire.expression.Member < Expression

A dotted lookup, as in user.name.

Fields

FieldTypeDescription
targetThe expression being read from.
nameThe key, field or method name.

Constructor

wire.expression.Member(target, name: string)

Parameters

  • target (Expression)
  • name (string)

Member.evaluate()

wire.expression.Member.evaluate(scope) -> any

Parameters

  • scope (Scope)

Returns any

Index

class wire.expression.Index < Expression

A bracketed lookup, as in items[index], where the key is itself an expression.

Fields

FieldTypeDescription
targetThe expression being read from.
keyThe expression giving the key.

Constructor

wire.expression.Index(target, key)

Parameters

  • target (Expression)
  • key (Expression)

Index.evaluate()

wire.expression.Index.evaluate(scope) -> any

Parameters

  • scope (Scope)

Returns any

Call

class wire.expression.Call < Expression

A call, as in route('home') or user.display_name().

Fields

FieldTypeDescription
calleeThe expression giving the thing to call.
argumentsThe argument expressions, in order.
labelHow the callee was written, for the error message when it turns out not to be callable.

Constructor

wire.expression.Call(callee, arguments: list, label: string)

Parameters

  • callee (Expression)
  • arguments (list)
  • label (string)

Call.evaluate()

wire.expression.Call.evaluate(scope) -> any

Parameters

  • scope (Scope)

Returns any

ListLiteral

class wire.expression.ListLiteral < Expression

A list literal.

Fields

FieldTypeDescription
itemsThe item expressions, in order.

Constructor

wire.expression.ListLiteral(items: list)

Parameters

  • items (list)

ListLiteral.evaluate()

wire.expression.ListLiteral.evaluate(scope) -> list

Parameters

  • scope (Scope)

Returns list

DictLiteral

class wire.expression.DictLiteral < Expression

A dictionary literal.

Fields

FieldTypeDescription
entriesThe entries, as a list of [key, value] expression pairs.

Constructor

wire.expression.DictLiteral(entries: list)

Parameters

  • entries (list)

DictLiteral.evaluate()

wire.expression.DictLiteral.evaluate(scope) -> dict

Parameters

  • scope (Scope)

Returns dict

Unary

class wire.expression.Unary < Expression

not x, !x or -x.

Fields

FieldTypeDescription
operatorThe operator: not or -.
operandThe expression it applies to.

Constructor

wire.expression.Unary(operator: string, operand)

Parameters

  • operator (string)
  • operand (Expression)

Unary.evaluate()

wire.expression.Unary.evaluate(scope) -> any

Parameters

  • scope (Scope)

Returns any

Binary

class wire.expression.Binary < Expression

An arithmetic or comparison operator.

Fields

FieldTypeDescription
operatorThe operator.
leftThe left operand.
rightThe right operand.

Constructor

wire.expression.Binary(operator: string, left, right)

Parameters

  • operator (string)
  • left (Expression)
  • right (Expression)

Binary.evaluate()

wire.expression.Binary.evaluate(scope) -> any

Parameters

  • scope (Scope)

Returns any

Logical

class wire.expression.Logical < Expression

and, or or ??, each of which decides whether to evaluate its right side after looking at its left.

Fields

FieldTypeDescription
operatorThe operator: and, or or ??.
leftThe left operand.
rightThe right operand.

Constructor

wire.expression.Logical(operator: string, left, right)

Parameters

  • operator (string)
  • left (Expression)
  • right (Expression)

Logical.evaluate()

wire.expression.Logical.evaluate(scope) -> any

Parameters

  • scope (Scope)

Returns any

Conditional

class wire.expression.Conditional < Expression

condition ? consequence : alternative.

Fields

FieldTypeDescription
conditionThe expression being tested.
consequenceThe value when the test passes.
alternativeThe value when it does not.

Constructor

wire.expression.Conditional(condition, consequence, alternative)

Parameters

  • condition (Expression)
  • consequence (Expression)
  • alternative (Expression)

Conditional.evaluate()

wire.expression.Conditional.evaluate(scope) -> any

Parameters

  • scope (Scope)

Returns any

Filter

class wire.expression.Filter < Expression

A value passed through a filter, as in name|upper.

Fields

FieldTypeDescription
targetThe expression giving the value being filtered.
nameThe filter’s name.
argumentsThe argument expressions, in order, after the value itself.

Constructor

wire.expression.Filter(target, name: string, arguments: list)

Parameters

  • target (Expression)
  • name (string)
  • arguments (list)

Filter.evaluate()

wire.expression.Filter.evaluate(scope) -> any

Parameters

  • scope (Scope)

Returns any

Token

class wire.expression.Token

One piece of an expression’s source: its kind, its value, and where in the expression it started.

Fields

FieldTypeDescription
type'name', 'number', 'string', 'operator' or 'end'.
valueThe token’s text, or for a string or number its decoded value.
offsetHow far into the expression the token started, counting from 0.

Constructor

wire.expression.Token(type: string, value, offset: number)

Parameters

  • type (string)
  • value (any)
  • offset (number)

Parser

class wire.expression.Parser

Builds a syntax tree from an expression’s tokens.

There is one of these per expression compiled, and it is thrown away once the tree is built.

Fields

FieldTypeDescription
tokensThe tokens left to read.
atHow far through them the parser is.
sourceThe expression’s source, for error messages.
pathThe template the expression came from.
lineThe line of the tag the expression was written on.
columnThe column of that tag.

Constructor

wire.expression.Parser(source: string, path: string, line: number, column: number)

Parameters

  • source (string)
  • path (string)
  • line (number)
  • column (number)

Parser.parse()

wire.expression.Parser.parse() -> Expression

Parses the whole expression and insists that nothing is left over.

Returns Expression

Raises TemplateSyntaxError

Parser.parse_ternary()

wire.expression.Parser.parse_ternary() -> Expression

condition ? consequence : alternative, which is the loosest binding thing in the language.

Returns Expression

Parser.parse_coalesce()

wire.expression.Parser.parse_coalesce() -> Expression

a ?? b.

Returns Expression

Parser.parse_range()

wire.expression.Parser.parse_range() -> Expression

a..b, the same range Zuri writes the same way.

Ranges do not chain, since 1..2..3 means nothing.

Returns Expression

Parser.parse_or()

wire.expression.Parser.parse_or() -> Expression

a or b, also spelled a || b.

Returns Expression

Parser.parse_and()

wire.expression.Parser.parse_and() -> Expression

a and b, also spelled a && b.

Returns Expression

Parser.parse_equality()

wire.expression.Parser.parse_equality() -> Expression

a == b and a != b.

Returns Expression

Parser.parse_comparison()

wire.expression.Parser.parse_comparison() -> Expression

The ordering operators, plus in and not in.

Returns Expression

Parser.parse_additive()

wire.expression.Parser.parse_additive() -> Expression

a + b and a - b.

Returns Expression

Parser.parse_multiplicative()

wire.expression.Parser.parse_multiplicative() -> Expression

a * b, a / b and a % b.

Returns Expression

Parser.parse_unary()

wire.expression.Parser.parse_unary() -> Expression

not a, !a and -a.

Returns Expression

Parser.parse_filtered()

wire.expression.Parser.parse_filtered() -> Expression

A value with any filters applied to it.

Filters bind tighter than every operator, so items|length > 3 compares the length rather than filtering the comparison.

Returns Expression

Parser.parse_filter()

wire.expression.Parser.parse_filter(target) -> Filter

One filter application, in either of the two forms it can take.

Parameters

  • target (Expression)

Returns Filter

Parser.parse_postfix()

wire.expression.Parser.parse_postfix() -> Expression

A value with any lookups and calls applied to it.

Returns Expression

Parser.parse_arguments()

wire.expression.Parser.parse_arguments() -> list

A comma separated argument list, up to and including its closing parenthesis.

Returns list

Parser.parse_primary()

wire.expression.Parser.parse_primary() -> Expression

A literal, a name, a bracketed expression, or a list or dictionary literal.

Returns Expression

Parser.parse_list()

wire.expression.Parser.parse_list() -> ListLiteral

A list literal, with the opening bracket already consumed.

Returns ListLiteral

Parser.parse_dict()

wire.expression.Parser.parse_dict() -> DictLiteral

A dictionary literal, with the opening brace already consumed.

A key may be a bare name, a quoted string or a bracketed expression. An entry written as a bare name on its own is the shorthand Zuri uses: { user } means { user: user }.

Returns DictLiteral

Parser.parse_dict_entry()

wire.expression.Parser.parse_dict_entry() -> list

One key: value pair, or the bare name shorthand.

Returns list

Parser.peek()

wire.expression.Parser.peek() -> Token

The token the parser is looking at.

Returns Token

Parser.take_operator()

wire.expression.Parser.take_operator(value: string) -> bool

Consumes the current token when it is the operator value.

Parameters

  • value (string)

Returns bool

Parser.take_any_operator()

wire.expression.Parser.take_any_operator(values: list) -> ?string

Consumes the current token when it is any of values, returning which one it was or nil.

Parameters

  • values (list)

Returns ?string

Parser.take_keyword()

wire.expression.Parser.take_keyword(word: string) -> bool

Consumes the current token when it is the keyword word.

Parameters

  • word (string)

Returns bool

Parser.peek_keyword()

wire.expression.Parser.peek_keyword(word: string) -> bool

Whether the current token is the keyword word.

Parameters

  • word (string)

Returns bool

Parser.peek_keyword_at()

wire.expression.Parser.peek_keyword_at(ahead: number, word: string) -> bool

Whether the token ahead places further on is the keyword word.

Parameters

  • ahead (number)
  • word (string)

Returns bool

Parser.expect_operator()

wire.expression.Parser.expect_operator(value: string)

Consumes the operator value or reports what was there instead.

Parameters

  • value (string)

Raises TemplateSyntaxError

Parser.describe_span()

wire.expression.Parser.describe_span() -> string

How the source read from the start of the value just parsed, used to name a callee that turned out not to be callable.

Returns string

Parser.describe()

wire.expression.Parser.describe(token) -> string

A readable rendering of token for an error message.

Parameters

  • token (Token)

Returns string

Parser.fail()

wire.expression.Parser.fail(reason: string)

Raises a syntax error naming the expression it happened in.

Parameters

  • reason (string)

Raises TemplateSyntaxError


2026, Richard Ore and The Zuri Contributors

wire.filters

import wire

wire exposes this as wire.filters, so import wire is enough and the names are called as wire.filters.*. import wire.filters reaches the same definitions directly.

The filters every Wire template starts with.

A filter takes the value on the left of the | as its first argument and whatever the template passed as the rest, so the one written price|round(2) is called as round(price, 2). An argument a template leaves out arrives as nil, which is why nearly every filter here states its default in its own doc block rather than insisting the template spell it out.

Wire.register_filter() adds to this set and can replace anything in it, so an application that wants date to mean its own thing is free to say so.

On escaping

Almost every filter here returns a plain string, which Wire then escapes for wherever it is going. The three that return markup instead are raw, nl2br and json_script, and each of them says so and says what it does about the text it was given. A filter that returns markup built out of untrusted input is how a template engine becomes a security hole, so the two that build markup escape their input first.

Constants

BUILTIN

wire.filters.BUILTIN = {...}

Every filter Wire starts with, keyed by the name a template calls it by.

Two of them are spelled differently here than the function is: Zuri already has escape and default on other things, so the functions carry longer names and the template-facing names live here.

Functions

raw()

wire.filters.raw(value) -> Safe

Marks a value as markup so Wire writes it out without escaping it.

This is the only way to get live HTML out of a variable, and it is a promise that the value is safe where it lands. Applying it to anything a user supplied is a cross-site scripting hole; applying it to markup your own code built is exactly what it is for.

<div class="body">{{ article.rendered_html|raw }}</div>

Parameters

  • value (any)

Returns Safe

escape_value()

wire.filters.escape_value(value, context) -> Safe

Escapes a value for a context other than the one it is being written into, or escapes a value that was already marked safe.

Wire escapes for the right context on its own, so this is only needed when a value has to carry escaping it would not otherwise get: a URL built into an ordinary attribute, say, or markup that reached the template as Safe but should be shown rather than rendered.

context is 'text', 'attribute', 'url', 'script' or 'style' and defaults to 'text'.

<code>{{ snippet|escape }}</code>
<a data-target="{{ link|escape('url') }}">go</a>

Parameters

  • value (any)
  • context (?string)

Returns Safe

Raises ArgumentError when context is not one Wire knows.

upper()

wire.filters.upper(value) -> string

The value in upper case.

{{ code|upper }}

Parameters

  • value (any)

Returns string

lower()

wire.filters.lower(value) -> string

The value in lower case.

Parameters

  • value (any)

Returns string

title()

wire.filters.title(value) -> string

The value with the first letter of every word in upper case and the rest in lower case.

Words are separated by whitespace, so o'brien becomes O'brien rather than O'Brien. Names are harder than a filter can be.

{{ 'jane IS here'|title }}   {# Jane Is Here #}

Parameters

  • value (any)

Returns string

capitalize()

wire.filters.capitalize(value) -> string

The value with its first letter in upper case and the rest left alone.

Unlike title this touches nothing but the first character, which is what a sentence wants.

{{ 'hello THERE'|capitalize }}   {# Hello THERE #}

Parameters

  • value (any)

Returns string

trim()

wire.filters.trim(value) -> string

The value with leading and trailing whitespace removed.

Parameters

  • value (any)

Returns string

truncate()

wire.filters.truncate(value, length, suffix) -> string

The value cut down to length characters, with suffix put on the end when anything was actually cut.

length counts the characters kept, not including the suffix. suffix defaults to a single ellipsis. A value already short enough is returned untouched, suffix and all left off.

{{ article.summary|truncate(80) }}
{{ article.summary|truncate(80, ' [more]') }}

Parameters

  • value (any)
  • length (number)
  • suffix (?string)

Returns string

Raises ArgumentError when length is missing or negative.

replace()

wire.filters.replace(value, search, replacement) -> string

The value with every occurrence of search replaced by replacement.

search is matched literally, never as a regular expression, so a value full of punctuation cannot turn into a pattern by accident. replacement defaults to an empty string, which makes |replace(',') a way to strip something out.

Parameters

  • value (any)
  • search (string)
  • replacement (?string)

Returns string

Raises ArgumentError when search is missing or empty.

lpad()

wire.filters.lpad(value, width, fill) -> string

The value padded on the left with fill until it is width characters long.

fill defaults to a space. A value already at least width long is returned untouched.

Parameters

  • value (any)
  • width (number)
  • fill (?string)

Returns string

Raises ArgumentError when width is missing or not a number.

rpad()

wire.filters.rpad(value, width, fill) -> string

The value padded on the right with fill until it is width characters long.

Parameters

  • value (any)
  • width (number)
  • fill (?string)

Returns string

Raises ArgumentError when width is missing or not a number.

repeat()

wire.filters.repeat(value, count) -> string

The value repeated count times.

A count of zero gives an empty string.

Parameters

  • value (any)
  • count (number)

Returns string

Raises ArgumentError when count is missing or negative.

nl2br()

wire.filters.nl2br(value) -> Safe

The value with its line breaks turned into <br> elements.

The text is escaped first and the result is marked as markup, so this is safe on anything a user typed. Both \n and \r\n count as a line break.

<p>{{ comment.body|nl2br }}</p>

Parameters

  • value (any)

Returns Safe

strip_tags()

wire.filters.strip_tags(value) -> string

The value with every HTML tag removed, leaving only its text.

The markup is really parsed rather than pattern-matched away, so <p title="a>b">x</p> gives x and not b">x. The result is plain text and is escaped like anything else when it is written out.

<meta name="description" content="{{ article.body|strip_tags|truncate(150) }}">

Parameters

  • value (any)

Returns string

slug()

wire.filters.slug(value) -> string

The value as a lowercase, hyphen separated slug.

Runs of anything that is not a letter or a digit become a single hyphen, and hyphens at either end are trimmed. Non-ASCII letters are kept, so a slug can still be readable in a language that needs them.

<a href="/posts/{{ post.title|slug }}">{{ post.title }}</a>

Parameters

  • value (any)

Returns string

url_encode()

wire.filters.url_encode(value) -> string

The value percent encoded for use inside a URL.

Parameters

  • value (any)

Returns string

json()

wire.filters.json(value) -> string

The value as JSON.

The result is plain text, so writing it into a page escapes it like anything else. Inside a <script> Wire already encodes every interpolation as JSON, which makes this filter unnecessary there.

Parameters

  • value (any)

Returns string

json_script()

wire.filters.json_script(value, id) -> Safe

The value as JSON wrapped in a <script type="application/json"> element, ready to be read back by a script on the page.

id becomes the element’s id so the script can find it, and defaults to leaving the id off. This is the safe way to hand data to the browser: the JSON never touches an executable context, so no value in it can become code.

{{ initial_state|json_script('state') }}
<script>
  var state = JSON.parse(document.getElementById('state').textContent)
</script>

Parameters

  • value (any)
  • id (?string)

Returns Safe

abs()

wire.filters.abs(value) -> number

The value without its sign.

Parameters

  • value (any)

Returns number

Raises TypeError when the value is not a number.

round()

wire.filters.round(value, places) -> number

The value rounded to places decimal places.

places defaults to 0, which rounds to a whole number. Halves round away from zero, so 2.5 becomes 3 and -2.5 becomes -3.

{{ order.total|round(2) }}

Parameters

  • value (any)
  • places (?number)

Returns number

Raises TypeError when the value is not a number.

floor()

wire.filters.floor(value) -> number

The largest whole number at or below the value.

Parameters

  • value (any)

Returns number

Raises TypeError when the value is not a number.

ceil()

wire.filters.ceil(value) -> number

The smallest whole number at or above the value.

Parameters

  • value (any)

Returns number

Raises TypeError when the value is not a number.

number_format()

wire.filters.number_format(value, places, point, separator) -> string

The value written out with thousands separated and a fixed number of decimal places.

places defaults to 0, point to '.' and separator to ',', which is the convention most of the English speaking world uses. Passing the other two the other way round gives the European one.

{{ 1234567.891|number_format(2) }}          {# 1,234,567.89 #}
{{ 1234567.891|number_format(2, ',', '.') }} {# 1.234.567,89 #}

Parameters

  • value (any)
  • places (?number)
  • point (?string)
  • separator (?string)

Returns string

Raises TypeError when the value is not a number.

filesize()

wire.filters.filesize(value, binary) -> string

A byte count written the way a person reads it.

binary chooses the units: left out or false gives the decimal ones a disk is sold in (kB of 1000 bytes), true gives the binary ones memory is measured in (KiB of 1024). Values below a kilobyte are written as a plain count of bytes.

{{ upload.size|filesize }}        {# 1.4 MB  #}
{{ upload.size|filesize(true) }}  {# 1.3 MiB #}

Parameters

  • value (any)
  • binary (?bool)

Returns string

Raises TypeError when the value is not a number.

length()

wire.filters.length(value) -> number

How many entries the value has.

Works on a string, a list, a dictionary or a byte string. nil has a length of zero rather than being an error, so x-if="items|length" reads correctly for a variable that was never supplied.

Parameters

  • value (any)

Returns number

Raises TypeError when the value is not something with a length.

first()

wire.filters.first(value) -> any

The first entry, or nil when there is none.

Parameters

  • value (any)

Returns any

Raises TypeError when the value cannot be indexed.

last()

wire.filters.last(value) -> any

The last entry, or nil when there is none.

Parameters

  • value (any)

Returns any

Raises TypeError when the value cannot be indexed.

join()

wire.filters.join(value, glue) -> string

The entries joined into one string with glue between them.

glue defaults to an empty string. Each entry is rendered the way it would be if it were written on its own, so a list of numbers joins without any ceremony.

{{ tags|join(', ') }}

Parameters

  • value (any)
  • glue (?string)

Returns string

Raises TypeError when the value is not a sequence.

sort()

wire.filters.sort(value, key) -> list

The entries in ascending order.

key names a field to sort by, for a list of dictionaries or instances; leaving it out sorts the entries themselves. Sorting is stable, and the original is not changed.

<li x-for="users|sort('name')" x-value="user">{{ user.name }}</li>

Parameters

  • value (any)
  • key (?string)

Returns list

Raises TypeError when the value is not a sequence.

reverse()

wire.filters.reverse(value) -> any

The entries in the opposite order, or a string backwards.

Parameters

  • value (any)

Returns any

Raises TypeError when the value is not a sequence.

unique()

wire.filters.unique(value) -> list

The entries with later duplicates removed, keeping the first of each.

Parameters

  • value (any)

Returns list

Raises TypeError when the value is not a sequence.

keys()

wire.filters.keys(value) -> list

A dictionary’s keys, in insertion order.

Parameters

  • value (any)

Returns list

Raises TypeError when the value is not a dictionary.

values()

wire.filters.values(value) -> list

A dictionary’s values, in insertion order.

Parameters

  • value (any)

Returns list

Raises TypeError when the value is not a dictionary.

slice()

wire.filters.slice(value, start, end) -> any

The entries from start up to but not including end.

end defaults to the end of the sequence. A negative position counts back from the end, and a range that falls outside the sequence gives back whatever part of it does overlap rather than raising.

<li x-for="posts|slice(0, 5)" x-value="post">{{ post.title }}</li>

Parameters

  • value (any)
  • start (number)
  • end (?number)

Returns any

Raises TypeError when the value is not a sequence.

sum()

wire.filters.sum(value, key) -> number

The entries added together.

key names a field to add up, for a list of dictionaries or instances. An empty sequence sums to 0.

<p>Total: {{ items|sum('price')|number_format(2) }}</p>

Parameters

  • value (any)
  • key (?string)

Returns number

Raises TypeError when an entry is not a number.

split()

wire.filters.split(value, separator) -> list

The value split into a list on separator.

separator is matched literally and defaults to whitespace, which splits on any run of it.

Parameters

  • value (any)
  • separator (?string)

Returns list

default_to()

wire.filters.default_to(value, fallback) -> any

fallback when the value is falsy, otherwise the value.

Falsy here is Wire’s falsy: nil, false, zero, an empty string and an empty collection. A negative number is not falsy and is kept. Use ?? in an expression when only a missing value should fall back and a zero should stand.

<p>{{ user.nickname|default('friend') }}</p>

Parameters

  • value (any)
  • fallback (any)

Returns any

empty()

wire.filters.empty(value) -> bool

Whether the value has nothing in it.

nil is empty, an empty string and an empty collection are empty, and a number never is. That last part is what separates this from plain falsiness: 0|empty is false where 0 on its own is falsy.

<p x-if="results|empty">Nothing found.</p>

Parameters

  • value (any)

Returns bool

is()

wire.filters.is(value, expected) -> bool

Whether the value equals expected.

Kept from the first version of Wire, where it was the only way to compare anything. x-if="status == 'active'" says the same thing and reads better, so prefer that in new templates.

Parameters

  • value (any)
  • expected (any)

Returns bool

not()

wire.filters.not(value, expected) -> bool

Whether the value differs from expected.

Parameters

  • value (any)
  • expected (any)

Returns bool

date()

wire.filters.date(value, format) -> string

A date written out with the given format.

The value may be a date.Date, a Unix timestamp in seconds, or a string in any of the formats date.parse() understands. format uses the same directives as Date.format() and defaults to 'Y-m-d H:i:s'.

<time datetime="{{ post.created|date('Y-m-d') }}">
  {{ post.created|date('jS F Y') }}
</time>

Parameters

  • value (any)
  • format (?string)

Returns string

Raises TypeError when the value is not something Wire can read as a date.


2026, Richard Ore and The Zuri Contributors

wire.loader

import wire.loader

wire does not re-export this module, so it is reached only by importing it directly.

Turning the path written in an x-include into a file on disk, and refusing to when it points somewhere it should not.

The root is a boundary, not a starting point

Every template path resolves inside the root directory and nowhere else. A path that climbs out of it with .., and an absolute path that names somewhere else entirely, are both refused rather than followed.

That matters because template paths are not always written by the person who wrote the template. The moment a path is built out of anything a request supplied, as x-include="themes/{{ theme }}/head" does, an unbounded loader turns an include into a way to read any file the process can reach. Wire treats the root as a boundary so that the worst a hostile value can do is fail to find a template.

Symbolic links are followed and then checked, so a link inside the root pointing outside it is refused too.

Classes

Loader

class wire.loader.Loader

Finds template files under one root directory.

Fields

FieldTypeDescription
rootThe directory every path resolves inside.
extensionThe extension tried when the path as written names no file.

Constructor

wire.loader.Loader(root: string, extension: ?string)

Parameters

  • root (string)
  • extension (?string)

Loader.resolve()

wire.loader.Loader.resolve(path: string, from: ?string, line: ?number, column: ?number) -> string

The file path names, as an absolute path.

The path is tried as written first and then with the configured extension appended, so render('home') finds home.html while render('home.txt') finds exactly that.

from and line and column describe where the include was written and are only used to place the error.

Parameters

  • path (string)
  • from (?string)
  • line (?number)
  • column (?number)

Returns string

Raises TemplateNotFoundError when there is no such file, or when the path resolves outside the root.

Loader.read()

wire.loader.Loader.read(full: string) -> string

The contents of the file at full.

Parameters

  • full (string)

Returns string

Raises TemplateNotFoundError when the file cannot be read.

Loader.fingerprint()

wire.loader.Loader.fingerprint(full: string) -> ?string

What the file at full looks like now, for telling a cached template apart from a changed one.

Both the modification time and the size are used, because a file rewritten within the same second still almost always changes length. A file that cannot be stated fingerprints as nil, which makes the cache treat it as changed and read it again.

Parameters

  • full (string)

Returns ?string


2026, Richard Ore and The Zuri Contributors

wire.normalize

import wire

wire exposes this as wire.normalize, so import wire is enough and the names are called as wire.normalize.*. import wire.normalize reaches the same definitions directly.

Rewriting Wire’s pseudo elements into something HTML’s own tree construction will not move, drop or reshape.

Why this exists

<include>, <extend>, <declare> and <define> are not HTML elements, and a standards-following parser has firm opinions about elements it does not know. Inside <head> one is pushed out into <body>. Inside a table one is foster parented out in front of the table. Inside a <select> one is dropped on the floor. And written as <include path="x" />, the trailing slash means nothing on an unknown element, so it swallows the whole rest of the document as its children.

All four of those are places a template genuinely wants an include: a shared block of <meta> tags, a partial holding table rows, a partial holding options. So Wire does not hand its own tag names to the parser at all. While the source is being tokenized, every pseudo element is turned into a <template> carrying the directive it means, and <template> is the one element the standard lets through untouched everywhere.

The rewrite happens on the token stream rather than on the text, so it inherits the tokenizer’s own understanding of comments, of raw text inside <script> and <style>, and of quoting. A <include> written inside a comment or inside a script stays exactly where it was.

What else it notices

A template may be a whole page or a fragment of one, and the tree builder gives you <html>, <head> and <body> either way. Telling the two apart afterwards is guesswork, so the flags are collected here while the real tokens go past: whether the source actually spelled a doctype, an <html>, a <head> or a <body>. The compiler emits each of those only when the author wrote it.

Functions

parse()

wire.normalize.parse(source: string, context: ?string) -> dict

Parses source into a tree with Wire’s pseudo elements already rewritten.

context names the element the source is being parsed inside. Pass nil to parse it as a whole document, which is what a template being rendered on its own is; pass a tag name to parse it as a fragment, so that a partial holding table rows keeps its rows when it is included inside a table.

Returns a dictionary of:

  • root: the Document for a document parse, or the DocumentFragment holding the parsed nodes for a fragment parse.
  • document: true when the parse was a document parse.
  • saw_doctype, saw_html, saw_head, saw_body: whether the source really contained each, as opposed to the parser having supplied it.
  • errors: the parse errors the tokenizer and tree builder found.
import wire

var parsed = wire.normalize.parse('<include path="nav.html" />', nil)
echo parsed.root.body().inner_html()
# <template x-include="nav.html"></template>

Parameters

  • source (string)
  • context (?string)

Returns dict

Classes

Normalizer

class wire.normalize.Normalizer

A tokenizer that rewrites Wire’s pseudo elements on the way past.

This stands where a html.Tokenizer normally stands and answers everything the tree builder asks of one, so html.TreeBuilder cannot tell the difference.

Fields

FieldTypeDescription
cdata_okWhether the tree builder will accept a CDATA section at the current insertion point.
errorsThe parse errors the underlying tokenizer has collected.
saw_doctypeTrue once a <!DOCTYPE> has gone past.
saw_htmlTrue once an <html> start tag has gone past.
saw_headTrue once a <head> start tag has gone past.
saw_bodyTrue once a <body> start tag has gone past.

Constructor

wire.normalize.Normalizer(source)

Parameters

  • source (Tokenizer)

Normalizer.next_token()

wire.normalize.Normalizer.next_token() -> Token

The next token, rewritten when it names a pseudo element.

Returns Token

Normalizer.set_state()

wire.normalize.Normalizer.set_state(state: string)

Passes the tree builder’s tokenizer state change along.

Parameters

  • state (string)

Normalizer.set_last_start_tag()

wire.normalize.Normalizer.set_last_start_tag(name: string)

Passes the tree builder’s record of the last start tag along, which is how the tokenizer knows whether a </script> it is looking at closes the script it is inside.

Parameters

  • name (string)

2026, Richard Ore and The Zuri Contributors

wire.render

import wire.render

wire does not re-export this module, so it is reached only by importing it directly.

Walking a compiled template and writing the page out.

Nothing is parsed here and no markup is reassembled from text. The compiler already decided what every instruction is and what escaping every interpolation needs, so rendering is a walk that evaluates expressions and appends strings, and a value can never turn into structure on its way to the page.

The renderer is also the scope an expression is evaluated against: it is what answers what a name is bound to, what a filter does, and where to point when something goes wrong.

Classes

Frame

class wire.render.Frame

One level of variables, pointing at the level around it.

A loop pushes one of these rather than copying everything it can see, so iterating a long list costs one small dictionary per iteration instead of a copy of the whole page’s variables.

Fields

FieldTypeDescription
variablesThe variables bound at this level.
outerThe level around this one, or nil at the outermost.

Constructor

wire.render.Frame(variables: dict, outer)

Parameters

  • variables (dict)
  • outer (?Frame)

Frame.lookup()

wire.render.Frame.lookup(name: string) -> any

What name is bound to at this level or any level around it, or nil when nothing binds it.

Parameters

  • name (string)

Returns any

Definition

class wire.render.Definition

One definition of a region, and what to render it against.

A region’s body does not always belong to the template placing it. An extending template’s x-define is written against the same variables the base is, so it renders in whatever scope is current. An include’s children are written at the call site and have to render in the scope they were written in, or <include path="card"><p>{{ orders|length }} </p></include> would be asking the card about the caller’s orders and getting nothing.

Fields

FieldTypeDescription
bodyThe instructions to render.
frameThe scope to render them in, or nil to use whatever is current.
pathThe template the body was written in, for error messages.

Constructor

wire.render.Definition(body: list, frame, path: string)

Parameters

  • body (list)
  • frame (?Frame)
  • path (string)

Renderer

class wire.render.Renderer

Renders compiled templates.

One of these is built per render and thrown away afterwards, so a compiled template can be rendered from several places at once without them treading on each other.

Fields

FieldTypeDescription
environmentThe Wire the render belongs to, which owns the filters, the registered elements, the loader and the compile…
pathThe template currently being rendered, for error messages.
piecesThe pieces of the page so far, joined at the end.
frameThe variables in scope right now.
blocksThe regions an extending template supplied, keyed by name.
overriddenThe definitions an x-super would reach, while a region is being rendered.
depthHow many includes and extends deep the render is.
lineThe line of the instruction being rendered, for error messages.
columnThe column of that instruction.

Constructor

wire.render.Renderer(environment)

Parameters

  • environment (Wire)

Renderer.run()

wire.render.Renderer.run(template, variables: dict) -> string

Renders template with variables and returns the page.

Parameters

  • template (Template)
  • variables (dict)

Returns string

Raises WireError

Renderer.template()

wire.render.Renderer.template(template, variables: dict, supplied: dict, depth: number)

Renders template, following whatever it extends and placing the regions it and its children define.

supplied holds regions coming from outside the inheritance chain, which is how an include hands its own children to the template it is including.

Parameters

  • template (Template)
  • variables (dict)
  • supplied (dict)
  • depth (number)

Raises WireError

Renderer.nodes()

wire.render.Renderer.nodes(nodes: list)

Renders a list of instructions in order.

Parameters

  • nodes (list)

Renderer.node()

wire.render.Renderer.node(node)

Renders one instruction.

Parameters

  • node (Instruction)

Renderer.definition()

wire.render.Renderer.definition(definition)

Renders one definition of a region, in its own scope when it has one.

Parameters

  • definition (Definition)

Renderer.plain()

wire.render.Renderer.plain(segments: list) -> string

A run of segments joined with no escaping at all, for a template path.

Parameters

  • segments (list)

Returns string

Renderer.lookup()

wire.render.Renderer.lookup(name: string) -> any

What name is bound to, for an expression being evaluated.

Parameters

  • name (string)

Returns any

Renderer.apply_filter()

wire.render.Renderer.apply_filter(name: string, value, arguments: list) -> any

Runs the filter name over value, for an expression being evaluated.

Parameters

  • name (string)
  • value (any)
  • arguments (list)

Returns any

Raises RenderError when there is no such filter, or when the filter rejects what it was given.

Renderer.fail()

wire.render.Renderer.fail(reason: string)

Stops the render, pointing at the instruction currently being rendered.

Parameters

  • reason (string)

Raises RenderError


2026, Richard Ore and The Zuri Contributors

wire.values

import wire.values

wire lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled wire.values.* needs import wire.values.

How Wire reads the values a template is given: what counts as true, what a value looks like once it reaches the page, and how a string says that it is already markup and must not be escaped again.

Constants

MAX_ARGUMENTS

wire.values.MAX_ARGUMENTS = 8

The most arguments a filter or a template function can be called with.

Zuri has no way to spread a list into a call, so an argument list built at render time has to be handed over one position at a time and the positions have to be written out. Eight is far past what any filter has ever wanted and stops the dispatch below from growing without end.

Functions

safe()

wire.safe(value) -> Safe

Marks value as markup that Wire must not escape.

A Safe passed back in is returned unchanged rather than wrapped twice. Anything that is not already a string is rendered with stringify() first, so safe(nil) is an empty string rather than the word “nil”.

Parameters

  • value (any)

Returns Safe

is_safe()

wire.is_safe(value) -> bool

True when value is a Safe.

Parameters

  • value (any)

Returns bool

unwrap()

wire.values.unwrap(value) -> any

Strips the Safe wrapper off value, leaving anything else alone.

Filters work on plain values, so this runs on the way into one and the filter decides for itself whether its result is markup.

Parameters

  • value (any)

Returns any

stringify()

wire.stringify(value) -> string

What value looks like once it reaches the page, before escaping.

nil renders as an empty string rather than as the word “nil”, which is what lets an optional variable be dropped into a page without a guard around it. Everything else is rendered by its own to_string().

Parameters

  • value (any)

Returns string

truthy()

wire.truthy(value) -> bool

Whether value counts as true in an x-if, an x-not, or a boolean operator inside an expression.

Wire deliberately does not reuse Zuri’s own truthiness here, because one of Zuri’s answers is wrong for a template: Zuri treats an empty list and an empty dictionary as true, so x-if="items" would render a section for a search that found nothing. Wire treats an empty collection as false.

Everything else agrees with Zuri: nil and false are false, zero and NaN are false while every other number is true, an empty string is false, and anything Wire has no special knowledge of (an instance, a function, a range) is true.

A variable that was never supplied arrives here as nil, which is why an optional section can be written as x-if="user" with nothing else around it.

Parameters

  • value (any)

Returns bool

is_empty()

wire.values.is_empty(value) -> bool

Whether value has nothing in it, for the empty filter and for anything else that wants the question asked of a collection rather than of a value in general.

nil is empty. A number is never empty, not even zero; that is a question about truthiness, not about emptiness.

Parameters

  • value (any)

Returns bool

is_blank()

wire.values.is_blank(text: string) -> bool

Whether text is empty or is nothing but whitespace.

Parameters

  • text (string)

Returns bool

compare()

wire.values.compare(a, b) -> number

Orders a before, with or after b, returning -1, 0 or 1.

Zuri’s ordering operators only take numbers, so anything a template sorts or compares that is not one has to be ordered here. Strings are compared a character at a time by code point, which puts them in the order a person expects for one alphabet and in a defined order for everything else. Two values of different kinds are ordered by how they render, which is arbitrary but stable, and stable is what a sort actually needs.

nil sorts before everything, so a record missing the field being sorted on still appears rather than dropping out of the list.

Parameters

  • a (any)
  • b (any)

Returns number

invoke()

wire.values.invoke(target, arguments: list) -> any

Calls target with arguments spread into its parameters.

Passing fewer arguments than the function declares leaves the rest nil, which is how a filter written as def round(value, places) can be used as a bare |round.

Parameters

  • target (callable)
  • arguments (list)

Returns any

Raises ArgumentError when there are more than MAX_ARGUMENTS.

type_name()

wire.values.type_name(value) -> string

Wire’s name for value’s type, used in error messages so that a complaint reads “expected a list, got a number” rather than naming an internal class.

Parameters

  • value (any)

Returns string

Classes

Safe

class wire.Safe

A string that is already markup and must be written out as it is.

Wire escapes everything it interpolates. That is the right default and it is not negotiable per-variable from the outside, so the only way to get live markup into a page is to say so explicitly, either with the raw filter in a template or by handing Wire one of these from Zuri code.

import wire

var tpl = wire.wire()

# Escaped, because that is the default.
echo tpl.render_string('{{ note }}', { note: '<b>hi</b>' })
# &lt;b&gt;hi&lt;/b&gt;

# Not escaped, because the value says it is markup.
echo tpl.render_string('{{ note }}', { note: wire.safe('<b>hi</b>') })
# <b>hi</b>

Wrapping a value is a promise that it is safe in an HTML context. Wrapping something that came from a user is how a template engine becomes a cross-site scripting hole, so wrap the markup you built, never the input you received.

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
valueThe markup this value carries.

Constructor

wire.Safe(value: string)

Parameters

  • value (string)

Safe.to_string()

wire.Safe.to_string() -> string

The markup, unchanged.

Returns string


2026, Richard Ore and The Zuri Contributors

url

import url

This module provides classes and functions for parsing, building, resolving and processing URLs (and URL-like identifiers, such as mailto: and tel: links) per RFC 3986.

The scope of this module is not limited to HTTP or any single scheme: it will happily parse ftp://, ssh://, mailto:, or any scheme it has never heard of, and only makes scheme-specific decisions (like a default port) where the scheme is one this module actually recognizes.

Constructing a URL

%> import url
%> var link = url.Url('https', 'example.com', 9000)
%> link.absolute_url()
'https://example.com:9000'

Parsing a URL

The parse() function turns a URL string into a Url instance.

%> var link = url.parse('https://example.com:9000/path?q=1#frag')
%> link.scheme
'https'
%> link.host
'example.com'
%> link.port
9000
%> link.path
'/path'
%> link.query
'q=1'
%> link.hash
'frag'

Resolving relative URLs

resolve() implements RFC 3986’s relative-reference resolution algorithm, the same logic a browser uses to turn a relative href on a page into an absolute link.

%> var base = url.parse('https://example.com/a/b/c')
%> base.resolve('../d').to_string()
'https://example.com/a/d'

Query strings

query_params() decodes a URL’s query string into a dictionary of name -> [values] (a name can legally appear more than once in a query string, so every value is always a list).

%> url.parse('https://example.com?a=1&a=2&b=3').query_params()
{a: [1, 2], b: [3]}

The url API

Every public name in url, wherever it is declared. Each links to the page that documents it.

NameKindSummary
url.UrlclassThe Url class represents a parsed (or hand-built) URL and provides methods for inspecting, normalizing,…
url.UrlMalformedErrorclassRaised by parse() when strict is true and the given string cannot be interpreted as a well-formed URL.
url.decodefunctionDecodes URL-encoded string.
url.encodefunctionURL-encodes a string.
url.has_authorityfunctionReturns true if scheme (case-insensitively) allows the :// authority form directly; false for opaque…
url.parsefunctionParses given url string into a Url object.
url.parse_queryfunctionDecodes a raw query string (without a leading ?) into a dictionary mapping each parameter name to a list of…
url.remove_dot_segmentsfunctionRemoves . and .. path segments from path, per [RFC 3986…

Functions

remove_dot_segments()

url.remove_dot_segments(path: string) -> string

Removes . and .. path segments from path, per RFC 3986 §5.2.4. This is what keeps resolve() from leaking a base URL’s discarded segments into its result, e.g. /a/b/../c normalizes to /a/c.

Parameters

  • path (string)

Returns string

encode()

url.encode(url: string, strict: ?bool) -> string

URL-encodes a string.

This function is convenient when encoding a string to be used in a query part of a URL, as a convenient way to pass variables to the next page. Every byte of the string’s UTF-8 encoding that isn’t one of the reserved/unreserved characters defined by RFC 3986 is percent-encoded, so this is safe to use on text containing multi-byte characters.

If strict mode is enabled, space character is encoded with the percent (%) sign in order to conform with RFC 3986. Otherwise, is is encoded with the plus (+) sign in order to align with the default encoding used by modern browsers.

Parameters

  • url (string)
  • strict (?bool) — Default value is false

Returns string

decode()

url.decode(url: string) -> string

Decodes URL-encoded string. This function decodes any %## encoding in the given string and plus symbols (‘+’) to a space character.

Parameters

  • url (string)

Returns string

parse_query()

url.parse_query(query: string) -> dict

Decodes a raw query string (without a leading ?) into a dictionary mapping each parameter name to a list of its values. Equivalent to parse('?' + query).query_params(), but usable directly on a query string with no surrounding url.

%> url.parse_query('a=1&a=2&b=3')
{a: [1, 2], b: [3]}

Parameters

  • query (string)

Returns dict

has_authority()

url.has_authority(scheme: string) -> bool

Returns true if scheme (case-insensitively) allows the :// authority form directly; false for opaque schemes such as mailto or tel that address their content by an opaque string instead of a host.

Parameters

  • scheme (string)

Returns bool

parse()

url.parse(url: string, strict: ?bool) -> Url

Parses given url string into a Url object. If the strict argument is set to true, the parser will raise an UrlMalformedError when it encounters a malformed url; otherwise it makes a best effort and leaves whatever it couldn’t confidently parse as nil.

Parameters

  • url (string)
  • strict (?bool) — Default value is false

Returns Url

Raises UrlMalformedError if strict is true and the url is malformed.

Classes

UrlMalformedError

class url.UrlMalformedError < Error

Raised by parse() when strict is true and the given string cannot be interpreted as a well-formed URL.

Constructor

url.UrlMalformedError(message)

Parameters

  • message (string)

Url

class url.Url

The Url class represents a parsed (or hand-built) URL and provides methods for inspecting, normalizing, resolving and re-serializing it.

  • printable — has a @to_string(), so echo and print() show something useful
  • serializable — has a @to_json(), so it can be handed straight to json.encode()

Fields

FieldTypeDescription
schemeThe url scheme e.g. http, https, ftp, tcp etc. nil for a schemeless reference such as a bare path.
hostThe host information contained in the url.
portThe port contained in the url, as a number.
pathThe path of the URL.
hashThe url’s fragment/hash, without the leading #.
queryThe url’s raw query string, without the leading ?.
usernameThe username portion of the url’s userinfo, if any.
passwordThe password portion of the url’s userinfo, if any.
has_slashtrue if the url contains the :// (or bare //) authority marker.
empty_pathtrue if path is / only because no path was actually present in the original url (it was implied);…

Constructor

url.Url(scheme: ?string, host: ?string, port: ?number, path: ?string, query: ?string, hash: ?string, username: ?string, password: ?string, has_slash: ?bool, empty_path: ?bool)

Parameters

  • scheme (?string)
  • host (?string)
  • port (?number)
  • path (?string)
  • query (?string)
  • hash (?string)
  • username (?string)
  • password (?string)
  • has_slash (?bool)
  • empty_path (?bool)

Url.clone()

url.Url.clone() -> Url

Returns a clone of the current url.

Returns Url

Url.is_absolute()

url.Url.is_absolute() -> bool

Returns true if the url is absolute (has a scheme), false if it is a relative reference (e.g. a bare path).

Returns bool

Url.default_port()

url.Url.default_port() -> ?number

Returns the standard, well-known port for the url’s scheme (e.g. 80 for http, 443 for https), or nil if the scheme is unset or not one this module recognizes.

Returns ?number

Url.effective_port()

url.Url.effective_port() -> ?number

Returns port if the url specifies one, otherwise falls back to default_port().

Returns ?number

Url.query_params()

url.Url.query_params() -> dict

Decodes the url’s query string into a dictionary mapping each parameter name to a list of its values, since a query string can legally repeat the same name more than once. A name with no = (e.g. ?flag) decodes to a single empty-string value.

%> url.parse('https://example.com?a=1&a=2&b=3').query_params()
{a: [1, 2], b: [3]}

Returns dict

Url.get_param()

url.Url.get_param(name: string, fallback) -> any

Returns the first value of query parameter name, or fallback if it isn’t present. A convenience shortcut over query_params() for the common case of a parameter that only appears once.

Parameters

  • name (string)
  • fallback (any) — Default value is nil.

Returns any

Url.normalize()

url.Url.normalize() -> Url

Returns a new url with . and .. path segments resolved away, per RFC 3986 §5.2.4. Every other field is left untouched.

%> url.parse('https://example.com/a/b/../c').normalize().path
'/a/c'

Returns Url

Url.resolve()

url.Url.resolve(reference) -> Url

Resolves reference (a relative or absolute url string, or another Url instance) against this url as a base, per RFC 3986 §5, the same algorithm a browser uses to turn a page’s relative hrefs into absolute links.

%> var base = url.parse('https://example.com/a/b/c')
%> base.resolve('../d').to_string()
'https://example.com/a/d'
%> base.resolve('/x/y').to_string()
'https://example.com/x/y'
%> base.resolve('?q=1').to_string()
'https://example.com/a/b/c?q=1'

Parameters

  • reference (string|Url)

Returns Url

Url.authority()

url.Url.authority() -> string

Returns the url authority.

The authority component is preceded by a double slash (“//”) and is terminated by the next slash (“/”), question mark (“?”), or number sign (“#”) character, or by the end of the URI.

Returns string

Note: mailto and other opaque schemes have no authority. For this reason, they return an empty string as authority.

Url.host_for_display()

url.Url.host_for_display() -> string

Returns host, wrapped in [ ] brackets when it looks like an IPv6 literal, matching how it must appear when embedded back into a url string. Plain hostnames and IPv4 addresses are returned as-is.

Returns string

Url.host_is_ipv4()

url.Url.host_is_ipv4() -> bool

Returns true if the host of the url is a valid ipv4 address and false otherwise.

Returns bool

Url.host_is_ipv6()

url.Url.host_is_ipv6() -> bool

Returns true if the host of the url is a valid ipv6 address and false otherwise.

Returns bool

Url.absolute_url()

url.Url.absolute_url() -> string

Returns absolute url string of the url object.

Returns string

Url.to_string()

url.Url.to_string() -> string

Returns a string representation of the url object. This will only be the same as the absolute url if the original string is an absolute url.

Returns string

Url.equals()

url.Url.equals(other) -> bool

Returns true if other is a Url whose string form is identical to this one’s. Note that == does not call this: it always falls back to identity comparison for instances, so use a.equals(b) rather than a == b to compare two urls by value.

Parameters

  • other (any)

Returns bool


2021, Richard Ore and Zuri contributors

mime

import mime

This module provides functions that allow easy mime type detection from files. It offers support for detecting file type based on name or file headers and it is completely extensible so that you can add declarations for your own custom file types.

See defined functions for example.

The mime API

Every public name in mime, wherever it is declared. Each links to the page that documents it.

NameKindSummary
mime.MimeFormatclassMime format representation class.
mime.detectfunctionPerforms mimetype detection on a file.
mime.detect_from_headerfunctionDetects the mimetype of a file based on it’s file header.
mime.detect_from_namefunctionDetects the mimetype of a file based on the extension defined in it’s path.
mime.extendfunctionExtends the mime module with support for files with the given extension as defined in the given format.
mime.mime_to_extensionfunctionLooks up the typical file extension (including the leading .) registered for a given mime type: the reverse…

Functions

detect_from_name()

mime.detect_from_name(name: string) -> string

Detects the mimetype of a file based on the extension defined in it’s path.

When name has more than one dot-separated suffix (archive. tar.gz), the longest registered extension wins: .tar.gz if it’s registered, falling back to .gz otherwise: rather than whichever single-suffix entry happens to match first.

Example,

import mime
echo mime.detect_from_name('myimage.png')

Parameters

  • name (string)

Returns string

Note: Unrecognized extensions (including a name with no extension at all) return 'application/octet-stream' rather than raising.

detect_from_header()

mime.detect_from_header(file: file) -> string

Detects the mimetype of a file based on it’s file header.

When multiple file formats share very similar or shadowing file headers (such as the relationship between Zip files and Docx files), this method will perform an extension before returning it’s result.

Example,

import mime
var f = file('my_file.ext', 'rb')
echo mime.detect_from_header(f)

Parameters

  • file (file)

Returns string

Note: For dealing with files without extension, or where the accuracy of the file extension cannot be trusted, this method provides a more efficient lookup.

Note: This method may produce slightly more rigorous results

Note: This method requires that the file must be opened in binary mode.

detect()

mime.detect(file: file) -> string

Performs mimetype detection on a file.

this method is capable of detecting file mimetypes even in the absence of an extension.

If the file is opened in binary mode, it first attempt the more accurate header check. If the header check returns a generic result (i.e. application/octet-stream), it performs an extension lookup.

Example,

import mime
var f = file('myfile', 'rb')

# using 'rb' here for two reasons: 
# 1. Our file has no extension, so extension based detection is impossible
# 2. We want more accuracy by having Mime check file headers

echo mime.detect(f)

Parameters

  • file (file)

Returns string

Note: this method gives the best result, but slightly slower than a direct lookup of name or header.

extend()

mime.extend(extension: string, format: instance, overwrite: ?bool) -> bool

Extends the mime module with support for files with the given extension as defined in the given format.

Example,

%> import mime
%> mime.detect_from_name('myfile.ppk')
'application/octet-stream'
%> mime.extend('.ppk', mime.MimeFormat('file/ppk'))
true
%> mime.detect_from_name('myfile.ppk')
'file/ppk'

Parameters

  • extension (string)
  • format (MimeFormat)
  • overwrite (?bool) — Default false. When an entry for extension already exists (including one of this module’s own built-in mappings) and overwrite is false, the existing entry is left untouched and this returns false; pass true to replace it.

Returns bool

Note: the extension MUST start with .

mime_to_extension()

mime.mime_to_extension(mimetype: string)

Looks up the typical file extension (including the leading .) registered for a given mime type: the reverse of detect_from_name().

Example,

%> import mime
%> mime.mime_to_extension('image/png')
'.png'

Parameters

  • mimetype (string)

Returns — ?string: nil if no extension is registered for mimetype.

Note: Several extensions can share one mime type (.jpg/.jpeg, .htm/.html); whichever is registered first (in this module’s own declaration order for a built-in type, or whichever extend() call added it for a custom one) is returned.

Classes

MimeFormat

class mime.MimeFormat

Mime format representation class.

Constructor

mime.MimeFormat(mimetype: string, header: ?list)

Parameters

  • mimetype (string)
  • header (?list) — A list of one or more candidate magic-byte signatures, each itself a list of numbers (0-255) or nil for a byte position to skip (a size field embedded in the middle of a signature, say: see the .webp entry in this module’s own table for an example). nil when name-only detection is all that’s available for this format.

Note: only the first 16 bytes of a file’s header will ever be used; a signature longer than that can never match.


O2021, Richard Ore

colors

import colors

This module provides functionalities for color conversion and manipulation.

This module also provides functionalities that enable cross-platform colored terminal outputs that will allow you create beautiful console apps that are user friendly.

RGB conversion to other colors that return a floating point or a list of floating points do so to allow users get absolute precision since its really easy for callers to do a num.round() on the components of the resulting list.

Example

The example below uses this module to create a success message that will print correctly on almost all terminals (Only Windows 10 version 1901+ supported. All linux and OSX terminals are supported). Try it out!

import colors
colors.text('Successful!', colors.text_color.green)

The text() function can be nested. For example,

colors.text(colors.text('Successful!', colors.style.bold), colors.text_color.green)

The module also features multiple functions for color conversion. For example,

%> import colors
%> colors.rgb_to_cmyk(103, 13, 69)
[0, 87.37864077669903, 33.009708737864095, 59.6078431372549]

The terminal colors also have simple wrappers that allow supplied colors to text() from various color formats. For example, we can specify the color from the HTML hexadecimal color.

import colors
colors.text('Colored text!', colors.hex('#fc0'))

The colors API

Every public name in colors, wherever it is declared. Each links to the page that documents it.

NameKindSummary
colors.NAMEDconstantThe CSS Color Level 4 named colors, mapped to their hexadecimal values.
colors.ansi256_to_ansifunctionConverts ANSI-256 color number to ANSI-16 color number.
colors.backgroundconstantStandard ANSI background colors available for console applications.
colors.cmykfunctionConverts the given CMYK color to its terminal compatible color.
colors.cmyk_to_rgbfunctionConverts a CMYK color into its corresponding RGB color components.
colors.contrast_ratiofunctionReturns the WCAG 2.x contrast ratio between two colors, from 1 (no contrast) to 21 (black on white).
colors.darkenfunctionReturns hex darkened by amount percentage points in HSL space (clamped to 0-100).
colors.desaturatefunctionReturns hex with its saturation decreased by amount percentage points in HSL space (clamped to 0-100).
colors.grayscalefunctionReturns hex fully desaturated (HSL saturation set to 0), the same hue and lightness otherwise preserved.
colors.hexfunctionConverts the given hexadecimal color to its terminal compatible color.
colors.hex_to_ansifunctionConverts the given hexadecimal color to its ANSI-16 number.
colors.hex_to_ansi256functionConverts the given hexadecimal color to its ANSI-256 number.
colors.hex_to_rgbfunctionConverts the hexadecimal string h to its RGBA component.
colors.hslfunctionConverts the given HSL color to its terminal compatible color.
colors.hsl_to_hsvfunctionConverts a HSL color into its corresponding HSV color components.
colors.hsl_to_rgbfunctionConverts a HSL color into its corresponding RGB color components.
colors.hsvfunctionConverts the given HSV color to its terminal compatible color.
colors.hsv_to_hslfunctionConverts a HSV color into its corresponding HSL color components.
colors.hsv_to_rgbfunctionConverts a HSV color into its corresponding RGB color components.
colors.hwbfunctionConverts the given HWB color to its terminal compatible color.
colors.hwb_to_rgbfunctionConverts a HWB color into its corresponding RGB color components.
colors.invertfunctionReturns hex with each RGB channel inverted (255 - channel).
colors.is_hexfunctionReturns true if color is a hexadecimal color that [[colors.hex_to_rgb]] would accept, and false…
colors.is_namedfunctionReturns true if name is a CSS color name that [[colors.named]] would resolve, and false otherwise.
colors.lab_to_rgbfunctionConverts a LAB color into its corresponding RGB color components.
colors.lightenfunctionReturns hex lightened by amount percentage points in HSL space (clamped to 0-100).
colors.mixfunctionLinearly interpolates between two colors, including their alpha channels.
colors.namedfunctionReturns the hexadecimal value of a CSS named color.
colors.relative_luminancefunctionReturns the relative luminance of an RGB color, from 0 (black) to 1 (white), as defined by WCAG 2.x.
colors.rgbfunctionConverts the given RGB color to its terminal compatible color.
colors.rgb_to_ansi256functionConverts RGB color to ASI-256 color number.
colors.rgb_to_cmykfunctionConverts a RGB color into its corresponding CMYK components.
colors.rgb_to_hexfunctionConverts a RGB components into its corresponding hexadecimal color.
colors.rgb_to_hslfunctionConverts a RGB color into its corresponding HSL components.
colors.rgb_to_hsvfunctionConverts a RGB color into its corresponding HSV components.
colors.rgb_to_hwbfunctionConverts a RGB color into its corresponding HWB components.
colors.rgb_to_labfunctionConverts a RGB color into its corresponding LAB color components.
colors.rgb_to_xyzfunctionConverts a RGB color into its corresponding XYZ color space components.
colors.saturatefunctionReturns hex with its saturation increased by amount percentage points in HSL space (clamped to 0-100).
colors.styleconstantANSI font styles available for console applications.
colors.textfunctionReturns a terminal printable text with the given color (or style) and background if given.
colors.text_colorconstantStandard ANSI text colors available for console applications.
colors.xyzfunctionConverts the given XYZ color to its terminal compatible color.
colors.xyz_to_rgbfunctionConverts a XYZ color into its corresponding RGB color components.

Constants

style

colors.style = {...}

ANSI font styles available for console applications.

text_color

colors.text_color = {...}

Standard ANSI text colors available for console applications.

background

colors.background = {...}

Standard ANSI background colors available for console applications.

NAMED

colors.NAMED: dict = {...}

The CSS Color Level 4 named colors, mapped to their hexadecimal values.

Values carry no leading #, matching every other function in this module that produces a hexadecimal color. Both the American and British spellings of the greys are present, as are rebeccapurple and transparent, which is the only entry with an alpha component and therefore the only 8-digit one.

Keys are lower case with no separators; use [[colors.named]] rather than indexing this directly if the name might arrive in mixed case or spaced out.

Functions

text()

colors.text(value, color, bg) -> string

Returns a terminal printable text with the given color (or style) and background if given.

Parameters

  • value (string)
  • color (?int)
  • bg (?int)

Returns string

Note: The color argument can be replace with a style.

rgb_to_ansi256()

colors.rgb_to_ansi256(r: int, g: int, b: int) -> number

Converts RGB color to ASI-256 color number.

Parameters

  • r (int)
  • g (int)
  • b (int)

Returns number

ansi256_to_ansi()

colors.ansi256_to_ansi(code: int) -> number

Converts ANSI-256 color number to ANSI-16 color number.

Parameters

  • code (int)

Returns number

is_hex()

colors.is_hex(color) -> bool

Returns true if color is a hexadecimal color that [[colors.hex_to_rgb]] would accept, and false otherwise.

A leading # is optional and the digits may be in either case. This raises nothing, so it is the way to test a color before converting it rather than converting inside a catch.

%> import colors
%> colors.is_hex('#4f46e5')
true
%> colors.is_hex('rebeccapurple')
false

Parameters

  • color (any)

Returns bool

hex_to_rgb()

colors.hex_to_rgb(h: string) -> list

Converts the hexadecimal string h to its RGBA component.

Accepts an optional leading #, is case-insensitive, and accepts the 3/4/6/8-digit forms (#f0c, #f0c8, #ff00cc, #ff00ccbb). Following the CSS Color 4 convention, alpha is the last component when present (RRGGBBAA/RGBA, not AARRGGBB/ARGB): an omitted alpha means fully opaque (1).

Parameters

  • h (string)

Returns list — [r, g, b, a]

Raises ValueError When h is not a valid hexadecimal color.

hex_to_ansi256()

colors.hex_to_ansi256(color: string) -> number

Converts the given hexadecimal color to its ANSI-256 number.

Parameters

  • color (string)

Returns number

hex_to_ansi()

colors.hex_to_ansi(color: string) -> number

Converts the given hexadecimal color to its ANSI-16 number.

Parameters

  • color (string)

Returns number

Note: For use with text(), this should be preferred over hex_to_ansi256

hex()

colors.hex(color: string) -> number

Converts the given hexadecimal color to its terminal compatible color.

Parameters

  • color (string)

Returns number

Note: For use with text(), this should be preferred over hex_to_ansi256 and hex_to_ansi

Note: color can include the ‘#’ character. E.g. #ff0.

rgb()

colors.rgb(r: int, g: int, b: int) -> number

Converts the given RGB color to its terminal compatible color.

Parameters

  • r (number)
  • g (number)
  • b (number)

Returns number

hsl()

colors.hsl(h: number, s: number, l: number) -> number

Converts the given HSL color to its terminal compatible color.

Parameters

  • h (number)
  • s (number)
  • l (number)

Returns number

hsv()

colors.hsv(h: number, s: number, v: number) -> number

Converts the given HSV color to its terminal compatible color.

Parameters

  • h (number)
  • s (number)
  • v (number)

Returns number

hwb()

colors.hwb(h: number, w: number, b: number) -> number

Converts the given HWB color to its terminal compatible color.

Parameters

  • h (number)
  • w (number)
  • b (number)

Returns number

cmyk()

colors.cmyk(c: number, m: number, y: number, k: number) -> number

Converts the given CMYK color to its terminal compatible color.

Parameters

  • c (number)
  • m (number)
  • y (number)
  • k (number)

Returns number

xyz()

colors.xyz(x: number, y: number, z: number) -> number

Converts the given XYZ color to its terminal compatible color.

Parameters

  • x (number)
  • y (number)
  • z (number)

Returns number

rgb_to_hex()

colors.rgb_to_hex(r: int, g: int, b: int, a: ?int) -> string

Converts a RGB components into its corresponding hexadecimal color.

Following the CSS Color 4 convention, alpha (when given) is appended as the last component (RRGGBBAA), and every channel is zero-padded to two digits.

Parameters

  • r (int)
  • g (int)
  • b (int)
  • a (?int)

Returns string

rgb_to_hsl()

colors.rgb_to_hsl(r: int, g: int, b: int) -> list[float]

Converts a RGB color into its corresponding HSL components.

Parameters

  • r (int)
  • g (int)
  • b (int)

Returns list[float]

rgb_to_hsv()

colors.rgb_to_hsv(r: int, g: int, b: int) -> list[float]

Converts a RGB color into its corresponding HSV components.

Parameters

  • r (int)
  • g (int)
  • b (int)

Returns list[float]

rgb_to_hwb()

colors.rgb_to_hwb(r: int, g: int, b: int) -> list[float]

Converts a RGB color into its corresponding HWB components.

Parameters

  • r (int)
  • g (int)
  • b (int)

Returns list[float]

rgb_to_cmyk()

colors.rgb_to_cmyk(r: int, g: int, b: int) -> list[float]

Converts a RGB color into its corresponding CMYK components.

Parameters

  • r (int)
  • g (int)
  • b (int)

Returns list[float]

rgb_to_xyz()

colors.rgb_to_xyz(r: int, g: int, b: int) -> list[float]

Converts a RGB color into its corresponding XYZ color space components.

Parameters

  • r (int)
  • g (int)
  • b (int)

Returns list[float]

rgb_to_lab()

colors.rgb_to_lab(r: int, g: int, b: int) -> list[float]

Converts a RGB color into its corresponding LAB color components.

Parameters

  • r (int)
  • g (int)
  • b (int)

Returns list[float]

hsl_to_rgb()

colors.hsl_to_rgb(h: number, s: number, l: number) -> list[float]

Converts a HSL color into its corresponding RGB color components.

Parameters

  • h (number)
  • s (number)
  • l (number)

Returns list[float]

hsl_to_hsv()

colors.hsl_to_hsv(h: number, s: number, l: number) -> list[float]

Converts a HSL color into its corresponding HSV color components.

Parameters

  • h (number)
  • s (number)
  • l (number)

Returns list[float]

hsv_to_rgb()

colors.hsv_to_rgb(h: number, s: number, v: number) -> list[float]

Converts a HSV color into its corresponding RGB color components.

Parameters

  • h (number)
  • s (number)
  • v (number)

Returns list[float]

hsv_to_hsl()

colors.hsv_to_hsl(h: number, s: number, v: number) -> list[float]

Converts a HSV color into its corresponding HSL color components.

Parameters

  • h (number)
  • s (number)
  • v (number)

Returns list[float]

hwb_to_rgb()

colors.hwb_to_rgb(h: number, w: number, b: number) -> list[float]

Converts a HWB color into its corresponding RGB color components.

Parameters

  • h (number)
  • w (number)
  • b (number)

Returns list[float]

cmyk_to_rgb()

colors.cmyk_to_rgb(c: number, m: number, y: number, k: number) -> list[float]

Converts a CMYK color into its corresponding RGB color components.

Parameters

  • c (number)
  • m (number)
  • y (number)
  • k (number)

Returns list[float]

xyz_to_rgb()

colors.xyz_to_rgb(x: number, y: number, z: number) -> list[float]

Converts a XYZ color into its corresponding RGB color components.

Parameters

  • x (number)
  • y (number)
  • z (number)

Returns list[float]

lab_to_rgb()

colors.lab_to_rgb(l: number, a: number, b: number) -> list[float]

Converts a LAB color into its corresponding RGB color components. The inverse of rgb_to_lab.

Parameters

  • l (number)
  • a (number)
  • b (number)

Returns list[float]

lighten()

colors.lighten(hex: string, amount: number) -> string

Returns hex lightened by amount percentage points in HSL space (clamped to 0-100).

Parameters

  • hex (string)
  • amount (number)

Returns string

darken()

colors.darken(hex: string, amount: number) -> string

Returns hex darkened by amount percentage points in HSL space (clamped to 0-100). The inverse of lighten.

Parameters

  • hex (string)
  • amount (number)

Returns string

saturate()

colors.saturate(hex: string, amount: number) -> string

Returns hex with its saturation increased by amount percentage points in HSL space (clamped to 0-100).

Parameters

  • hex (string)
  • amount (number)

Returns string

desaturate()

colors.desaturate(hex: string, amount: number) -> string

Returns hex with its saturation decreased by amount percentage points in HSL space (clamped to 0-100). The inverse of saturate.

Parameters

  • hex (string)
  • amount (number)

Returns string

grayscale()

colors.grayscale(hex) -> string

Returns hex fully desaturated (HSL saturation set to 0), the same hue and lightness otherwise preserved. This is an HSL desaturation, not a perceptual-luminance-weighted grayscale.

Parameters

  • hex (string)

Returns string

invert()

colors.invert(hex: string) -> string

Returns hex with each RGB channel inverted (255 - channel). Alpha, if present, is left unchanged.

Parameters

  • hex (string)

Returns string

mix()

colors.mix(hex_a: string, hex_b: string, weight: ?number) -> string

Linearly interpolates between two colors, including their alpha channels.

Parameters

  • hex_a (string)
  • hex_b (string)
  • weight (?number) — How far from hex_a (0) to hex_b (1). Default: 0.5 (the midpoint).

Returns string

relative_luminance()

colors.relative_luminance(r, g, b) -> number

Returns the relative luminance of an RGB color, from 0 (black) to 1 (white), as defined by WCAG 2.x.

This is perceived brightness in linear light, not the l of HSL: pure green is far brighter to the eye than pure blue even though HSL gives both a lightness of 0.5. It is what contrast_ratio() is built on, and what to compare against a threshold when deciding whether light or dark text belongs on a background.

%> import colors
%> colors.relative_luminance(255, 255, 255)
1
%> colors.relative_luminance(0, 255, 0)
0.7152

Parameters

  • r (int)
  • g (int)
  • b (int)

Returns number

contrast_ratio()

colors.contrast_ratio(hex_a: string, hex_b: string) -> number

Returns the WCAG 2.x contrast ratio between two colors, from 1 (no contrast) to 21 (black on white).

A ratio of at least 4.5 meets WCAG AA for normal text (3 for large text); 7 meets AAA (4.5 for large text).

Parameters

  • hex_a (string)
  • hex_b (string)

Returns number

named()

colors.named(name: string) -> string

Returns the hexadecimal value of a CSS named color.

Matching ignores case, whitespace, hyphens and underscores, so 'Tomato', 'tomato' and ' TOMATO ' all resolve, as do both 'darkseagreen' and 'Dark Sea Green'.

The result carries no leading #, the same as [[colors.rgb_to_hex]] and [[colors.lighten]], so it can be handed straight to any function here that takes a hexadecimal color. Every name returns 6 digits except transparent, which returns the 8-digit '00000000'.

%> import colors
%> colors.named('rebeccapurple')
'663399'
%> colors.named('Dark Sea Green')
'8fbc8f'

Parameters

  • name (string)

Returns string

Raises ValueError When name is not a CSS color name.

Note: Spaces inside the name are ignored, so both the CSS spelling and the spaced-out reading of a name resolve to the same color.

is_named()

colors.is_named(name) -> bool

Returns true if name is a CSS color name that [[colors.named]] would resolve, and false otherwise.

Parameters

  • name (string)

Returns bool


2022, Richard Ore and The Zuri Contributors

json

import json

Provides APIs for encoding and decoding JSON data.

JavaScript Object Notation (JSON) is a lightweight, text-based, language-independent data interchange format. It was derived from the ECMAScript Programming Language Standard. JSON defines a small set of formatting rules for the portable representation of structured data.

This implementation complies with RFC 8259.

JSON to Zuri value mapping

JSONZuri
NullNil
StringString
NumberNumber
BooleanBoolean
ArrayList
ObjectDict

Zuri to JSON object mapping

ZuriJSON
nilNull
IntegerNumber
NumberNumber
CharString
StringString
ListArray
DictObject
Instance of class implementing to_json() decoratorAny

Example,

%> import json
%> json.encode([1, 2, 3])
'[1,2,3]'
%>
%> json.encode({name: 'Zuri', version: '0.1.0'})
'{"name":"Zuri","version":"0.1.0"}'
%>
%> json.encode({name: 'Zuri', version: '0.1.0'}, false)
'{
  "name": "Zuri",
  "version": "0.1.0"
}'

The json API

Every public name in json, wherever it is declared. Each links to the page that documents it.

NameKindSummary
json.decodefunctionDecodes the input JSON string into Zuri objects
json.dumpfunctionDumps the given value into a json file at the specified path.
json.encodefunctionJSON encodes the given value with a recursive depth up to max_depth.
json.parsefunctionParses a file containing json data.

Functions

encode()

json.encode(value, compact, max_depth) -> string

JSON encodes the given value with a recursive depth up to max_depth.

If compact is true, the resulting json string will be tightly packed. i.e. spaces will be trimmed from objects and arrays. Otherwise, the JSON output will be pretty formatted.

Parameters

  • value (any)
  • compact (?bool) — Default value is true.
  • max_depth (?number) — is the maximum recursive depth for encoding, default = 1024.

Returns string

Raises Error: If the value cannot be encoded to json.

Note: pretty formatting use 2 spaces instead of tabs.

decode()

json.decode(value, allow_comments)

Decodes the input JSON string into Zuri objects

Parameters

  • value (string) — The string to decode
  • allow_comments (?bool) — Can be set to enable/disable C-style comments in json [default = true]

Returns — object

Raises Error: If the json is invalid.

parse()

json.parse(path, allow_comments)

Parses a file containing json data.

Parameters

  • path (string)
  • allow_comments (?bool) — Can be set to enable/disable C-style comments in json [default = true]

Returns — object

Raises Error: If the file cannot be read or if the json is invalid.

dump()

json.dump(value, path, compact, max_depth)

Dumps the given value into a json file at the specified path.

Parameters

  • value (any)
  • path (string)
  • compact (?bool) — Default value is true.
  • max_depth (?number) — is the maximum recursive depth for encoding, default = 1024.

Raises Error: If the file cannot be written to.

Note: pretty formatting use 2 spaces instead of tabs.


2021, Richard Ore and Zuri contributors

yaml

import yaml

A complete, YAML 1.2.2-compliant library for parsing and emitting YAML (YAML Ain’t Markup Language) documents.

This module implements the YAML 1.2.2 specification published at https://yaml.org/spec/1.2.2/ using the Core Schema for implicit type resolution, which is the schema used by most modern YAML implementations.

Supported features

Scalars

  • Plain (unquoted) scalars with Core Schema type resolution - Single-quoted scalars (no escape processing; '' → ') - Double-quoted scalars with full escape sequence processing (\n \t \r \\ \" \/ \b \f \a \v \e \0 \xNN \uNNNN \UNNNNNNNN \N \_ \L \P)
  • Literal block scalars (|) with clip, strip, and keep chomping
  • Folded block scalars (>) with clip, strip, and keep chomping

Collections

  • Block mappings (key: value pairs, indented) - Block sequences (dash-prefixed lists, indented) - Flow mappings ({ key: value, ... })
  • Flow sequences ([ item, item, ... ]) - Nested collections of any depth - Complex (non-scalar) mapping keys with ? indicator

Document structure

  • Multi-document streams separated by --- - Document end markers ... - %YAML directives (parsed; the major version must be 1, and the version string must be well-formed, or parsing raises: the minor version isn’t otherwise checked) - %TAG directives (parsed and consumed; custom tag handles registered this way are not yet substituted into !handle!suffix tags) - UTF-8 input only, with BOM detection/stripping for that encoding. UTF-16/UTF-32 input is not supported.

Node properties

  • Anchors (&name) and aliases (*name) with full reference resolution - Merge keys (<<: *anchor) for dict merging - Explicit tags (!!str, !!int, !!float, !!bool, !!null, !!seq, !!map, !!binary, !<tag:yaml.org,2002:type>)

Core Schema type resolution (implicit)

  • null / Null / NULL / ~ / empty → nil - true / True / TRUE / false / False / FALSE → bool - Decimal integers (with optional +/- sign) → number - Octal integers 0o[0-7]+ → number - Hexadecimal integers 0x[0-9A-Fa-f]+ → number - Decimal floats (with exponent) → number - .inf / .Inf / .INF / ±.inf → math.Infinity / -math.Infinity - .nan / .NaN / .NAN → 0/0 (NaN) - Everything else → string

Emitter features

  • Round-trips scalars correctly (chooses minimal quoting) - Emits block or flow style per value type - Anchors and aliases for repeated object references - Configurable indentation (default: 2) - Configurable line width for scalar wrapping (default: 80)

Quick start

import yaml

# Parse a YAML string.
var data = yaml.parse('name: Alice\nage: 30\nscores: [98, 72, 85]')
echo data['name']      # Alice
echo data['age']       # 30
echo data['scores'][0] # 98

# Parse multiple documents.
var docs = yaml.parse_all('---\na: 1\n---\nb: 2\n')
echo docs.length()  # 2

# Emit a Zuri value as YAML.
echo yaml.dump({ name: 'Bob', active: true, tags: ['x', 'y'] })
# name: Bob
# active: true
# tags: [x, y]

# Parse a YAML file.
var config = yaml.load_file('config.yaml')

# Emit to a file.
yaml.dump_file('out.yaml', data)

The yaml API

Every public name in yaml, wherever it is declared. Each links to the page that documents it.

NameKindSummary
yaml.YamlErrorclassError raised when a YAML parsing or emission error occurs.
yaml.dumpfunctionSerialises a Zuri value to a YAML string.
yaml.dump_allfunctionSerialises a list of values as a multi-document YAML stream.
yaml.dump_filefunctionSerialises value to YAML and writes it to the file at path.
yaml.load_filefunctionReads a YAML file from path and parses all documents in the stream.
yaml.load_single_filefunctionReads a YAML file from path and parses the first document.
yaml.parsefunctionParses a YAML string and returns the value of the first (or only) document.
yaml.parse_allfunctionParses a YAML stream containing one or more documents and returns a list of all document values.

Functions

parse()

yaml.parse(text: string) -> any

Parses a YAML string and returns the value of the first (or only) document.

Uses the Core Schema for implicit type resolution (see module description).

Examples

import yaml

# Scalar
yaml.parse('42')              # 42 (number)
yaml.parse('hello')           # 'hello' (string)
yaml.parse('true')            # true (bool)
yaml.parse('~')               # nil

# Mapping
yaml.parse('a: 1\nb: two')   # { a: 1, b: 'two' }

# Sequence
yaml.parse('- x\n- y\n- z')  # ['x', 'y', 'z']

# Flow styles
yaml.parse('{x: 1, y: [2, 3]}')  # { x: 1, y: [2, 3] }

# Anchors and aliases
yaml.parse('a: &ref hello\nb: *ref')  # { a: 'hello', b: 'hello' }

# Multi-line string
yaml.parse("msg: |\n  hello\n  world\n")  # { msg: "hello\nworld\n" }

Parameters

  • text (string) — The YAML text to parse.

Returns any — The parsed Zuri value.

Raises YamlError On any YAML syntax or structural error.

parse_all()

yaml.parse_all(text: string) -> list

Parses a YAML stream containing one or more documents and returns a list of all document values.

Documents are separated by --- markers. A stream with a single document and no --- marker returns a single-element list.

import yaml

var docs = yaml.parse_all("---\na: 1\n---\nb: 2\n")
echo docs.length()  # 2
echo docs[0]        # { a: 1 }
echo docs[1]        # { b: 2 }

Parameters

  • text (string) — The YAML stream text.

Returns list — List of parsed document values (one per document).

Raises YamlError On any YAML syntax or structural error.

load_single_file()

yaml.load_single_file(path: string) -> any

Reads a YAML file from path and parses the first document.

import yaml

var config = yaml.load_file('config.yaml')
echo config['database']['host']

Parameters

  • path (string) — Filesystem path to the YAML file.

Returns any — The parsed value of the first document.

Raises YamlError On parse error.

Raises Error On file I/O error.

load_file()

yaml.load_file(path: string) -> list

Reads a YAML file from path and parses all documents in the stream.

Parameters

  • path (string) — Filesystem path to the YAML file.

Returns list — List of parsed document values.

Raises YamlError On parse error.

Raises Error On file I/O error.

dump()

yaml.dump(value, indent: ?int, width: ?int) -> string

Serialises a Zuri value to a YAML string.

The output is a valid YAML 1.2 document terminated by a newline. Collections use block style by default; small flat sequences may be emitted in flow style when they fit within the line width.

Examples

import yaml

yaml.dump(nil)             # 'null\n'
yaml.dump(true)            # 'true\n'
yaml.dump(42)              # '42\n'
yaml.dump('hello world')  # 'hello world\n'
yaml.dump('null')         # "'null'\n"  (quoted to avoid misparse)

yaml.dump({ name: 'Alice', age: 30 })
# name: Alice
# age: 30

yaml.dump(['a', 'b', 'c'])
# - a
# - b
# - c

# Nested structure.
yaml.dump({ servers: [{ host: 'a', port: 80 }, { host: 'b', port: 443 }] })
# servers:
#   -
#     host: a
#     port: 80
#   -
#     host: b
#     port: 443

Parameters

  • value (any) — The Zuri value to serialise.
  • indent (?int) — Spaces per indentation level (default: 2).
  • width (?int) — Target line width for flow-style decisions (default: 80).

Returns string — The YAML text (always ends with \n).

Raises YamlError On emission error (e.g. unserializable value).

dump_all()

yaml.dump_all(values: list, indent: ?int, width: ?int) -> string

Serialises a list of values as a multi-document YAML stream.

Each value is emitted as a separate YAML document, separated by ---.

import yaml

yaml.dump_all([{ a: 1 }, { b: 2 }])
# ---
# a: 1
# ---
# b: 2

Parameters

  • values (list) — List of Zuri values to serialise.
  • indent (?int) — Spaces per indentation level (default: 2).
  • width (?int) — Target line width (default: 80).

Returns string — The YAML stream text.

Raises YamlError On emission error.

dump_file()

yaml.dump_file(path: string, value, indent: ?int, width: ?int)

Serialises value to YAML and writes it to the file at path.

Creates or overwrites the file.

import yaml

yaml.dump_file('config.yaml', {
  host: 'localhost',
  port: 5432,
  debug: false,
})

Parameters

  • path (string) — Filesystem path to write to.
  • value (any) — The value to serialise.
  • indent (?int) — Indentation level (default: 2).
  • width (?int) — Line width (default: 80).

Raises YamlError On emission error.

Raises Error On file I/O error.

Classes

YamlError

class yaml.YamlError < Error

Error raised when a YAML parsing or emission error occurs.

Fields

FieldTypeDescription
line
column
context

Constructor

yaml.YamlError(message, line, column, context)

Parameters

  • message (string) — Error description.
  • line (number) — Line number (1-based), or nil.
  • column (number) — Column number (1-based), or nil.
  • context (string) — Optional context (surrounding text), or nil.

2026, Richard Ore and The Zuri Contributors

toml

import toml

TOML, read for its values or edited in place without disturbing the file around them.

TOML is the format configuration files settle on when they have to be edited by people and by programs both. This module answers both halves. parse() and dump() treat a document as data, the way json and yaml do. edit() treats it as a file somebody wrote, and keeps every comment, blank line and quoting choice through a change.

The implementation follows TOML v1.0.0 in full.

import toml

var config = toml.parse('[server]\nhost = "localhost"\nport = 8080\n')

echo config['server']['port']
8080

What a value decodes to

TOMLZuri
stringstring
integernumber, or bigint past 2^53
floatnumber
booleanbool
offset date-timeDateTime
local date-timeLocalDateTime
local dateLocalDate
local timeLocalTime
arraylist
tabledict

Zuri’s number is an IEEE-754 double, exact only to 2^53. TOML requires the full signed 64-bit range, so an integer past what a double can carry decodes to a bigint instead of rounding silently. An integer outside the 64-bit range is a parse error, which is what TOML asks for.

Integers, floats, and the one thing that does not round-trip

The same double is behind 1 and 1.0, and nothing afterwards tells them apart. Encoding has to guess, and it guesses integer whenever a number has no fractional part. Wrap a number in Float to settle it:

import toml

echo toml.dump({ a: 1.0, b: toml.Float(1.0) })
a = 1
b = 1.0

This is the only place a value changes type across a parse() and dump() pair. edit() is not affected: it never re-spells a value it was not asked to change.

Editing a file in place

edit() returns a Document, which knows the text it came from. Rendering one nobody touched reproduces the input byte for byte, and an edit disturbs only the line it lands on:

import toml

var doc = toml.edit('# what we ship\n[package]\nname = "app"\nversion = "0.9.0"  # bump me\n')

doc.set('package.version', '1.0.0')
doc.table('dependencies').set('http', '^2.0')

echo doc.to_string()
# what we ship
[package]
name = "app"
version = "1.0.0"  # bump me

[dependencies]
http = "^2.0"

The comment stayed, the header stayed, and the trailing comment on the line that changed stayed with it.

The toml API

Every public name in toml, wherever it is declared. Each links to the page that documents it.

NameKindSummary
toml.ArrayclassA TOML array, holding its items and the trivia between them.
toml.ArrayOfTablesclassA [[header]] array of tables.
toml.DateTimeclassA date and time with a UTC offset, TOML’s offset-date-time.
toml.DocumentclassA whole TOML document: every table in it, and every byte between them.
toml.EditErrorclassAn edit that the document cannot accept.
toml.EncodeErrorclassA Zuri value that cannot be written as TOML.
toml.EntryclassOne key = value pair.
toml.FloatclassA number that must be written as a TOML float.
toml.InlineTableclassA TOML inline table, holding its entries and the trivia between them.
toml.LocalDateclassA calendar date with no time and no offset, TOML’s local-date.
toml.LocalDateTimeclassA date and a wall-clock time with no offset, TOML’s local-date-time.
toml.LocalTimeclassA wall-clock time with no date and no offset, TOML’s local-time.
toml.ParseErrorclassText that is not valid TOML.
toml.TableclassA table: a [header] block, the root of a document, or a parent that only exists because something below it…
toml.TomlErrorclassBase class for every error this module raises.
toml.ValueclassOne value in a document: a scalar, an array, or an inline table.
toml.documentfunctionBuilds a Document from a plain Zuri value.
toml.document.spell_basic_stringfunctionReturns text as a TOML basic string, quoted and escaped.
toml.document.spell_floatfunctionReturns value as a TOML float, including the inf and nan spellings TOML defines.
toml.document.spell_integerfunctionReturns value as a TOML integer.
toml.document.spell_keyfunctionReturns name as TOML would spell it as a key: bare when every character allows it, and quoted when not.
toml.document.spell_multiline_stringfunctionReturns text as a TOML multi-line basic string.
toml.document.spell_valuefunctionBuilds the Value node for a plain Zuri value, choosing the TOML spelling as it goes.
toml.dumpfunctionReturns value as TOML text.
toml.dump_filefunctionWrites value to the file at path as TOML, creating or overwriting it.
toml.editfunctionParses TOML text into a Document, which remembers how it was written.
toml.edit_filefunctionReads the TOML file at path into a Document.
toml.encoder.encodefunctionBuilds the document for a Zuri dictionary.
toml.load_filefunctionReads the TOML file at path and returns it as a plain Zuri dictionary.
toml.parsefunctionParses TOML text and returns it as a plain Zuri dictionary.
toml.parser.parsefunctionReads TOML text and returns the document tree it describes.

Submodules

ModuleReached asSummary
toml.documenttoml.document.*The document tree, which is the text.
toml.encoderimport toml.encoderTurning a plain Zuri value into a document.
toml.errorstoml.errors.*Every error the toml module raises, under one root.
toml.parserimport toml.parserReading TOML text into the document tree, losing nothing.
toml.valuestoml.values.*The TOML types that Zuri has no built-in equal for.

Functions

parse()

toml.parse(text: string) -> dict

Parses TOML text and returns it as a plain Zuri dictionary.

A TOML document is always a table, so the result is always a dictionary, empty when the text held nothing but comments.

import toml

var config = toml.parse('title = "example"\n[owner]\nname = "Ada"\n')

echo config['title']
echo config['owner']['name']
example
Ada

Parameters

  • text (string) — The TOML text.

Returns dict

Raises ParseError On any syntax or structural error, carrying the line and column it stopped at.

load_file()

toml.load_file(path: string) -> dict

Reads the TOML file at path and returns it as a plain Zuri dictionary.

import toml

var config = toml.load_file('zuri.toml')

echo config['package']['name']

Parameters

  • path (string) — Filesystem path to the TOML file.

Returns dict

Raises ParseError On any syntax or structural error.

Raises Error On any file error.

dump()

toml.dump(value, options: ?dict) -> string

Returns value as TOML text.

The result always ends in a line break. Keys keep the order the dictionary holds them in unless sort_keys says otherwise.

import toml

echo toml.dump({
  title: 'example',
  owner: { name: 'Ada', active: true },
  ports: [8000, 8001],
})
title = "example"
ports = [8000, 8001]

[owner]
name = "Ada"
active = true

Parameters

  • value (dict) — The value to write. A TOML document is a table, so this has to be a dictionary.
  • options (?dict) — sort_keys (default false) writes keys in sorted order instead of insertion order. inline_threshold (default 0, off) writes a flat table inline when its inline form is no longer than this many characters. multiline_strings (default true) writes a string holding a newline in triple quotes rather than escaping it. array_width (default 80) is the length past which an array breaks onto one line per item.

Returns string

Raises EncodeError If value is not a dictionary, holds a nil or a value with no TOML spelling, has a non-string key, or contains itself.

dump_file()

toml.dump_file(path: string, value, options: ?dict)

Writes value to the file at path as TOML, creating or overwriting it.

import toml

toml.dump_file('config.toml', {
  server: { host: '0.0.0.0', port: 8080 },
})

Parameters

  • path (string) — Filesystem path to write to.
  • value (dict) — The value to write.
  • options (?dict) — The same options dump() takes.

Raises EncodeError If the value cannot be written.

Raises Error On any file error.

edit()

toml.edit(text: string) -> Document

Parses TOML text into a Document, which remembers how it was written.

This is the one to reach for when the file already exists and belongs to somebody. A Document renders back byte for byte until it is changed, and a change touches only what it names.

import toml

var doc = toml.edit('[a]\nx = 1   # keep me\ny = 2\n')

doc.set('a.x', 99)
doc.remove('a.y')

echo doc.to_string()
[a]
x = 99   # keep me

Parameters

  • text (string) — The TOML text.

Returns Document

Raises ParseError On any syntax or structural error.

edit_file()

toml.edit_file(path: string) -> Document

Reads the TOML file at path into a Document.

Document.save() writes it back.

import toml

var doc = toml.edit_file('zuri.toml')

doc.set('package.version', '2.0.0')
doc.save('zuri.toml')

Parameters

  • path (string) — Filesystem path to the TOML file.

Returns Document

Raises ParseError On any syntax or structural error.

Raises Error On any file error.

document()

toml.document(value, options: ?dict) -> Document

Builds a Document from a plain Zuri value.

dump() is this followed by to_string(). Reach for this one when the document is going to be edited before it is written, or when a generated file should be handed to the same code that handles a parsed one.

import toml

var doc = toml.document({ package: { name: 'app' } })

doc.set('package.version', '0.1.0')

echo doc.to_string()
[package]
name = "app"
version = "0.1.0"

Parameters

  • value (dict) — The value to write.
  • options (?dict) — The same options dump() takes.

Returns Document

Raises EncodeError If the value cannot be written.


2026, Richard Ore and The Zuri Contributors

toml.document

import toml.document

toml lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled toml.document.* needs import toml.document.

The document tree, which is the text.

A TOML file read for its values can throw away everything that is not a value. A TOML file read so that it can be written back cannot throw away anything at all: the comment above a key, the blank line between two tables, the choice of 0x1F over 31, the single quotes around a Windows path. A program that rewrites a file it did not write has to leave all of it alone.

So every node here holds two things: what it means, and the exact text it was. Value keeps the decoded value beside the source spelling that produced it. Entry keeps the whitespace on each side of its =. Table keeps the trivia before its header. Rendering is concatenation, and a document nobody edited comes back byte for byte.

Nodes are built by parser.zu when reading and by encoder.zu when writing. Reach them through Document, which is what the module’s public API hands out.

Functions

spell_key()

toml.document.spell_key(name: string) -> string

Returns name as TOML would spell it as a key: bare when every character allows it, and quoted when not.

An empty key is legal in TOML and must be written "".

Parameters

  • name (string)

Returns string

spell_basic_string()

toml.document.spell_basic_string(text: string) -> string

Returns text as a TOML basic string, quoted and escaped.

Control characters that have no short escape are written as \uXXXX. Every other character is passed through as itself, so the result stays readable for anything that is not a control character.

Parameters

  • text (string)

Returns string

spell_multiline_string()

toml.document.spell_multiline_string(text: string) -> string

Returns text as a TOML multi-line basic string.

Newlines and single quotes pass through untouched; a run of three quotes and a trailing quote are escaped, since either would close the string early.

Parameters

  • text (string)

Returns string

spell_integer()

toml.document.spell_integer(value) -> string

Returns value as a TOML integer.

Parameters

  • value (number|bigint)

Returns string

spell_float()

toml.document.spell_float(value) -> string

Returns value as a TOML float, including the inf and nan spellings TOML defines.

A whole number gains a .0, because a float that renders without one reads back as an integer.

Parameters

  • value (number)

Returns string

spell_value()

toml.document.spell_value(value, path) -> Value

Builds the Value node for a plain Zuri value, choosing the TOML spelling as it goes.

A string with a newline in it becomes a multi-line basic string; a whole number becomes an integer; a Float becomes a float whatever its value. A list becomes a single-line array and a dictionary becomes an inline table, because this is the spelling a value takes when it is written in place. Document.table() is how a value becomes a table block instead.

Parameters

  • value (any)
  • path (?string) — Dotted path used in an error message.

Returns Value

Raises EncodeError If the value has no TOML spelling, or if a list or dictionary contains itself.

Classes

Value

class toml.Value

One value in a document: a scalar, an array, or an inline table.

kind names the TOML type. value is the decoded Zuri value for a scalar, or the Array or InlineTable node for a container. raw is the exact source text of a scalar, which is what makes 0x1F survive a round trip as 0x1F rather than as 31.

prefix and suffix hold the trivia on each side: whatever sat between the = and the value, and whatever followed it on the same line, a trailing comment included.

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
kind
value
raw
prefix
suffix

Constructor

toml.Value(kind, value, raw)

Parameters

  • kind (string) — One of string, integer, float, boolean, datetime, local-datetime, local-date, local-time, array or inline-table.
  • value (any) — The decoded value, or the container node.
  • raw (?string) — The exact source text, for a scalar.

Value.to_string()

toml.Value.to_string() -> string

Returns the value’s own text, without its surrounding trivia.

Returns string

Value.to_value()

toml.Value.to_value() -> any

Returns the value as a plain Zuri value, with every container below it converted too.

Returns any

Array

class toml.Array

A TOML array, holding its items and the trivia between them.

An array may run across lines and carry comments between its items, so each item keeps its own prefix and suffix and the array keeps whatever sits before the closing bracket.

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
items
trailing_comma
trailing

Constructor

toml.Array()

Array.to_string()

toml.Array.to_string() -> string

Returns the array as TOML text, brackets included.

Returns string

Array.to_value()

toml.Array.to_value() -> list

Returns the array as a plain Zuri list.

Returns list

InlineTable

class toml.InlineTable

A TOML inline table, holding its entries and the trivia between them.

An inline table is sealed once written: TOML forbids adding to one after the fact, and this module enforces that on edit as well as on parse.

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
entries
trailing

Constructor

toml.InlineTable()

InlineTable.to_string()

toml.InlineTable.to_string() -> string

Returns the inline table as TOML text, braces included.

Returns string

InlineTable.to_value()

toml.InlineTable.to_value() -> dict

Returns the inline table as a plain Zuri dictionary, with dotted keys expanded into nested dictionaries.

Returns dict

Entry

class toml.Entry

One key = value pair.

path is the decoded key, which has more than one element when the key was dotted. key_raw is the key exactly as written, dots, quotes and interior spaces included, so a . b stays a . b.

prefix is everything from the end of the previous line to the first character of the key, which is where a comment above the key lives. newline is the line terminator, and is empty for an entry inside an inline table.

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
path
key_raw
prefix
pre_eq
value
newline

Constructor

toml.Entry(path, key_raw, value)

Parameters

  • path (list) — The decoded key path.
  • key_raw (string) — The key exactly as written.
  • value (Value) — The value this key holds.

Entry.to_string()

toml.Entry.to_string() -> string

Returns the whole pair as TOML text, its own trivia included.

Returns string

Entry.comment()

toml.Entry.comment() -> string|nil

Returns the comment written above this key, without its #, the single space that usually follows it, or the indentation. Returns nil when there is no comment.

Returns string|nil

Table

class toml.Table

A table: a [header] block, the root of a document, or a parent that only exists because something below it was written.

entries holds the pairs written directly under this table’s header, in document order. children maps a name to the Table or ArrayOfTables below it.

A table is explicit when a header was written for it, and implicit when it exists only because a deeper table named it. A table brought into being by a dotted key is marked dotted: TOML allows sub-tables to be added under one, but forbids reopening it with a header of its own.

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
document
path
header_raw
prefix
suffix
newline
entries
children
explicit
dotted
is_aot_element

Constructor

toml.Table(path)

Parameters

  • path (list) — The full key path from the root.

Table.to_string()

toml.Table.to_string() -> string

Returns this table’s own text: its header when it has one, then every pair written under it.

Child tables are not included. A document renders them in the order their headers appeared, which is not necessarily the order the tree nests them in.

Returns string

Table.to_value()

toml.Table.to_value() -> dict

Returns this table and everything under it as a plain Zuri dictionary.

Returns dict

Table.entry_at()

toml.Table.entry_at(path: list) -> Entry|nil

Returns the Entry whose key path is exactly path, or nil.

Parameters

  • path (list)

Returns Entry|nil

Table.keys()

toml.Table.keys() -> list

Returns the names of everything this table holds, pairs first and then child tables, in the order they were written.

A dotted key contributes only its first element, which is the name this table actually holds.

Returns list

Table.length()

toml.Table.length() -> number

Returns how many names this table holds.

Returns number

Table.get()

toml.Table.get(path) -> any

Returns the value stored at path, as a plain Zuri value, or nil when nothing is there.

path is a dotted string or a list of key names. A dotted string splits on ., so a key that contains a literal dot has to be passed as a list.

Parameters

  • path (string|list)

Returns any

Table.has()

toml.Table.has(path) -> bool

Returns true when something is stored at path.

Parameters

  • path (string|list)

Returns bool

Table.set()

toml.Table.set(path, value) -> Entry

Writes value at path, relative to this table.

A key that is already there keeps its line: only the value text changes, so the spacing around the = and a trailing comment both survive. A key that is not there is appended to this table, indented to match the pairs already in it.

Parent tables along path are created as table blocks when they do not exist. A dictionary passed as value is written as an inline table; Document.table() is how a dictionary becomes a table block of its own instead.

Parameters

  • path (string|list) — A dotted key path, or a list of names.
  • value (any)

Returns Entry — The pair that was written.

Raises EditError If path runs through something that is not a table, or names a table that already exists.

Raises EncodeError If value has no TOML spelling.

Table.remove()

toml.Table.remove(path) -> bool

Removes whatever sits at path, relative to this table.

A removed pair takes the comment written directly above it with it, because that comment described the pair. The blank lines around it are left alone.

Parameters

  • path (string|list)

Returns bool — True when something was there to remove.

Table.comment()

toml.Table.comment(key) -> string|nil

Returns the comment written above key, without its #, the single space that usually follows it, or the indentation. Returns nil when there is no comment.

Parameters

  • key (string|list)

Returns string|nil

Table.set_comment()

toml.Table.set_comment(key, text)

Writes text as the comment above key, replacing any comment already there. Pass nil to remove it.

A text containing newlines becomes one # line per line.

Parameters

  • key (string|list)
  • text (string|nil)

Raises EditError If key is not in this table.

ArrayOfTables

class toml.ArrayOfTables

A [[header]] array of tables.

Each element is a Table of its own, carrying its own header text and its own trivia, because each was written separately.

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
document
path
elements

Constructor

toml.ArrayOfTables(path)

Parameters

  • path (list) — The full key path from the root.

ArrayOfTables.to_value()

toml.ArrayOfTables.to_value() -> list

Returns every element as a plain Zuri list of dictionaries.

Returns list

ArrayOfTables.length()

toml.ArrayOfTables.length() -> number

Returns how many elements the array holds.

Returns number

ArrayOfTables.to_string()

toml.ArrayOfTables.to_string()

ArrayOfTables.append()

toml.ArrayOfTables.append() -> Table

Adds a new [[header]] block to the end of the array and returns the Table it made, which is where that element’s keys go.

Returns Table

ArrayOfTables.element()

toml.ArrayOfTables.element(index: number) -> Table

Returns the element at index, counting from zero.

Parameters

  • index (number)

Returns Table

Raises EditError If index is outside the array.

Document

class toml.Document

A whole TOML document: every table in it, and every byte between them.

A document that nobody has edited renders back exactly as it was read, byte for byte. An edited one keeps everything the edit did not touch, so a rewritten configuration file still reads as the file somebody wrote.

order is the sequence the table headers appeared in, which is not the order the tree nests them in: a document may open [a], then [b], then [a.c], and all three keep their places.

import toml

var doc = toml.edit('# the package\n[package]\nname = "app"\n')
doc.set('package.version', '1.0.0')

echo doc.to_string()
# the package
[package]
name = "app"
version = "1.0.0"

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
root
order
trailing
bom
newline

Constructor

toml.Document()

Document.to_string()

toml.Document.to_string() -> string

Returns the whole document as TOML text.

Returns string

Document.to_value()

toml.Document.to_value() -> dict

Returns the document as a plain Zuri dictionary, with every table, array and scalar below it converted too.

Returns dict

Document.get()

toml.Document.get(path) -> any

Returns the value at path, or nil when nothing is there.

Parameters

  • path (string|list) — A dotted key path, or a list of key names for keys that contain a literal dot.

Returns any

Document.has()

toml.Document.has(path) -> bool

Returns true when something is stored at path.

Parameters

  • path (string|list)

Returns bool

Document.keys()

toml.Document.keys() -> list

Returns the top-level names the document holds, in the order they were written.

Returns list

Document.length()

toml.Document.length() -> number

Returns how many top-level names the document holds.

Returns number

Document.set()

toml.Document.set(path, value) -> Entry

Writes value at path.

A key that is already there keeps its line, its spacing and its trailing comment; only the value text changes. A key that is not there is appended to the table that should hold it, and the table blocks along path are created when they do not exist.

import toml

var doc = toml.edit('[server]\nhost = "localhost"  # bind address\n')
doc.set('server.host', '0.0.0.0')
doc.set('server.port', 8080)

echo doc.to_string()
[server]
host = "0.0.0.0"  # bind address
port = 8080

Parameters

  • path (string|list) — A dotted key path, or a list of names for keys that contain a literal dot.
  • value (any)

Returns Entry — The pair that was written.

Raises EditError If path runs through something that is not a table, or names a table that already exists.

Raises EncodeError If value has no TOML spelling.

Document.remove()

toml.Document.remove(path) -> bool

Removes whatever sits at path, a pair or a whole table.

A removed pair takes the comment written directly above it with it. Removing a table removes everything under it.

Parameters

  • path (string|list)

Returns bool — True when something was there to remove.

Document.table()

toml.Document.table(path) -> Table

Returns the table at path, creating it as a table block when it is not there.

This is how a dictionary becomes a [header] block rather than the inline table set() would write.

import toml

var doc = toml.edit('name = "app"\n')
doc.table('dependencies').set('http', '^2.0')

echo doc.to_string()
name = "app"

[dependencies]
http = "^2.0"

Parameters

  • path (string|list)

Returns Table

Raises EditError If path runs through something that is not a table.

Document.array_of_tables()

toml.Document.array_of_tables(path) -> ArrayOfTables

Returns the array of tables at path, creating an empty one when it is not there.

append() on the result adds a [[header]] block and returns the Table it made, which is where the new element’s keys go.

import toml

var doc = toml.edit('')
doc.array_of_tables('bin').append().set('name', 'serve')

echo doc.to_string()
[[bin]]
name = "serve"

Parameters

  • path (string|list)

Returns ArrayOfTables

Raises EditError If path names something that is not an array of tables.

Document.save()

toml.Document.save(path: string)

Writes the document to path, creating or overwriting the file.

Parameters

  • path (string) — Filesystem path to write to.

Raises Error On any file error.


2026, Richard Ore and The Zuri Contributors

toml.encoder

import toml.encoder

toml does not re-export this module, so it is reached only by importing it directly.

Turning a plain Zuri value into a document.

The parser’s job is to lose nothing. This one’s is the opposite: there is no original text to preserve, so every choice has to be made from scratch. Which dictionaries become [table] blocks and which become inline tables, which lists become [[array of table]] blocks, where a long array breaks across lines, whether a string with a newline in it is escaped or written out in triple quotes.

Every one of those is an option with a stated default, and the result is a Document like any other, so a freshly encoded value and a parsed file are the same kind of thing afterwards.

Functions

encode()

toml.encoder.encode(value, options) -> Document

Builds the document for a Zuri dictionary.

A TOML file is a table, so the value has to be a dictionary. Everything under it is placed by what it is: a dictionary becomes a [table] block, a non-empty list of dictionaries becomes a [[array of tables]], and everything else becomes a pair on a line.

Parameters

  • value (dict) — The value to write.
  • options (?dict) — sort_keys (default false) writes keys in sorted order instead of insertion order. inline_threshold (default 0, off) writes a flat table inline when its inline form is no longer than this many characters. multiline_strings (default true) writes a string holding a newline in triple quotes rather than escaping it. array_width (default 80) is the length past which an array breaks onto one line per item.

Returns Document

Raises EncodeError If the value is not a dictionary, holds a nil or a value with no TOML spelling, has a non-string key, or contains itself.


2026, Richard Ore and The Zuri Contributors

toml.errors

import toml.errors

toml lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled toml.errors.* needs import toml.errors.

Every error the toml module raises, under one root.

TomlError is that root. The three subclasses separate the cases a caller handles differently: text that is not TOML, a Zuri value that has no TOML spelling, and an edit that contradicts the document it was applied to.

Each one carries the detail a message alone cannot. ParseError knows the line and column it stopped at, EncodeError knows the type it could not write, and EditError knows the key path it could not follow.

Classes

TomlError

class toml.TomlError < Error

Base class for every error this module raises.

Catch this to catch anything toml can do.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

toml.TomlError(message)

Parameters

  • message (string) — Human-readable description of the fault.

TomlError.to_string()

toml.TomlError.to_string()

ParseError

class toml.ParseError < TomlError

Text that is not valid TOML.

line and column are both 1-based and point at the character the parser gave up on, which is where the fix goes. Both are -1 when the fault belongs to the document as a whole rather than to one position in it.

import toml

catch {
  toml.parse('key = ')
} as error {
  echo '${error.line}:${error.column} ${error.message}'
}
1:7 expected a value after '='
  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
line
column

Constructor

toml.ParseError(message, line, column)

Parameters

  • message (string) — Human-readable description of the fault.
  • line (?number) — 1-based line, or -1 when not applicable.
  • column (?number) — 1-based column, or -1 when not applicable.

ParseError.to_string()

toml.ParseError.to_string()

EncodeError

class toml.EncodeError < TomlError

A Zuri value that cannot be written as TOML.

TOML has no null, so a nil anywhere in a value raises this rather than being written as something that reads back differently. The same goes for a function, a class, a cyclic structure, and a dictionary key that is not a string.

path is the dotted key path the offending value sat at, so a rejected leaf in a deep structure can be found without a search.

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
path

Constructor

toml.EncodeError(message, path)

Parameters

  • message (string) — Human-readable description of the fault.
  • path (?string) — Dotted key path to the offending value.

EncodeError.to_string()

toml.EncodeError.to_string()

EditError

class toml.EditError < TomlError

An edit that the document cannot accept.

Raised when a key path runs through something that is not a table, such as set('a.b', 1) on a document where a is already a string, and when a path is malformed or empty.

path is the full dotted path as it was given, so the message names what the caller asked for rather than the fragment that failed.

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
path

Constructor

toml.EditError(message, path)

Parameters

  • message (string) — Human-readable description of the fault.
  • path (?string) — The dotted key path the edit was applied to.

EditError.to_string()

toml.EditError.to_string()

2026, Richard Ore and The Zuri Contributors

toml.parser

import toml.parser

toml does not re-export this module, so it is reached only by importing it directly.

Reading TOML text into the document tree, losing nothing.

The parser is a single pass over the characters. It builds the nodes in document.zu, and every byte it consumes ends up in one of them: a value’s text in its raw, the spaces around an = in the pair that holds it, a comment in the prefix of whatever it sits above. Nothing is normalised on the way in, so nothing has to be reconstructed on the way out.

Alongside the tree it keeps a second, smaller structure recording what has been declared. TOML’s rules about redefinition are not about text at all: a table may be created implicitly by a header below it, created by a dotted key, or written out in full, and which of those happened decides whether the next header naming it is legal. Keeping that separate from the format tree is what keeps both readable.

Functions

parse()

toml.parser.parse(text: string) -> Document

Reads TOML text and returns the document tree it describes.

Every byte of the input ends up somewhere in the tree, so rendering the result without editing it reproduces the input exactly.

Parameters

  • text (string)

Returns Document

Raises ParseError On any syntax or structural error, with the line and column it stopped at.


2026, Richard Ore and The Zuri Contributors

toml.values

import toml.values

toml lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled toml.values.* needs import toml.values.

The TOML types that Zuri has no built-in equal for.

TOML carries four temporal types and draws a line between an integer and a float. Zuri has one number, which is an IEEE-754 double, and a date.Date that always knows an offset. Neither gap can be closed by picking a near-enough built-in: a local date that decoded as a Date would grow a UTC offset it never had, and a float that decoded as a number would be written back out as an integer.

So the four temporal types are their own classes here, and Float marks a number that must stay a float. Each temporal class validates on construction, renders itself in the exact form TOML specifies, and answers equals() rather than ==, which compares instances by identity.

import toml

var released = toml.LocalDate(2026, 9, 19)
echo released.to_string()

var stamp = toml.DateTime(2026, 9, 19, 13, 4, 5, 0, -300)
echo stamp.to_string()
2026-09-19
2026-09-19T13:04:05-05:00

Classes

LocalDate

class toml.LocalDate

A calendar date with no time and no offset, TOML’s local-date.

Spelled 1979-05-27. This is a date on the wall calendar: a birthday, a release day, a contract term. It names no instant, so two LocalDates from different timezones are comparable directly.

import toml

var released = toml.LocalDate(2026, 9, 19)

echo released.to_string()
echo released.year
echo released.equals(toml.LocalDate(2026, 9, 19))
2026-09-19
2026
true
  • printable — has a @to_string(), so echo and print() show something useful

Constructor

toml.LocalDate(year, month, day)

Parameters

  • year (number) — From 0 to 9999.
  • month (number) — From 1 to 12.
  • day (number) — From 1 to the length of that month, leap years included.

Raises TomlError If any field is fractional or out of range, or if the day does not exist in that month.

LocalDate.to_string()

toml.LocalDate.to_string() -> string

Returns the date as TOML spells it, YYYY-MM-DD.

Returns string

LocalDate.equals()

toml.LocalDate.equals(other) -> bool

Returns true when other is a LocalDate naming the same day. == makes the same comparison.

Parameters

  • other (any)

Returns bool

LocalDate.to_date()

toml.LocalDate.to_date() -> Date

Returns this date as a date.Date at midnight UTC.

The offset is an artefact of the conversion: a date.Date always has one and a LocalDate never did. Use it for calendar arithmetic, not to claim an instant the TOML file did not state.

Returns Date

LocalTime

class toml.LocalTime

A wall-clock time with no date and no offset, TOML’s local-time.

Spelled 07:32:00 or 07:32:00.999999. This is a time of day that recurs: an opening hour, a cron-like schedule, a curfew.

Fractional seconds are kept to nanosecond precision and render with trailing zeros removed, so half a second is .5 rather than .500000000.

import toml

echo toml.LocalTime(7, 32, 0).to_string()
echo toml.LocalTime(7, 32, 0, 500000000).to_string()
07:32:00
07:32:00.5
  • printable — has a @to_string(), so echo and print() show something useful

Constructor

toml.LocalTime(hour, minute, second, nanosecond)

Parameters

  • hour (number) — From 0 to 23.
  • minute (number) — From 0 to 59.
  • second (number) — From 0 to 60, the 60 admitting a leap second.
  • nanosecond (?number) — From 0 to 999999999. Defaults to 0.

Raises TomlError If any field is fractional or out of range.

LocalTime.to_string()

toml.LocalTime.to_string() -> string

Returns the time as TOML spells it, HH:MM:SS with a fractional part only when there is one.

Returns string

LocalTime.equals()

toml.LocalTime.equals(other) -> bool

Returns true when other is a LocalTime naming the same instant of the clock, fractional seconds included.

Parameters

  • other (any)

Returns bool

LocalDateTime

class toml.LocalDateTime

A date and a wall-clock time with no offset, TOML’s local-date-time.

Spelled 1979-05-27T07:32:00. It names a moment on somebody’s calendar and clock without saying whose, which is what a log format or a schedule written for one site means by a timestamp.

import toml

echo toml.LocalDateTime(1979, 5, 27, 7, 32, 0).to_string()
1979-05-27T07:32:00
  • printable — has a @to_string(), so echo and print() show something useful

Constructor

toml.LocalDateTime(year, month, day, hour, minute, second, nanosecond)

Parameters

  • year (number) — From 0 to 9999.
  • month (number) — From 1 to 12.
  • day (number) — From 1 to the length of that month.
  • hour (number) — From 0 to 23.
  • minute (number) — From 0 to 59.
  • second (number) — From 0 to 60.
  • nanosecond (?number) — From 0 to 999999999. Defaults to 0.

Raises TomlError If any field is fractional or out of range.

LocalDateTime.to_string()

toml.LocalDateTime.to_string() -> string

Returns the timestamp as TOML spells it, the date and the time joined by T.

Returns string

LocalDateTime.equals()

toml.LocalDateTime.equals(other) -> bool

Returns true when other is a LocalDateTime naming the same date and time.

Parameters

  • other (any)

Returns bool

LocalDateTime.to_date()

toml.LocalDateTime.to_date() -> Date

Returns this timestamp as a date.Date read as UTC.

The offset is an artefact of the conversion. A LocalDateTime deliberately does not know one, so reading the result as an instant asserts something the TOML file did not.

Returns Date

DateTime

class toml.DateTime

A date and time with a UTC offset, TOML’s offset-date-time.

Spelled 1979-05-27T07:32:00Z or 1979-05-27T00:32:00-07:00. This is the only one of the four that names an unambiguous instant, and the only one that can be compared across sites.

The offset is held in minutes, signed, so -300 is -05:00. A zero offset renders as Z; a document that spelled it +00:00 keeps that spelling through an edit, because the raw text is preserved alongside the value.

import toml

echo toml.DateTime(1979, 5, 27, 7, 32, 0, 0, 0).to_string()
echo toml.DateTime(1979, 5, 27, 0, 32, 0, 0, -420).to_string()
1979-05-27T07:32:00Z
1979-05-27T00:32:00-07:00
  • printable — has a @to_string(), so echo and print() show something useful

Constructor

toml.DateTime(year, month, day, hour, minute, second, nanosecond, offset_minutes)

Parameters

  • year (number) — From 0 to 9999.
  • month (number) — From 1 to 12.
  • day (number) — From 1 to the length of that month.
  • hour (number) — From 0 to 23.
  • minute (number) — From 0 to 59.
  • second (number) — From 0 to 60.
  • nanosecond (?number) — From 0 to 999999999. Defaults to 0.
  • offset_minutes (?number) — Minutes east of UTC, from -1439 to 1439. Defaults to 0, which renders as Z.

Raises TomlError If any field is fractional or out of range.

DateTime.to_string()

toml.DateTime.to_string() -> string

Returns the timestamp as TOML spells it, with Z for a zero offset and +HH:MM or -HH:MM otherwise.

Returns string

DateTime.equals()

toml.DateTime.equals(other) -> bool

Returns true when other is a DateTime with the same fields and the same offset.

Two timestamps naming the same instant through different offsets are not equal here. Compare to_date().to_time() for that.

Parameters

  • other (any)

Returns bool

DateTime.to_date()

toml.DateTime.to_date() -> Date

Returns this timestamp as a date.Date carrying the same offset.

This is the one temporal type that converts without inventing anything: both sides name the same instant.

Returns Date

Float

class toml.Float

A number that must be written as a TOML float.

Zuri’s number is an IEEE-754 double, so 1.0 and 1 are one value and nothing distinguishes them afterwards. TOML does distinguish them, and an encoder with only the value to go on has to guess: it writes an integer whenever the number has no fractional part, which turns a 1.0 into a 1.

Wrap the number to settle it. Float means float, whatever the value happens to be:

import toml

echo toml.dump({ ratio: 1.0 })
echo toml.dump({ ratio: toml.Float(1.0) })
ratio = 1

ratio = 1.0

Decoding never produces one: a TOML float decodes to a plain number, because a wrapper on every float would make every read pay for a distinction most programs do not use.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

toml.Float(value)

Parameters

  • value (number) — The number to write as a float. inf, -inf and nan are all accepted; TOML spells them inf, -inf and nan.

Raises TomlError If value is not a number.

Float.to_string()

toml.Float.to_string() -> string

Returns the wrapped number as TOML spells it.

Returns string

Float.equals()

toml.Float.equals(other) -> bool

Returns true when other is a Float wrapping the same number.

nan never equals anything, this included, which matches what the bare comparison does.

Parameters

  • other (any)

Returns bool


2026, Richard Ore and The Zuri Contributors

csv

import csv

A complete, RFC 4180-compliant library for reading and writing Comma-Separated Values (CSV) data.

This module implements the format defined in RFC 4180: “Common Format and MIME Type for Comma-Separated Values (CSV) Files”: and extends it with the practical accommodations required by real-world CSV data, including configurable delimiters, optional quoting strategies, BOM detection, and lenient parsing of files that deviate from the strict spec.

Key CSV rules

  • Records are separated by CRLF (\r\n). The last record may omit the trailing line break. - Fields containing the delimiter, a double-quote, CR, or LF must be enclosed in double-quotes. - A double-quote inside a quoted field is escaped by doubling it (""). - An optional header record may appear as the first line. - All records must contain the same number of fields.

Extensions beyond RFC 4180

  • Configurable delimiter: any single character (tab for TSV, pipe, etc.) - Configurable quote character: defaults to ". - LF-only and CR-only line endings: accepted on input; CRLF on output.
  • BOM stripping: UTF-8 BOM (\xEF\xBB\xBF) is silently removed. - Empty last line: a trailing newline after the last record is ignored. - Quoting strategies: QUOTE_MINIMAL (default), QUOTE_ALL, QUOTE_NONNUMERIC, QUOTE_NONE. - Lenient mode: tolerate unquoted fields containing the delimiter and other minor deviations common in real-world CSV files.

Quick start

Reading

import csv

# Parse a CSV string directly.
var table = csv.parse('name,age\r\nAlice,30\r\nBob,25')
echo table[0]   # { name: 'Alice', age: '30' }
echo table[1]   # { name: 'Bob',   age: '25' }

# Stream a file row by row.
var reader = csv.Reader(file('data.csv'))
for row in reader {
  echo row
}
reader.close()

Writing

import csv

# Encode a list of dicts to a CSV string.
var data = [
  { name: 'Alice', age: 30 },
  { name: 'Bob',   age: 25 },
]
echo csv.stringify(data)
# name,age\r\nAlice,30\r\nBob,25\r\n

# Stream rows to a file.
var writer = csv.Writer(file('out.csv', 'w'))
writer.write_header(['name', 'age'])
writer.write_row(['Alice', 30])
writer.write_row(['Bob', 25])
writer.close()

The csv API

Every public name in csv, wherever it is declared. Each links to the page that documents it.

NameKindSummary
csv.CsvErrorclassError raised when a CSV parsing or encoding error is encountered.
csv.DialectclassEncapsulates the formatting parameters that govern how CSV data is read or written.
csv.QUOTE_ALLconstantQuote every field unconditionally, regardless of content.
csv.QUOTE_MINIMALconstantQuote only fields that contain the delimiter, the quote character, or a line break.
csv.QUOTE_NONEconstantNever quote fields.
csv.QUOTE_NONNUMERICconstantQuote all non-numeric fields.
csv.ReaderclassAn incremental, row-by-row CSV reader that wraps a Zuri file object (or any object implementing .read()…
csv.WriterclassAn incremental, row-by-row CSV writer that wraps a Zuri file object (or any object implementing .write()…
csv.format_recordfunctionEncodes a single list of values as one CSV record and returns the string (including the line terminator).
csv.parsefunctionParses a CSV string and returns all records as a list.
csv.parse_recordfunctionParses a single CSV record string and returns the fields as a list.
csv.read_filefunctionReads a CSV file and returns all records as a list.
csv.sniff_dialectfunctionAttempts to detect the field delimiter used in sample and returns a Dialect configured with it.
csv.stringifyfunctionEncodes a list of records into a CSV string and returns it.
csv.write_filefunctionEncodes a list of records and writes them to a CSV file, creating or overwriting the file at path.

Constants

QUOTE_MINIMAL

csv.QUOTE_MINIMAL = 0

Quote only fields that contain the delimiter, the quote character, or a line break. This is the default and produces the most compact output. Compliant with RFC 4180 §2 rule 6.

QUOTE_ALL

csv.QUOTE_ALL = 1

Quote every field unconditionally, regardless of content. Produces the most portable output; useful when the consumer is known to require quoted fields.

QUOTE_NONNUMERIC

csv.QUOTE_NONNUMERIC = 2

Quote all non-numeric fields. Fields whose string representation is a valid integer or floating-point number are written unquoted.

QUOTE_NONE

csv.QUOTE_NONE = 3

Never quote fields. If a field contains the delimiter or a line break, a CsvError is raised (in strict mode) or the character is written as-is (in lenient mode). Use only when you are certain the data contains no special characters.

Functions

sniff_dialect()

csv.sniff_dialect(sample, candidate_delimiters) -> Dialect

Attempts to detect the field delimiter used in sample and returns a Dialect configured with it.

Each candidate delimiter is scored by how consistently it occurs across the sample’s lines: a real delimiter tends to appear the same number of times on every line, while one that just happens to show up in the data (a comma inside a sentence, say) doesn’t. The best-scoring candidate wins; if none of them appear consistently anywhere in sample, the first candidate (, by default) is used.

This only detects the delimiter: not the quote character, whether a header row is present, or any other Dialect field. Set those on the returned Dialect yourself if they differ from the defaults.

Example,

%> var dialect = csv.sniff_dialect("a;b;c\n1;2;3\n4;5;6")
%> dialect.delimiter
';'

Parameters

  • sample (string) — Doesn’t need to be the whole file: a few representative lines are enough.
  • candidate_delimiters (?list) — Default [',', '\t', ';', '|'].

Returns Dialect

parse()

csv.parse(text, dialect) -> list

Parses a CSV string and returns all records as a list.

When dialect.has_header is true (or has_header is passed as true), the first record is treated as a header and every subsequent record is returned as a dict keyed by column name. Otherwise, records are returned as lists of strings.

This function is the simplest way to parse a complete CSV document that already resides in memory.

Examples

import csv

# No header: returns list of lists.
var rows = csv.parse('a,b,c\r\n1,2,3\r\n4,5,6')
echo rows[0]   # ['a', 'b', 'c']
echo rows[1]   # ['1', '2', '3']

# With header: returns list of dicts.
var d = csv.Dialect()
d.has_header = true
rows = csv.parse('name,age\r\nAlice,30\r\nBob,25', d)
echo rows[0]   # { name: 'Alice', age: '30' }
echo rows[1]   # { name: 'Bob',   age: '25' }

# Tab-separated, no header.
var td = csv.Dialect()
td.delimiter = '\t'
rows = csv.parse('x\t1\ny\t2', td)
echo rows[0]   # ['x', '1']

Parameters

  • text (string) — The CSV text to parse.
  • dialect (?Dialect) — Optional dialect (defaults to Dialect()).

Returns list — List of lists or dicts depending on has_header.

Raises CsvError On parse error in strict mode.

stringify()

csv.stringify(rows, dialect, keys) -> string

Encodes a list of records into a CSV string and returns it.

Each element of rows may be either a list (field values in order) or a dict. When rows contains dicts, keys controls the column order; if omitted, the keys of the first dict determine the order. A header row is written automatically when rows contains dicts.

Examples

import csv

# From lists: no header.
echo csv.stringify([['Alice', 30], ['Bob', 25]])
# Alice,30\r\nBob,25\r\n

# From dicts: header inferred.
echo csv.stringify([
  { name: 'Alice', age: 30 },
  { name: 'Bob',   age: 25 },
])
# name,age\r\nAlice,30\r\nBob,25\r\n

# Custom dialect.
var d = csv.Dialect()
d.quoting = csv.QUOTE_ALL
echo csv.stringify([['hello, world', 42]], d)
# "hello, world","42"\r\n

Parameters

  • rows (list) — List of lists or dicts to encode.
  • dialect (?Dialect) — Optional dialect (defaults to Dialect()).
  • keys (?list) — Optional column key order when rows are dicts.

Returns string — The encoded CSV text.

Raises CsvError On encoding error.

parse_record()

csv.parse_record(line, dialect) -> list

Parses a single CSV record string and returns the fields as a list.

This is a lightweight utility for parsing a single line when you already have it isolated (e.g. from a line-buffered reader). No record separator detection is performed.

import csv

csv.parse_record('"hello, world",42,true')
# ['hello, world', '42', 'true']

csv.parse_record('"He said ""hi""",ok')
# ['He said "hi"', 'ok']

Parameters

  • line (string) — A single CSV record (with or without line ending).
  • dialect (?Dialect) — Optional dialect.

Returns list — The parsed field values.

Raises CsvError On parse error.

format_record()

csv.format_record(row, dialect) -> string

Encodes a single list of values as one CSV record and returns the string (including the line terminator).

import csv

csv.format_record(['Alice', 'New York, NY', 'She said "hello"'])
# 'Alice,"New York, NY","She said ""hello"""\r\n'

Parameters

  • row (list) — Field values to encode.
  • dialect (?Dialect) — Optional dialect.

Returns string — The encoded record with line terminator.

Raises CsvError On encoding error.

read_file()

csv.read_file(path, dialect) -> list

Reads a CSV file and returns all records as a list.

Convenience wrapper around Reader that opens, reads, and closes in one call. Use Reader directly when you need streaming or finer control.

import csv

var d = csv.Dialect()
d.has_header = true
var rows = csv.read_file('employees.csv', d)
echo rows[0]['name']

Parameters

  • path (string) — Filesystem path to the CSV file.
  • dialect (?Dialect) — Optional dialect.

Returns list — All records as lists or dicts.

Raises CsvError On parse error.

Raises Error On file I/O error.

write_file()

csv.write_file(path, rows, dialect, keys)

Encodes a list of records and writes them to a CSV file, creating or overwriting the file at path.

Convenience wrapper around Writer. Use Writer directly when you need streaming or finer control.

import csv

csv.write_file('out.csv', [
  { name: 'Alice', score: 98 },
  { name: 'Bob',   score: 72 },
])

Parameters

  • path (string) — Filesystem path to write to.
  • rows (list) — List of lists or dicts.
  • dialect (?Dialect) — Optional dialect.
  • keys (?list) — Optional column key order for dict rows.

Raises CsvError On encoding error.

Raises Error On file I/O error.

Classes

CsvError

class csv.CsvError < Error

Error raised when a CSV parsing or encoding error is encountered.

Fields

FieldTypeDescription
line
column

Constructor

csv.CsvError(message, line, column)

Parameters

  • message (string) — Error description.
  • line (number) — Line number (1-based).
  • column (number) — Column number (1-based).

Dialect

class csv.Dialect

Encapsulates the formatting parameters that govern how CSV data is read or written. Pass a Dialect instance to Reader or Writer to override the defaults.

Example: tab-separated values (TSV)
import csv

var tsv = csv.Dialect()
tsv.delimiter = '\t'
var reader = csv.Reader(file, tsv)
Example: pipe-delimited, quote-all
var d = csv.Dialect()
d.delimiter    = '|'
d.quoting      = csv.QUOTE_ALL
var writer = csv.Writer(file, d)

Fields

FieldTypeDescription
delimiterstringThe single-character field delimiter.
quote_charstringThe character used to quote fields.
line_terminatorstringThe line terminator written between records.
quotingnumberQuoting strategy.
trimboolWhen true (default), leading and trailing whitespace is stripped from unquoted fields during reading.
lenientboolWhen true, the parser accepts minor deviations from RFC 4180 such as a quote character that appears in the…
has_headerboolWhen true, reading expects the first record to be a header row and returns subsequent records as dicts…
max_field_sizenumberMaximum number of characters allowed in a single field.

Dialect.validate()

csv.Dialect.validate()

Validates that all dialect fields hold consistent, legal values.

Raises CsvError If any field is invalid.

Reader

class csv.Reader

An incremental, row-by-row CSV reader that wraps a Zuri file object (or any object implementing .read() and .close()).

Reader implements the iterator protocol so it can be used directly in a for loop.

Example: reading a file with a header row
import csv
import io

var d = csv.Dialect()
d.has_header = true

var reader = csv.Reader(file('employees.csv'), d)
for row in reader {
  echo row['name'] + ' earns ' + row['salary']
}
reader.close()
Example: tab-separated file, no header
import csv
import io

var d = csv.Dialect()
d.delimiter = '\t'

var reader = csv.Reader(file('data.tsv'), d)
for row in reader {
  echo row   # list: ['field1', 'field2', ...]
}
reader.close()
Notes
  • The file is read all at once into memory. For very large files (> a few hundred MB), consider streaming line by line using read_record(). - When dialect.has_header is true, the first record is consumed as the header and is accessible via the headers property.
  • When dialect.has_header is true and a data row has fewer fields than the header, the missing fields are set to nil. - When dialect.has_header is true and a row has more fields than the header, the extra fields are silently discarded (lenient mode) or raise CsvError (strict mode).

Fields

FieldTypeDescription
dialectDialectThe dialect governing parsing behaviour.
headerslistThe header row, as a list of strings, once the first record has been consumed.
record_numnumberThe 1-based index of the most recently returned record (not counting the header row).

Constructor

csv.Reader(file, dialect)

Creates a Reader from a file object.

Parameters

  • file (file) — An open, readable Zuri file object.
  • dialect (?Dialect) — Optional dialect (defaults to Dialect()).

Raises CsvError If dialect validation fails.

Reader.read_record()

csv.Reader.read_record() -> list

Reads and returns the next record, or nil when all records have been consumed.

When dialect.has_header is true, returns a dict keyed by header name. Otherwise returns a list of field strings.

Returns list — | dict | nil

Raises CsvError On parse error in strict mode.

Reader.read_all()

csv.Reader.read_all() -> list

Reads all remaining records into a list and returns it.

Returns list — A list of lists (or dicts when dialect.has_header is true).

Reader.close()

csv.Reader.close()

Closes the underlying file object. Always call close() when done, even if an error occurred.

Writer

class csv.Writer

An incremental, row-by-row CSV writer that wraps a Zuri file object (or any object implementing .write() and .close()).

Example: writing dicts (with header)
import csv
import io

var writer = csv.Writer(file('out.csv', 'w'))
writer.write_header(['id', 'name', 'score'])
writer.write_row([1, 'Alice', 98.5])
writer.write_row([2, 'Bob',   72.0])
writer.close()
Example: QUOTE_ALL with tab delimiter
import csv
import io

var d = csv.Dialect()
d.delimiter = '\t'
d.quoting   = csv.QUOTE_ALL

var writer = csv.Writer(file('out.tsv', 'w'), d)
writer.write_row(['Alice', 'Engineer', 'New York'])
writer.close()
Notes
  • write_row() accepts a list of any values; each is converted to a string with .to_string() before encoding. - write_dict() accepts a dict and a keys list that controls the field order and which keys are written. - The writer does not buffer: every call to write_row() immediately writes to the underlying file.

Fields

FieldTypeDescription
dialectDialectThe dialect governing encoding behaviour.
record_numnumberThe number of records written so far (not counting the header row).

Constructor

csv.Writer(file, dialect)

Creates a Writer targeting a file object.

Parameters

  • file (file) — An open, writable Zuri file object.
  • dialect (?Dialect) — Optional dialect (defaults to Dialect()).

Raises CsvError If dialect validation fails.

Writer.write_row()

csv.Writer.write_row(row)

Encodes and writes a single record from a list of values.

Each element of row is converted to a string via .to_string() and then encoded according to the dialect’s quoting strategy.

Parameters

  • row (list) — The field values for this record.

Raises CsvError On encoding error (e.g. QUOTE_NONE with special chars).

Writer.write_header()

csv.Writer.write_header(headers)

Encodes and writes a header record from a list of column name strings.

Functionally identical to write_row() but does not increment record_num, making it semantically clear that this is metadata.

Parameters

  • headers (list) — The column names.

Raises CsvError On encoding error.

Writer.write_dict()

csv.Writer.write_dict(row, keys)

Encodes and writes a single record from a dict, using keys to determine field order. Missing keys produce empty fields.

writer.write_dict({ name: 'Alice', age: 30 }, ['name', 'age'])

Parameters

  • row (dict) — The record as a key-value dict.
  • keys (list) — Ordered list of keys to extract from row.

Raises CsvError On encoding error.

Writer.write_dicts()

csv.Writer.write_dicts(rows, keys)

Writes a list of records from a list of dicts, preceded by a header row whose columns are derived from the keys of the first record.

If keys is provided it controls column order; otherwise the keys of the first row are used in their natural iteration order.

writer.write_dicts([
  { name: 'Alice', age: 30 },
  { name: 'Bob',   age: 25 },
])

Parameters

  • rows (list) — List of dicts, each representing one record.
  • keys (?list) — Optional ordered key list. Inferred from the first row when omitted.

Raises CsvError On encoding error.

Writer.close()

csv.Writer.close()

Closes the underlying file object. Always call close() when done, even if an error occurred.


2026, Richard Ore and The Zuri Contributors

base64

import base64

This module provides interface for encoding binary data into strings and decoding such encoded strings back into binary data based on the base64 encoding specified in RFC4648

The base64 API

Every public name in base64, wherever it is declared. Each links to the page that documents it.

NameKindSummary
base64.decodefunctionDecodes a base64 string into it’s corresponding bytes.
base64.encodefunctionEncodes a byte array into a base64 string

Functions

encode()

base64.encode(data: bytes) -> string

Encodes a byte array into a base64 string

Parameters

  • data (bytes)

Returns string

decode()

base64.decode(data: string) -> bytes

Decodes a base64 string into it’s corresponding bytes.

Parameters

  • data (string)

Returns bytes


2021, Richard Ore and Zuri contributors

struct

import struct

This module provides functions for converting between Zuri values and C/C++/Rust structs and vice-versa in the binary format.

The pack and unpack functions behave similarly as the pack and unpack functions from Perl and PHP (more similar to the PHP version) with few major extensions to their format language.

Format language

A format string is a sequence of /-separated segments. Each segment is one or more CODE[COUNT] groups, optionally followed by :NAME to label the field(s) that group produces when unpacking:

  "Nsize:len/A16:name/C4"
  • CODE is one of the format characters in the table below. - COUNT is a decimal integer, or * to mean “the rest” (the whole remaining argument for a string-like code, or every remaining argument/byte for a numeric code). Omitted means 1.
  • :NAME names the field(s) produced by that ONE group when unpacking. A count > 1 numbers the keys NAME1, NAME2, … A segment carrying a :NAME may only contain a single group. - A group with no :NAME gets a purely numeric key that runs once, globally, for the whole format string; it never resets and so never silently overwrites an earlier unnamed field.

Format codes

Code | Bytes | Meaning —–|—––|–––– a | count | NUL-padded string A | count | SPACE-padded string (trailing NUL/space trimmed on unpack) Z | count | NUL-padded, NUL-terminated string (C-string semantics) h | ceil(count/2) | Hex string, low nibble first H | ceil(count/2) | Hex string, high nibble first c | 1 | signed 8-bit integer C | 1 | unsigned 8-bit integer ? | 1 | boolean s | 2 | signed 16-bit integer, native byte order S | 2 | unsigned 16-bit integer, native byte order n | 2 | unsigned 16-bit integer, big-endian v | 2 | unsigned 16-bit integer, little-endian i | 4 | signed 32-bit integer, native byte order I | 4 | unsigned 32-bit integer, native byte order l | 4 | signed 32-bit integer, native byte order L | 4 | unsigned 32-bit integer, native byte order N | 4 | unsigned 32-bit integer, big-endian V | 4 | unsigned 32-bit integer, little-endian q | 8 | signed 64-bit integer, native byte order Q | 8 | unsigned 64-bit integer, native byte order J | 8 | unsigned 64-bit integer, big-endian P | 8 | unsigned 64-bit integer, little-endian u | 16 | signed 128-bit integer, little-endian U | 16 | unsigned 128-bit integer, little-endian f | 4 | float, native byte order g | 4 | float, little-endian G | 4 | float, big-endian d | 8 | double, native byte order e | 8 | double, little-endian E | 8 | double, big-endian w | 2 | IEEE-754 half-precision float, little-endian W | 2 | IEEE-754 half-precision float, big-endian x | count | NUL byte(s); consumes no argument X | count | back up count byte(s) Z |; | (see above) @ |; | seek/pad to absolute position count

Integer precision

Zuri numbers are IEEE-754 doubles, which can only represent integers exactly up to 2^53. Every integer-producing code here (q/Q/J/P, and the new u/U) automatically promotes its result to a bigint Value instead of a number whenever the unpacked value falls outside that safe range, rather than silently losing precision. Packing accepts either a number or a bigint for every integer code.

The struct API

Every public name in struct, wherever it is declared. Each links to the page that documents it.

NameKindSummary
struct.calcsizefunctionCalculates the size of the buffer needed to pack the given values according to the specified format.
struct.iter_unpackfunctionUnpacks a buffer as a repeated sequence of fixed-size records until exhausted, returning a list of…
struct.packfunctionPacks the given arguments into a bytes object according to the specified format.
struct.pack_fromfunctionSame as pack() except that instead of accepting arbitrary values after format, it expects the values to be…
struct.pack_intofunctionPacks directly into an existing bytes object at offset, growing it (zero-padded) if it isn’t long enough…
struct.unpackfunctionUnpacks from bytes or a string into a dictionary based on the given format.
struct.unpack_fromfunctionThe pack_from() equivalent of unpack().

Functions

pack()

struct.pack(format: string, ...values: list) -> bytes

Packs the given arguments into a bytes object according to the specified format.

Parameters

  • format (string)
  • any... — args

Returns bytes

unpack()

struct.unpack(format: string, data: bytes|string, offset: ?number) -> any

Unpacks from bytes or a string into a dictionary based on the given format.

  • You may have to name the different format codes and separate them by a slash / to return a string indexed dictionary for easy reference of the destructed parts. If a repeater argument is present, then each of the dictionary keys will have a sequence number behind the given name.

  • offset is the index of the bytes/string to begin unpacking from

Important If you do not name an element, numeric indices starting from 1 are used. Be aware that if you have more than one unnamed element, some data is overwritten because the numbering restarts from 1 for each element.

Parameters

  • format (string)
  • data (bytes|string)
  • offset (?number) — Default value is 0

Returns any

pack_from()

struct.pack_from(format: string, args: list) -> bytes

Same as pack() except that instead of accepting arbitrary values after format, it expects the values to be in a list.

Parameters

  • format (string)
  • args (list)

Returns bytes

unpack_from()

struct.unpack_from(format: string, data: bytes|string, offset: ?number) -> any

The pack_from() equivalent of unpack(). This function is essentially the same as unpack() and was kept for symmetry with pack_from().

Parameters

  • format (string)
  • data (bytes|string)
  • offset (?number) — Default value is 0

Returns any

See also: unpack()

pack_into()

struct.pack_into(format: string, buffer: bytes, offset: number, ...values: list) -> number

Packs directly into an existing bytes object at offset, growing it (zero-padded) if it isn’t long enough and returns the number of elements written.

This function avoids an allocate-then-copy round trip when assembling a larger buffer field by field.

Parameters

  • format (string)
  • buffer (bytes)
  • offset (number)
  • any... — values

Returns number

calcsize()

struct.calcsize(format: string) -> number

Calculates the size of the buffer needed to pack the given values according to the specified format. It raises an error if the format is invalid or if the values cannot be packed according to the format or if it contains a * repeat anywhere, since that has no size independent of actual data.

Parameters

  • format (string)

Returns number

Raises Error

iter_unpack()

struct.iter_unpack(format: string, data: bytes|string) -> list

Unpacks a buffer as a repeated sequence of fixed-size records until exhausted, returning a list of dictionaries. The format string must not contain any * repeaters, since that would make the record size variable.

Parameters

  • format (string)
  • data (bytes|string)

Returns list


2022, Richard Ore and The Zuri Contributors

math

import math

This module contains functions and constants to make trigonometric and non-trigonometric mathematics a breeze. The module also defines a couple of commonly used scientific and mathematical constants such as PI.

The math API

Every public name in math, wherever it is declared. Each links to the page that documents it.

NameKindSummary
math.Econstantrepresents Euler’s number, the base of natural logarithms
math.InfinityconstantMathematical infinity
math.LOG_10constantrepresents the natural logarithm of 10
math.LOG_10_Econstantrepresents the base 10 logarithm of e
math.LOG_2constantrepresents the natural logarithm of 2
math.LOG_2_Econstantrepresents the base 2 logarithm of e
math.NaNconstantMathematical NaN
math.PIconstantrepresents the ratio of the circumference of a circle to its diameter
math.ROOT_2constantrepresents the square root of 2
math.ROOT_3constantrepresents the square root of 3
math.ROOT_HALFconstantrepresents the square root of 1/2

Constants

PI

math.PI

represents the ratio of the circumference of a circle to its diameter

E

math.E

represents Euler’s number, the base of natural logarithms

LOG_10

math.LOG_10

represents the natural logarithm of 10

LOG_10_E

math.LOG_10_E

represents the base 10 logarithm of e

LOG_2

math.LOG_2

represents the natural logarithm of 2

LOG_2_E

math.LOG_2_E

represents the base 2 logarithm of e

ROOT_2

math.ROOT_2

represents the square root of 2

ROOT_3

math.ROOT_3

represents the square root of 3

ROOT_HALF

math.ROOT_HALF

represents the square root of 1/2

Infinity

math.Infinity

Mathematical infinity

NaN

math.NaN

Mathematical NaN


2021, Richard Ore and Zuri contributors

array

import array

This module provides fixed-width typed array classes: twos- complement integers (Int8Array/UInt8Array through Int64Array/UInt64Array) and IEEE-754 floating-point numbers (FloatArray, DoubleArray), each storing its elements packed in the platform byte order. They complement the bytes object and allow higher-level binary data manipulation without giving up direct access to the underlying bytes (to_bytes()).

All ten classes share the same interface (indexing, append()/ insert()/remove_at()/set()/pop(), slice(), fill(), equals(), iteration via for x in arr, and conversion via to_list()/to_bytes()): see [[array.Int16Array]] for the fully documented shape every other class in this module follows.

Int64Array/UInt64Array accept and return bigint values (in addition to plain integers) for anything past what a regular number can hold exactly: see is_bigint().

Example

import array

var a = array.Int32Array([1, 2, 3])
a.append(4)
for x in a {
  echo x
}
echo a.slice(1, 3).to_list()   # [2, 3]

The array API

Every public name in array, wherever it is declared. Each links to the page that documents it.

NameKindSummary
array.DOUBLE_MAXconstantMaximum value that “should” exist in a list passed to DoubleArray.
array.DOUBLE_MINconstantMinimum value that “should” exist in a list passed to DoubleArray.
array.DoubleArrayclassclass DoubleArray represents an array of 64-bit IEEE-754 floating-point numbers (doubles) in the platform…
array.FLOAT_MAXconstantMaximum value that “should” exist in a list passed to FloatArray.
array.FLOAT_MINconstantMinimum value that “should” exist in a list passed to FloatArray.
array.FloatArrayclassclass FloatArray represents an array of 32-bit IEEE-754 floating-point numbers in the platform byte order.
array.INT16_MAXconstantMaximum value that “should” exist in a list passed to Int16Array.
array.INT16_MINconstantMinimum value that “should” exist in a list passed to Int16Array.
array.INT32_MAXconstantMaximum value that “should” exist in a list passed to Int32Array.
array.INT32_MINconstantMinimum value that “should” exist in a list passed to Int32Array.
array.INT64_MAXconstantMaximum value that “should” exist in a list passed to Int64Array.
array.INT64_MINconstantMinimum value that “should” exist in a list passed to Int64Array.
array.INT8_MAXconstantMaximum value that “should” exist in a list passed to Int8Array.
array.INT8_MINconstantMinimum value that “should” exist in a list passed to Int8Array.
array.Int16Arrayclassclass Int16Array represents an array of twos-complement 16-bit signed integers in the platform byte order.
array.Int32Arrayclassclass Int32Array represents an array of twos-complement 32-bit signed integers in the platform byte order.
array.Int64Arrayclassclass Int64Array represents an array of twos-complement 64-bit signed integers in the platform byte order.
array.Int8Arrayclassclass Int8Array represents an array of twos-complement 8-bit signed integers.
array.UINT16_MAXconstantMaximum value that “should” exist in a list passed to UInt16Array.
array.UINT32_MAXconstantMaximum value that “should” exist in a list passed to UInt32Array.
array.UINT64_MAXconstantMaximum value that “should” exist in a list passed to UInt64Array.
array.UINT8_MAXconstantMaximum value that “should” exist in a list passed to UInt8Array.
array.UInt16Arrayclassclass UInt16Array represents an array of twos-complement 16-bit unsigned integers in the platform byte order.
array.UInt32Arrayclassclass UInt32Array represents an array of twos-complement 32-bit unsigned integers in the platform byte order.
array.UInt64Arrayclassclass UInt64Array represents an array of twos-complement 64-bit unsigned integers in the platform byte order.
array.UInt8Arrayclassclass UInt8Array represents an array of twos-complement 8-bit unsigned integers.

Submodules

ModuleReached asSummary
array.doublearray.*DoubleArray: a packed array of double-precision IEEE 754 floats, 8 bytes per element.
array.floatarray.*FloatArray: a packed array of single-precision IEEE 754 floats, 4 bytes per element.
array.int16array.*Int16Array: a packed array of twos-complement 16-bit signed integers, 2 bytes per element.
array.int32array.*Int32Array: a packed array of twos-complement 32-bit signed integers, 4 bytes per element.
array.int64array.*Int64Array: a packed array of twos-complement 64-bit signed integers, 8 bytes per element.
array.int8array.*Int8Array: a packed array of twos-complement 8-bit signed integers, 1 byte per element.
array.uint16array.*UInt16Array: a packed array of unsigned 16-bit integers, 2 bytes per element.
array.uint32array.*UInt32Array: a packed array of unsigned 32-bit integers, 4 bytes per element.
array.uint64array.*UInt64Array: a packed array of unsigned 64-bit integers, 8 bytes per element.
array.uint8array.*UInt8Array: a packed array of unsigned 8-bit integers, 1 byte per element.

2022, Richard Ore and The Zuri Contributors

array.double

import array

Everything here is re-exported by array, so import array is enough and the names are called as array.*. Importing array.double on its own works too and reaches the same definitions.

DoubleArray: a packed array of double-precision IEEE 754 floats, 8 bytes per element.

DOUBLE_MAX and DOUBLE_MIN are the largest and smallest finite values the type can hold.

Constants

DOUBLE_MAX

array.DOUBLE_MAX: number

Maximum value that “should” exist in a list passed to DoubleArray.

DOUBLE_MIN

array.DOUBLE_MIN: number

Minimum value that “should” exist in a list passed to DoubleArray.

Classes

DoubleArray

class array.DoubleArray < Array

class DoubleArray represents an array of 64-bit IEEE-754 floating-point numbers (doubles) in the platform byte order.

  • printable — has a @to_string(), so echo and print() show something useful
  • serializable — has a @to_json(), so it can be handed straight to json.encode()
  • valueable — has a @to_value(), so it converts to a plain Zuri value

DoubleArray.reverse()

array.DoubleArray.reverse() -> DoubleArray

Returns a new array containing the elements in the original array in reverse order.

Returns DoubleArray

DoubleArray.clone()

array.DoubleArray.clone() -> DoubleArray

Returns a new DoubleArray containing all items from the current array. The new array is a shallow copy of the original array.

Returns DoubleArray

DoubleArray.slice()

array.DoubleArray.slice(start, end) -> DoubleArray

Returns a new DoubleArray over [start, end) of this array (both default to the whole array; negative bounds count from the end).

Parameters

  • start (?number)
  • end (?number)

Returns DoubleArray


2021, Richard Ore and Zuri contributors

array.float

import array

Everything here is re-exported by array, so import array is enough and the names are called as array.*. Importing array.float on its own works too and reaches the same definitions.

FloatArray: a packed array of single-precision IEEE 754 floats, 4 bytes per element.

FLOAT_MAX and FLOAT_MIN are the largest and smallest finite values the type can hold.

Constants

FLOAT_MAX

array.FLOAT_MAX: number

Maximum value that “should” exist in a list passed to FloatArray.

FLOAT_MIN

array.FLOAT_MIN: number

Minimum value that “should” exist in a list passed to FloatArray.

Classes

FloatArray

class array.FloatArray < Array

class FloatArray represents an array of 32-bit IEEE-754 floating-point numbers in the platform byte order.

  • printable — has a @to_string(), so echo and print() show something useful
  • serializable — has a @to_json(), so it can be handed straight to json.encode()
  • valueable — has a @to_value(), so it converts to a plain Zuri value

FloatArray.reverse()

array.FloatArray.reverse() -> FloatArray

Returns a new array containing the elements in the original array in reverse order.

Returns FloatArray

FloatArray.clone()

array.FloatArray.clone() -> FloatArray

Returns a new FloatArray containing all items from the current array. The new array is a shallow copy of the original array.

Returns FloatArray

FloatArray.slice()

array.FloatArray.slice(start, end) -> FloatArray

Returns a new FloatArray over [start, end) of this array (both default to the whole array; negative bounds count from the end).

Parameters

  • start (?number)
  • end (?number)

Returns FloatArray


2021, Richard Ore and Zuri contributors

array.int16

import array

Everything here is re-exported by array, so import array is enough and the names are called as array.*. Importing array.int16 on its own works too and reaches the same definitions.

Int16Array: a packed array of twos-complement 16-bit signed integers, 2 bytes per element.

INT16_MAX and INT16_MIN bound what an element may be; a value outside them raises rather than wrapping.

Constants

INT16_MAX

array.INT16_MAX: number = 32767

Maximum value that “should” exist in a list passed to Int16Array.

INT16_MIN

array.INT16_MIN: number

Minimum value that “should” exist in a list passed to Int16Array.

Classes

Int16Array

class array.Int16Array < Array

class Int16Array represents an array of twos-complement 16-bit signed integers in the platform byte order.

  • printable — has a @to_string(), so echo and print() show something useful
  • serializable — has a @to_json(), so it can be handed straight to json.encode()
  • valueable — has a @to_value(), so it converts to a plain Zuri value

Int16Array.reverse()

array.Int16Array.reverse() -> Int16Array

Returns a new array containing the elements in the original array in reverse order.

Returns Int16Array

Int16Array.clone()

array.Int16Array.clone() -> Int16Array

Returns a new Int16Array containing all items from the current array. The new array is a shallow copy of the original array.

Returns Int16Array

Int16Array.slice()

array.Int16Array.slice(start, end) -> Int16Array

Returns a new Int16Array over [start, end) of this array (both default to the whole array; negative bounds count from the end).

Parameters

  • start (?number)
  • end (?number)

Returns Int16Array


2021, Richard Ore and Zuri contributors

array.int32

import array

Everything here is re-exported by array, so import array is enough and the names are called as array.*. Importing array.int32 on its own works too and reaches the same definitions.

Int32Array: a packed array of twos-complement 32-bit signed integers, 4 bytes per element.

INT32_MAX and INT32_MIN bound what an element may be; a value outside them raises rather than wrapping.

Constants

INT32_MAX

array.INT32_MAX: number = 2147483647

Maximum value that “should” exist in a list passed to Int32Array.

INT32_MIN

array.INT32_MIN: number

Minimum value that “should” exist in a list passed to Int32Array.

Classes

Int32Array

class array.Int32Array < Array

class Int32Array represents an array of twos-complement 32-bit signed integers in the platform byte order.

  • printable — has a @to_string(), so echo and print() show something useful
  • serializable — has a @to_json(), so it can be handed straight to json.encode()
  • valueable — has a @to_value(), so it converts to a plain Zuri value

Int32Array.reverse()

array.Int32Array.reverse() -> Int32Array

Returns a new array containing the elements in the original array in reverse order.

Returns Int32Array

Int32Array.clone()

array.Int32Array.clone() -> Int32Array

Returns a new Int32Array containing all items from the current array. The new array is a shallow copy of the original array.

Returns Int32Array

Int32Array.slice()

array.Int32Array.slice(start, end) -> Int32Array

Returns a new Int32Array over [start, end) of this array (both default to the whole array; negative bounds count from the end).

Parameters

  • start (?number)
  • end (?number)

Returns Int32Array


2021, Richard Ore and Zuri contributors

array.int64

import array

Everything here is re-exported by array, so import array is enough and the names are called as array.*. Importing array.int64 on its own works too and reaches the same definitions.

Int64Array: a packed array of twos-complement 64-bit signed integers, 8 bytes per element.

INT64_MAX and INT64_MIN bound what an element may be; a value outside them raises rather than wrapping.

Constants

INT64_MAX

array.INT64_MAX: bigint

Maximum value that “should” exist in a list passed to Int64Array. Written with the explicit n bigint suffix since the bare literal 9223372036854775807 parses as a lossy number (a known, pre-existing limitation of plain integer literals at exactly i64::MAX: see tests/big-integer-literal-overflow.zu), 193 away from the true value.

INT64_MIN

array.INT64_MIN: bigint

Minimum value that “should” exist in a list passed to Int64Array.

Classes

Int64Array

class array.Int64Array < Array

class Int64Array represents an array of twos-complement 64-bit signed integers in the platform byte order.

Values at or beyond ±2^53 need Zuri’s bigint type to be represented exactly (a regular number is an IEEE-754 double, and loses precision past that point): append()/set() accept either a plain integer or a bigint (is_bigint()) for that reason.

  • printable — has a @to_string(), so echo and print() show something useful
  • serializable — has a @to_json(), so it can be handed straight to json.encode()
  • valueable — has a @to_value(), so it converts to a plain Zuri value

Int64Array.reverse()

array.Int64Array.reverse() -> Int64Array

Returns a new array containing the elements in the original array in reverse order.

Returns Int64Array

Int64Array.clone()

array.Int64Array.clone() -> Int64Array

Returns a new Int64Array containing all items from the current array. The new array is a shallow copy of the original array.

Returns Int64Array

Int64Array.slice()

array.Int64Array.slice(start, end) -> Int64Array

Returns a new Int64Array over [start, end) of this array (both default to the whole array; negative bounds count from the end).

Parameters

  • start (?number)
  • end (?number)

Returns Int64Array


2021, Richard Ore and Zuri contributors

array.int8

import array

Everything here is re-exported by array, so import array is enough and the names are called as array.*. Importing array.int8 on its own works too and reaches the same definitions.

Int8Array: a packed array of twos-complement 8-bit signed integers, 1 byte per element.

INT8_MAX and INT8_MIN bound what an element may be; a value outside them raises rather than wrapping.

Constants

INT8_MAX

array.INT8_MAX: number = 127

Maximum value that “should” exist in a list passed to Int8Array.

INT8_MIN

array.INT8_MIN: number

Minimum value that “should” exist in a list passed to Int8Array.

Classes

Int8Array

class array.Int8Array < Array

class Int8Array represents an array of twos-complement 8-bit signed integers.

  • printable — has a @to_string(), so echo and print() show something useful
  • serializable — has a @to_json(), so it can be handed straight to json.encode()
  • valueable — has a @to_value(), so it converts to a plain Zuri value

Int8Array.reverse()

array.Int8Array.reverse() -> Int8Array

Returns a new array containing the elements in the original array in reverse order.

Returns Int8Array

Int8Array.clone()

array.Int8Array.clone() -> Int8Array

Returns a new Int8Array containing all items from the current array. The new array is a shallow copy of the original array.

Returns Int8Array

Int8Array.slice()

array.Int8Array.slice(start, end) -> Int8Array

Returns a new Int8Array over [start, end) of this array (both default to the whole array; negative bounds count from the end).

Parameters

  • start (?number)
  • end (?number)

Returns Int8Array


2021, Richard Ore and Zuri contributors

array.uint16

import array

Everything here is re-exported by array, so import array is enough and the names are called as array.*. Importing array.uint16 on its own works too and reaches the same definitions.

UInt16Array: a packed array of unsigned 16-bit integers, 2 bytes per element.

UINT16_MAX and UINT16_MIN bound what an element may be; a value outside them raises rather than wrapping.

Constants

UINT16_MAX

array.UINT16_MAX: number = 65535

Maximum value that “should” exist in a list passed to UInt16Array.

Classes

UInt16Array

class array.UInt16Array < Array

class UInt16Array represents an array of twos-complement 16-bit unsigned integers in the platform byte order.

  • printable — has a @to_string(), so echo and print() show something useful
  • serializable — has a @to_json(), so it can be handed straight to json.encode()
  • valueable — has a @to_value(), so it converts to a plain Zuri value

UInt16Array.reverse()

array.UInt16Array.reverse() -> UInt16Array

Returns a new array containing the elements in the original array in reverse order.

Returns UInt16Array

UInt16Array.clone()

array.UInt16Array.clone() -> UInt16Array

Returns a new UInt16Array containing all items from the current array. The new array is a shallow copy of the original array.

Returns UInt16Array

UInt16Array.slice()

array.UInt16Array.slice(start, end) -> UInt16Array

Returns a new UInt16Array over [start, end) of this array (both default to the whole array; negative bounds count from the end).

Parameters

  • start (?number)
  • end (?number)

Returns UInt16Array


2021, Richard Ore and Zuri contributors

array.uint32

import array

Everything here is re-exported by array, so import array is enough and the names are called as array.*. Importing array.uint32 on its own works too and reaches the same definitions.

UInt32Array: a packed array of unsigned 32-bit integers, 4 bytes per element.

UINT32_MAX and UINT32_MIN bound what an element may be; a value outside them raises rather than wrapping.

Constants

UINT32_MAX

array.UINT32_MAX: number = 4294967295

Maximum value that “should” exist in a list passed to UInt32Array.

Classes

UInt32Array

class array.UInt32Array < Array

class UInt32Array represents an array of twos-complement 32-bit unsigned integers in the platform byte order.

  • printable — has a @to_string(), so echo and print() show something useful
  • serializable — has a @to_json(), so it can be handed straight to json.encode()
  • valueable — has a @to_value(), so it converts to a plain Zuri value

UInt32Array.reverse()

array.UInt32Array.reverse() -> UInt32Array

Returns a new array containing the elements in the original array in reverse order.

Returns UInt32Array

UInt32Array.clone()

array.UInt32Array.clone() -> UInt32Array

Returns a new UInt32Array containing all items from the current array. The new array is a shallow copy of the original array.

Returns UInt32Array

UInt32Array.slice()

array.UInt32Array.slice(start, end) -> UInt32Array

Returns a new UInt32Array over [start, end) of this array (both default to the whole array; negative bounds count from the end).

Parameters

  • start (?number)
  • end (?number)

Returns UInt32Array


2021, Richard Ore and Zuri contributors

array.uint64

import array

Everything here is re-exported by array, so import array is enough and the names are called as array.*. Importing array.uint64 on its own works too and reaches the same definitions.

UInt64Array: a packed array of unsigned 64-bit integers, 8 bytes per element.

UINT64_MAX and UINT64_MIN bound what an element may be; a value outside them raises rather than wrapping.

Constants

UINT64_MAX

array.UINT64_MAX: bigint

Maximum value that “should” exist in a list passed to UInt64Array.

Classes

UInt64Array

class array.UInt64Array < Array

class UInt64Array represents an array of twos-complement 64-bit unsigned integers in the platform byte order.

Values at or beyond 2^53 need Zuri’s bigint type to be represented exactly (a regular number is an IEEE-754 double, and loses precision past that point): append()/set() accept either a plain integer or a bigint (is_bigint()) for that reason.

  • printable — has a @to_string(), so echo and print() show something useful
  • serializable — has a @to_json(), so it can be handed straight to json.encode()
  • valueable — has a @to_value(), so it converts to a plain Zuri value

UInt64Array.reverse()

array.UInt64Array.reverse() -> UInt64Array

Returns a new array containing the elements in the original array in reverse order.

Returns UInt64Array

UInt64Array.clone()

array.UInt64Array.clone() -> UInt64Array

Returns a new UInt64Array containing all items from the current array. The new array is a shallow copy of the original array.

Returns UInt64Array

UInt64Array.slice()

array.UInt64Array.slice(start, end) -> UInt64Array

Returns a new UInt64Array over [start, end) of this array (both default to the whole array; negative bounds count from the end).

Parameters

  • start (?number)
  • end (?number)

Returns UInt64Array


2021, Richard Ore and Zuri contributors

array.uint8

import array

Everything here is re-exported by array, so import array is enough and the names are called as array.*. Importing array.uint8 on its own works too and reaches the same definitions.

UInt8Array: a packed array of unsigned 8-bit integers, 1 byte per element.

UINT8_MAX and UINT8_MIN bound what an element may be; a value outside them raises rather than wrapping.

Constants

UINT8_MAX

array.UINT8_MAX: number = 255

Maximum value that “should” exist in a list passed to UInt8Array.

Classes

UInt8Array

class array.UInt8Array < Array

class UInt8Array represents an array of twos-complement 8-bit unsigned integers.

  • printable — has a @to_string(), so echo and print() show something useful
  • serializable — has a @to_json(), so it can be handed straight to json.encode()
  • valueable — has a @to_value(), so it converts to a plain Zuri value

UInt8Array.reverse()

array.UInt8Array.reverse() -> UInt8Array

Returns a new array containing the elements in the original array in reverse order.

Returns UInt8Array

UInt8Array.clone()

array.UInt8Array.clone() -> UInt8Array

Returns a new UInt8Array containing all items from the current array. The new array is a shallow copy of the original array.

Returns UInt8Array

UInt8Array.slice()

array.UInt8Array.slice(start, end) -> UInt8Array

Returns a new UInt8Array over [start, end) of this array (both default to the whole array; negative bounds count from the end).

Parameters

  • start (?number)
  • end (?number)

Returns UInt8Array


2021, Richard Ore and Zuri contributors

set

import set

This module provides functionalities for working with mathematical sets.

Sets are collections of unique values. You can iterate through sets in the same order in which they were initialized or set by set.Set.add(). Two Sets containing exactly the same elements, irrespective of order, are equal, whether compared with == or with set.Set.equals().

The example below shows a brief introduction to working with sets.

import set

var my_set = set()

my_set.add(11) # <Set(1) {11}>
my_set.add(56) # <Set(2) {11, 56}>
my_set.add(97) # <Set(3) {11, 56, 97}>
my_set.add('some text') # <Set(4) {11, 56, 97, some text}>
var o = { a: 1, b: 2 }
my_set.add(o)

# o is referencing a different object, but contains the same value
# so it will not be added
my_set.add({ a: 1, b: 2 })

my_set.contains(11) # true
my_set.contains(32) # false, since 32 has not been added to the set
my_set.contains(97) # true
my_set.contains(121 ** 0.5) # true
my_set.contains('Some Text'.lower()) # true
my_set.contains(o) # true

Sets objects can also be created with initial values (or their actual values) by passing a list or a dictionary to the constructor. If a list is passed, all unique elements in the list will be added to the Set object.

For example,

import set

var set_a = set([1, 2, 3, 4, 5])
var set_b = set([4, 5, 6, 7, 8])

echo set_a.intersect(set_b) # <Set(2) {4, 5}>

When a dictionary is passed to the Set constructor, the dictionary keys which are unique themselves will be added to the Set object.

For example:

import set

var set_a = set({
  a: 10,
  b: 21,
})

echo set_a # <Set(2) {a, b}>

The module is callable. set(...) is the same call as set.set(...), documented below.

The set API

Every public name in set, wherever it is declared. Each links to the page that documents it.

NameKindSummary
set.SetclassThe Set class provides some methods that allow you to compose sets like you would with mathematical…
set.setfunctionDefault export function for the set.Set class.

Functions

set()

set.set(items: ?list|dict) -> set.Set

Default export function for the set.Set class.

Parameters

  • items (?list|dict)

Returns set.Set

Classes

Set

class set.Set

The Set class provides some methods that allow you to compose sets like you would with mathematical operations.

  • printable — has a @to_string(), so echo and print() show something useful
  • serializable — has a @to_json(), so it can be handed straight to json.encode()
  • valueable — has a @to_value(), so it converts to a plain Zuri value
  • numeric — converts to a number

Constructor

set.Set(items: ?list|dict)

Creates a new Set object from a list or dictionary or an empty Set object when no argument is passed.

Parameters

  • items (?list|dict)

Set.union()

set.Set.union(other: instance) -> set.Set

Returns a new set containing elements which are in either or both of this set and the given set.

Parameters

  • other (set.Set)

Returns set.Set

Set.intersect()

set.Set.intersect(other: instance) -> set.Set

Returns a new set containing elements in both this set and the given set.

Parameters

  • other (set.Set)

Returns set.Set

Set.difference()

set.Set.difference(other: instance) -> set.Set

Returns a new set containing elements in this set but not in the given set.

Parameters

  • other (set.Set)

Returns set.Set

Set.symmetric_difference()

set.Set.symmetric_difference(other: instance) -> set.Set

Returns a new set containing elements which are in either this set or the given set, but not in both.

Parameters

  • other (set.Set)

Returns set.Set

Set.is_disjoint()

set.Set.is_disjoint(other: instance) -> bool

Returns a boolean indicating if this set has no elements in common with the given set.

Parameters

  • other (set.Set)

Returns bool

Set.is_subset()

set.Set.is_subset(other: instance) -> bool

Returns a boolean indicating if all elements of this set are in the given set.

Parameters

  • other (set.Set)

Returns bool

Set.is_superset()

set.Set.is_superset(other: instance) -> bool

Returns a boolean indicating if all elements of the given set are in this set.

Parameters

  • other (set.Set)

Returns bool

Set.is_empty()

set.Set.is_empty() -> bool

Returns a boolean value indicating whether this set is an empty set or not.

Returns bool

Set.contains()

set.Set.contains(value) -> bool

Returns a boolean asserting whether an element is present with the given value in the Set or not.

Parameters

  • value (any)

Returns bool

Set.length()

set.Set.length() -> number

Returns the number of values in the Set object.

Returns number

Set.clear()

set.Set.clear() -> bool

Removes all elements from the Set object.

Returns bool

Set.remove()

set.Set.remove(value) -> bool

Removes the element associated to the value and returns a boolean asserting whether an element was successfully removed or not. Once an element is removed, calling set.contains(value) will return false afterwards.

Parameters

  • value (any)

Returns bool

Set.pop()

set.Set.pop() -> any

Removes and returns an arbitrary element from the Set.

Returns any

Raises Error if the Set is empty.

Set.add()

set.Set.add(value) -> bool

Inserts a new element with the specified value in to the Set object, if there isn’t an element with the same value already in the Set.

Parameters

  • value (any)

Returns bool

Set.clone()

set.Set.clone() -> set.Set

Returns a new Set which is an exact replica of the current Set.

Returns set.Set

Set.equals()

set.Set.equals(other: instance) -> bool

Returns a boolean indicating whether this Set and other contain exactly the same elements, irrespective of order. == makes the same comparison.

Parameters

  • other (set.Set)

Returns bool

Set.update()

set.Set.update(other: instance) -> set.Set

Adds every element of other to this Set in place, and returns this Set (so calls can chain). Unlike union(), this mutates self instead of returning a new Set.

Parameters

  • other (set.Set)

Returns set.Set

Set.intersection_update()

set.Set.intersection_update(other: instance) -> set.Set

Keeps only the elements this Set has in common with other, in place, and returns this Set. Unlike intersect(), this mutates self instead of returning a new Set.

Parameters

  • other (set.Set)

Returns set.Set

Set.difference_update()

set.Set.difference_update(other: instance) -> set.Set

Removes every element other also contains from this Set, in place, and returns this Set. Unlike difference(), this mutates self instead of returning a new Set.

Parameters

  • other (set.Set)

Returns set.Set

Set.symmetric_difference_update()

set.Set.symmetric_difference_update(other: instance) -> set.Set

Keeps only the elements in exactly one of this Set or other, in place, and returns this Set. Unlike symmetric_difference(), this mutates self instead of returning a new Set.

Parameters

  • other (set.Set)

Returns set.Set

Set.each()

set.Set.each(callback: function)

Calls function callback once for each value present in the Set, in insertion order.

Parameters

  • callback (function)

Set.to_string()

set.Set.to_string() -> string

Returns a string that represents the current Set.

Returns string

Set.to_list()

set.Set.to_list() -> list

Returns the current Set as a list of elements.

Returns list


Richard Ore, 2025

date

import date

This modules provides Zuri’s implementation of date and time manipulation methods. This module implements civil dates as well as julian dates.

Definitions

  • The calendar date (class Date) is a particular day of a calendar year, identified by its ordinal number within a calendar month within that year.

  • The Julian date number (jd) is in elapsed days and time since noon (Greenwich Mean Time) on January 1, 4713 BCE (in the Julian calendar).

Date instances support ordering (<, <=, >, >=) and equals() by comparing the underlying instant in time: see [[date.Date.equals]] for how that interacts with gmt_offset. Arithmetic is done through named methods rather than +/- (add_seconds, add_days, add_months, …; see [[date.Date.add_seconds]] and [[date.Date.add_months]]) since a plain number added to a date is otherwise ambiguous about its unit.

The module is callable. date(...) is the same call as date.date(...), documented below.

The date API

Every public name in date, wherever it is declared. Each links to the page that documents it.

NameKindSummary
date.DateclassDate and Time manipulation class
date.MAX_DAYconstantMaximum day supported.
date.MAX_HOURconstantMaximum hour supported.
date.MAX_MINUTEconstantMaximum minute supported.
date.MAX_MONTHconstantMaximum year supported.
date.MAX_SECONDSconstantMaximum seconds supported.
date.MAX_YEARconstantMaximum year supported.
date.MIN_DAYconstantMinimum day supported.
date.MIN_MONTHconstantMinimum month supported.
date.MIN_YEARconstantMinimum year supported.
date.datefunctionReturns a new Date instance representing the given system date or the current date if no argument is…
date.from_jdfunctionReturns a date instance representing the julian date.
date.from_timefunctionReturns a date object from a unix timestamp.
date.from_timezonefunctionReturns a Date from wall-clock fields understood as a local time IN the real IANA timezone name, rather…
date.gmtimefunctionReturns a dictionary representing the current time without timezone adjustment.
date.is_valid_timezonefunctionReturns true when name is a real IANA timezone identifier (e.g. 'Africa/Lagos', 'America/New_York',…
date.list_timezonesfunctionReturns every IANA timezone identifier this build’s timezone database recognizes, e.g. 'Africa/Lagos',…
date.localtimefunctionReturns a dictionary representing the current time after adjusting for the current timezone
date.mktimefunctionConvert the broken-out time into a time value with the same encoding as that of the values returned by the…
date.parsefunctionParses a date string into a Date instance, automatically detecting the format.
date.parse_formatfunctionParses a date string using an explicit format pattern and returns a Date instance.

Constants

MIN_YEAR

date.MIN_YEAR: number = 1

Minimum year supported.

MAX_YEAR

date.MAX_YEAR: number = 9999

Maximum year supported.

MIN_DAY

date.MIN_DAY: number = 1

Minimum day supported.

MAX_DAY

date.MAX_DAY: number = 31

Maximum day supported.

MIN_MONTH

date.MIN_MONTH: number = 1

Minimum month supported.

MAX_MONTH

date.MAX_MONTH: number = 12

Maximum year supported.

MAX_HOUR

date.MAX_HOUR: number = 23

Maximum hour supported.

MAX_MINUTE

date.MAX_MINUTE: number = 59

Maximum minute supported.

MAX_SECONDS

date.MAX_SECONDS: number = 59

Maximum seconds supported.

Functions

gmtime()

date.gmtime()

Returns a dictionary representing the current time without timezone adjustment.

Example,

%> echo date.gmtime()
{year: 2022, month: 3, day: 5, week_day: 6, year_day: 63, hour: 17, minute: 30, 
seconds: 55, microseconds: 620290, is_dst: false, zone: UTC, gmt_offset: 0}

Returns — dictionary

Note: year_day here is the platform’s own tm_yday, counting Jan 1st as day 0. Date.year_day counts it as day 1; the two differ by one on purpose, since this dictionary is the raw reading and Date is the module’s own representation.

localtime()

date.localtime()

Returns a dictionary representing the current time after adjusting for the current timezone

Example:

%> echo date.localtime()
{year: 2022, month: 3, day: 5, week_day: 6, year_day: 63, hour: 18, minute: 18, 
seconds: 35, microseconds: 598166, is_dst: false, zone: WAT, gmt_offset: 3600}

Returns — dictionary

mktime()

date.mktime(year, month, day, hour, minute, seconds, is_dst) -> number

Convert the broken-out time into a time value with the same encoding as that of the values returned by the time() function (that is, seconds from the Epoch, UTC) according to the timezone settings.


Example:

%> import date
%> echo date.mktime(2021, 2, 12, 13, 43, 11, false)
1613133791

Parameters

  • year (number)
  • month (number)
  • day (number)
  • hour (number)
  • minute (number)
  • seconds (number)
  • is_dst (bool)

Returns number

is_valid_timezone()

date.is_valid_timezone(name) -> bool

Returns true when name is a real IANA timezone identifier (e.g. 'Africa/Lagos', 'America/New_York', 'UTC'), backed by the timezone database embedded in this build. This is a real, complete IANA database: unlike parse()’s own timezone handling, which only recognizes UTC/GMT/Z/numeric offsets and treats every other name as an unvalidated label: so this is the function to reach for whenever a timezone name actually needs checking.

%> date.is_valid_timezone('Africa/Lagos')
true
%> date.is_valid_timezone('Neverland/Nowhere')
false

Parameters

  • name (string)

Returns bool

Raises TypeError When name is not a string.

list_timezones()

date.list_timezones() -> list[string]

Returns every IANA timezone identifier this build’s timezone database recognizes, e.g. 'Africa/Lagos', 'America/New_York', 'UTC'. There are several hundred of these; this is mainly useful for populating a picker or validating against the full set at once rather than one name at a time.

Returns list[string]

from_time()

date.from_time(time) -> Date

Returns a date object from a unix timestamp.

Example,

%> date.from_time(time()).to_string()
'<Date year: 2022, month: 3, day: 5, hour: 18, minute: 34, seconds: 1>'

Parameters

  • time (number)

Returns Date

Note: Time must be in seconds.

from_timezone()

date.from_timezone(name, year, month, day, hour, minute, seconds, is_dst) -> Date

Returns a Date from wall-clock fields understood as a local time IN the real IANA timezone name, rather than in UTC (the plain Date(...) constructor) or the process’s own system zone (mktime()). The offset, zone abbreviation, and DST state are all worked out from name’s real rules at that wall-clock moment.

A wall-clock reading can be ambiguous once a year: the hour a clock falls back repeats, e.g. 1:30am happens twice on the day America/New_York leaves daylight saving: is_dst picks which occurrence is meant, the same way it does for mktime(). A reading a clock skips entirely (the hour a clock springs forward) has no valid answer at all and raises, rather than silently returning a nearby instant.

var d = date.from_timezone('America/New_York', 2024, 7, 1, 0, 0, 0)
echo d.to_time()   # the UTC instant of 2024-07-01 00:00:00 EDT
echo d.zone        # EDT
echo d.gmt_offset  # -14400

Parameters

  • name (string) — An IANA timezone identifier.
  • year (number)
  • month (number)
  • day (number)
  • hour (number)
  • minute (number)
  • seconds (number)
  • is_dst (?bool) — Disambiguates a wall-clock reading that occurs twice; ignored otherwise. Defaults to the earlier occurrence when omitted.

Returns Date

Raises TypeError When name is not a string, or a date/time field is not a number.

Raises Error When name is not a recognized timezone, the fields are out of range, or the wall-clock reading falls in a DST gap.

from_jd()

date.from_jd(jdate) -> number

Returns a date instance representing the julian date.

Example,

%> date.from_jd(22063).to_string()
'<Date year: 2022, month: 3, day: 5, hour: 18, minute: 35, seconds: 0>'

Parameters

  • jdate (number)

Returns number

parse()

date.parse(date_string) -> Date

Parses a date string into a Date instance, automatically detecting the format.

parse recognises a broad set of standard date and datetime formats without requiring the caller to specify the format string:

Recognised formats

CategoryExamples
ISO 8601 extended2024-06-15, 2024-06-15T14:30:00Z
ISO 8601 extended2024-06-15 14:30:00+01:00
ISO 8601 compact20240615, 20240615T143000Z
RFC 2822Thu, 21 Dec 2000 16:01:07 +0200
HTTP dateSat, 05 Mar 2022 06:23:32 GMT
Full month name firstMarch 05, 2022 6:24 PM
Full month name firstJanuary 1 2000
Day-month-year21 Dec 2000 16:01:07 +0200
US slash-separated06/15/2024, 6/15/2024 2:30 PM
EU dot-separated15.06.2024, 15.06.2024 14:30:00
Hyphen numeric15-06-2024
ISO order slash2024/06/15
Time only14:30:00, 2:30 PM, 14:30:00.123456 UTC

Timezone handling

When a timezone offset is present in the string it is stored in gmt_offset (seconds) and zone (name string). When absent, zone defaults to "UTC" and gmt_offset to 0.

Ambiguity policy

For slash-separated numeric dates where both the first and second fields are ≤ 12 (e.g. 06/05/2024), the US convention is assumed: month first (MM/DD/YYYY). When the input is known to be DD/MM/YYYY, use parse_format with '%d/%m/%Y' instead.

Example

import date

var d = date.parse('2024-06-15T14:30:00Z')
echo d.year    # 2024
echo d.month   # 6
echo d.hour    # 14
echo d.zone    # UTC

var d2 = date.parse('March 05, 2022 6:24 PM')
echo d2.year   # 2022
echo d2.hour   # 18   (PM applied)

var d3 = date.parse('Thu, 21 Dec 2000 16:01:07 +0200')
echo d3.year        # 2000
echo d3.gmt_offset  # 7200

Parameters

  • date_string (string) — The date string to parse.

Returns Date

Raises TypeError When date_string is not a string.

Raises ValueError When the string cannot be parsed as a date.

parse_format()

date.parse_format(date_string, format) -> Date

Parses a date string using an explicit format pattern and returns a Date instance.

The format pattern uses the same character codes as the Date.format() method, making it straightforward to round-trip a date through format() and back through parse_format().

Format character reference

The following characters are interpreted as directives in the format string. All other characters are treated as literals and must appear verbatim in the input. Prefix a character with a backslash (\) to force literal treatment even for directive characters.

CharacterConsumesExample input
Y4-digit year2024 y
→ 1970–1999)24 mMonth with leading zero (01–12)
Month without leading zero (1–12)6 MAbbreviated month name
(case-insensitive)Jun FFull month name (case-insensitive)
June dDay with leading zero (01–31)05 j
leading zero (1–31)5 DAbbreviated weekday name (consumed and
ignored)Thu lFull weekday name (consumed and ignored)
Thursday H24-hour hour with leading zero (00–23)14 G
24-hour hour without leading zero (0–23)14 h12-hour hour with
leading zero (01–12)02 g12-hour hour without leading zero
(1–12)2 iMinutes with leading zero (00–59)
Seconds with leading zero (00–59)00 uMicroseconds (up to 6
digits)123456 vMilliseconds (up to 3 digits)
Uppercase AM/PMPM aLowercase am/pm
name/identifierUTC OTimezone offset without colon
PTimezone offset with colon+02:00 Z
seconds (signed integer)7200 cISO 8601 full datetime
(delegated to parse)2024-06-15T14:30:00+02:00 rRFC 2822 full
datetime (delegated to parse)Thu, 21 Dec 2000 16:01:07 +0200 S
Ordinal suffix (consumed and ignored)th
t,L,N,w,z,W,IOutput-only fields (skipped):

Example

import date

# Basic date
var d = date.parse_format('15/06/2024', 'd/m/Y')
echo d.year    # 2024
echo d.month   # 6
echo d.day     # 15

# DateTime with AM/PM
var d2 = date.parse_format('March 05, 2022 6:24 PM', 'F d, Y g:i A')
echo d2.hour   # 18

# Round-trip through format()
var original = date.Date(2024, 6, 15, 14, 30, 0)
var fmt      = 'D, d M Y H:i:s O'
var reparsed = date.parse_format(original.format(fmt), fmt)
echo reparsed.day    # 15
echo reparsed.month  # 6

# European DD/MM/YYYY (ambiguous with auto-detect: use parse_format)
var eu = date.parse_format('05/06/2024', 'd/m/Y')
echo eu.month  # 6
echo eu.day    # 5

# Timezone-aware
var tz = date.parse_format('2024-06-15T14:30:00+05:30', 'Y-m-d\\TH:i:sP')
echo tz.gmt_offset  # 19800 (5.5 hours in seconds)

Parameters

  • date_string (string) — The input string to parse.
  • format (string) — The format pattern describing date_string.

Returns Date

Raises TypeError When either argument is not a string.

Raises ValueError When the string does not match the expected format.

date()

date.date(year, month, day, hour, minute, seconds) -> Date

Returns a new Date instance representing the given system date or the current date if no argument is specified.

Parameters

  • year (?number)
  • month (?number)
  • day (?number)
  • hour (?number)
  • minute (?number)
  • seconds (?number)
  • is_dst (?bool)

Returns Date

Classes

Date

class date.Date

Date and Time manipulation class

A date here refers to a calendar datetime consisting of year, month, day, hour, minute and seconds.

The Date class manages both Date and DateTime and this module does not make any distinction between the two as Date is a subset of DateTime.

Example,

%> import date
%> var d = date(2021)
%> d.to_string()
'<Date year: 2021, month: 1, day: 1, hour: 0, minute: 0, seconds: 0>'
%> d = date()
%> d.to_string()
'<Date year: 2022, month: 3, day: 5, hour: 19, minute: 25, seconds: 58>'
  • printable — has a @to_string(), so echo and print() show something useful
  • serializable — has a @to_json(), so it can be handed straight to json.encode()
  • numeric — converts to a number

Constructor

date.Date(year, month, day, hour, minute, seconds, microseconds, gmt_offset, is_dst)

Parameters

  • year (?number)
  • month (?number)
  • day (?number)
  • hour (?number)
  • minute (?number)
  • seconds (?number)
  • microseconds (?number)
  • gmt_offset (?number)
  • is_dst (?bool)

Note: All arguments are optional

Note: When no argument is given, the date will be set to the current system date.

Note: when the date fields are given, the timezone is always in UTC irrespective of the system timezone or if a gmt_offset is given or not.

Note: year_day on the resulting instance counts Jan 1st as day 1, which is what to_ordinal() and format('z') expect. The raw dictionaries from localtime() and gmtime() are the one place that differs: those carry the platform’s own tm_yday, which counts Jan 1st as day 0.

Date.is_leap()

date.Date.is_leap() -> bool

Returns true if the year is a leap year or false otherwise.

Example,

%> date(2018).is_leap()
false
%> date(2020).is_leap()
true

Returns bool

Date.to_ordinal()

date.Date.to_ordinal() -> number

Returns this date’s ordinal day number in the proleptic Gregorian calendar, where day 1 is 0001-01-01. Two dates can be compared or subtracted via their ordinals to get an exact day count between them, regardless of leap years.

Example,

%> date(1, 1, 1).to_ordinal()
1
%> date(2021, 5, 11).to_ordinal()
737921

Returns number

Date.days_before_month()

date.Date.days_before_month(month) -> number

Returns the number of days between the first day of month (in this date’s own year) and this date. Positive when month is later in the year than this date, negative when earlier, zero when month is this date’s own month and day is 1.

Example,

%> date(2021, 5, 11).days_before_month(7)
51

Returns number

Date.days_before_year()

date.Date.days_before_year(year) -> number

Returns the number of days between January 1st of year and this date. Positive when year is after this date’s own year, negative when before (or the same year but earlier in it).

Example,

%> date(2021, 5, 11).days_before_year(2024)
965

Parameters

  • year (int)

Returns number

Date.days_in_month()

date.Date.days_in_month() -> number

Returns the number of days in month for the specified year.

Example,

%> date(2021, 6).days_in_month()
30

Returns number

Date.weekday()

date.Date.weekday() -> number

Returns the numbered day of the week.

Example,

%> date(2021, 5, 11).weekday()
2

Returns number

Date.week_number()

date.Date.week_number() -> number

Returns the number of the current week in the year.

Example,

%> date(2021, 5, 11).week_number()
19

Returns number

Date.format()

date.Date.format(format) -> string

Formats the current date based on the specified string

Zuri’s Date formatting table

CharacterDescriptionExample
Auppercase Ante meridian and Post meridianAM or PM a
Ante meridian and Post meridianam or pm dday of the month with
leading zero01 to 31 Dtextual representation of a day, three
lettersMon - Sun jday of the month without leading zero
lfull textual representation of the day of the weekMonday - Sunday
NISO-8601 numeric representation of the day of the week1 - 7 S
English ordinal suffix for the day of the monthst, nd, rd or th w
numeric representation of the day of the week0 - 6 zthe day of the
year (starting from 0)0 - 365 WISO-8601 week number of year, weeks
starting on MondayE.g. 33 (the 33rd week of the year) Ffull
textual representation of a monthJanuary - December mnumeric
representation of a month, with leading zeros01 - 12 nnumeric
representation of a month, without leading zeros1 - 12 Mshort
textual representation of a month, three lettersJan - Dec tnumber
of days in the given month28 - 31 Lwhether it’s a leap year
true, 0 otherwise ytwo digit representation of a yeare.g. 09 or 99
Yfull numeric representation of a year using 4 digitse.g. 2009 or
1999 h12 hour format of an hour with leading zeros01 - 12 H
hour format of an hour with leading zeros01 - 24 g12 hour format
of an hour without leading zeros1 - 12 G24 hour format of an hour
without leading zeros1 - 24 iminutes with leading zero
seconds with leading zero00 - 59 umicroseconds
millisecondse.g. 987 etimezone identifier
whether or not the date is in daylight saving time1 for true, 0
otherwise Odifference to GMT without colon between hours and minutes
e.g. +0100 Pdifference to GMT with colon between hours and minutes
e.g. +01:00 Ztimezone offset in seconds-43200 - 50400 c
8601 datee.g. 2020-03-04T15:19:21+00:00 rRFC 2822 formatted date
e.g. Thu, 21 Dec 2000 16:01:07 +0200

Example,

%> date().format('F d, Y g:i A')
'March 05, 2022 6:24 PM'

You can prevent a format character in the format string from being expanded by escaping it with a preceding backslash. If the character with a backslash is already a special sequence, you may need to also escape the backslash.

For example:

%> date().format('l jS \o\\f F Y h:i:s A')
'Wednesday 17th of May 2021 01:39:08 PM'

Parameters

  • format (string)

Returns string

Date.http()

date.Date.http() -> string

Returns the HTTP date representation of the current date.

For example,

%> date().http()
'Sat, 05 Mar 2022 06:23:32 GMT'

Returns string

Date.jd()

date.Date.jd() -> number

Converts the current date to a julian day and time.

Example,

%> date(2021, 5, 11).jd()
2459345

Returns number

Date.unix_time()

date.Date.unix_time() -> number

Returns unix mktime equivalent of the current date.

Returns number

Deprecated. Use to_time() instead as it offers more precision.

Date.to_time()

date.Date.to_time() -> number

Returns the Epoch timestamp in seconds for the given date.

Returns number

Date.clone()

date.Date.clone() -> Date

Returns an independent copy of this date.

Returns Date

Date.equals()

date.Date.equals(other) -> bool

Returns true when other is a Date representing the exact same instant in time as this one. Two dates with different gmt_offset/zone labels but the same underlying UTC instant are considered equal: the same convention Python’s timezone-aware datetime equality uses.

Parameters

  • other (any)

Returns bool

Date.diff()

date.Date.diff(other) -> number

Returns the signed difference, in seconds, between this date and other (self.to_time() - other.to_time()). Positive when this date is later than other, negative when earlier.

Parameters

  • other (Date)

Returns number

Date.add_seconds()

date.Date.add_seconds(n) -> Date

Returns a new Date, n seconds after this one (or before, if n is negative). The result keeps this date’s own gmt_offset/zone/is_dst. self is not modified.

Parameters

  • n (number)

Returns Date

Date.add_minutes()

date.Date.add_minutes(n) -> Date

Returns a new Date, n minutes after this one. See add_seconds().

Parameters

  • n (number)

Returns Date

Date.add_hours()

date.Date.add_hours(n) -> Date

Returns a new Date, n hours after this one. See add_seconds().

Parameters

  • n (number)

Returns Date

Date.add_days()

date.Date.add_days(n) -> Date

Returns a new Date, n days after this one. See add_seconds().

Parameters

  • n (number)

Returns Date

Date.add_months()

date.Date.add_months(n) -> Date

Returns a new Date, n months after this one (or before, if n is negative), keeping the same time-of-day and gmt_offset/zone/is_dst. self is not modified.

When the resulting month doesn’t have this date’s own day number (e.g. Jan 31 plus one month), the result is clamped to the last valid day of that month (Jan 31 + 1 month → Feb 28 or 29, never an overflow into March): the convention to know about, since date libraries genuinely differ on this.

Parameters

  • n (number)

Returns Date

Date.add_years()

date.Date.add_years(n) -> Date

Returns a new Date, n years after this one. See add_months() for the day-clamping rule this also follows (relevant for Feb 29 plus a non-leap number of years).

Parameters

  • n (number)

Returns Date

Date.to_offset()

date.Date.to_offset(gmt_offset, zone) -> Date

Returns a new Date representing the exact same instant as this one, re-expressed under a different gmt_offset. This is the way to convert a date from one timezone’s wall-clock reading to another’s: unlike add_seconds() and friends, which shift the underlying instant, this keeps the instant fixed and only changes how it’s displayed.

Parameters

  • gmt_offset (number) — Seconds east of UTC.
  • zone (?string) — Optional label for the new offset (e.g. 'EST'); when omitted, this date’s own zone is kept as-is.

Returns Date

Date.to_timezone()

date.Date.to_timezone(name) -> Date

Returns a new Date representing the exact same instant as this one, re-expressed under the real IANA timezone name (e.g. 'America/New_York', 'Africa/Lagos'). Unlike to_offset(), which takes the numeric offset to shift to and leaves it to the caller to already know it, this looks up name’s actual offset: correctly accounting for daylight saving: at this date’s own instant, so gmt_offset, zone, and is_dst all come out right without the caller needing to work any of that out first.

var d = date.parse('2024-07-01T00:00:00Z')
var ny = d.to_timezone('America/New_York')
echo ny.hour        # 20 (the previous day, EDT is UTC-4)
echo ny.zone        # EDT
echo ny.is_dst      # true

Parameters

  • name (string) — An IANA timezone identifier.

Returns Date

Raises TypeError When name is not a string.

Raises Error When name is not a recognized timezone (see is_valid_timezone()).

Date.to_string()

date.Date.to_string() -> string

Returns a string representation of the date

Returns string

Date.to_dict()

date.Date.to_dict() -> dict

Returns the date object as a dictionary.

Returns dict

Date.to_number()

date.Date.to_number()

2021, Richard Ore and Zuri contributors

os

import os

The os module is Zuri’s interface to the underlying operating system: environment variables, the filesystem, other processes, and the machine itself. Per-file operations: reading, writing, deleting, or copying one particular file, or inspecting its size/permissions/timestamps: live on the file class instead; see file.stats(), file.delete(), and file.copy(). Everything that isn’t about one specific already-open file lives here.

Environment variables

import os

var port = os.get_env('PORT', '8080')
os.set_env('APP_ENV', 'production')

for name in os.environ() {
  echo '${name} = ${os.environ()[name]}'
}

Paths

Paths are resolved and compared purely as strings, without touching the filesystem, so they work the same whether or not anything actually exists at the path yet.

var config = os.join_paths(os.home_dir(), '.config', 'zuri')
var absolute = os.abs_path('../lib/main.zu')
var rel = os.relative_path('/srv/app', '/srv/app/logs/out.log')
# rel == 'logs/out.log'

The filesystem

os.create_dir('build')

for entry in os.read_dir('.', false) {
  if os.is_dir(entry) continue
  echo entry
}

var git = os.which('git')  # full path, or nil if not on PATH
var sources = os.glob('*.zu')

Running other programs

exec() is the simple case: run a command, block until it finishes, get back its exit code and output.

var result = os.exec('git', 'rev-parse', 'HEAD')
if result.exit_code == 0 {
  echo 'HEAD is at ${result.output.trim()}'
}

spawn() is for everything exec() doesn’t cover: a long-running command, one that needs input written to it, or one whose output needs to be read as it arrives rather than all at once at the end.

var proc = os.spawn('grep', ['error'], { stdin: 'pipe' })
proc.write_stdin('startup ok\nerror: disk full\n')
proc.close_stdin()
echo proc.read_stdout().to_string()  # 'error: disk full\n'
proc.wait()

on_signal() traps SIGINT/SIGTERM/etc. so a long-running script can shut down cleanly instead of being torn down mid-work. Falling off the end of the callback lets the signal do what it would have done anyway, so this cleans up and then exits:

os.on_signal('INT', @() {
  echo 'shutting down...'
  flush_caches()
})

Return a truthy value instead to swallow the signal and keep running.

Windows has no signals. A callback registered there runs for console events like Ctrl+C, and never for anything kill() sends.

The machine

echo 'running on ${os.num_cpus()} cores'
echo '${os.free_memory() / 1_000_000} MB free of ${os.total_memory() / 1_000_000} MB'

Scratch space

create_temp_file()/create_temp_dir() reserve a uniquely named path under temp_dir() atomically, so two processes racing to create scratch space can never collide on the same name:

var path = os.create_temp_file('upload-', '.tmp')
file(path, 'w').write(incoming_data)
# ... use it, then clean up when done
file(path).delete()

The os API

Every public name in os, wherever it is declared. Each links to the page that documents it.

NameKindSummary
os.DT_BLKconstantBlock device file type
os.DT_CHRconstantCharacter device file type
os.DT_DIRconstantDirectory file type
os.DT_FIFOconstantNamed pipe file type
os.DT_LNKconstantSymbolic link file type
os.DT_REGconstantRegular file type
os.DT_SOCKconstantLocal-domain socket file type
os.DT_UNKNOWNconstantUnknown file type
os.DT_WHTconstantWhiteout file type (only meaningful on UNIX and some unofficial Linux versions).
os.ProcessclassA running (or finished) child process created by spawn().
os.abs_pathfunctionReturns the absolute form of path: relative paths are resolved against the current working directory, and…
os.argsconstantThe command line, as the running script sees it.
os.at_exitfunctionRegisters callback to run when the program ends.
os.base_namefunctionThe base_name() function returns the last component from the pathname pointed to by path, deleting any…
os.change_dirfunctionNavigates the working directory into the specified path.
os.chmodfunctionChanges the permission set on a directory to the given mode.
os.chownfunctionChanges the owning user and group of path to uid and gid.
os.create_dirfunctionCreates the given directory with the specified permission and optionally add new files into it if any is…
os.create_temp_dirfunctionAtomically creates a new, empty, uniquely named directory inside temp_dir() and returns its path.
os.create_temp_filefunctionAtomically creates a new, empty, uniquely named file inside temp_dir() and returns its path.
os.cwdfunctionThe current working directory.
os.dir_existsfunctionReturns true if path exists and is a directory, false otherwise (including when path exists but is a…
os.dir_namefunctionReturns the parent directory of the pathname pointed to by path.
os.environfunctionReturns every environment variable currently visible to this process as a dictionary of name/value pairs.
os.exe_pathconstantThe full path to the running Zuri executable.
os.execfunctionExecutes the given shell (or command prompt for Windows) commands and returns a dictionary containing the…
os.exitfunctionExit the current process and quits the Zuri runtime.
os.expand_userfunctionExpands a leading ~ in path into the current user’s home directory, the same way a shell would before…
os.expand_varsfunctionExpands $NAME/$NAME`` references in text (and, on Windows, %NAME% references as well) using the…
os.free_memoryfunctionAn estimate of how much physical memory is available for new allocations right now, in bytes: free memory…
os.get_envfunctionReturns the given environment variable if it exists or default_value (nil if not given) otherwise.
os.globfunctionReturns every entry under base_path (the current working directory if not given) whose path matches the…
os.home_dirfunctionThe current machine user’s home directory.
os.hostnamefunctionThe current machine’s hostname.
os.infofunctionReturns information about the current operation system and machine as a dictionary.
os.is_dirfunctionReturns true if the path is a directory or false otherwise.
os.is_symlinkfunctionReturns true if path exists and is a symbolic link, false otherwise (including when path doesn’t…
os.join_pathsfunctionConcatenates the given paths together into a format that is valid on the current operating system.
os.killfunctionSends signal to the process identified by pid.
os.num_cpusfunctionThe number of logical CPUs available to this process.
os.on_signalfunctionRegisters callback to run when this process receives the named signal, for handling things like Ctrl+C…
os.path_containsfunctionReturns true if the resolved form of candidate is base itself or lies somewhere underneath it, and…
os.path_separatorconstantThe standard path separator for the current operating system.
os.pidfunctionThe current process’s id.
os.platformconstantThe name of the current platform in string or unknown if the platform name could not be determined.
os.ppidfunctionThe current process’s parent’s id.
os.read_dirfunctionScans the given directory and returns a list of the names it contains, sorted by name, led by . and ...
os.readlinkfunctionReturns the target path points to, if path is a symbolic link.
os.real_pathfunctionReturns the original path to a relative path.
os.relative_pathfunctionReturns the relative path from base to target: the shortest ./..-based path such that, starting…
os.remove_dirfunctionDeletes a non-empty directory.
os.renamefunctionRenames the file or directory specified by old_name to the name given by new_name.
os.set_envfunctionSets the named environment variable to the given value.
os.set_exit_codefunctionRecords the status the process should end with, without ending it.
os.sleepfunctionCauses the current thread to sleep for the specified number of seconds.
os.spawnfunctionSpawns cmd as a new subprocess and returns a Process handle to it immediately, without waiting for it to…
os.targetconstantThe platform the running runtime was built for, as a target triple: x86_64-unknown-linux-gnu,…
os.temp_dirfunctionThe platform’s directory for temporary files (e.g. /tmp on Unix, whatever %TEMP% points to on Windows).
os.total_memoryfunctionThe total physical memory installed on this machine, in bytes.
os.umaskfunctionGets or sets the process’s file-creation mask: the set of permission bits stripped from every file/directory…
os.unset_envfunctionRemoves the named environment variable, if it’s set.
os.uptimefunctionHow long the machine has been running since it last booted, in seconds.
os.versionconstantThe current Zuri version.
os.vm_versionconstantThe current Zuri VM version.
os.whichfunctionSearches every directory in the PATH environment variable, in order, for an executable file named name,…

Submodules

ModuleReached asSummary
os.envos.*Reading, writing, and enumerating the current process’s environment variables.
os.fsos.*Directory and filesystem-entry operations: creating, listing, and removing directories, permissions and…
os.pathos.*Path string manipulation: joining, resolving, and comparing paths.
os.processos.*Process identity, subprocess execution, and signal handling.
os.systemos.*Facts about the current process, the Zuri runtime, and the machine it’s running on.
os.tempfileos.*Locating the platform’s temporary-file directory and creating uniquely named scratch files/directories inside…

2021, Richard Ore and Zuri contributors

os.env

import os

Everything here is re-exported by os, so import os is enough and the names are called as os.*. Importing os.env on its own works too and reaches the same definitions.

Reading, writing, and enumerating the current process’s environment variables.

Functions

get_env()

os.get_env(name: string, default_value) -> ?string

Returns the given environment variable if it exists or default_value (nil if not given) otherwise.

Example,

%> import os
%> os.get_env('ENV1')
'20'

Parameters

  • name (string)
  • default_value (?any)

Returns ?string

set_env()

os.set_env(name: string, value: string, overwrite: ?bool) -> bool

Sets the named environment variable to the given value.

Example,

%> os.set_env('ENV1', 'New value')
true
%> os.get_env('ENV1')
'New value'

If you are in the REPL and have tried the last example in get_env(), you may notice that the value of ENV1 doesn’t change. This is because unless you specify, set_env() will not overwrite existing environment variables. For that, you will need to specify true as the third parameter to set_env().

For example,

%> os.set_env('ENV1', 'New value again', true)
true
%> os.get_env('ENV1')
'New value again'

Parameters

  • name (string)
  • value (string)
  • overwrite (?bool) — Default value is false.

Returns bool

Note: Environment variables set will not persist after application exists.

unset_env()

os.unset_env(name: string)

Removes the named environment variable, if it’s set.

Example,

%> os.set_env('ENV1', '20')
true
%> os.unset_env('ENV1')
true
%> os.get_env('ENV1')
nil
%> os.unset_env('ENV1')
false

Parameters

  • name (string)

Returns — bool: true if the variable existed and was removed, false if it wasn’t set to begin with.

Note: Just like set_env(), this change does not persist past the lifetime of the current process.

environ()

os.environ() -> dict

Returns every environment variable currently visible to this process as a dictionary of name/value pairs.

Example,

%> os.environ()
{HOME: /home/username, SHELL: /bin/bash, ...}

Returns dict

Note: the returned dictionary is a snapshot taken at the time of the call; later calls to set_env()/unset_env() don’t retroactively change a dictionary you’re already holding.

expand_vars()

os.expand_vars(text: string) -> string

Expands $NAME/${NAME} references in text (and, on Windows, %NAME% references as well) using the current process’s environment, the same way a shell would before running a command.

A reference to a variable that isn’t set expands to an empty string, matching typical shell behavior, rather than being left untouched or raising.

text is meant to come from somewhere that isn’t itself a Zuri string literal: a config file, a template on disk, a value passed in on the command line: since a ${NAME} written directly in Zuri source is interpolated by Zuri’s own string syntax before expand_vars() ever sees it.

Example,

%> os.set_env('NAME', 'zuri')
true
%> os.expand_vars('hello, $NAME!')
'hello, zuri!'
%> os.expand_vars(file('greeting.template').read())
'hello, zuri!'

Parameters

  • text (string)

Returns string


2021, Richard Ore and Zuri contributors

os.fs

import os

Everything here is re-exported by os, so import os is enough and the names are called as os.*. Importing os.fs on its own works too and reaches the same definitions.

Directory and filesystem-entry operations: creating, listing, and removing directories, permissions and ownership, symbolic links, searching PATH for an executable, and glob-style pattern matching. Per-file operations (reading, writing, deleting, or copying a single file, or getting its size/mtime/etc.) live on the file class instead: see file.stats(), file.delete(), file.copy(), and friends.

Constants

DT_UNKNOWN

os.DT_UNKNOWN: number

Unknown file type

DT_BLK

os.DT_BLK: number

Block device file type

DT_CHR

os.DT_CHR: number

Character device file type

DT_DIR

os.DT_DIR: number

Directory file type

DT_FIFO

os.DT_FIFO: number

Named pipe file type

DT_LNK

os.DT_LNK: number

Symbolic link file type

DT_REG

os.DT_REG: number

Regular file type

DT_SOCK

os.DT_SOCK: number

Local-domain socket file type

DT_WHT

os.DT_WHT: number

Whiteout file type (only meaningful on UNIX and some unofficial Linux versions).

Note: value is -1 on systems where it is not supported.

Functions

create_dir()

os.create_dir(path: string, permission: ?int, recursive: ?bool) -> boolean

Creates the given directory with the specified permission and optionally add new files into it if any is given.

Parameters

  • path (string)
  • permission (?int) — Default value is 0c777
  • recursive (?bool) — Default value is true.

Returns boolean

Note: if the directory already exists, it returns false otherwise, it returns true.

Note: permission should be given as octal number.

read_dir()

os.read_dir(path: string, recursive: ?bool) -> list[string]

Scans the given directory and returns a list of the names it contains, sorted by name, led by . and ...

Example,

%> os.read_dir('./tests')
[., .., buggy.zu, myprogram.zu, single_thread.zu, test.zu]

When the recursive argument is set to true, sub-directories are descended into as well, and their contents come back as paths relative to path rather than as bare names. A directory’s contents follow immediately after the directory itself, and entries are sorted by name within every level:

%> os.read_dir('./tests', true)
[., .., buggy.zu, data, data/input.csv, test.zu]

A symbolic link to a directory is listed but not descended into, whether or not it points somewhere inside path.

Parameters

  • path (string)
  • recursive (?bool) — Default is false.

Returns list[string]

Note: . indicates current directory and can be used as argument to os.read_dir as well.

Note: .. indicates parent directory and can be used as argument to os.read_dir as well.

chmod()

os.chmod(path: string, mode: int) -> boolean

Changes the permission set on a directory to the given mode. It is advisable to set the mode with an octal number (e.g. 0c777) as this is consistent with operating system values.

Parameters

  • path (string)
  • mode (int)

Returns boolean

is_dir()

os.is_dir(path: string) -> bool

Returns true if the path is a directory or false otherwise.

Parameters

  • path (string)

Returns bool

remove_dir()

os.remove_dir(path: string, recursive: ?bool) -> bool

Deletes a non-empty directory. If recursive is true, non-empty directories will have their contents deleted first.

Parameters

  • path (string)
  • recursive (?bool) — Default value is false.

Returns bool

dir_exists()

os.dir_exists(path: string) -> bool

Returns true if path exists and is a directory, false otherwise (including when path exists but is a regular file or some other non-directory entry).

Parameters

  • path (string)

Returns bool

rename()

os.rename(old_name: string, new_name: string) -> bool

Renames the file or directory specified by old_name to the name given by new_name.

If old_name and new_name are existing hard links referring to the same file, then it does nothing, and returns a success status.

If old_name specifies a directory, new_name must either not exist, or it must specify an empty directory.

If old_name refers to a symbolic link, the link is renamed; if new_name refers to a symbolic link, the link will be overwritten.

Parameters

  • old_name (string)
  • new_name (string)

Returns bool

Raises Error

chown()

os.chown(path: string, uid: int, gid: int) -> bool

Changes the owning user and group of path to uid and gid.

Parameters

  • path (string)
  • uid (int)
  • gid (int)

Returns bool

Raises Error

Note: this is a Unix-only operation; ownership isn’t a concept Windows has a direct equivalent for, so this raises there.

umask()

os.umask(mask: ?int) -> int

Gets or sets the process’s file-creation mask: the set of permission bits stripped from every file/directory this process creates from now on.

Called with no argument (or nil), returns the current mask without changing it. Called with a mask, sets it and returns whatever the previous mask was.

Parameters

  • mask (?int)

Returns int

Raises Error

Note: this is a Unix-only operation; Windows has no umask concept, and this raises there.

os.is_symlink(path: string) -> bool

Returns true if path exists and is a symbolic link, false otherwise (including when path doesn’t exist at all).

Parameters

  • path (string)

Returns bool

os.readlink(path: string) -> string

Returns the target path points to, if path is a symbolic link. Unlike os.real_path(), this reads exactly one link level and doesn’t recursively resolve further: if the target is itself a symlink, its own target is what gets returned, not the final destination.

Parameters

  • path (string)

Returns string

Raises Error if path doesn’t exist or isn’t a symbolic link.

which()

os.which(name: string) -> ?string

Searches every directory in the PATH environment variable, in order, for an executable file named name, the same way a shell decides what running a bare command name actually runs. Returns the full path to the first match, or nil if none of them have a matching executable.

Example,

%> os.which('git')
'/usr/bin/git'
%> os.which('does-not-exist')
nil

Parameters

  • name (string)

Returns ?string

glob()

os.glob(pattern: string, base_path: ?string) -> list[string]

Returns every entry under base_path (the current working directory if not given) whose path matches the glob pattern.

pattern supports * (anything except a path separator), ? (exactly one character, except a path separator), and a doubled star (anything, including path separators, for matching across nested directories).

Example,

%> os.glob('*.zu')
['helper.zu', 'main.zu']
%> os.glob('lib?.zu')
['libc.zu', 'libz.zu']

Matches are returned in the order read_dir() walks the tree, which means sorted by name within each directory, with a directory’s own matches following it.

Parameters

  • pattern (string)
  • base_path (?string) — Default is the current working directory.

Returns list[string]


2021, Richard Ore and Zuri contributors

os.path

import os

Everything here is re-exported by os, so import os is enough and the names are called as os.*. Importing os.path on its own works too and reaches the same definitions.

Path string manipulation: joining, resolving, and comparing paths. Nothing in this file touches the filesystem except where explicitly documented (real_path(), and abs_path()’s optional strict mode): everything else here is pure string handling, so it works the same whether or not the path in question actually exists.

Functions

join_paths()

os.join_paths(...paths: list) -> string

Concatenates the given paths together into a format that is valid on the current operating system.

Example,

%> os.join_paths('/home/user', 'path/to/myfile.ext')
'/home/user/path/to/myfile.ext'

Parameters

  • string... — paths

Returns string

real_path()

os.real_path(path: string) -> string

Returns the original path to a relative path.

Parameters

  • path (string)

Returns string

Note: if the path is a file, see abs_path().

abs_path()

os.abs_path(path: string, strict: ?bool) -> string

Returns the absolute form of path: relative paths are resolved against the current working directory, and “.” / “..” segments are collapsed: purely as string manipulation, with no filesystem access at all by default. Unlike real_path(), this works even when path (or any part of it) doesn’t exist on disk, and it never resolves symbolic links; use real_path() when you need the true canonical path of something that already exists.

On Windows, a path that starts at a root without naming a drive, such as \\srv\\app or /srv/app, is on the working directory’s drive, and a UNC path such as \\\\server\\share\\app keeps \\\\server\\share as its root, which .. never climbs out of.

Parameters

  • path (string)
  • strict (?bool) — Default false. When true, the resolved path is checked against the filesystem and an Error is raised if nothing exists there.

Returns string

dir_name()

os.dir_name(path: string) -> string

Returns the parent directory of the pathname pointed to by path. Any trailing / characters are not counted as part of the directory name. If path is an empty string, or contains no / characters, dir_name() returns the string “.”, signifying the current directory.

On Windows, a drive (C:) or UNC share (\\\\server\\share) at the start of path stays whole: the parent of C:\\ is C:\\ itself, the parent of C:\\work is C:\\, and the parent of C:work is C:. The root’s parent is the root, just as / is its own parent elsewhere, so a loop that walks up until the parent stops changing ends there.

Parameters

  • path (string)

Returns string

base_name()

os.base_name(path: string) -> string

The base_name() function returns the last component from the pathname pointed to by path, deleting any trailing / characters. If path consists entirely of / characters, the string ‘/’ is returned. If path is an empty string, the string ‘.’ is returned.

Parameters

  • path (string)

Returns string

relative_path()

os.relative_path(base: string, target: string) -> string

Returns the relative path from base to target: the shortest ./..-based path such that, starting inside base, following it lands on target. Both arguments are first resolved with abs_path(), so neither has to already be absolute or exist on disk.

Example,

%> os.relative_path('/home/user/project', '/home/user/project/src/main.zu')
'src/main.zu'
%> os.relative_path('/home/user/project/src', '/home/user/other/lib.zu')
'../../other/lib.zu'
%> os.relative_path('/home/user/project', '/home/user/project')
'.'

Parameters

  • base (string)
  • target (string)

Returns string

expand_user()

os.expand_user(path: string) -> string

Expands a leading ~ in path into the current user’s home directory, the same way a shell would before running a command. A bare ~ and a ~/...-prefixed path are both expanded; a path that doesn’t start with ~ is returned unchanged.

Parameters

  • path (string)

Returns string

Note: expanding another user’s home directory (~other_user/...) isn’t supported; a path in that form is returned unchanged, the same fallback Python’s os.path.expanduser uses when it can’t resolve one either.

path_contains()

os.path_contains(base: string, candidate: string) -> bool

Returns true if the resolved form of candidate is base itself or lies somewhere underneath it, and false otherwise. This includes when candidate escapes base via .. segments. Both paths are resolved with abs_path() first, so relative paths and mixed separators are handled the same way abs_path() itself handles them.

Example,

%> os.path_contains('/home/user/project', '/home/user/project/src/main.zu')
true
%> os.path_contains('/home/user/project', '/home/user/project/../../etc/passwd')
false

Parameters

  • base (string)
  • candidate (string)

Returns bool

home_dir()

os.home_dir() -> ?string

The current machine user’s home directory. If the home directory cannot be detected, it returns nil.

Returns ?string

cwd()

os.cwd() -> string

The current working directory.

Returns string

change_dir()

os.change_dir(path: string) -> bool

Navigates the working directory into the specified path.

Parameters

  • path (string)

Returns bool


2021, Richard Ore and Zuri contributors

os.process

import os

Everything here is re-exported by os, so import os is enough and the names are called as os.*. Importing os.process on its own works too and reaches the same definitions.

Process identity, subprocess execution, and signal handling.

Functions

exec()

os.exec(...args: list) -> string

Executes the given shell (or command prompt for Windows) commands and returns a dictionary containing the exit code and the output as string.

exec() accepts multiple string arguments and concatenates them together with a space in between before executing.

The returned dictionary contains the following:

  • exit_code: number - The exit code of the program. Any number other than zero (0) should be treated as a failure. - output: string - The output of the program.

exec() is the simple, blocking one-shot convenience: run a command, wait for it, get back its exit code and combined output. spawn() is for everything exec() doesn’t cover: streaming a child’s stdout/stderr as it’s produced, writing to its stdin, overriding its working directory or environment, or checking on it without blocking.

Example,

%> os.exec('ls', '-l')
{exit_code: 1, output: 'total 48
-rw-r--r--@ 1 username  staff  705 Aug 27  2021 buggy.zu
-rw-r--r--  1 username  staff  197 Mar  5 05:13 myprogram.zu'}

Parameters

  • cmd (...string)

Returns string

Note: this blocks until the command finishes and buffers its entire output in memory; for a long-running command, one you need to feed input to, or one whose output you want to process as it arrives, use spawn() instead.

exit()

os.exit(code: number)

Exit the current process and quits the Zuri runtime.

Parameters

  • code (number)

Returns

set_exit_code()

os.set_exit_code(code: number)

Records the status the process should end with, without ending it.

Unlike exit(), the program carries on: the rest of the script, every at_exit() handler, and an uncaught error’s report all still get their turn, and the status is applied once they are done.

import os

os.at_exit(@{
  echo 'one check did not pass'
  os.set_exit_code(1)
})

echo 'working'
working
one check did not pass

Parameters

  • code (number)

Note: an uncaught error still ends the program with 1, whatever was recorded here.

Note: the last call wins.

at_exit()

os.at_exit(callback: function)

Registers callback to run when the program ends.

Handlers run last registered first, whether the program reached the end of its script, called exit(), or stopped on an uncaught error. Registering several is fine, and a handler may register another one which will also run.

import os

var handle = file('report.txt', 'w')

os.at_exit(@{
  handle.close()
  echo 'report written'
})

handle.write('done')
echo 'working'
working
report written

Parameters

  • callback (function)

Note: a handler that raises is reported on standard error and the remaining handlers still run, so one failed cleanup cannot cancel the others.

Note: exit() called from inside a handler exits immediately with that code; the handlers that have not run yet do not run.

Note: a handler does not run when the process is killed by a signal nothing handled, since nothing gets to run at that point.

pid()

os.pid() -> int

The current process’s id.

Returns int

ppid()

os.ppid() -> int

The current process’s parent’s id.

Returns int

Note: Windows has no call that asks for this directly. It is answered there by searching a snapshot of the running processes for this one and reading the parent recorded against it, which yields 0 rather than raising if the snapshot cannot be taken or this process is not in it.

kill()

os.kill(pid: int, signal: ?int) -> bool

Sends signal to the process identified by pid.

Signal 0 sends nothing. It runs the same existence and permission checks every other signal does and then stops, which makes it the way to ask whether a process is still running and still yours to signal:

var alive = true

catch {
  os.kill(os.pid(), 0)
} as e {
  alive = false
}

echo alive  # true

Parameters

  • pid (int)
  • signal (?int) — Default 15 (SIGTERM), matching the kill(1) command’s own default.

Returns bool

Raises Error if pid doesn’t name a process this one has permission to signal, or if signal is one this platform cannot carry out.

Note: Windows has no way to signal a process by id, so it accepts only the numbers it can carry out exactly: 0 to test for the process, and 9 or 15 to end it. Every other number raises there rather than ending the process anyway, because ending a process outright is a different outcome from asking it to stop, not a near-enough one. A process ended this way never runs its own on_signal() callback and gets no chance to clean up. Its exit status is 128 + signal, the same number a Unix caller would have read for it.

on_signal()

os.on_signal(name: string, callback: function) -> bool

Registers callback to run when this process receives the named signal, for handling things like Ctrl+C gracefully instead of being torn down immediately.

callback takes no arguments. It runs on the main thread, between bytecode instructions: never from a background thread: so it’s safe for it to do anything ordinary Zuri code can do, including raising, which propagates the same way an uncaught error from any other code would.

What the callback returns decides what happens next

Returning a truthy value means the callback has taken responsibility for the signal, and the program carries on:

os.on_signal('INT', def {
  echo 'ignoring Ctrl+C, still working'
  return true
})

Returning anything falsy means it hasn’t, and the signal then gets the default action it would have had if nothing were registered: for 'INT', 'TERM' and 'HUP', terminating the process with the conventional 128 + signal number status. A callback that just cleans up and falls off the end returns nil, which is falsy, so it shuts down without needing to call exit():

os.on_signal('INT', def {
  echo 'shutting down...'
  flush_caches()
})

That default is deliberate. A handler that only logged and always kept running would leave Ctrl+C unable to stop the program at all, which is a much worse thing to do by accident than to shut down.

Truthiness here is the language’s own, so return 0 and return '' decline exactly as return false does. Call os.exit() when you want a specific exit status instead.

Supported signal names are 'INT' (Ctrl+C), 'TERM', 'HUP' (Unix only), and 'BREAK' (Windows only); names are matched case-insensitively, and carry no SIG prefix. Registering a new callback for a name that already has one replaces it: there’s no stacking of multiple handlers for the same signal.

On Windows

Windows has no signals. A callback registered here becomes a console control handler, so it runs for console events and nothing else: 'INT' for Ctrl+C, 'BREAK' for Ctrl+Break, and 'TERM' for the console window being closed. Nothing another process does with kill() ever reaches it, including this process calling kill() on its own id.

'TERM' is weaker there than it looks. The console is closing either way, so Windows allows the callback a few seconds and then ends the process regardless of what it returned: a truthy return cannot keep the program running, the way it can for a real SIGTERM. Treat 'TERM' on Windows as a short window to flush what matters, not as a signal you can decline.

Parameters

  • name (string)
  • callback (function)

Returns bool

Raises Error if name isn’t a recognized signal name, or isn’t supported on the current platform.

Note: only takes effect for the main thread’s own execution; code running inside an isolate worker isn’t affected by a handler registered here.

Note: a signal delivered during a blocking native call (sleep(), a blocking Process.wait(), blocking file/network I/O, …) isn’t seen until that call returns and bytecode execution resumes, the same limitation every cooperative signal-handling system has.

spawn()

os.spawn(cmd: string, args: ?list, options: ?dict) -> Process

Spawns cmd as a new subprocess and returns a Process handle to it immediately, without waiting for it to finish.

cmd names a program to run, not a command line for a shell to interpret. Nothing here expands a variable, splits on whitespace, follows a pipe or knows what a shell builtin is, so a name only a shell would recognise raises rather than running: echo is a builtin on Windows with no executable behind it, and cat is not present there at all. Use exec() when a shell is what you want.

options may include:

  • cwd: ?string: the child’s working directory. Defaults to this process’s own current working directory.
  • env: ?dict: extra environment variables for the child, merged into a copy of this process’s own environment by default (set env_replace: true for the child to receive only what’s in env, nothing inherited).
  • env_replace: ?bool: see env above. Default false.
  • stdin, stdout, stderr: ?string, one of 'pipe' (readable/writable through the returned Process), 'inherit' (shares this process’s own stream), or 'null' (discarded). stdin defaults to 'inherit'; stdout and stderr default to 'pipe'. Do not confuse 'null' with nil. 'null' alludes to /dev/null on unix devices which has the same behavior.

Example,

%> var p = os.spawn('grep', ['zuri'], { stdin: 'pipe' })
%> p.write_stdin('hello zuri\nhello world\n')
true
%> p.close_stdin()
%> p.read_stdout().to_string()
hello zuri

%> p.wait()
0

Parameters

  • cmd (string)
  • args (?list) — The command’s own arguments (not shell- interpreted the way exec()’s combined string is: each element is passed to the child as one literal argument).
  • options (?dict)

Returns Process

Raises Error if cmd couldn’t be spawned at all (not found, no permission, …).

Classes

Process

class os.Process

A running (or finished) child process created by spawn().

Note: constructed only by spawn() itself: there’s no meaningful way to build one by hand since there’s no “spawn it later” step to defer.

Constructor

os.Process(_ptr)

Process.write_stdin()

os.Process.write_stdin(data) -> bool

Writes data to the child’s stdin.

Parameters

  • data (string|bytes)

Returns bool

Raises Error if this process’s stdin wasn’t opened with stdin: 'pipe', or has already been closed via close_stdin().

Process.close_stdin()

os.Process.close_stdin()

Closes the write half of this process’s stdin, so the child sees EOF on it. Needed for any child that reads its own stdin until EOF: the pipe only actually closes once this is called (or the Process itself is garbage collected).

Returns

Process.read_stdout()

os.Process.read_stdout(length: ?int) -> bytes

Reads from the child’s stdout. While the child runs, this blocks until at least one byte has been produced or its stdout has hit EOF, and returns what has arrived so far. Once wait() or try_wait() has seen the child exit, it returns everything the child wrote that has not been read yet, waiting for its stdout to hit EOF first.

Parameters

  • length (?int) — The maximum number of bytes to return; all available output if not given.

Returns bytes

Raises Error if this process’s stdout wasn’t opened with stdout: 'pipe'.

Process.read_stderr()

os.Process.read_stderr(length: ?int) -> bytes

Same as read_stdout(), for the child’s stderr instead.

Parameters

  • length (?int)

Returns bytes

Raises Error if this process’s stderr wasn’t opened with stderr: 'pipe'.

Process.wait()

os.Process.wait(timeout_ms: ?int)

Blocks until the child exits (or timeout_ms elapses, whichever comes first) and returns its exit code.

Parameters

  • timeout_ms (?int) — Wait indefinitely if not given.

Returns — ?int: The exit code, or nil if timeout_ms elapsed before the child exited.

Process.try_wait()

os.Process.try_wait()

Checks whether the child has exited yet, without blocking.

Returns — ?int: The exit code if it has already exited, nil if it’s still running.

Process.kill()

os.Process.kill(signal: ?int) -> bool

Sends signal to this child process.

Parameters

  • signal (?int) — Default 15 (SIGTERM).

Returns bool

Raises Error if signal is one this platform cannot carry out.

Note: Windows accepts only the numbers it can carry out exactly, the same three the free kill() function takes there: 0, 9 and 15. Anything else raises. Signal 0 is a no-op on a child, since holding this handle is already proof the process exists.

Process.pid()

os.Process.pid() -> int

This child process’s id.

Returns int


2021, Richard Ore and Zuri contributors

os.system

import os

Everything here is re-exported by os, so import os is enough and the names are called as os.*. Importing os.system on its own works too and reaches the same definitions.

Facts about the current process, the Zuri runtime, and the machine it’s running on.

Constants

version

os.version: string

The current Zuri version.

target

os.target: string

The platform the running runtime was built for, as a target triple: x86_64-unknown-linux-gnu, aarch64-apple-darwin, x86_64-pc-windows-msvc and so on. Release archives are named by it, which is what makes it the way to find a runtime for this machine.

%> os.target
'x86_64-unknown-linux-gnu'

vm_version

os.vm_version: string

The current Zuri VM version.

platform

os.platform: string

The name of the current platform in string or unknown if the platform name could not be determined.

Example,

%> import os
%> os.platform
'osx'

args

os.args: list

The command line, as the running script sees it.

The first entry is the path to the Zuri executable and the second is the script being run. Everything after those two was meant for the script itself, so a program reads its own arguments from args[2,] and gets the same list whether it was launched with zuri run or as a command.

%> import os
%> os.args
['/usr/local/bin/zuri', '/home/ada/greet.zu', '--name', 'Ada']

The args module reads this for you, and reading it directly is only worth it for the simplest of programs.

path_separator

os.path_separator: string

The standard path separator for the current operating system.

exe_path

os.exe_path: string

The full path to the running Zuri executable.

Functions

sleep()

os.sleep(duration: number)

Causes the current thread to sleep for the specified number of seconds.

Parameters

  • duration (number)

info()

os.info() -> dict

Returns information about the current operation system and machine as a dictionary. The returned dictionary will contain:

  • sysname: The name of the operating system - nodename The name of the current machine - version: The operating system version - release: The release level/version - machine: The hardware/processor type.

Example,

%> os.info()
{sysname: Darwin, nodename: MacBook-Pro.local, version: Darwin Kernel Version
21.1.0: Wed Oct 13 17:33:24 PDT 2021; root:xnu-8019.41.5~1/RELEASE_ARM64_T8101,
release: 21.1.0, machine: arm64}

Returns dict

num_cpus()

os.num_cpus() -> int

The number of logical CPUs available to this process. Useful for sizing a worker pool: isolate.cpu_count() is the exact same underlying value, exposed here too so code that only needs this one fact doesn’t have to import the whole isolate module for it.

Returns int

hostname()

os.hostname() -> string

The current machine’s hostname.

Returns string

Raises Error if the hostname couldn’t be determined.

total_memory()

os.total_memory() -> number

The total physical memory installed on this machine, in bytes.

Returns number

Raises Error if this isn’t supported on the current platform.

free_memory()

os.free_memory() -> number

An estimate of how much physical memory is available for new allocations right now, in bytes: free memory plus easily reclaimable caches/buffers, the same figure a tool like free reports as “available” rather than the smaller, less useful “free” figure alone.

Returns number

Raises Error if this isn’t supported on the current platform.

uptime()

os.uptime() -> number

How long the machine has been running since it last booted, in seconds.

Returns number

Raises Error if this isn’t supported on the current platform.


2021, Richard Ore and Zuri contributors

os.tempfile

import os

Everything here is re-exported by os, so import os is enough and the names are called as os.*. Importing os.tempfile on its own works too and reaches the same definitions.

Locating the platform’s temporary-file directory and creating uniquely named scratch files/directories inside it without a check-then-create race: create_temp_file()/create_temp_dir() reserve the path atomically (the same guarantee the Unix mkstemp() family gives), so two concurrent callers can never be handed the same path.

Functions

temp_dir()

os.temp_dir() -> string

The platform’s directory for temporary files (e.g. /tmp on Unix, whatever %TEMP% points to on Windows).

Returns string

create_temp_file()

os.create_temp_file(prefix: ?string, suffix: ?string) -> string

Atomically creates a new, empty, uniquely named file inside temp_dir() and returns its path. The file is created (and left open-then-closed) purely to reserve the name without a race between checking whether it exists and creating it: this function only hands back the path; open it yourself with file() to actually read or write it.

Parameters

  • prefix (?string) — Default ''.
  • suffix (?string) — Default ''.

Returns string

Raises Error if a unique name couldn’t be found, or the file couldn’t be created.

Note: the caller is responsible for deleting the file (via file(path).delete()) once it’s no longer needed; nothing cleans temp files up automatically.

create_temp_dir()

os.create_temp_dir(prefix: ?string) -> string

Atomically creates a new, empty, uniquely named directory inside temp_dir() and returns its path.

Parameters

  • prefix (?string) — Default ''.

Returns string

Raises Error if a unique name couldn’t be found, or the directory couldn’t be created.

Note: the caller is responsible for removing the directory (via os.remove_dir(path, true)) once it’s no longer needed; nothing cleans temp directories up automatically.


2021, Richard Ore and Zuri contributors

env

import env

Configuration from a file, in the environment, in the type you wanted.

A program’s configuration belongs in its environment, and a developer’s machine has nowhere convenient to put it. env closes that gap: it reads a .env file into the process environment at startup, and it reads values back out already converted to the number, boolean or list the program is going to use.

import env

env.load()

var port = env.int('PORT', 8080)
var debug = env.bool('DEBUG', false)
var secret = env.require('SESSION_SECRET')

The file

A .env file sits beside the program and is not committed. One name per line:

# The address to bind on.
HOST=127.0.0.1
PORT=8080

DATABASE_URL="postgres://app:secret@${HOST}/app"
SESSION_SECRET='r4w $tring, taken exactly as written'

PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqh...
-----END PRIVATE KEY-----"

Unquoted values are trimmed and run to a comment or the end of the line. Double quotes resolve backslash escapes and allow $ references. Single quotes and backticks take every character exactly as written. Values in any of the three quotes may span lines. parse() states the grammar in full.

What already exists wins

A name that is already set in the process environment is left alone, and the file fills in the rest. This is the whole point of the default: the file carries what a developer needs to run the program at all, and production sets the real values through the shell, the orchestrator or the CI runner without the file having to know.

Result says exactly what happened, and override() reverses the rule where a file genuinely should win:

var result = env.loader().path('.env').override().load()

echo result.applied   # the names this load set
echo result.skipped   # the names that were already set

Throughout the module, a variable set to the empty string counts as unset. FLAG= in a file, an empty shell variable and a name nobody ever set all mean the same thing, which is the one thing anybody means by them.

References between values

Values may refer to other values and to the environment around them, with the shell’s syntax and three of its modifiers:

HOST=localhost
PORT=${PORT:-8080}
PUBLIC_URL=http://${HOST}:${PORT}
CACHE_DIR=${XDG_CACHE_HOME:-${HOME}/.cache}/app
SESSION_SECRET=${SESSION_SECRET:?the deploy must supply a session secret}

A reference resolves to the value the name will hold once the load has finished, so PORT=${PORT:-8080} reads “whatever the environment already says, and 8080 when it says nothing” no matter which line of the file defines it. \$ is a literal dollar sign, and a value in single quotes never expands at all.

Layering files

Sources are read in the order they are added, and the last one to define a name defines it:

env.loader()
  .path('.env')         # what the team shares, committed as .env.example
  .path('.env.local')   # what this machine does differently
  .load()

source() adds text instead of a file, so configuration that arrived from a secret store or a test fixture goes through the same parsing, expansion and precedence.

Reading values back

get() and require() answer with text. int(), float(), bool() and list() answer with the type and raise a ValueError naming the variable when the text is not one:

var port    = env.int('PORT', 8080)
var timeout = env.float('REQUEST_TIMEOUT', 2.5)
var debug   = env.bool('DEBUG', false)
var origins = env.list('CORS_ORIGINS', ',', [])

They read the process environment, not the file, so they answer the same whether a value came from .env or from the shell. That is what makes the same program run unchanged in both places.

Do not commit it

A .env file holds the values that differ between one deployment and the next, which is to say it holds the secrets. Commit a .env.example with every name and no real value, add .env to .gitignore, and let required() fail loudly on a machine where the file was never made.

The env API

Every public name in env, wherever it is declared. Each links to the page that documents it.

NameKindSummary
env.EnvErrorclassBase class for every error this module raises.
env.LoaderclassBuilds a load: the sources it reads and the four decisions it makes about them.
env.MissingFileclassA file the loader was told to require is not there.
env.MissingVariableclassA variable a program said it needed is not configured.
env.ParseErrorclassA source could not be parsed.
env.ResultclassWhat a load did.
env.boolfunctionReturns the value of name as a boolean.
env.expand.expandfunctionReplaces every $ reference in value with what resolve answers, and returns the result.
env.floatfunctionReturns the value of name as a number.
env.getfunctionReturns the value of name, or default_value when it is unset or empty.
env.hasfunctionReturns true when name is set to a non-empty value.
env.intfunctionReturns the value of name as an integer.
env.is_namefunctionReturns true when name is a legal environment variable name.
env.listfunctionReturns the value of name split into a list.
env.loadfunctionReads an environment file and writes what it holds into the process environment.
env.loaderfunctionReturns a new Loader, for a load that needs more than the default.
env.parsefunctionReads the text of an environment file and returns its names and values.
env.requirefunctionReturns the value of name, and raises when it is unset or empty.
env.stringifyfunctionTurns a dictionary of names and values into the text of an environment file.

Submodules

ModuleReached asSummary
env.errorsenv.errors.*Every error the env module raises, under one root.
env.expandimport env.expandResolving the $ references inside a value.
env.loadingenv.loading.*The loader: which sources to read, in what order, and what to do with the names that come out of them.
env.parserenv.parser.*The scanner that turns the text of an environment file into names and values, and the writer that turns them…
env.valuesenv.values.*Reading configuration back out of the process environment, in the type the program actually wants.

Functions

loader()

env.loader() -> Loader

Returns a new Loader, for a load that needs more than the default.

import env

var result = env.loader()
  .path('.env')
  .path('.env.local')
  .required()
  .load()

Returns Loader

load()

env.load(path: ?string) -> Result

Reads an environment file and writes what it holds into the process environment.

The one line most programs need, and the first line most of them run. path defaults to .env in the current working directory, a leading ~ is expanded, and a file that is not there leaves the environment as it stands.

Names already set to a non-empty value are left alone. Loader is where that, and everything else about a load, can be changed.

import env

env.load()
env.load('config/production.env')

Parameters

  • path (?string) — Defaults to '.env'.

Returns Result

Raises ParseError when the file is not a valid environment file.

Raises MissingVariable when a ${NAME:?reason} reference finds nothing.


2026, Richard Ore and Zuri contributors

env.errors

import env.errors

env lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled env.errors.* needs import env.errors.

Every error the env module raises, under one root.

EnvError is that root. The three subclasses separate the cases worth handling differently: a file that will not parse, a file that was demanded and is not there, and a variable a program cannot run without.

Each one carries the detail a message alone cannot. ParseError knows the line and column it stopped at, MissingFile knows the path it looked for, and MissingVariable knows the name it wanted.

Classes

EnvError

class env.EnvError < Error

Base class for every error this module raises.

Catch this to catch anything env can do.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

env.EnvError(message)

EnvError.to_string()

env.EnvError.to_string()

ParseError

class env.ParseError < EnvError

A source could not be parsed.

line and column are both 1-based and point at the character the parser gave up on, which is where the fix goes.

catch {
  env.parse('12FACTOR=yes')
} as error {
  echo '${error.message} (line ${error.line}, column ${error.column})'
}
  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
linenumberThe 1-based line the parser stopped at.
columnnumberThe 1-based column within that line.

Constructor

env.ParseError(message: string, line: number, column: number)

Parameters

  • message (string)
  • line (number)
  • column (number)

ParseError.to_string()

env.ParseError.to_string()

MissingFile

class env.MissingFile < EnvError

A file the loader was told to require is not there.

Only Loader.required() produces this. A loader left at its default treats an absent file as an empty one and records the path in Result.missing instead.

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
pathstringThe path that was looked for, after ~ was expanded.

Constructor

env.MissingFile(path: string)

Parameters

  • path (string)

MissingFile.to_string()

env.MissingFile.to_string()

MissingVariable

class env.MissingVariable < EnvError

A variable a program said it needed is not configured.

Raised by require() and by the ${NAME:?reason} form during expansion.

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
namestringThe name of the variable that was wanted.

Constructor

env.MissingVariable(name: string, message: ?string)

Parameters

  • name (string)
  • message (?string) — A reason to use instead of the default text.

MissingVariable.to_string()

env.MissingVariable.to_string()

2026, Richard Ore and Zuri contributors

env.expand

import env.expand

env does not re-export this module, so it is reached only by importing it directly.

Resolving the $ references inside a value.

The syntax is the shell’s, because that is what a .env file looks like to everyone who has ever written one, and the modifiers are the four from POSIX parameter expansion that earn their place in a configuration file: a fallback, an override, and a demand.

Nothing here reads the environment itself. Every lookup goes through the resolve function the caller supplies, which is what lets the loader answer with the value a name is about to have rather than the one it has now.

Functions

expand()

env.expand.expand(value: string, resolve: function) -> string

Replaces every $ reference in value with what resolve answers, and returns the result.

resolve is called with one variable name and returns its value, or nil when the name has none. Everything the caller wants to decide about precedence, recursion and cycles is decided inside it.

The forms understood are $NAME, ${NAME}, and ${NAME} carrying one of three modifiers:

WrittenMeans
${NAME:-fallback}the value, or fallback when it is unset or empty
${NAME-fallback}the value, or fallback when it is unset
${NAME:+instead}instead when the value is set and not empty, otherwise nothing
${NAME+instead}instead when the value is set, otherwise nothing
${NAME:?reason}the value, or a MissingVariable carrying reason
${NAME?reason}the same, counting an empty value as set

The text after a modifier is itself expanded, so ${HOST:-${FALLBACK_HOST}} does what it looks like. A $ that names nothing stands for itself, and \$ is always a literal dollar sign.

Parameters

  • value (string)
  • resolve (function)

Returns string

Raises EnvError when a reference is not closed or carries a modifier that does not exist.

Raises MissingVariable when a ${NAME:?reason} reference finds nothing.


2026, Richard Ore and Zuri contributors

env.loading

import env.loading

env lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled env.loading.* needs import env.loading.

The loader: which sources to read, in what order, and what to do with the names that come out of them.

A load is three steps that stay separate. The sources are scanned into names and values, the $ references among them are resolved, and then the result is written into the process environment. Calling read() instead of load() stops after the second, which is what makes a .env file inspectable without anything being set.

Classes

Result

class env.Result

What a load did.

values is everything the sources defined, after expansion, in the order the names were first seen. The three lists beside it account for each name and each file, so a program never has to guess whether its configuration actually arrived.

var result = env.load()

if result.files.is_empty() {
  log.warn('no .env file was found; using the environment as it stands')
}

for name in result.skipped {
  log.debug('${name} was already set, so the file did not change it')
}
  • printable — has a @to_string(), so echo and print() show something useful

Constructor

env.Result()

Result.to_string()

env.Result.to_string()

Loader

class env.Loader

Builds a load: the sources it reads and the four decisions it makes about them.

Every setting returns the loader, so a whole configuration is one expression:

import env

var result = env.loader()
  .path('.env')
  .path('.env.local')
  .override()
  .load()

Sources are read in the order they were added and layered on top of one another, so the last source to define a name is the one that defines it. That is what makes the pair above work: .env holds what the team shares and .env.local holds what one machine does differently.

A loader is reusable. Nothing about it changes when load() runs, so the same one can be kept and run again.

Constructor

env.Loader()

Loader.path()

env.Loader.path(path: string) -> Loader

Adds a file to read.

A leading ~ is expanded to the home directory, and a relative path is resolved against the current working directory. A file that is not there is not an error unless required() says so.

Parameters

  • path (string)

Returns Loader

Loader.paths()

env.Loader.paths(paths: list) -> Loader

Adds several files to read, in the order given.

Parameters

  • paths (list)

Returns Loader

Loader.source()

env.Loader.source(text: string) -> Loader

Adds text to read, as though it were the contents of a file.

This is how configuration that arrived over the network, out of a secret store, or from a test fixture goes through exactly the same parsing, expansion and precedence as a file on disk.

Parameters

  • text (string)

Returns Loader

Loader.override()

env.Loader.override(enabled: ?bool) -> Loader

Sets whether a name already present in the process environment is replaced by the one the sources define.

Off by default, which is what makes a .env file a set of defaults: whatever the shell, the orchestrator or the CI runner already set survives, and the file fills in the rest. Turn it on and the file wins instead.

A variable set to the empty string counts as unset either way.

Parameters

  • enabled (?bool) — Defaults to true when the argument is left out.

Returns Loader

Loader.expand()

env.Loader.expand(enabled: ?bool) -> Loader

Sets whether $ references inside values are resolved.

On by default. Turn it off and every value is used exactly as it was written, which is what a file of opaque secrets wants.

Parameters

  • enabled (?bool) — Defaults to true when the argument is left out.

Returns Loader

Loader.required()

env.Loader.required(enabled: ?bool) -> Loader

Sets whether a file that is not there is an error.

Off by default, because the same program usually runs both on a laptop with a .env file and on a server where the orchestrator supplies the environment directly. Turn it on where the file is genuinely part of the deployment and its absence should stop the program rather than surface later as a missing setting.

Parameters

  • enabled (?bool) — Defaults to true when the argument is left out.

Returns Loader

Raises MissingFile from load() and read() when a file is absent.

Loader.read()

env.Loader.read() -> Result

Reads the sources and returns what they hold, without touching the process environment.

$ references still resolve, and they resolve the same way they would during a real load, so what comes back is what load() would have set.

The applied and skipped lists on the result are empty, because nothing was applied and nothing was skipped.

Returns Result

Raises ParseError when a source is not a valid environment file.

Raises MissingFile when required() is on and a file is absent.

Loader.load()

env.Loader.load() -> Result

Reads the sources and writes what they hold into the process environment.

A name already set to a non-empty value is left alone and recorded in Result.skipped, unless override() is on. Everything else is set and recorded in Result.applied.

import env

env.loader().path('.env').required().load()

Returns Result

Raises ParseError when a source is not a valid environment file.

Raises MissingFile when required() is on and a file is absent.

Raises MissingVariable when a ${NAME:?reason} reference finds nothing.


2026, Richard Ore and Zuri contributors

env.parser

import env.parser

env lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled env.parser.* needs import env.parser.

The scanner that turns the text of an environment file into names and values, and the writer that turns them back into text.

The grammar is small enough to state in full. A source is a sequence of lines. A line is blank, a comment, or an assignment:

assignment := 'export'? NAME '=' value?
NAME       := [A-Za-z_][A-Za-z0-9_]*
value      := bare | '...' | "..." | `...`

Everything interesting is in what each kind of value means, and that is documented on parse() where people will look for it.

Functions

is_name()

env.is_name(name: string) -> bool

Returns true when name is a legal environment variable name.

That means a letter or underscore, then any number of letters, digits and underscores. It is the same rule POSIX shells apply, and the same one the parser and stringify() enforce.

Parameters

  • name (string)

Returns bool

parse()

env.parse(source: string|bytes) -> dict

Reads the text of an environment file and returns its names and values.

This is the syntax layer on its own: no $ reference is resolved and nothing is written to the process environment. load() is what does both.

import env

var values = env.parse('PORT=8080\nGREETING="hello there"')

echo values.PORT
echo values.GREETING
8080
hello there

The rules

A name is a letter or underscore followed by letters, digits and underscores. Anything else is a ParseError rather than a line quietly dropped, because a name a shell could never export is a typo every time.

Blank lines are skipped, and so is everything from a # to the end of the line. Inside an unquoted value a # only opens a comment when it starts the value or follows a space, so KEY=db#1 keeps its #.

A leading export is allowed and ignored, so the same file can be fed to source in a shell.

Values come in four forms:

WrittenMeans
KEY=valuethe text up to a comment or the end of the line, trimmed at both ends
KEY='value'every character exactly as written, newlines included
KEY=`value`the same as single quotes, for values that contain both kinds
KEY="value"backslash escapes resolved, $ references left for expansion

KEY= is an empty value, and an empty value counts as unconfigured everywhere else in this module.

Inside double quotes, \n, \r, \t, \f, \v, \b, \a, \e and \0 mean what they do in Zuri, \xHH, \uHHHH and \u{H...} name a codepoint, a backslash before a newline joins the two lines, and \$ is a literal $ that expansion will not touch. A backslash before anything else keeps both characters, so "C:\Users" survives intact.

Line endings are normalised to \n, and a byte-order mark at the head of the file is discarded.

Duplicates

The last assignment to a name in a source wins, which is what makes commenting out the line above the one you want work.

Parameters

  • source (string|bytes)

Returns dict

Raises ParseError when the source is not a valid environment file.

stringify()

env.stringify(values: dict) -> string

Turns a dictionary of names and values into the text of an environment file.

The output round-trips: feeding it back to parse() returns the same names and the same values. Values are written bare where that is unambiguous and double-quoted where it is not, and a $ inside a value is escaped so that loading the file back does not expand it.

import env

print(env.stringify({
  PORT: 8080,
  GREETING: 'hello there',
  DEBUG: false,
}))
PORT=8080
GREETING="hello there"
DEBUG=false

Names are written in the order the dictionary holds them. A number, a bigint, a boolean or a bytes value is converted to its text; any other type is a TypeError, because guessing what a list should look like in an environment file is how configuration goes wrong silently.

Parameters

  • values (dict)

Returns string

Raises ValueError when a key is not a legal variable name.

Raises TypeError when a value is of a type that has no spelling here.


2026, Richard Ore and Zuri contributors

env.values

import env.values

env lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled env.values.* needs import env.values.

Reading configuration back out of the process environment, in the type the program actually wants.

An environment variable is always text. A port is a number, a feature flag is a boolean, and an allow-list is a list, so somewhere that text has to be checked and converted. Doing it here means it is done the same way everywhere, and that a PORT=eighty is a loud error at startup rather than a silent zero halfway through the first request.

Empty means unconfigured

Every function here treats a variable set to the empty string exactly as it treats one that was never set at all: the default applies, and require() raises. This is the same rule ${NAME:-...} follows, and it is what makes a FLAG= line, an unset shell variable and an orchestrator passing an empty value all mean one thing instead of three.

os.get_env() is the unfiltered view for the rare case that needs to tell those apart.

Functions

has()

env.has(name: string) -> bool

Returns true when name is set to a non-empty value.

if env.has('SENTRY_DSN') {
  reporting.enable(env.require('SENTRY_DSN'))
}

Parameters

  • name (string)

Returns bool

get()

env.get(name: string, default_value) -> ?any

Returns the value of name, or default_value when it is unset or empty.

var host = env.get('HOST', '127.0.0.1')

Parameters

  • name (string)
  • default_value (?any) — Returned unchanged; nil when not given.

Returns ?any

require()

env.require(name: string, message: ?string) -> string

Returns the value of name, and raises when it is unset or empty.

This is the line to write for a setting the program cannot invent a default for. Failing here, at startup, beats failing later at the first request that needed it.

var secret = env.require('SESSION_SECRET')

Parameters

  • name (string)
  • message (?string) — A reason to report instead of the default text.

Returns string

Raises MissingVariable when name is unset or empty.

int()

env.int(name: string, default_value: ?number) -> ?number

Returns the value of name as an integer.

The value must be a decimal integer, optionally signed. Anything else is a ValueError naming the variable, because a port of eighty is a mistake in the configuration and not a reason to fall back to a default.

var port = env.int('PORT', 8080)

Parameters

  • name (string)
  • default_value (?number) — Returned when name is unset or empty.

Returns ?number

Raises ValueError when the value is not a decimal integer.

float()

env.float(name: string, default_value: ?number) -> ?number

Returns the value of name as a number.

Decimals and exponents are both accepted. Anything else is a ValueError naming the variable.

var timeout = env.float('REQUEST_TIMEOUT', 2.5)

Parameters

  • name (string)
  • default_value (?number) — Returned when name is unset or empty.

Returns ?number

Raises ValueError when the value is not a number.

bool()

env.bool(name: string, default_value: ?bool) -> ?bool

Returns the value of name as a boolean.

1, true, yes, y and on are true; 0, false, no, n and off are false. Case and surrounding whitespace do not matter. Anything else is a ValueError, so a DEBUG=maybe is caught rather than quietly read as true.

if env.bool('DEBUG', false) {
  log.set_level(log.Debug)
}

Parameters

  • name (string)
  • default_value (?bool) — Returned when name is unset or empty.

Returns ?bool

Raises ValueError when the value is not one of the spellings above.

list()

env.list(name: string, separator: ?string, default_value: ?list) -> ?list

Returns the value of name split into a list.

The value is split on separator, each item loses the whitespace around it, and empty items are dropped, so A, B,,C and A,B,C give the same three items.

var origins = env.list('CORS_ORIGINS', ',', [])

Parameters

  • name (string)
  • separator (?string) — Defaults to ','.
  • default_value (?list) — Returned when name is unset or empty.

Returns ?list


2026, Richard Ore and Zuri contributors

io

import io

This module provides interfaces for working with to I/O stream and TTYs as well as expose the operating system standard I/O for easy access.

Some I/O operations that should belong to this module have been merged as core features and offered as built-in functions for Zuri. Specifically file I/O features that can be accessed via the built-in file() function.

The standard I/O streams are also files and you can call almost all file methods on them. Whenever a file method is not supported, you’ll get an error message telling you that such operation is not supported for standard streams.

Example

The following example shows how to use the io module for accepting user name and printing the result.

import io

var name = io.readline('What is your name?')
echo name

The io API

Every public name in io, wherever it is declared. Each links to the page that documents it.

NameKindSummary
io.BytesIOclassThe BytesIO class implements a bytearray based I/O system that allows you use treat bytearray (bytes) as if…
io.SEEK_CURconstantSet I/O position from the current position.
io.SEEK_ENDconstantSet I/O position from the end.
io.SEEK_SETconstantSet I/O position from the beginning.
io.TTYclassclass TTY is an interface to TTY terminals this class contains definitions to control TTY terminals
io.capturefunctionRuns body with standard output captured, and returns everything it printed.
io.capture_beginfunctionBegins capturing standard output.
io.capture_depthfunctionThe number of capture frames currently open.
io.capture_endfunctionEnds the innermost capture and returns everything it collected.
io.flushfunctionFlushes the content of the given file handle
io.getcfunctionReads character(s) from standard input.
io.getchfunctionReads a single character from standard input without printing to standard output.
io.is_replconstantReturns true if the current environment is the Zuri REPL, false otherwise.
io.putcfunctionWrites character c to the screen.
io.readlinefunctionReads an entire line from standard input.
io.stderrconstantStderr is a file handle to the standard error file of the system.
io.stdinconstantStdin is a file handle to the standard input file of the system.
io.stdoutconstantStdout is a file handle to the standard output file of the system.

Submodules

ModuleReached asSummary
io.bytesioio.bytesio.*BytesIO: an in-memory buffer that behaves like a file.
io.ttyio.tty.*TTY: control over a terminal attached to a file handle.

Constants

SEEK_SET

io.SEEK_SET: int = 0

Set I/O position from the beginning.

SEEK_CUR

io.SEEK_CUR: int = 1

Set I/O position from the current position.

SEEK_END

io.SEEK_END: int = 2

Set I/O position from the end.

stdin

io.stdin: file

Stdin is a file handle to the standard input file of the system.

It is opened in binary mode ('rb'), so stdin.read() and stdin.gets() return bytes, never a string. Whatever is piped into a program is arbitrary data, and decoding it as text before the program has asked for text can only lose information.

Call .to_string() on the result when the input really is text:

import io

var raw = io.stdin.read()          # bytes
var text = raw.to_string()         # decoded, replacing bad bytes

to_string() substitutes U+FFFD for undecodable bytes rather than raising. To reject malformed input instead, inspect the bytes yourself before decoding.

stdout

io.stdout: file

Stdout is a file handle to the standard output file of the system.

Writes accept either a string or bytes, so unlike stdin there is no text/binary distinction to make here.

stderr

io.stderr: file

Stderr is a file handle to the standard error file of the system.

is_repl

io.is_repl: bool

Returns true if the current environment is the Zuri REPL, false otherwise.

Functions

capture_begin()

io.capture_begin()

Begins capturing standard output.

From this call until the matching capture_end(), everything Zuri writes to standard output is collected into a buffer instead of reaching the terminal. That covers echo, print(), and writes through io.stdout. It does not cover io.stderr, output written by a child process started with os.spawn(), or bytes a native module writes straight to the descriptor.

Captures nest. Each capture_begin() opens a new frame and the innermost open frame receives everything, so an inner capture never leaks into the one around it.

Capture is per-thread: one opened inside an isolate collects only that isolate’s own output.

A frame that is never closed is flushed to the terminal when the program ends, so output is never silently lost. Prefer capture() unless you need the output of a body that raises.

import io

io.capture_begin()
echo 'not printed yet'
var text = io.capture_end()

echo 'captured: ' + text.trim('\n')
captured: not printed yet

capture_end()

io.capture_end()

Ends the innermost capture and returns everything it collected.

The text comes back exactly as written, trailing newline included. Bytes that are not valid UTF-8 decode to U+FFFD rather than raising, so a whole frame is never lost over one stray byte.

Returns — string: the captured output, or nil when no capture was open.

capture_depth()

io.capture_depth() -> int

The number of capture frames currently open. 0 means output is going to the terminal.

Returns int

capture()

io.capture(body: function) -> string

Runs body with standard output captured, and returns everything it printed.

import io

def greet(name) {
  echo 'Hello, ${name}!'
}

var out = io.capture(@{ greet('Ada') })
echo out.length()
12

Parameters

  • body (function)

Returns string

Note: when body raises, the capture is closed and the error is re-raised, but whatever was collected before it is discarded. Use capture_begin()/capture_end() around your own catch when you need both.

flush()

io.flush(file)

Flushes the content of the given file handle

putc()

io.putc(c)

Writes character c to the screen.

Parameters

  • char|number — c

getc()

io.getc(length)

Reads character(s) from standard input.

When length is given, gets length number of characters else, gets a single character.

length counts bytes, not codepoints, so asking for fewer bytes than a multi-byte character occupies yields a replacement character rather than half of one.

Returns — char|string

getch()

io.getch()

Reads a single character from standard input without printing to standard output.

Returns — char|string

readline()

io.readline(message, secure, obscure_text) -> string

Reads an entire line from standard input. If a message is given, the message will be printed before it begins to wait for a user input. If secure is true, the user’s input will not be printing and obscure_text will be printed instead.

Parameters

  • message (?string)
  • secure (?bool)
  • obscure_text (?string) — Default value is *.

Returns string

Note: Newlines will not be added automatically for messages.


2021, Richard Ore and Zuri contributors

io.bytesio

import io.bytesio

io lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled io.bytesio.* needs import io.bytesio.

BytesIO: an in-memory buffer that behaves like a file.

Anything that reads or writes through a file handle can be pointed at one of these instead, which is what makes a function that writes to disk testable without touching the disk.

Classes

BytesIO

class io.BytesIO

The BytesIO class implements a bytearray based I/O system that allows you use treat bytearray (bytes) as if they were a file.

The class implements the essentials of a file except those that ties it to the operating system filesystem such as symbolic links, chmod and set time.

See the chapter on files in The Zuri Programming Language for more information.

Constructor

io.BytesIO(source, mode)

Returns a new instance of BytesIO

Parameters

  • source (bytes)
  • mode (string) — The I/O open mode - Default is r

BytesIO.exists()

io.BytesIO.exists() -> bool

Returns true as BytesIO always exist.

Returns bool

BytesIO.close()

io.BytesIO.close()

Closes the stream to an opened BytesIO. You’ll rarely ever need to call this method yourself in most use cases.

BytesIO.open()

io.BytesIO.open() -> bool

Opens the stream to a BytesIO for the operation originally specified on the BytesIO object during creation.

You may need to call this method after a call to read() if the length isn’t specified or write() if you wish to read or write again as the BytesIO will already be closed.

Returns bool

BytesIO.read()

io.BytesIO.read(length) -> bytes

Reads the content of an opened BytesIO up to the specified length and returns it as string or bytes if the BytesIO was opened in the binary mode. If the length is not specified, the BytesIO will be read to the end.

This method requires that the BytesIO be opened in the read mode (default mode) or a mode that supports reading. If you aren’t reading the full length of the BytesIO, you’ll need to call the close() method to free the BytesIO for further reading, otherwise, the close() method will be automatically called for you.

Parameters

  • length (number) — Default = -1

Returns bytes

Raises Error

BytesIO.gets()

io.BytesIO.gets(length) -> bytes

Same as read(), but doesn’t open or close the BytesIO automatically.

Parameters

  • length (number) — Default = -1

Returns bytes

Raises Error

BytesIO.write()

io.BytesIO.write(data) -> number

Writes a string or bytes to an opened BytesIO at the current insertion point. When the BytesIO is opened with the a mode enabled, write will always start from the end of the BytesIO.

If the seek() method has been previously called, write will begin from the seeked position, otherwise it will start at the beginning of the BytesIO.

Parameters

  • `` (bytes|string)

Returns number

BytesIO.puts()

io.BytesIO.puts(data) -> number

Same as write(), but doesn’t open or close the BytesIO automatically.

Parameters

  • `` (bytes|string)

Returns number

BytesIO.number()

io.BytesIO.number() -> number

Returns the integer file descriptor number that is used by the underlying implementation to request I/O operations from the operating system. This can be very useful for low-level interfaces that uses or act as BytesIO descriptors.

Returns number

BytesIO.is_tty()

io.BytesIO.is_tty() -> bool

Always returns false as a BytesIO is not a TTY device.

Returns bool

BytesIO.is_open()

io.BytesIO.is_open() -> bool

Returns true if the BytesIO is open for reading or writing and false otherwise.

Returns bool

BytesIO.is_closed()

io.BytesIO.is_closed()

Returns true if the BytesIO is closed for reading or writing and false otherwise.

BytesIO.flush()

io.BytesIO.flush()

Does nothing for a BytesIO

io.BytesIO.symlink(path) -> bool

Does nothing for BytesIO but simply returns false because BytesIO cannot be symbolically linked.

Returns bool

BytesIO.stats()

io.BytesIO.stats() -> dict

Returns the statistics or details of the BytesIO.

See the working with files documentation for more information about the stats() method.

Returns dict

BytesIO.delete()

io.BytesIO.delete() -> bool

Clears the bytearray and closes it for reading or writing.

Any further attempt to perform most operations on the BytesIO after calling delete() will raise an error.

Returns bool

BytesIO.rename()

io.BytesIO.rename(new_name) -> bool

Returns false because BytesIO cannot be renamed.

Returns bool

BytesIO.copy()

io.BytesIO.copy() -> [[io.BytesIO]]

Returns a new BytesIO with the source cloned and opened with the same mode as the current BytesIO.

Returns [[io.BytesIO]]

BytesIO.path()

io.BytesIO.path() -> string

Returns an empty string because BytesIO do not have any physical path.

Returns string

BytesIO.abs_path()

io.BytesIO.abs_path() -> string

Same as [[io.BytesIO.path()]].

Returns string

BytesIO.truncate()

io.BytesIO.truncate(length) -> bool

Truncates the entire BytesIO if length is not given or truncates the BytesIO such that only length number of bytes is left in it.

Returns bool

BytesIO.chmod()

io.BytesIO.chmod(number) -> bool

Returns false because BytesIO do not have a permission scheme.

Returns bool

BytesIO.set_times()

io.BytesIO.set_times(atime, mtime) -> bool

Sets the last access time and last modified time of the BytesIO.

Returns bool

BytesIO.seek()

io.BytesIO.seek(position, seek_type) -> bool

Sets the position of a BytesIO reader or writer in a BytesIO.

The position must be within the range of the BytesIO size. The seek_type argument must be on of [[io.SEEK_SET]], [[io.SEEK_CUR]] or [[io.SEEK_END]].

Returns bool

BytesIO.tell()

io.BytesIO.tell() -> number

Returns the current position of the reader/writer in the BytesIO.

Returns number

BytesIO.mode()

io.BytesIO.mode() -> string

Returns the mode in which the current BytesIO was opened.

Returns string

BytesIO.name()

io.BytesIO.name() -> string

Returns an empty string since BytesIO do not have a name.

Returns string


2026, Richard Ore and Zuri contributors

io.tty

import io.tty

io lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled io.tty.* needs import io.tty.

TTY: control over a terminal attached to a file handle.

Raw and cooked modes, echo, cursor visibility and terminal size all live here. The underlying constants are read from the platform’s own libc at load time rather than hard-coded, because they differ between operating systems.

Classes

TTY

class io.TTY

class TTY is an interface to TTY terminals this class contains definitions to control TTY terminals

Termios flag words are a Unix concept, so get_attr() and set_attr() raise on Windows and every flag constant below reads as 0 there. What those two are usually wanted for is not Unix-only, though: set_raw() and exit_raw() work on both, Windows reaching its console mode directly instead of through the flags.

exit_raw() and flush() never raise on any platform. Each reports whether it had anything to do, so cleanup code can call them without first establishing what state the terminal was in.

Constructor

io.TTY(std)

Parameters

  • std (file)

Note: file must be one of stdout and stderr

TTY.get_attr()

io.TTY.get_attr() -> dict

Returns the attributes of the current tty session. The returned value is a dict keyed by the TTY_ group constants.

Returns dict

Raises Error on a platform without termios, or if the stream has no terminal behind it.

Note: Unix only. Termios flag words have no console equivalent, so this raises on Windows. set_raw() and exit_raw() cover what attributes are usually reached for and work on both.

TTY.set_attr()

io.TTY.set_attr(option: int, attrs: dict) -> bool

sets the attributes of the current tty session

NOTE: - option must be one ot the TCSA options above (see their description above) - attrs must be a dictionary keyed by the TTY_ group constants above (TTY_IFLAG, TTY_OFLAG, TTY_CFLAG, TTY_LFLAG, TTY_ISPEED, TTY_OSPEED hold a single bit-flag number each; TTY_CC holds a list of control character values, indexed by the VEOF/VERASE/etc constants above) - one can safely omit any of the TTY_ groups listed above and Zuri will fill in the default values as it exists.

  • This flags will be merged and not overwritten

Parameters

  • option (number)
  • attr (dict)

Returns bool

Raises Error on a platform without termios, or if the stream has no terminal behind it.

Note: Unix only, for the same reason get_attr() is: there is no console representation of a termios flag word to apply, and accepting one would mean reporting success for a change that never happened.

TTY.set_raw()

io.TTY.set_raw() -> bool

Sets the current tty to raw mode: input arrives a keystroke at a time, nothing is echoed back, and Ctrl+C reaches the program as a keystroke rather than being turned into a signal first.

Works on Windows as well as Unix.

Returns bool

Raises Error if the stream has no terminal behind it, which is what a redirected or captured stream is.

TTY.exit_raw()

io.TTY.exit_raw() -> bool

Disables the raw mode flags on the current tty, putting it back the way set_raw() found it.

Never raises, so it is safe to call on a cleanup path without knowing whether raw mode was ever entered. The return value says whether anything was actually restored: false for a stream this TTY never put into raw mode, and false on Windows, which has no raw mode to leave.

Returns bool

TTY.get_size()

io.TTY.get_size() -> dict

Returns the size of the current TTY device as a dictionary of cols and rows.

  • cols: the number of text columns that fit into the TTY device.
  • rows: the number of text rows that fit into the TTY device.

Works on Windows as well as Unix.

Returns dict

Raises Error if the stream has no terminal behind it, which is what a redirected or captured stream is.

TTY.flush()

io.TTY.flush() -> bool

Discards whatever is still queued on this TTY’s stream, both input that has been typed but not yet read and output that has been written but not yet sent.

Note that this throws that data away rather than writing it out, which is the opposite of what file.flush() does.

Never raises. The return value says whether anything was discarded: false for a stream with no terminal behind it, and false on Windows, which keeps no such queue.

Returns bool


2026, Richard Ore and Zuri contributors

args

import args

This module provides a complete, batteries-included framework for building command-line interfaces. It supports options (flags), positional arguments, sub-commands with their own option sets, automatic help generation, type coercion, required arguments, deprecation warnings, choice validation, abbreviated long-option matching, -- end-of-options, and @file argument expansion.

How a value reaches an option

A long option takes its value either as the next argument or attached with =; both spellings mean the same thing:

$ zuri run myprogram.zu --name Alice
$ zuri run myprogram.zu --name=Alice

The split is on the first =, so --path=a=b sets path to a=b. The attached form is the only way to pass a value that begins with a dash, since --name -5 reads -5 as an option:

$ zuri run myprogram.zu --offset=-5

A short option takes its value as the next argument only. -n Alice works; -n=Alice does not, because a short token is a bundle of single-character flags and = is not one of them. Every character in such a bundle has to name an option, so a typo in -vq is an error rather than a silently dropped flag.

Quick start

import args

var parser = args.Parser('myprogram')
parser.description = 'A friendly CLI tool.'

parser.add_option('name', 'Person to greet', {short_name: 'n', type: args.STRING})
parser.add_option('count', 'Number of greetings', {short_name: 'c', type: args.INT, value: 1})

var cmd = parser.add_command('call', 'Make a phone call')
cmd.add_option('verbose', 'Enable verbose output', {short_name: 'v'})

parser.parse()

Running the following command:

zuri run myprogram.zu -h

Prints the following help output:

Usage: myprogram [OPTIONS] [COMMAND]

  A friendly CLI tool.

OPTIONS:
  -h, --help           Show this help message and exit
  -n, --name <name>    Person to greet
  -c, --count <count>  Number of greetings (default: 1)

COMMANDS:
  call  Make a phone call

Run "myprogram --help [COMMAND]" for help on a specific command.

Typical invocations:

$ zuri run myprogram.zu -h
$ zuri run myprogram.zu --name Alice --count 3
$ zuri run myprogram.zu call --verbose
$ zuri run myprogram.zu call --help

If we change the last line of the program to echo parser.parse() so that we can see the result of the parsing, the following CLI call will yield the given result.

$ zuri run myprogram.zu --name "Kirk"
{options: {name: Kirk, count: 1}, command: nil, indexes: []}

$ zuri run myprogram.zu call
{options: {count: 1}, command: {name: call, value: nil}, indexes: []}

$ zuri run myprogram.zu call -v
{options: {verbose: true, count: 1}, command: {name: call, value: nil}, indexes: []}

Calling name without an option will yield the following result/error:

$ zuri run myprogram.zu --name
error: option --name expects <name>

You may even get help on a command directly like below:

$ zuri run myprogram.zu --help call
Usage: myprogram call [OPTIONS]

  Make a phone call

OPTIONS:
  -v, --verbose  Enable verbose output

GLOBAL OPTIONS:
  -h, --help           Show this help message and exit
  -n, --name <name>    Person to greet
  -c, --count <count>  Number of greetings (default: 1)

Options declared on the parser itself are global: they are accepted before a command and after it alike, and a command’s help lists them under GLOBAL OPTIONS. When a command declares an option of the same name, that option is the one its arguments reach.

Return value of parse() is in the format:

{
  options: {name: 'Alice', count: 3},
  command: {name: 'call', value: nil},
  indexes: []
}

The args API

Every public name in args, wherever it is declared. Each links to the page that documents it.

NameKindSummary
args.ArgsErrorclassError raised for argument parsing errors.
args.BOOLconstantvalue type boolean (accepts 1/0, true/false, yes/no, on/off).
args.CHOICEconstantvalue type choice: value must be one of the choices list/dict
args.INTconstantvalue type integer (accepts numbers, floors to integer)
args.LISTconstantvalue type list: the option may be supplied multiple times
args.NONEconstantvalue type none: the option is a boolean flag
args.NUMBERconstantvalue type number
args.OPTIONALconstantvalue type optional: value is consumed if the next token is not a flag
args.ParserclassA configurable command-line parser.
args.STRINGconstantvalue type string

Constants

NONE

args.NONE = 0

value type none: the option is a boolean flag

INT

args.INT = 1

value type integer (accepts numbers, floors to integer)

NUMBER

args.NUMBER = 2

value type number

BOOL

args.BOOL = 3

value type boolean (accepts 1/0, true/false, yes/no, on/off).

STRING

args.STRING = 4

value type string

LIST

args.LIST = 5

value type list: the option may be supplied multiple times

CHOICE

args.CHOICE = 6

value type choice: value must be one of the choices list/dict

OPTIONAL

args.OPTIONAL = 7

value type optional: value is consumed if the next token is not a flag

Classes

ArgsError

class args.ArgsError < Error

Error raised for argument parsing errors.

Parser

class args.Parser < _Optionable

A configurable command-line parser.

Properties you can set after construction
  • description (string): paragraph shown between USAGE and OPTIONS.
  • epilog (string): paragraph shown after all sections.
  • allow_abbrev (bool): allow unambiguous long-option prefix matching. Defaults to true, like Python’s argparse.
  • allow_atfile (bool): expand @filename tokens by reading arguments from that file. Defaults to true.

Fields

FieldTypeDescription
commandsList of sub-commands registered with add_command.
indexesList of positional arguments registered with add_index.
description
epilog
allow_abbrev
allow_atfile
terminal_widthThe column width help text wraps to.

Constructor

args.Parser(name, default_help)

Creates a new parser instance.

Parameters

  • name (string) — The program name shown in usage lines.
  • default_help (?bool) — Show help when invoked with no arguments. Defaults to true.

Parser.set_terminal_width()

args.Parser.set_terminal_width(width)

Overrides the column width help text wraps to, in place of the terminal_width this parser auto-detected at construction time (a real query of the terminal, the COLUMNS environment variable, or 80, in that order of preference; see _terminal_width()). Useful both for a program that wants a fixed layout regardless of environment, and for tests that need deterministic wrapping independent of whatever terminal actually ran them.

Equivalent to setting terminal_width directly; this exists purely for discoverability and to validate its argument.

Parameters

  • width (number)

Parser.add_option()

args.Parser.add_option(name, help, opts)

Adds an option (flag) to the top-level parser.

opts keys can include any of:

  • short_name (string): single-character alias (-x).
  • type (int): one of the type constants; default NONE.
  • value (any): default value when the option is absent.
  • choices (list|dict): restrict allowed values.
  • required (bool): error if the option is absent.
  • metavar (string): placeholder shown in help, defaulting to the option’s own name, lower-cased.
  • deprecated (bool): print a warning when the option is used.

Parameters

  • name (string)
  • help (?string)
  • opts (?dict)

Parser.add_command()

args.Parser.add_command(name, help, opts)

Adds a sub-command.

opts keys: - type {int}: expected type for the command’s value argument - action {function}: called with (options [, value]) after parsing - choices {list|dict}— restrict allowed values when type is CHOICE - metavar {string}: placeholder shown in help for the command’s value, e.g. 'message' for git commit -m <message> (default: the value type’s name, lower-cased)

Returns the _Command object so you can chain add_option calls:

parser.add_command('push', 'Push changes').
       add_option('force', 'Force push', {short_name: 'f'})

Parameters

  • name (string)
  • help (?string)
  • opts (?dict)

Returns — _Command

Parser.add_index()

args.Parser.add_index(name, help, opts)

Adds a positional (index-based) argument.

opts keys: - type {int}: coercion type; default STRING - value {any}: default when argument is absent - choices {list|dict}: restrict allowed values - required {bool}: error if argument is absent (default false) - metavar {string}: display name in help

Without a value, a positional nobody supplied is simply missing from parse()’s indexes rather than sitting there as nil.

A positional of type LIST takes every positional word from where it starts to the end of the command line, flags in between or not, and arrives in indexes as one list. It has to be the last positional declared, since nothing after it could ever receive a word.

Parameters

  • name (string)
  • help (?string)
  • opts (?dict)

Raises ArgsError when a positional is added after a LIST one.

Parser.parse()

args.Parser.parse(custom_args: ?list) -> dict

Parses command-line arguments and returns a dictionary of command, options, and indexes.

By default this reads the real process arguments (os.args, skipping the interpreter and script path). Pass custom_args (a list of strings) to parse something else instead, e.g. a config-driven argument list, or a fixed list in a test.

Result shape:

{
  options: dict,                # collected option values
  command: nil | {name, value}, # command name and value (if any)
  indexes: list                 # collected positional values
}

indexes holds one entry per positional that has a value, in the order they were declared: what the user supplied, then the declared default of any that follow. A positional that was not supplied and has no default is absent rather than nil, so indexes.is_empty() and indexes.length() answer what they look like they answer, and a program that wants its own fallback can reach for it:

var path = parsed.indexes.is_empty() ? os.cwd() : parsed.indexes[0]

Parameters

  • custom_args (?list)

Returns dict

Parser.help()

args.Parser.help()

Print the full help text and exit(0).


2021, Richard Ore and Zuri contributors

log

import log

This module implements a simple and flexible event logging system for all Zuri applications and modules. With support for multiple transport systems as well as custom transports, this module allows easy application logging and log shipping.

The module selects defaults that is familiar for most end use-cases in order to allow for a minimal need for configurations so that you can start logging right out of the box.

Below is a very simple but powerful and complete usage of this module:

%> import log
%> log.info('Starting my application...')
'2025-03-07T08:00:33+01:00 INFO [.]: Starting my application...'

IMPORTANT!

Did you notice that [.]? That’s because we are running in a REPL. By default, the log module provides information regarding the source of the log i.e. the application from which the log came from thereby allowing multiple applications log into the same transport pool without ambiguity.

You can customize this name by setting the name on the transport via [[log.Transport.set_name]].

This module provides all functionalities at the module level, allowing configurations to be carried across all files and modules in the lifetime of an application.

While allowing creation of custom transports for logs, the module provides transports for logging to the console ([[log.ConsoleTransport]]) and files ([[log.FileTransport]]) out of the box. This covers the most simple use-cases for most applications.

The default transport enabled is the [[log.ConsoleTransport]] known as the [[log.default_transport()]] and need no extra work to enable unless you have previously disabled it. The example below shows how to enable the file transport to log to a file on disk.

import log

var transport = log.FileTransport('mylog.log')
log.add_transport(transport)

log.info('Finished setting up file log...')

If you check the file mylog.log now, you should see something like this:

2025-03-01T12:00:00+01:00 INFO [tmp]: Finished setting up file log...

In addition to the log appearing on the console, you can now see the log in a persistent file. There are many ways to turn off the console output and log to file alone.

Firstly, you can simple disable the default transport.

log.default_transport().disable()

The advantage to this approach is that while it disables the default transport, the transport is still registered and you can simple enable it at any time during the lifetime of the application by doing the reverse:

log.default_transport().enable()

The same strategy applies to all transports as the enable() and disable() method will be inherited from the [[log.Transport]] class.

The second approach is to completely remove the transport from the list of registered transports.

log.remove_transport(log.default_transport())

With this second approach, you’ll need to register the transport again should you want to continue logging to the console. The same applies to all transport types.

For more complex uses, the process of creating a custom transport is really simple. To create a custom transport, you’ll need to create a class that inherits from [[log.Transport]] and implement the write() method at a minimum.

The example below shows the creation of a custom transport that outputs structured JSON data to the console.

# my_custom_transport.zu

import log { Transport }
import json
import enum

class JsonConsoleTransport < Transport {
  format(records, level, context) {
    return {
      records,
      level,
    }
  }

  write(message, level) {
    echo json.encode({
      logtime: time(),
      records: message.records,
      name: self.get_name(),
      level: enum.to_value_dict(log.LogLevel)[message.level]
    })
  }
}
import log
import .my_custom_transport { JsonConsoleTransport }

log.add_transport(JsonConsoleTransport())

log.info('Finished setting up json console log transport...')

You should be seeing something similar to the below if you run the code:

{"logtime":1741376459,"records":["Finished setting up json console log transport..."],"name":"tmp","level":"Info"}

You can set multiple transports at the same time as well as set them to only work at different log levels. Every transport inherits the method [[log.Transport.set_level]] which allows us set the minimum level at which a transport is available.

There is also a global [[log.set_level]] function that allows us to set the minimum log level at which all transports can start logging.

import log

log.set_level(log.Warning)

log.info('Finished setting up json console log transport...')

If you try the above code, you won’t be seeing anything in the console. This is because level [[log.Info]] is lower than the minium required [[log.Warning]].

While every Transport implements the format() method, users can override the format method by providing a format function via the [[log.Transport.set_formatter]] method. This allows using the same formatter across different transports.

For example, in the previous example, if we had wanted to serialize all logging irrespective of the transport into the JSON format, a more appropriate solution would have been to create a log format function and reuse it in all transports instead of implementing a JsonConsoleTransport. The next example shows one such implementation.

import log
import json
import date

def json_format(records, level, transport) {
  return json.encode({
    time: date().format(transport.get_time_format()),
    level: log.get_level_name(level),
    name: transport.get_name(),
    records,
  })
}

var file_transport = log.FileTransport('mylog.log')
log.add_transport(file_transport)

log.default_transport().set_formatter(json_format)
file_transport.set_formatter(json_format)

log.debug('This is a debug information')

Transport implementations should take note of this critical information.

IMPORTANT!

Because the log module exports the a function, if you are not interested in all the shenanigans of logging level and simply want to do some quick logging, you can ignore the whole logging levels altogether and log at level [[log.None]] by simply calling the log module itself.

import log

log('An anonymous log!')

This bypasses every level filter (including the global [[log.set_level]] threshold) on every enabled transport; only [[log.Transport.disable]] can still silence it.

Structured and namespaced logging

A trailing dict argument on any log call is treated as structured fields rather than a plain message, rendered as key=value pairs after the message:

log.info('user signed in', { user_id: 42 })
# ... INFO [.]: user signed in user_id=42

For a whole subsystem that should tag every one of its own log lines with a name (and, optionally, a fixed set of fields), use [[log.get_logger]] instead of the flat module-level functions:

var db_log = log.get_logger('db').bind({ pool: 'primary' })
db_log.warn('slow query', { duration_ms: 850 })
# ... WARNING [db]: slow query pool=primary duration_ms=850

The log API

Every public name in log, wherever it is declared. Each links to the page that documents it.

NameKindSummary
log.ConsoleTransportclassConsoleTransport is a log transport that facilitates sending log streams to the console.
log.CriticalconstantModule level declaration of LogLevel.Critical
log.DebugconstantModule level declaration of LogLevel.Debug
log.ErrorconstantModule level declaration of LogLevel.Error
log.FileTransportclassFileTransport is a log transport that facilitates sending log streams to an on-disk file.
log.InfoconstantModule level declaration of LogLevel.Info
log.LogLevelconstantThe Log levels in order
log.LoggerclassA namespaced logger that tags every record it writes with a name and, optionally, a set of bound structured…
log.NoneconstantModule level declaration of LogLevel.None
log.TransportclassThe Transport class acts as the base class for log transports and handle the actual logging of the specified…
log.WarningconstantModule level declaration of LogLevel.Warning
log.add_transportfunctionAdds a new transport service to the list of registered transports.
log.criticalfunctionLogs a message with level [[log.Critical]] on all registered transports.
log.debugfunctionLogs a message with level [[log.Debug]] on all registered transports.
log.default_transportfunctionReturns the instance [[log.ConsoleTransport]] which is used as the default transport by the module.
log.errorfunctionLogs a message with level [[log.Error]] on all registered transports.
log.exceptionfunctionLogs an exception with level [[log.Error]] on all registered transports, including its message and stacktrace.
log.get_level_namefunctionReturns the name of a log level as a string.
log.get_loggerfunctionReturns a [[log.Logger]] namespaced under the given name.
log.infofunctionLogs a message with level [[log.Info]] on all registered transports.
log.logfunctionLogs a message with level [[log.None]] on all registered transports.
log.remove_transportfunctionRemoves the given transport service from the list of registered transports.
log.set_levelfunctionSets the threshold level for the default transport to handle.
log.set_namefunctionSets the name of the default transport.
log.warnfunctionLogs a message with level [[log.Warning]] on all registered transports.

Submodules

ModuleReached asSummary
log.consolelog.*ConsoleTransport: the default transport, printing to stdout (or stderr for Error/Critical) with optional…
log.dispatchlog.*The default transport instance and the module-level functions that configure it.
log.filelog.*FileTransport: sends log streams to a file on disk, with optional size-based rotation.
log.levellog.*The LogLevel enum, its module-level constant exports, and the single shared “default level” that every…
log.loggerlog.*The flat, module-level logging functions (log(), info(), debug(), warn(), error(), critical(),…
log.transportlog.*The Transport base class every log sink (console, file, or a custom subclass) is built on, plus the…

2025, Richard Ore and The Zuri Contributors

log.console

import log

Everything here is re-exported by log, so import log is enough and the names are called as log.*. Importing log.console on its own works too and reaches the same definitions.

ConsoleTransport: the default transport, printing to stdout (or stderr for Error/Critical) with optional ANSI coloring by level.

Classes

ConsoleTransport

class log.ConsoleTransport < Transport

ConsoleTransport is a log transport that facilitates sending log streams to the console.

ConsoleTransport.set_colors()

log.ConsoleTransport.set_colors(enabled: bool)

Enables or disables ANSI coloring of messages by level. Enabled by default unless the NO_COLOR environment variable is set.

Parameters

  • enabled (bool)

Returns — self

ConsoleTransport.write()

log.ConsoleTransport.write(message, level)

ConsoleTransport.flush()

log.ConsoleTransport.flush()

log.dispatch

import log

Everything here is re-exported by log, so import log is enough and the names are called as log.*. Importing log.dispatch on its own works too and reaches the same definitions.

The default transport instance and the module-level functions that configure it.

Functions

set_level()

log.set_level(level)

Sets the threshold level for the default transport to handle.

Parameters

  • level (log.LogLevel)

Returns — default_transport

set_name()

log.set_name(name)

Sets the name of the default transport.

Parameters

  • name (string)

Returns — default_transport

default_transport()

log.default_transport() -> [[log.ConsoleTransport]]

Returns the instance [[log.ConsoleTransport]] which is used as the default transport by the module.

Returns [[log.ConsoleTransport]]

log.file

import log

Everything here is re-exported by log, so import log is enough and the names are called as log.*. Importing log.file on its own works too and reaches the same definitions.

FileTransport: sends log streams to a file on disk, with optional size-based rotation.

Classes

FileTransport

class log.FileTransport < Transport

FileTransport is a log transport that facilitates sending log streams to an on-disk file.

Fields

FieldTypeDescription
file

Constructor

log.FileTransport(path: string)

Returns a new instance of FileTransport and opens a file handle to the the file specified in the path.

Parameters

  • path (string)

FileTransport.set_max_bytes()

log.FileTransport.set_max_bytes(n: ?number)

Sets the size, in bytes, past which the log file is rotated to <path>.1 (shifting any existing numbered backups up by one) and a fresh file is opened at path. Rotation is disabled (nil) by default.

Parameters

  • n (?number)

Returns — self

FileTransport.set_backup_count()

log.FileTransport.set_backup_count(n: number)

Sets how many rotated backups (<path>.1 through <path>.<n>) are kept. The oldest backup beyond this count is deleted on the next rotation. Default: 5.

Parameters

  • n (number)

Returns — self

FileTransport.write()

log.FileTransport.write(message, level)

FileTransport.flush()

log.FileTransport.flush()

FileTransport.close()

log.FileTransport.close()

log.level

import log

Everything here is re-exported by log, so import log is enough and the names are called as log.*. Importing log.level on its own works too and reaches the same definitions.

The LogLevel enum, its module-level constant exports, and the single shared “default level” that every registered transport is filtered against unless it has its own level configured.

Constants

LogLevel

log.LogLevel

The Log levels in order

  • None - Debug - Info - Warning - Error - Critical

None

log.None

Module level declaration of LogLevel.None

Debug

log.Debug

Module level declaration of LogLevel.Debug

Info

log.Info

Module level declaration of LogLevel.Info

Warning

log.Warning

Module level declaration of LogLevel.Warning

Error

log.Error

Module level declaration of LogLevel.Error

Critical

log.Critical

Module level declaration of LogLevel.Critical

Functions

get_level_name()

log.get_level_name(level) -> string

Returns the name of a log level as a string.

Parameters

  • level (log.LogLevel)

Returns string

log.logger

import log

Everything here is re-exported by log, so import log is enough and the names are called as log.*. Importing log.logger on its own works too and reaches the same definitions.

The flat, module-level logging functions (log(), info(), debug(), warn(), error(), critical(), exception()), and Logger: namespaced, structured-field-bound loggers built on top of the exact same dispatch path.

The module is callable. log(...) is the same call as log.log(...), documented below.

Functions

log()

log.log(...args: list)

Logs a message with level [[log.None]] on all registered transports.

The format of the output log is dependent on the specific transport service as well as limitations on the nature and type of arguments that is passed to the function.

Parameters

  • any...

info()

log.info(...args: list)

Logs a message with level [[log.Info]] on all registered transports. The arguments and limitations as same as in [[log.log()]].

Parameters

  • any...

debug()

log.debug(...args: list)

Logs a message with level [[log.Debug]] on all registered transports. The arguments and limitations as same as in [[log.log()]].

Parameters

  • any...

warn()

log.warn(...args: list)

Logs a message with level [[log.Warning]] on all registered transports. The arguments and limitations as same as in [[log.log()]].

Parameters

  • any...

error()

log.error(...args: list)

Logs a message with level [[log.Error]] on all registered transports. The arguments and limitations as same as in [[log.log()]].

Parameters

  • any...

critical()

log.critical(...args: list)

Logs a message with level [[log.Critical]] on all registered transports. The arguments and limitations as same as in [[log.log()]].

Parameters

  • any...

exception()

log.exception(ex, message)

Logs an exception with level [[log.Error]] on all registered transports, including its message and stacktrace.

Parameters

  • ex (any) — Expected to be an Error instance (from a catch block); anything else is logged via its string form instead.
  • message (?any) — Optional additional context to log alongside the exception.

get_logger()

log.get_logger(name: string) -> [[log.Logger]]

Returns a [[log.Logger]] namespaced under the given name. Repeated calls with the same name return the same instance, matching Python’s logging.getLogger(): so binding fields on a logger obtained one place is visible everywhere else that same name is requested.

Parameters

  • name (string)

Returns [[log.Logger]]

Classes

Logger

class log.Logger

A namespaced logger that tags every record it writes with a name and, optionally, a set of bound structured fields. Get one via [[log.get_logger]] rather than constructing directly.

Example
import log

var db_log = log.get_logger('db')
db_log.info('connection established')
# ... [db]: connection established

var pool_log = db_log.child('pool').bind({ pool_size: 10 })
pool_log.warn('pool exhausted')
# ... [db.pool]: pool exhausted pool_size=10

Fields

FieldTypeDescription
name

Constructor

log.Logger(name: string, fields)

Logger.bind()

log.Logger.bind(fields: dict)

Logger.child()

log.Logger.child(sub_name: string)

Logger.info()

log.Logger.info(...args: list)

Logger.debug()

log.Logger.debug(...args: list)

Logger.warn()

log.Logger.warn(...args: list)

Logger.error()

log.Logger.error(...args: list)

Logger.critical()

log.Logger.critical(...args: list)

Logger.exception()

log.Logger.exception(ex, message)

log.transport

import log

Everything here is re-exported by log, so import log is enough and the names are called as log.*. Importing log.transport on its own works too and reaches the same definitions.

The Transport base class every log sink (console, file, or a custom subclass) is built on, plus the registry of active transports and the shared dispatch loop every level function writes through. These live in the same file as Transport itself (rather than off in dispatch.zu alongside default_transport()) specifically so Transport.close() can remove itself from the registry without a cross-file call back into a file that in turn depends on this one: console.zu/file.zu need Transport from here, and dispatch.zu needs ConsoleTransport from console.zu, so a Transport depending on dispatch.zu for remove_transport() would be a genuine import cycle. Keeping the registry here instead keeps every dependency pointing one direction.

Functions

add_transport()

log.add_transport(transport)

Adds a new transport service to the list of registered transports. If the transport has been previously added, this function will do nothing.

Parameters

  • transport (log.Transport)

remove_transport()

log.remove_transport(transport)

Removes the given transport service from the list of registered transports.

Parameters

  • transport (log.Transport)

Classes

Transport

class log.Transport

The Transport class acts as the base class for log transports and handle the actual logging of the specified log records.

Transport.set_level()

log.Transport.set_level(level)

Sets the threshold level for this transport to handle. Logging messages which are less severe than level will be ignored. Unless overridden by the transport implementation, when a handler is created, the level is set to [[log.None]] (which causes all messages to be processed).

Parameters

  • level (log.LogLevel)

Returns — self

Transport.get_level()

log.Transport.get_level() -> [[log.LogLevel]]

The threshold level of this transport. The default level is [[log.LogLevel.None]].

Returns [[log.LogLevel]]

Transport.set_max_level()

log.Transport.set_max_level(level)

Sets the maximum threshold level for this transport to handle. Logging messages which are more severe than level will be ignored. Unless overridden by the transport implementation, when a handler is created, the maximum level is set to [[log.Critical]] (which causes all messages to be processed).

Parameters

  • level (log.LogLevel)

Returns — self

Transport.get_max_level()

log.Transport.get_max_level() -> [[log.LogLevel]]

The maximum threshold level of this transport. The default maximum level is [[log.Critical]].

Returns [[log.LogLevel]]

Transport.set_name()

log.Transport.set_name(name)

Sets the name of the current transport.

Parameters

  • name (string)

Returns — self

Transport.get_name()

log.Transport.get_name() -> string

Returns the name of the current transport. By default, name will be equal to the name of the directory containing the root file.

Returns string

Transport.set_time_format()

log.Transport.set_time_format(format)

Sets the time formatting string used by the transport when [[log.Transport.show_time]] is set to true.

Parameters

  • format (string)

Returns — self

Transport.get_time_format()

log.Transport.get_time_format() -> string

Returns the time formatting string used by the current transport. The default value is c.

Returns string

Transport.show_name()

log.Transport.show_name(show)

Enable or disable showing transport names in the logs based on the passed boolean value.

Parameters

  • show (bool)

Returns — self

Transport.show_time()

log.Transport.show_time(show)

Enables or disables showing logging time in the logs based on the passed boolean value.

Parameters

  • show (bool)

Returns — self

Transport.show_level()

log.Transport.show_level(show)

Enable or disable showing transport log level in the logs based on the passed boolean value.

Parameters

  • show (bool)

Returns — self

Transport.set_formatter()

log.Transport.set_formatter(formatter)

Sets a formatter function that overrides a transports format() function.

The formatter function is a function that when it is set overrides the transports default format method and must MUST have the contract def my_function(records, level, transport). The transport parameter here is the instance of the current transport. See [[log.Transport.format]] for what records, and level means.

Parameters

  • formatter (function)

Returns — self

Transport.get_formatter()

log.Transport.get_formatter() -> ?function

Returns the format method override that has been set for the current transport or nil if none has been set.

Returns ?function

Transport.can_log()

log.Transport.can_log(level) -> bool

Returns a boolean value which indicates if a message of severity level can be processed by this transport.

[[log.LogLevel.None]] is a documented escape hatch that bypasses level filtering entirely (only enable()/disable() still applies to it): see the module doc’s “quick logging” section.

Parameters

  • level (log.LogLevel)

Returns bool

Transport.enable()

log.Transport.enable()

Enables and starts processing of logs by the current transport.

Returns — self

Transport.disable()

log.Transport.disable()

Disables and stops the current transport from processing further logs.

Returns — self

Transport.format()

log.Transport.format(records, level, context) -> any

Formats the log records for the current level for writing to the transport’s stream. The default implementation of this method is exactly as seen when using the [[log.default_transport]] which logs to the console. The method should be overridden by subclasses to get a custom formatting.

When the last element of records is a dict, it’s treated as structured fields rather than a plain message argument and rendered as trailing key=value pairs: log.info('processing', { user_id: 5 }) renders as ... processing user_id=5. Fields bound via a [[log.Logger]] (through context) are merged in the same way.

IMPORTANT!

The result of this function will be passed into the [[log.Transport.write()]] function so transport implementations MUST ensure to expect the same type as is returned from this function in the [[log.Transport.write()]] function message parameter.

Transport implementations should be aware of the following available private fields in the transport class:

  • [[log.LogLevel]] self._level - string self._log_name - bool self._show_name - bool self._show_time - bool self._show_level

Parameters

  • records (list[any])
  • level (log.LogLevel)
  • context (?dict) — Optional {name: ?string, fields: dict}, supplied by a [[log.Logger]] call; absent for the flat module-level log.info()-style calls.

Returns any

Transport.write()

log.Transport.write(message, level)

Do whatever it takes to actually log the specified logging record. This method is intended to be implemented by subclasses and so raises an Error if called directly from Transport.

Parameters

  • message (any)
  • level (log.LogLevel)

Raises NotImplementedError

Transport.flush()

log.Transport.flush()

Ensure all logging output has been flushed to the target stream. This default version does nothing and is intended to be implemented by subclasses.

Transport.close()

log.Transport.close()

Tidy up any resources used by the transport. This default version does no output but removes the handler from an internal list of handlers. Subclasses should ensure that this gets called from overridden close() methods.

stat

import stat

The module provides constants and functions for interpreting results of file.stat().

The stat API

Every public name in stat, wherever it is declared. Each links to the page that documents it.

NameKindSummary
stat.FILE_ATTRIBUTE_ARCHIVEconstant
stat.FILE_ATTRIBUTE_COMPRESSEDconstant
stat.FILE_ATTRIBUTE_DEVICEconstant
stat.FILE_ATTRIBUTE_DIRECTORYconstant
stat.FILE_ATTRIBUTE_ENCRYPTEDconstant
stat.FILE_ATTRIBUTE_HIDDENconstant
stat.FILE_ATTRIBUTE_INTEGRITY_STREAMconstant
stat.FILE_ATTRIBUTE_NORMALconstant
stat.FILE_ATTRIBUTE_NOT_CONTENT_INDEXEDconstant
stat.FILE_ATTRIBUTE_NO_SCRUB_DATAconstant
stat.FILE_ATTRIBUTE_OFFLINEconstant
stat.FILE_ATTRIBUTE_READONLYconstant
stat.FILE_ATTRIBUTE_REPARSE_POINTconstant
stat.FILE_ATTRIBUTE_SPARSE_FILEconstant
stat.FILE_ATTRIBUTE_SYSTEMconstant
stat.FILE_ATTRIBUTE_TEMPORARYconstant
stat.FILE_ATTRIBUTE_VIRTUALconstant
stat.SF_APPENDconstant
stat.SF_ARCHIVEDconstant
stat.SF_IMMUTABLEconstant
stat.SF_NOUNLINKconstant
stat.SF_SNAPSHOTconstant
stat.ST_ATIMEconstant
stat.ST_CTIMEconstant
stat.ST_DEVconstant
stat.ST_GIDconstant
stat.ST_INOconstant
stat.ST_MODEconstant
stat.ST_MTIMEconstant
stat.ST_NLINKconstant
stat.ST_SIZEconstant
stat.ST_UIDconstant
stat.S_ENFMTconstant
stat.S_IEXECconstant
stat.S_IFBLKconstant
stat.S_IFCHRconstant
stat.S_IFDIRconstant
stat.S_IFDOORconstant
stat.S_IFIFOconstant
stat.S_IFLNKconstant
stat.S_IFMTfunctionReturn the portion of the file’s mode that describes the file type.
stat.S_IFPORTconstant
stat.S_IFREGconstant
stat.S_IFSOCKconstant
stat.S_IFWHTconstant
stat.S_IMODEfunctionReturn the portion of the file’s mode that can be set by file.chmod().
stat.S_IREADconstant
stat.S_IRGRPconstant
stat.S_IROTHconstant
stat.S_IRUSRconstant
stat.S_IRWXGconstant
stat.S_IRWXOconstant
stat.S_IRWXUconstant
stat.S_ISBLKfunctionReturn true if mode is from a block special device file.
stat.S_ISCHRfunctionReturn true if mode is from a character special device file.
stat.S_ISDIRfunctionReturn true if mode is from a directory.
stat.S_ISDOORfunctionReturn true if mode is from a door.
stat.S_ISFIFOfunctionReturn true if mode is from a FIFO (named pipe).
stat.S_ISGIDconstant
stat.S_ISLNKfunctionReturn true if mode is from a symbolic link.
stat.S_ISPORTfunctionReturn true if mode is from an event port.
stat.S_ISREGfunctionReturn true if mode is from a regular file.
stat.S_ISSOCKfunctionReturn true if mode is from a socket.
stat.S_ISUIDconstant
stat.S_ISVTXconstant
stat.S_ISWHTfunctionReturn true if mode is from a whiteout.
stat.S_IWGRPconstant
stat.S_IWOTHconstant
stat.S_IWRITEconstant
stat.S_IWUSRconstant
stat.S_IXGRPconstant
stat.S_IXOTHconstant
stat.S_IXUSRconstant
stat.UF_APPENDconstant
stat.UF_COMPRESSEDconstant
stat.UF_HIDDENconstant
stat.UF_IMMUTABLEconstant
stat.UF_NODUMPconstant
stat.UF_NOUNLINKconstant
stat.UF_OPAQUEconstant
stat.file_modefunctionConvert a file’s mode to a string of the form ‘-rwxrwxrwx’.

Constants

ST_MODE

stat.ST_MODE = 0

ST_INO

stat.ST_INO = 1

ST_DEV

stat.ST_DEV = 2
stat.ST_NLINK = 3

ST_UID

stat.ST_UID = 4

ST_GID

stat.ST_GID = 5

ST_SIZE

stat.ST_SIZE = 6

ST_ATIME

stat.ST_ATIME = 7

ST_MTIME

stat.ST_MTIME = 8

ST_CTIME

stat.ST_CTIME = 9

S_IFDIR

stat.S_IFDIR = 16384

S_IFCHR

stat.S_IFCHR = 8192

S_IFBLK

stat.S_IFBLK = 24576

S_IFREG

stat.S_IFREG = 32768

S_IFIFO

stat.S_IFIFO = 4096

S_IFLNK

stat.S_IFLNK = 40960

S_IFSOCK

stat.S_IFSOCK = 49152

S_IFDOOR

stat.S_IFDOOR = 0

S_IFPORT

stat.S_IFPORT = 0

S_IFWHT

stat.S_IFWHT = 0

S_ISUID

stat.S_ISUID = 2048

S_ISGID

stat.S_ISGID = 1024

S_ENFMT

stat.S_ENFMT

S_ISVTX

stat.S_ISVTX = 512

S_IREAD

stat.S_IREAD = 256

S_IWRITE

stat.S_IWRITE = 128

S_IEXEC

stat.S_IEXEC = 64

S_IRWXU

stat.S_IRWXU = 448

S_IRUSR

stat.S_IRUSR = 256

S_IWUSR

stat.S_IWUSR = 128

S_IXUSR

stat.S_IXUSR = 64

S_IRWXG

stat.S_IRWXG = 56

S_IRGRP

stat.S_IRGRP = 32

S_IWGRP

stat.S_IWGRP = 16

S_IXGRP

stat.S_IXGRP = 8

S_IRWXO

stat.S_IRWXO = 7

S_IROTH

stat.S_IROTH = 4

S_IWOTH

stat.S_IWOTH = 2

S_IXOTH

stat.S_IXOTH = 1

UF_NODUMP

stat.UF_NODUMP = 1

UF_IMMUTABLE

stat.UF_IMMUTABLE = 2

UF_APPEND

stat.UF_APPEND = 4

UF_OPAQUE

stat.UF_OPAQUE = 8
stat.UF_NOUNLINK = 16

UF_COMPRESSED

stat.UF_COMPRESSED = 32

UF_HIDDEN

stat.UF_HIDDEN = 32768

SF_ARCHIVED

stat.SF_ARCHIVED = 65536

SF_IMMUTABLE

stat.SF_IMMUTABLE = 131072

SF_APPEND

stat.SF_APPEND = 262144
stat.SF_NOUNLINK = 1048576

SF_SNAPSHOT

stat.SF_SNAPSHOT = 2097152

FILE_ATTRIBUTE_ARCHIVE

stat.FILE_ATTRIBUTE_ARCHIVE = 32

FILE_ATTRIBUTE_COMPRESSED

stat.FILE_ATTRIBUTE_COMPRESSED = 2048

FILE_ATTRIBUTE_DEVICE

stat.FILE_ATTRIBUTE_DEVICE = 64

FILE_ATTRIBUTE_DIRECTORY

stat.FILE_ATTRIBUTE_DIRECTORY = 16

FILE_ATTRIBUTE_ENCRYPTED

stat.FILE_ATTRIBUTE_ENCRYPTED = 16384

FILE_ATTRIBUTE_HIDDEN

stat.FILE_ATTRIBUTE_HIDDEN = 2

FILE_ATTRIBUTE_INTEGRITY_STREAM

stat.FILE_ATTRIBUTE_INTEGRITY_STREAM = 32768

FILE_ATTRIBUTE_NORMAL

stat.FILE_ATTRIBUTE_NORMAL = 128

FILE_ATTRIBUTE_NOT_CONTENT_INDEXED

stat.FILE_ATTRIBUTE_NOT_CONTENT_INDEXED = 8192

FILE_ATTRIBUTE_NO_SCRUB_DATA

stat.FILE_ATTRIBUTE_NO_SCRUB_DATA = 131072

FILE_ATTRIBUTE_OFFLINE

stat.FILE_ATTRIBUTE_OFFLINE = 4096

FILE_ATTRIBUTE_READONLY

stat.FILE_ATTRIBUTE_READONLY = 1

FILE_ATTRIBUTE_REPARSE_POINT

stat.FILE_ATTRIBUTE_REPARSE_POINT = 1024

FILE_ATTRIBUTE_SPARSE_FILE

stat.FILE_ATTRIBUTE_SPARSE_FILE = 512

FILE_ATTRIBUTE_SYSTEM

stat.FILE_ATTRIBUTE_SYSTEM = 4

FILE_ATTRIBUTE_TEMPORARY

stat.FILE_ATTRIBUTE_TEMPORARY = 256

FILE_ATTRIBUTE_VIRTUAL

stat.FILE_ATTRIBUTE_VIRTUAL = 65536

Functions

S_IMODE()

stat.S_IMODE(mode: number) -> number

Return the portion of the file’s mode that can be set by file.chmod().

Parameters

  • mode (number)

Returns number

S_IFMT()

stat.S_IFMT(mode: number) -> number

Return the portion of the file’s mode that describes the file type.

Parameters

  • mode (number)

Returns number

S_ISDIR()

stat.S_ISDIR(mode: number) -> number

Return true if mode is from a directory.

Parameters

  • mode (number)

Returns number

S_ISCHR()

stat.S_ISCHR(mode: number) -> number

Return true if mode is from a character special device file.

Parameters

  • mode (number)

Returns number

S_ISBLK()

stat.S_ISBLK(mode: number) -> number

Return true if mode is from a block special device file.

Parameters

  • mode (number)

Returns number

S_ISREG()

stat.S_ISREG(mode: number) -> number

Return true if mode is from a regular file.

Parameters

  • mode (number)

Returns number

S_ISFIFO()

stat.S_ISFIFO(mode: number) -> number

Return true if mode is from a FIFO (named pipe).

Parameters

  • mode (number)

Returns number

S_ISLNK()

stat.S_ISLNK(mode: number) -> number

Return true if mode is from a symbolic link.

Parameters

  • mode (number)

Returns number

S_ISSOCK()

stat.S_ISSOCK(mode: number) -> number

Return true if mode is from a socket.

Parameters

  • mode (number)

Returns number

S_ISDOOR()

stat.S_ISDOOR(mode: number) -> number

Return true if mode is from a door.

Parameters

  • mode (number)

Returns number

S_ISPORT()

stat.S_ISPORT(mode: number) -> number

Return true if mode is from an event port.

Parameters

  • mode (number)

Returns number

S_ISWHT()

stat.S_ISWHT(mode: number) -> number

Return true if mode is from a whiteout.

Parameters

  • mode (number)

Returns number

file_mode()

stat.file_mode(mode: number) -> string

Convert a file’s mode to a string of the form ‘-rwxrwxrwx’.

Parameters

  • mode (number)

Returns string


2022, Richard Ore and The Zuri Contributors

test

import test

A testing framework: suites, assertions, mocks, snapshots, lifecycle hooks, and reports worth reading.

import test { * }

describe('Calculator', @{

  it('adds', @{
    expect(2 + 2).to_be(4)
  })

  it('divides', @{
    expect(10 / 4).to_be_close_to(2.5)
  })

})

Run it like any other script:

$ zuri run tests/calculator.zu

Calculator
  ✓ adds
  ✓ divides

  PASS

  2 passed  •  2 total
  suites 1   time 1ms

Importing

import test { * } is the form to use in a test file. It brings describe, it, expect, mock, the hooks and run into scope, which is what makes a test read as a test rather than as a series of calls into a module.

import test is there for anything that is not a test file, and qualifies the same names: test.conduct('tests'), test.run(). The two can be combined when a test file wants both.

Declaring

describe(name, body) groups tests and may be nested as deeply as the subject calls for. it(name, body) declares one. Neither runs anything: they build a tree, and the tree is run once the file has finished declaring everything in it. That is what makes focusing, filtering and counting possible at all.

The run happens on its own, through os.at_exit(). Call run() explicitly when you want to pass options or read the Summary back; doing so takes over, and nothing runs a second time.

Focusing and skipping

describe_only('the one I am working on', @{ ... })
describe_skip('not now', @{ ... })

it_only('just this', @{ ... })
it_skip('known broken', @{ ... })
it_todo('handle an empty payload')

As soon as anything is marked only, everything not marked only and not inside something marked only is skipped. skip always wins over only.

it('name') with no body is a todo as well: it is reported as one and counts towards nothing.

Table-driven tests

it_each([
  [1, 1, 2],
  [2, 3, 5],
  [10, -4, 6],
], 'adds $0 and $1 to make $2', @(left, right, total) {
  expect(left + right).to_be(total)
})

Each row becomes one test. $0, $1 and so on in the name are replaced with that row’s values, and $# with the row number. describe_each() does the same for whole suites.

Per-test options

it('reaches the network', @{ ... }, { retries: 2, tags: ['slow'] })
it('reproduces issue 412', @{ ... }, { failing: true })
it('stays fast', @{ ... }, { timeout: 50 })
OptionWhat it does
retriesrun the body again on failure, up to this many times; passing after a retry is reported as flaky
failingthe test passes when the body fails, and fails when it passes
timeoutfail the test if it took longer than this many milliseconds
tagsa list of labels to filter on

Hooks

describe('Database', @{
  before_all(@{ db.connect() })
  after_all(@{ db.close() })

  before_each(@{ db.begin() })
  after_each(@{ db.rollback() })

  it('inserts a row', @{ ... })
})

before_all runs once, immediately before the first test in its suite that is actually going to run, and not at all for a suite everything was filtered out of. after_all runs after the last one, and runs even when before_all failed, so it must cope with a setup that got only part of the way. before_each runs outermost first and after_each innermost first, and after_each runs even when the test failed.

Asserting

expect(value) gives an object carrying every matcher, each of which negates through .not and returns itself so several read as one:

expect(port).to_be_int().to_be_between(1024, 65535)
expect(items).not.to_be_empty()

The full list is in test.expect. In short:

  • equality: to_be, to_equal, to_be_same_as, to_be_close_to, to_be_within, to_match_object
  • truthiness: to_be_true, to_be_false, to_be_truthy, to_be_falsy, to_be_nil, to_be_defined
  • types: to_be_a, to_be_string, to_be_number, to_be_int, to_be_float, to_be_bigint, to_be_bool, to_be_list, to_be_dict, to_be_bytes, to_be_function, to_be_callable, to_be_iterable, to_be_class, to_be_instance, to_be_instance_of
  • numbers: to_be_greater_than, to_be_greater_than_or_equal, to_be_less_than, to_be_less_than_or_equal, to_be_between, to_be_positive, to_be_negative, to_be_zero, to_be_divisible_by, to_be_even, to_be_odd, to_be_nan, to_be_finite, to_be_infinite
  • text: to_contain, to_contain_ignoring_case, to_equal_ignoring_case, to_start_with, to_end_with, to_match, to_be_blank
  • collections: to_have_length, to_be_empty, to_contain_equal, to_contain_all, to_contain_any, to_contain_none, to_contain_exactly, to_have_key, to_have_keys, to_have_value, to_have_property, to_be_sorted, to_have_unique_items
  • errors: to_raise, to_raise_instance_of, to_raise_with_message, to_not_raise
  • mocks: to_have_been_called, to_have_been_called_times, to_have_been_called_with, to_have_been_last_called_with, to_have_been_nth_called_with, to_have_returned, to_have_returned_with, to_have_raised
  • output: to_print, to_print_exactly, to_print_nothing
  • snapshots: to_match_snapshot
  • anything else: to_satisfy

fail(message) fails outright, for the branch that should never be reached. assertions(n) and has_assertions() fail a test that did not assert what it said it would, which is how a test proves that the callback it put its assertions in actually ran.

Mocking

var send = mock()
notify(send.fn)

expect(send).to_have_been_called_times(1)
expect(send).to_have_been_called_with('hello')

spy_on(object, 'key') replaces a function in place and restores it after the test, whether the test passed or not. See test.mock for what can and cannot be spied on.

Snapshots

expect(render(invoice)).to_match_snapshot()

The first run records the value beside the test file and passes; later runs compare against it. See test.snapshot for the file format, how to update them, and why a brand new snapshot fails on CI.

Running

run() takes an options dictionary:

OptionDefaultWhat it does
reporter'spec'spec, dot, tap, junit, json, ndjson, silent, or a Reporter of your own
filternilrun only tests whose full name contains this, or matches it when it is a regular expression
tagsnilrun only tests carrying one of these tags
exclude_tagsnilskip tests carrying one of these tags
bail0stop after this many failures; 0 never stops
shufflefalserun in a random order, to catch tests that depend on each other
seednilthe seed to shuffle with; one is chosen and reported when not given
slow300milliseconds past which a test’s time is highlighted
timeout0a budget applied to every test; 0 means none
retries0retries applied to every test
capturetruecollect each test’s output and show it only when it fails
verbosefalseshow a passing test’s output as well
update_snapshotsZURI_UPDATE_SNAPSHOTSrewrite snapshots instead of checking them
ciCItreat a brand new snapshot as a failure
exitfalseexit the process with the run’s status when it finishes

filter and reporter can also come from ZURI_TEST_FILTER and ZURI_TEST_REPORTER, which is what lets one command be pointed at one test without editing the file.

run() returns a Summary; summary.exit_code() is 0 when everything passed. A file that never calls run() gets the same status: the automatic run sets it through os.set_exit_code() when anything failed, so CI needs nothing added.

More than one file

conduct(directory) finds every test file under a directory, runs each in a process of its own, and reports them together. A file that hangs or crashes takes nothing else down with it. See test.conduct.

zuri test is that call behind a command line, and is all a project needs to run its tests directory:

$ zuri test
$ zuri test pricing      # just tests/pricing.zu

Call conduct() yourself when the run wants to be under the project’s own control:

# tests/index.zu
import os
import test

test.conduct(os.dir_name(__file__))

The test API

Every public name in test, wherever it is declared. Each links to the page that documents it.

NameKindSummary
test.AssertionErrorclassA matcher that did not hold.
test.CaseclassOne declared test, and what became of it.
test.ExpectclassThe subject of an assertion, and every matcher that can be applied to it.
test.FailureclassOne thing that went wrong.
test.MockclassA recording stand-in for a function.
test.ReporterclassThe interface the runner talks to, with every method doing nothing.
test.SuiteclassA describe() block: its tests, its nested suites, and its hooks.
test.SummaryclassWhat a whole run came to.
test.TestSetupErrorclassThe framework was asked to do something that does not make sense: a matcher given the wrong kind of argument,…
test.after_allfunctionRuns once after the last test in the enclosing suite, and only if before_all ran.
test.after_eachfunctionRuns after every test in the enclosing suite and everything nested inside it, innermost suite first, whether…
test.assertionsfunctionDeclares that this test makes exactly count assertions, and fails it if the number turns out to be…
test.before_allfunctionRuns once before the first test in the enclosing suite that is going to run.
test.before_eachfunctionRuns before every test in the enclosing suite and everything nested inside it, outermost suite first.
test.capture_outputfunctionRuns body and returns everything it printed to standard output.
test.conductfunctionDiscovers test files under directory and runs each in a process of its own.
test.conduct.CONDUCTEDconstant
test.conduct.FileResultclassWhat became of one file.
test.conduct.conductfunctionRuns every test file under directory, each in its own process, and reports them together.
test.conduct.discoverfunctionEvery test file under directory, in the order they will run.
test.conduct.is_conductedfunctionWhether this process was started by the conductor.
test.context.FrameclassThe test the runner currently has open, as far as anything outside the runner needs to know about it.
test.context.activefunctionWhether a test is running right now.
test.context.assertion_countfunctionHow many matchers have run in the current test.
test.context.beginfunctionOpens a frame for a test about to run.
test.context.checkfunctionChecks a finished test’s assertion promises against what it actually did.
test.context.currentfunctionThe frame for the test currently running, or nil outside one.
test.context.endfunctionCloses the current frame and returns it, so the runner can read the final assertion count off it.
test.context.expect_assertionsfunctionDeclares that the current test makes exactly count assertions.
test.context.expect_some_assertionsfunctionDeclares that the current test makes at least one assertion.
test.context.record_assertionfunctionCounts one matcher against the current test.
test.context.snapshot_keyfunctionClaims the next snapshot key for the current test.
test.declaredfunctionThe tests declared so far, as a tree, without running any of them.
test.describefunctionGroups tests, and may be nested.
test.describe_eachfunctionDeclares one suite per row of a table.
test.describe_onlyfunctionRuns only this suite, and anything else marked only.
test.describe_skipfunctionSkips this suite.
test.diff.MAX_DIFF_DEPTHconstant
test.diff.MAX_DIFF_LINESconstant
test.diff.equalfunctionWhether left and right are structurally equal.
test.diff.renderfunctionThe lines to print underneath a failure message, showing what the two values disagree about.
test.diff.subsetfunctionWhether every key in subset is present in value with a structurally equal value, ignoring any key value…
test.expectfunctionStarts an assertion about value.
test.failfunctionFails the current test outright.
test.format.DEFAULT_MAX_DEPTHconstant
test.format.DEFAULT_MAX_ITEMSconstant
test.format.DEFAULT_MAX_STRINGconstant
test.format.blockfunctionvalue rendered over as many lines as it takes, with nothing abbreviated away.
test.format.class_namefunctionThe name of the class value was built from, or nil when it is not an instance.
test.format.durationfunctionA duration in milliseconds, rendered the way a reader wants to see it: whole milliseconds below a second, and…
test.format.inlinefunctionvalue rendered as a single line, abbreviated to stay readable inside a sentence.
test.format.inline_plainfunctioninline() with colour suppressed, whatever the terminal supports.
test.format.pluralfunctioncount followed by singular or plural, whichever the count calls for.
test.format.properties_offunctionThe names of an instance’s own properties, in declaration order.
test.format.property_offunctionReads one of an instance’s own properties by name.
test.format.serializefunctionblock() with colour suppressed and dictionary keys sorted, which together make the output stable enough to…
test.format.type_labelfunctionHow to name value’s type in a sentence a person reads.
test.format.type_namefunctionThe name of value’s type: 'nil', 'bool', 'int', 'float', 'bigint', 'string', 'bytes',…
test.format.with_articlefunctionword with the article that belongs in front of it.
test.has_assertionsfunctionDeclares that this test makes at least one assertion, and fails it if none did.
test.itfunctionDeclares one test.
test.it_eachfunctionDeclares one test per row of a table.
test.it_onlyfunctionRuns only this test, and anything else marked only.
test.it_skipfunctionSkips this test, while still listing it in the report.
test.it_todofunctionNotes a test that has not been written yet.
test.mockfunctionA new recording function.
test.mock.CallclassOne recorded call.
test.mock.mock_offunctionThe Mock behind value, which may be a Mock already or the plain function one handed out.
test.mock.reset_allfunctionForgets the calls recorded by every live mock, leaving the mocks themselves and their behaviours in place.
test.mock.restore_allfunctionUndoes every spy and forgets every mock built so far.
test.reporter.DotclassOne character per test, wrapped to the terminal, then the same failure detail and summary the spec reporter…
test.reporter.JsonclassOne JSON document at the end, holding the summary and every test.
test.reporter.JunitclassJUnit XML, which is the format nearly every CI system knows how to turn into a test report page.
test.reporter.NdjsonclassOne JSON object per line, emitted as each thing happens.
test.reporter.PREFIXconstantWhat marks a line of ndjson output as protocol rather than as something the test file happened to print.
test.reporter.SilentclassNo output at all, for a caller reading the returned Summary instead.
test.reporter.SpecclassThe default: the suite tree, one line per test, and every failure written out in full at the end.
test.reporter.TapclassTAP version 14: one ok/not ok line per test, with failure detail in a YAML block underneath.
test.reporter.createfunctionBuilds a reporter by name.
test.resetfunctionForgets every declaration and every loaded snapshot file.
test.result.FAILEDconstantIt ran and something did not hold.
test.result.FLAKYconstantIt failed, was retried, and then passed.
test.result.PASSEDconstantIt ran and every assertion held.
test.result.PENDINGconstantA test was declared and not yet run.
test.result.SKIPPEDconstantIt was not run: skip, a filter, or another test’s only.
test.result.TODOconstantIt was declared with no body, as a note to write it later.
test.runfunctionRuns everything declared so far, and reports it.
test.runner.after_allfunction
test.runner.after_eachfunction
test.runner.before_allfunction
test.runner.before_eachfunction
test.runner.describefunctionOpens a suite, runs body to collect what is inside it, and closes it again.
test.runner.itfunctionDeclares one test.
test.runner.resetfunctionThrows away every declaration and every snapshot store, so a second run in the same process starts from…
test.runner.rootfunctionThe tree as it stands, for a caller that wants to look at what was declared without running it.
test.runner.runfunctionRuns everything declared so far and reports it.
test.snapshot.BODY_INDENTconstant
test.snapshot.HEADERconstant
test.snapshot.StoreclassEvery snapshot recorded for one test file, and the file they live in.
test.snapshot.checkfunctionCompares one value against what the snapshot file holds for key, writing it down when there is nothing…
test.snapshot.flushfunctionWrites every changed store back to disk, and in update mode drops entries the run never asked about.
test.snapshot.path_forfunctionThe .snap file that belongs to a given test file.
test.snapshot.recorded_linesfunctionThe lines of the snapshot at key, as a list, for a reporter that wants to show what was recorded.
test.snapshot.resetfunctionForgets every loaded store and zeroes the counters, so a second run in the same process starts clean.
test.snapshot.set_strictfunctionTurns the strict, new-snapshots-fail behaviour on or off.
test.snapshot.set_updatingfunctionTurns snapshot rewriting on or off.
test.snapshot.store_forfunctionThe store for a test file, read from disk the first time it is asked for and kept afterwards.
test.snapshot.strictfunctionWhether a brand new snapshot counts as a failure, which is what it should be anywhere nobody is going to look…
test.snapshot.summaryfunctionWhat happened to snapshots over the whole run.
test.snapshot.updatingfunctionWhether snapshots are being rewritten rather than checked.
test.source.FrameclassOne frame of a stack trace, pulled apart.
test.source.clear_cachefunctionForgets every file read for a code frame.
test.source.code_framefunctionThe source around a failure, with the offending line marked.
test.source.originfunctionThe innermost frame of stacktrace outside the test module: where the failing line actually is.
test.source.parse_framefunctionSplits one stack trace line into a Frame.
test.source.short_pathfunctionA path written relative to the working directory when it is under it, and left alone when it is not.
test.source.user_framesfunctionEvery frame of stacktrace that belongs to the code under test, innermost first.
test.spy_onfunctionReplaces target[key] with a recording stand-in that calls through to the original, and returns the Mock…
test.style.badgefunctionA filled, inverted label, the way a status banner reads in a CI log.
test.style.bluefunctionTodo entries and other informational notes.
test.style.boldfunctionEmphasis.
test.style.cyanfunctionStructure: suite names, headings.
test.style.dimfunctionDe-emphasis, for detail that should recede: timings, counts, the suite path above a failure.
test.style.enabledfunctionWhether colour is currently being emitted.
test.style.greenfunctionSuccess.
test.style.greyfunctionPunctuation and separators.
test.style.indentfunctionTwo spaces per level, the indent every nested suite and test line in the report is built from.
test.style.inversefunctionReserved for text that has to be found instantly on a busy screen.
test.style.magentafunctionValues in a rendered diff or failure message.
test.style.pad_leftfunctionPads text on the left to width visible columns, leaving it alone when it is already that wide or wider.
test.style.pad_rightfunctionPads text on the right to width visible columns, leaving it alone when it is already that wide or wider.
test.style.redfunctionFailure.
test.style.rulefunctioncount copies of character.
test.style.set_enabledfunctionForces colour on or off, overriding what the environment said.
test.style.set_unicodefunctionForces the Unicode symbol set on or off.
test.style.stripfunctiontext with every ANSI escape sequence removed.
test.style.symbolsfunctionThe symbol set in use, keyed by role.
test.style.truncatefunctionShortens text to at most width visible columns, marking the cut with an ellipsis.
test.style.unicodefunctionWhether the Unicode symbol set is in use, as opposed to the ASCII fallbacks.
test.style.visible_lengthfunctionHow many columns text occupies once its escape sequences are discounted.
test.style.whitefunctionPlain foreground, used to lift a key out of dimmed surroundings.
test.style.widthfunctionThe width to lay the report out to.
test.style.yellowfunctionSkipped and other deliberate non-results.
test.use_colorfunctionForces colour in the report on or off, overriding what the environment said.

Submodules

ModuleReached asSummary
test.conductimport test.conductRunning a directory of test files, each in a process of its own.
test.contextimport test.contextWhat is true right now, while one particular test is running.
test.diffimport test.diffStructural equality, and the rendering of what two unequal values disagree about.
test.errortest.error.*The errors the test module raises, kept in a module of their own so that the assertion side and the runner…
test.expecttest.expect.*expect(value) and everything that can be said about a value once you have one.
test.formatimport test.formatTurning any Zuri value into text a person can read in a failure message.
test.mocktest.mock.*Test doubles: functions that record how they were called, and stand-ins that replace a real one for the…
test.reportertest.reporter.*How a run is shown.
test.resulttest.result.*The shapes a test run is made of: the tree the declarations build, and what running it produced.
test.runnerimport test.runnerCollecting the tests a file declares, deciding which of them to run, and running them.
test.snapshotimport test.snapshotSnapshot testing: recording what a value looked like the first time and failing when it stops looking like…
test.sourceimport test.sourceWorking out where a failure came from, and showing the code that was there.
test.styleimport test.styleTerminal presentation for the test module: colour, symbols, width, and the padding helpers that keep a…

Functions

describe()

test.describe(name: string, body: function) -> Suite

Groups tests, and may be nested.

describe('Stack', @{
  describe('push()', @{
    it('grows the stack', @{ ... })
  })
})

Parameters

  • name (string)
  • body (function)

Returns Suite

describe_only()

test.describe_only(name: string, body: function) -> Suite

Runs only this suite, and anything else marked only.

Parameters

  • name (string)
  • body (function)

Returns Suite

describe_skip()

test.describe_skip(name: string, body: function) -> Suite

Skips this suite. Its tests are still declared, still counted, and still listed; they simply do not run.

Parameters

  • name (string)
  • body (function)

Returns Suite

it()

test.it(name: string, body: ?function, options: ?dict) -> Case

Declares one test.

it('returns the total', @{
  expect(total(cart)).to_be(42)
})

Parameters

  • name (string)
  • body (?function) — left out, this is a todo.
  • options (?dict) — retries, failing, timeout, tags. See the module documentation for what each does.

Returns Case

it_only()

test.it_only(name: string, body: function, options: ?dict) -> Case

Runs only this test, and anything else marked only.

Parameters

  • name (string)
  • body (function)
  • options (?dict)

Returns Case

it_skip()

test.it_skip(name: string, body: ?function, options: ?dict) -> Case

Skips this test, while still listing it in the report.

Parameters

  • name (string)
  • body (?function)
  • options (?dict)

Returns Case

it_todo()

test.it_todo(name: string) -> Case

Notes a test that has not been written yet.

It is listed in the report and counts towards nothing, which is the point: a todo is a reminder that survives being committed.

Parameters

  • name (string)

Returns Case

it_each()

test.it_each(table: list, name: string, body: function, options: ?dict)

Declares one test per row of a table.

it_each([
  ['', 0],
  ['a', 1],
  ['hello', 5],
], 'says $0 is $1 characters long', @(text, length) {
  expect(text.length()).to_be(length)
})

Each row is spread across body’s parameters, so a three-column table calls a three-parameter function. A row that is not a list counts as a single-column row.

Parameters

  • table (list) — the rows.
  • name (string) — $0, $1, … are replaced with that row’s values, and $# with the row number, counting from one.
  • body (function)
  • options (?dict) — applied to every test the table declares.

Returns — list: the declared tests.

describe_each()

test.describe_each(table: list, name: string, body: function)

Declares one suite per row of a table.

The row is spread across body’s parameters exactly as it_each() does, so the whole suite is written once against whatever the row holds.

describe_each([
  ['json', json_codec],
  ['yaml', yaml_codec],
], '$0 codec', @(name, codec) {
  it('round-trips a dictionary', @{
    expect(codec.decode(codec.encode({ a: 1 }))).to_equal({ a: 1 })
  })
})

Parameters

  • table (list)
  • name (string)
  • body (function)

Returns — list: the declared suites.

before_all()

test.before_all(body: function)

Runs once before the first test in the enclosing suite that is going to run.

Parameters

  • body (function)

after_all()

test.after_all(body: function)

Runs once after the last test in the enclosing suite, and only if before_all ran. It runs when before_all raised as well, since a setup that failed part way may already hold what needs releasing, so it must cope with whatever the setup got as far as.

Parameters

  • body (function)

before_each()

test.before_each(body: function)

Runs before every test in the enclosing suite and everything nested inside it, outermost suite first.

Parameters

  • body (function)

after_each()

test.after_each(body: function)

Runs after every test in the enclosing suite and everything nested inside it, innermost suite first, whether the test passed or not.

Parameters

  • body (function)

run()

test.run(options: ?dict) -> Summary

Runs everything declared so far, and reports it.

Calling this is optional. A file that declares tests and never says anything else runs them as it ends, and fails the process when they fail. Call it when you want to pass options or read the result:

run({ reporter: 'dot', bail: 1 })

var summary = run({ filter: 'parser' })
echo summary.passed

Parameters

  • options (?dict) — see the module documentation for every option and its default.

Returns Summary

Note: calling it takes over from the automatic run, so nothing is reported twice. Calling it twice does run everything twice.

conduct()

test.conduct(directory: ?string, options: ?dict)

Discovers test files under directory and runs each in a process of its own.

Parameters

  • directory (?string) — 'tests' by default.
  • options (?dict) — see {test.conduct}.

Returns — dict: the combined result.

declared()

test.declared() -> Suite

The tests declared so far, as a tree, without running any of them.

Returns Suite

reset()

test.reset()

Forgets every declaration and every loaded snapshot file.

For a process that runs more than one suite, and for the tests of this framework itself.

use_color()

test.use_color(on: bool)

Forces colour in the report on or off, overriding what the environment said.

Parameters

  • on (bool)

2021, Richard Ore and Zuri contributors

test.conduct

import test.conduct

test does not re-export this module, so it is reached only by importing it directly.

Running a directory of test files, each in a process of its own.

import test

test.conduct('tests')

zuri test is this call behind a command line, with every option below as a flag, so a project gets a test runner without writing one. Call conduct() directly when the run wants to be under the project’s own control.

One process per file

A test file is an ordinary Zuri script, and each one is run as one:

  • A file that loops forever is killed, and the rest still run.
  • A file that segfaults, or calls os.exit() halfway through, is reported as a crashed file rather than taking the run with it.
  • Global state, a module loaded with a side effect, a signal handler, a changed working directory: none of it leaks from one file into the next.
  • timeout is enforced by kill, so it holds for a file that never returns.

The index is not a test file

index.zu is left out of discovery. A directory handed to zuri run runs its index.zu, so a suite that wants to start itself puts one there:

# tests/index.zu
import os
import test

test.conduct(os.dir_name(__file__))
$ zuri run tests

The script that is currently running is left out as well, whatever it is called, so no arrangement of files makes a run start itself again.

What a test file has to do

Nothing special. It declares its tests. The conductor sets ZURI_TEST_CONDUCTED in the child, and the run switches to a machine-readable reporter when it sees it, so the same file is equally runnable on its own.

A file that declares no tests at all, or exits before it can report, is called out rather than silently counting as zero tests.

Ordering

Files are reported in the order they were discovered, whatever order they finish in, so a run is reproducible with jobs turned up.

Constants

CONDUCTED

test.conduct.CONDUCTED = 'ZURI_TEST_CONDUCTED'

Functions

discover()

test.conduct.discover(directory: string, options: ?dict)

Every test file under directory, in the order they will run.

index.zu is never one of them. A directory handed to zuri run runs its index.zu, so that file is the thing that starts a suite rather than a part of one, and conducting a directory from inside its own index is the shape this is written for. Pass an ignore list of your own when a file of that name really is a test.

The script that is currently running is left out too, whatever it is called, so no arrangement of files can make a run start itself again.

Parameters

  • directory (string)
  • options (?dict) — match, ignore and recursive, as conduct() takes them.

Returns — list: paths, relative to directory.

conduct()

test.conduct.conduct(directory: ?string, options: ?dict)

Runs every test file under directory, each in its own process, and reports them together.

import test

test.conduct('tests', { timeout: 30000, jobs: 4 })

The process ends with status 1 when anything failed, through os.set_exit_code(), so nothing has to be added for CI. The returned dictionary is there for a caller that wants to look at the run rather than just be told about it.

  • match: filename patterns to run, ['*.zu'] by default. * and ? are the only wildcards.
    • ignore: patterns for files and directories to skip, ['_*', '.*', 'index.zu'] by default, which is what keeps helper files, __snapshots__ and the index that starts the suite out of it.
    • recursive: whether to look in subdirectories. true.
    • jobs: how many files to run at once. 1, because a test file that binds a port or writes a fixture was probably not written to share the machine. Reporting order is by discovery whatever this is set to.
    • timeout: milliseconds to give one file before killing it. 0, meaning no limit.
    • bail: stop after this many failing files. 0 never stops.
    • env: extra environment variables for every child.

Parameters

  • directory (?string) — 'tests' by default.
  • options (?dict)

Returns — dict: { files, totals, duration, exit_code, ok }, where files is a list of FileResult.

Raises TestSetupError when directory does not exist.

Note: index.zu is never one of the files it runs, and neither is the script that called it, so a tests/index.zu conducting its own directory works.

is_conducted()

test.conduct.is_conducted() -> bool

Whether this process was started by the conductor.

run() uses it to switch to the machine-readable reporter. A test file has no reason to care.

Returns bool

Classes

FileResult

class test.conduct.FileResult

What became of one file.

Fields

FieldTypeDescription
pathThe path, relative to the directory that was conducted.
codeThe child’s exit status.
durationHow long the child ran, in milliseconds.
testsThe test_finished payloads the child reported.
summaryThe child’s own summary, or nil when it never reported one.
outputAnything the child printed that was not part of the protocol.
problemWhy the file did not report properly, when it did not.

Constructor

test.conduct.FileResult(path)

FileResult.ok()

test.conduct.FileResult.ok()

Whether the file ran cleanly and everything in it passed. @returns bool

test.context

import test.context

test does not re-export this module, so it is reached only by importing it directly.

What is true right now, while one particular test is running.

The assertion side and the runner side both need this, and neither should have to import the other, so it lives on its own. Nothing in here is interesting to a test author; the API a test uses is test.assertions() and test.has_assertions(), both of which are thin wrappers over what is below.

The state is a single module-level value because a Zuri program has one call stack and runs one test at a time. There is nothing to key it by.

Functions

begin()

test.context.begin(name: string, suite_path: list, file: string) -> Frame

Opens a frame for a test about to run. Called by the runner, once per attempt.

Parameters

  • name (string)
  • suite_path (list)
  • file (string)

Returns Frame

end()

test.context.end()

Closes the current frame and returns it, so the runner can read the final assertion count off it.

Returns — Frame: nil when no test was open.

current()

test.context.current() -> Frame

The frame for the test currently running, or nil outside one.

Returns Frame

active()

test.context.active() -> bool

Whether a test is running right now.

Matchers use this to tell a real assertion failure, which the runner will catch and report, from one raised in a suite body or at the top level of a file, where there is nothing to attribute it to.

Returns bool

record_assertion()

test.context.record_assertion()

Counts one matcher against the current test. Every matcher calls this, whether it passes or fails.

assertion_count()

test.context.assertion_count()

How many matchers have run in the current test.

Returns — int: 0 outside a test.

expect_assertions()

test.context.expect_assertions(count: int)

Declares that the current test makes exactly count assertions.

Parameters

  • count (int)

Raises TestSetupError outside a test, where there is nothing to hold to the promise.

expect_some_assertions()

test.context.expect_some_assertions()

Declares that the current test makes at least one assertion.

Raises TestSetupError outside a test.

check()

test.context.check(frame)

Checks a finished test’s assertion promises against what it actually did.

Parameters

  • frame (Frame)

Returns — string: what went wrong, or nil when nothing did.

snapshot_key()

test.context.snapshot_key(name: ?string) -> string

Claims the next snapshot key for the current test.

A named snapshot keeps its name. An unnamed one is numbered in the order it was taken, so reordering the snapshots inside one test is a change the snapshot file will notice.

Parameters

  • name (?string)

Returns string

Raises TestSetupError outside a test.

Classes

Frame

class test.context.Frame

The test the runner currently has open, as far as anything outside the runner needs to know about it.

Fields

FieldTypeDescription
nameThe test’s own name.
suite_pathEnclosing suite names, outermost first.
fileThe file the test was declared in.
assertionsMatchers that have run since the test started.
expected_assertionsHow many assertions the test said it would make, or -1 when it did not say.
requires_assertionsWhether the test asked to be failed if it asserts nothing.
snapshot_countSnapshot names already used by this test, so an unnamed snapshot can be numbered 1, 2, 3 in the order…

Constructor

test.context.Frame(name, suite_path, file)

Frame.full_name()

test.context.Frame.full_name() -> string

The test’s full name, suite path included, as it appears in a report and in a snapshot file.

Returns string

test.diff

import test.diff

test does not re-export this module, so it is reached only by importing it directly.

Structural equality, and the rendering of what two unequal values disagree about.

equal() is what to_equal() is built on, and it is worth knowing exactly how it differs from ==:

==equal()
list, dict, bytescompares contentscompares contents
instancecompares identitycompares class, then every field
NaNnever equal to anythingequal to NaN
-0 and 0equalequal
int and bigintnever equalnever equal
cyclic valueshangscompares, treating a repeated pair as equal

The one that surprises people is instances: Point(1, 2) == Point(1, 2) is false, because two separately constructed objects are two objects. equal() is the one to reach for when what you care about is the contents.

render() turns a failed comparison into lines to print. It picks the shape that shows the most: a line diff for multi-line strings, a character marker for single-line ones, a keyed structural diff for dictionaries and instances, a positional one for lists, and a plain two-line expected/received block for anything else.

Constants

MAX_DIFF_DEPTH

test.diff.MAX_DIFF_DEPTH = 6

MAX_DIFF_LINES

test.diff.MAX_DIFF_LINES = 30

Functions

equal()

test.diff.equal(left, right) -> bool

Whether left and right are structurally equal.

Parameters

  • left (any)
  • right (any)

Returns bool

subset()

test.diff.subset(expected: dict, value) -> bool

Whether every key in subset is present in value with a structurally equal value, ignoring any key value has that subset does not mention.

Nested dictionaries recurse, so { user: { id: 1 } } matches { user: { id: 1, name: 'Ada' } }. Lists do not: a list in subset must equal the list in value outright, since “some of these elements, in no particular place” is a different question and has its own matcher.

Instances are matched by property, so a dictionary of expectations can be checked against a real object.

Parameters

  • subset (dict)
  • value (any)

Returns bool

render()

test.diff.render(expected, received)

The lines to print underneath a failure message, showing what the two values disagree about.

Every line is already coloured and carries no indent of its own; the reporter decides how far in the whole block sits.

import test.diff

for line in diff.render([1, 2, 3], [1, 9, 3]) {
  echo line
}

Parameters

  • expected (any)
  • received (any)

Returns — list: the lines, or an empty list when the two are equal.

test.error

import test.error

test lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled test.error.* needs import test.error.

The errors the test module raises, kept in a module of their own so that the assertion side and the runner side can both name them without importing each other.

Only AssertionError normally reaches a test author, and only by being caught for them. Everything else in here marks a mistake in how the framework itself was used, and is meant to stop the run.

Classes

AssertionError

class test.AssertionError < Error

A matcher that did not hold.

The runner catches these, so a test body never has to. What separates one from any other error raised inside a test is that the framework knows how to display it: it carries the two values that disagreed, and whatever extra lines (a diff, a hint) the matcher wanted shown underneath.

import test { AssertionError }

catch {
  expect(1).to_be(2)
} as error {
  echo error.matcher
  echo error.expected
  echo error.received
}
to_be
2
1

Fields

FieldTypeDescription
matcherThe matcher that failed, e.g. 'to_be', 'to_contain'.
expectedWhat the matcher was told to expect.
receivedWhat it was actually given.
has_valuesWhether expected and received are worth showing.
detailsPre-rendered lines to print underneath the message: a structural diff, the calls a mock actually received, a…

Constructor

test.AssertionError(message, info)

Parameters

  • message (string) — the one-line summary.
  • info (?dict) — any of matcher, expected, received, has_values, details.

TestSetupError

class test.TestSetupError < Error

The framework was asked to do something that does not make sense: a matcher given the wrong kind of argument, a hook registered outside any suite, a reporter name that does not exist.

This is never a test failing. It stops the run, because carrying on would report results that do not mean what they say.

Constructor

test.TestSetupError(message)

test.expect

import test.expect

test lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled test.expect.* needs import test.expect.

expect(value) and everything that can be said about a value once you have one.

expect(total).to_be(42)
expect(user).to_match_object({ name: 'Ada' })
expect(items).not.to_be_empty()
expect(@{ parse('') }).to_raise_instance_of(ValueError)

Negation

Every matcher negates through .not, and there is exactly one implementation behind both directions, so a matcher and its negation can never disagree about what they mean.

Chaining

Matchers return the Expect they were called on, so several statements about one value read as one:

expect(port).to_be_int().to_be_between(1024, 65535)

Labels

A second argument to expect() names the value, which is worth doing when the number alone would not say which number it was:

expect(response.status, 'status').to_be(200)
Expected status (404) to be 200

Choosing between to_be and to_equal

to_be is Zuri’s ==. That already compares lists, dictionaries and bytes by their contents, so it is the right matcher for most things. It compares two instances by identity, though, so two separately constructed objects are never to_be each other. to_equal is the structural one: it walks instances field by field, treats NaN as equal to itself, and survives a value that contains itself.

Functions

expect()

test.expect(value, label: ?string) -> Expect

Starts an assertion about value.

expect(total).to_be(42)
expect(total, 'order total').to_be(42)

Parameters

  • value (any)
  • label (?string) — a name for the value, shown in failure messages ahead of the value itself.

Returns Expect

fail()

test.fail(message: ?string)

Fails the current test outright.

For the branch that should never be reached:

using response.status {
  when 200 handle_ok()
  when 404 handle_missing()
  default fail('unexpected status ${response.status}')
}

Parameters

  • message (?string)

Raises AssertionError always.

assertions()

test.assertions(count: int)

Declares that this test makes exactly count assertions, and fails it if the number turns out to be different.

This is how a test proves that a branch it expected to reach was actually reached, which matters most when the assertions are inside a callback that might never run:

it('reports every error', @{
  assertions(2)

  validate(bad_input, @(error) {
    expect(error.field).to_be_string()
  })
})

Parameters

  • count (int)

Raises TestSetupError outside a test.

has_assertions()

test.has_assertions()

Declares that this test makes at least one assertion, and fails it if none did.

Raises TestSetupError outside a test.

capture_output()

test.capture_output(body: function) -> string

Runs body and returns everything it printed to standard output.

The composable half of to_print(): use this when the assertion to make about the output is not “it contains this”.

var printed = capture_output(@{ report(rows) })
expect(printed.lines()).to_have_length(4)

Parameters

  • body (function)

Returns string

Classes

Expect

class test.Expect

The subject of an assertion, and every matcher that can be applied to it.

Built by expect(); there is no reason to construct one directly.

Fields

FieldTypeDescription
notThe same subject, with every matcher asserting the opposite.

Constructor

test.Expect(value, label, negated, twin)

Parameters

  • value (any) — the value under test.
  • label (?string) — a name for it, used in failure messages.
  • negated (?bool)
  • twin (?Expect) — the opposite-polarity object to point not at. Left out, one is built; this is what stops the pair from constructing each other forever.

Expect.to_be()

test.Expect.to_be(expected) -> Expect

Asserts ==.

Numbers, strings and booleans compare by value; lists, dictionaries and bytes compare by contents; instances and functions compare by identity.

expect(2 + 2).to_be(4)
expect([1, 2]).to_be([1, 2])

Parameters

  • expected (any)

Returns Expect

Expect.to_equal()

test.Expect.to_equal(expected) -> Expect

Asserts structural equality: instances are compared field by field rather than by identity, NaN equals NaN, and a value that contains itself is handled rather than hung on.

expect(Point(1, 2)).to_equal(Point(1, 2))

Parameters

  • expected (any)

Returns Expect

Expect.to_be_same_as()

test.Expect.to_be_same_as(expected) -> Expect

Asserts that this is the very same object, not merely one that looks the same. Two lists with identical contents are to_be each other but not to_be_same_as each other.

var original = [1, 2]
expect(passed_through(original)).to_be_same_as(original)

Parameters

  • expected (any)

Returns Expect

Expect.to_be_close_to()

test.Expect.to_be_close_to(expected: number, digits: ?int) -> Expect

Asserts two numbers agree to digits decimal places, which is the only sane way to compare anything that has been through floating-point arithmetic.

expect(0.1 + 0.2).to_be_close_to(0.3)

Parameters

  • expected (number)
  • digits (?int) — decimal places, 2 by default, so the default tolerance is 0.005.

Returns Expect

Expect.to_be_within()

test.Expect.to_be_within(expected: number, delta: number) -> Expect

Asserts two numbers are no more than delta apart, when the tolerance is better stated as an absolute amount than as a number of decimal places.

expect(elapsed_ms).to_be_within(500, 50)

Parameters

  • expected (number)
  • delta (number) — inclusive.

Returns Expect

Expect.to_match_object()

test.Expect.to_match_object(expected: dict) -> Expect

Asserts that every key in expected is present with a matching value, ignoring anything else the subject carries.

Nested dictionaries recurse, and the subject may be a dictionary or an instance, so one expectation can be checked against either.

expect(create_user('Ada')).to_match_object({ name: 'Ada', active: true })

Parameters

  • expected (dict)

Returns Expect

Expect.to_be_true()

test.Expect.to_be_true()

Asserts the value is exactly true. @returns Expect

Expect.to_be_false()

test.Expect.to_be_false()

Asserts the value is exactly false. @returns Expect

Expect.to_be_truthy()

test.Expect.to_be_truthy() -> Expect

Asserts the value is truthy.

Returns Expect

Note: Zuri’s falsy values are false, nil, zero, NaN, a zero bigint, the empty string and an empty bytes buffer. Every negative number is truthy, and so is an empty list or dictionary; use to_be_empty() for those.

Expect.to_be_falsy()

test.Expect.to_be_falsy() -> Expect

Asserts the value is falsy.

Returns Expect

Note: see to_be_truthy() for the full list of falsy values.

Expect.to_be_nil()

test.Expect.to_be_nil()

Asserts the value is nil. @returns Expect

Expect.to_be_defined()

test.Expect.to_be_defined() -> Expect

Asserts the value is anything other than nil, which is the assertion to reach for after a lookup that can come back empty.

Returns Expect

Expect.to_be_a()

test.Expect.to_be_a(type) -> Expect

Asserts the value’s type.

type may be a type name as a string, one of the names format.type_name() produces, or a class, in which case this means the same as to_be_instance_of().

expect(payload).to_be_a('dict')
expect(result).to_be_a(Point)

Parameters

  • type (string|class)

Returns Expect

Expect.to_be_string()

test.Expect.to_be_string()

Asserts the value is a string. @returns Expect

Expect.to_be_number()

test.Expect.to_be_number()

Asserts the value is a number, of either kind. @returns Expect

Expect.to_be_int()

test.Expect.to_be_int()

Asserts the value is a number with no fractional part. @returns Expect

Expect.to_be_float()

test.Expect.to_be_float()

Asserts the value is a number with a fractional part. @returns Expect

Expect.to_be_bigint()

test.Expect.to_be_bigint()

Asserts the value is an arbitrary-precision integer. @returns Expect

Expect.to_be_bool()

test.Expect.to_be_bool()

Asserts the value is a boolean. @returns Expect

Expect.to_be_list()

test.Expect.to_be_list()

Asserts the value is a list. @returns Expect

Expect.to_be_dict()

test.Expect.to_be_dict()

Asserts the value is a dictionary. @returns Expect

Expect.to_be_bytes()

test.Expect.to_be_bytes()

Asserts the value is a bytes buffer. @returns Expect

Expect.to_be_function()

test.Expect.to_be_function()

Asserts the value is a function. @returns Expect

Expect.to_be_callable()

test.Expect.to_be_callable() -> Expect

Asserts the value can be called, which a class and a bound method can be as well as a function.

Returns Expect

Expect.to_be_iterable()

test.Expect.to_be_iterable() -> Expect

Asserts the value can be iterated with for, which covers lists, dictionaries, strings, ranges, bytes, and any class defining @key and @value.

Returns Expect

Expect.to_be_class()

test.Expect.to_be_class()

Asserts the value is a class, not an instance of one. @returns Expect

Expect.to_be_instance()

test.Expect.to_be_instance()

Asserts the value is an instance of some class. @returns Expect

Expect.to_be_instance_of()

test.Expect.to_be_instance_of(klass: type) -> Expect

Asserts the value is an instance of klass or of anything that inherits from it.

expect(error).to_be_instance_of(ValueError)

Parameters

  • type — klass

Returns Expect

Expect.to_be_greater_than()

test.Expect.to_be_greater_than(bound: number)

Parameters

  • bound (number) — @returns Expect

Expect.to_be_greater_than_or_equal()

test.Expect.to_be_greater_than_or_equal(bound: number)

Parameters

  • bound (number) — @returns Expect

Expect.to_be_less_than()

test.Expect.to_be_less_than(bound: number)

Parameters

  • bound (number) — @returns Expect

Expect.to_be_less_than_or_equal()

test.Expect.to_be_less_than_or_equal(bound: number)

Parameters

  • bound (number) — @returns Expect

Expect.to_be_between()

test.Expect.to_be_between(low: number, high: number) -> Expect

Asserts the value falls in [low, high], both ends included.

Parameters

  • low (number)
  • high (number)

Returns Expect

Expect.to_be_positive()

test.Expect.to_be_positive()

Asserts the value is greater than zero. @returns Expect

Expect.to_be_negative()

test.Expect.to_be_negative()

Asserts the value is less than zero. @returns Expect

Expect.to_be_zero()

test.Expect.to_be_zero()

Asserts the value is zero, of either sign. @returns Expect

Expect.to_be_divisible_by()

test.Expect.to_be_divisible_by(divisor: number) -> Expect

Asserts the value divides by divisor with nothing left over.

Parameters

  • divisor (number)

Returns Expect

Expect.to_be_even()

test.Expect.to_be_even()

Asserts the value is an even whole number. @returns Expect

Expect.to_be_odd()

test.Expect.to_be_odd()

Asserts the value is an odd whole number. @returns Expect

Expect.to_be_nan()

test.Expect.to_be_nan() -> Expect

Asserts the value is the floating-point not-a-number.

NaN is not == to itself, so this is the only way to check for it.

Returns Expect

Expect.to_be_finite()

test.Expect.to_be_finite()

Asserts the value is a number that is neither infinite nor NaN. @returns Expect

Expect.to_be_infinite()

test.Expect.to_be_infinite()

Asserts the value is positive or negative infinity. @returns Expect

Expect.to_contain()

test.Expect.to_contain(item) -> Expect

Asserts the subject contains item.

What “contains” means follows the subject: a substring of a string, an element of a list or a bytes buffer, a key of a dictionary. Comparison is ==; to_contain_equal() is the structural version.

Parameters

  • item (any)

Returns Expect

Expect.to_contain_equal()

test.Expect.to_contain_equal(item) -> Expect

Asserts the subject contains an element deeply equal to item, which is what finding an object in a list needs.

Parameters

  • item (any)

Returns Expect

Expect.to_contain_ignoring_case()

test.Expect.to_contain_ignoring_case(part: string) -> Expect

Asserts the string contains part, ignoring case.

Parameters

  • part (string)

Returns Expect

Expect.to_equal_ignoring_case()

test.Expect.to_equal_ignoring_case(expected: string) -> Expect

Asserts the string equals expected, ignoring case.

Parameters

  • expected (string)

Returns Expect

Expect.to_start_with()

test.Expect.to_start_with(prefix: string) -> Expect

Parameters

  • prefix (string)

Returns Expect

Expect.to_end_with()

test.Expect.to_end_with(suffix: string) -> Expect

Parameters

  • suffix (string)

Returns Expect

Expect.to_match()

test.Expect.to_match(pattern: string) -> Expect

Asserts the string matches a regular expression.

expect(id).to_match('/^user-[0-9]+$/')

Parameters

  • pattern (string) — a Zuri regular expression, delimiters included.

Returns Expect

Expect.to_be_blank()

test.Expect.to_be_blank() -> Expect

Asserts the string is empty or contains nothing but whitespace.

Returns Expect

Expect.to_have_length()

test.Expect.to_have_length(expected: int) -> Expect

Asserts the subject’s length.

Parameters

  • expected (int)

Returns Expect

Expect.to_be_empty()

test.Expect.to_be_empty() -> Expect

Asserts the subject has no elements, no entries, or no characters.

Returns Expect

Expect.to_contain_all()

test.Expect.to_contain_all(items: list) -> Expect

Asserts every one of items is present, in any order.

Parameters

  • items (list)

Returns Expect

Expect.to_contain_any()

test.Expect.to_contain_any(items: list) -> Expect

Asserts at least one of items is present.

Parameters

  • items (list)

Returns Expect

Expect.to_contain_none()

test.Expect.to_contain_none(items: list) -> Expect

Asserts none of items is present.

Parameters

  • items (list)

Returns Expect

Expect.to_contain_exactly()

test.Expect.to_contain_exactly(items: list) -> Expect

Asserts the list holds exactly items, in any order and with the same number of duplicates.

This is the matcher for a collection whose order is not part of the contract, such as one built from a dictionary’s keys.

Parameters

  • items (list)

Returns Expect

Expect.to_have_key()

test.Expect.to_have_key(key) -> Expect

Asserts the dictionary has key, whatever it maps to.

Parameters

  • key (any)

Returns Expect

Expect.to_have_keys()

test.Expect.to_have_keys(keys: list) -> Expect

Asserts the dictionary has every one of keys.

Parameters

  • keys (list)

Returns Expect

Expect.to_have_value()

test.Expect.to_have_value(value) -> Expect

Asserts the dictionary maps some key to value.

Parameters

  • value (any)

Returns Expect

Expect.to_have_property()

test.Expect.to_have_property(path: string, value) -> Expect

Asserts something exists at a dotted path, and optionally that it equals value.

The path walks dictionary keys, instance properties and list indices alike, so one expression reaches into a whole decoded response:

expect(payload).to_have_property('data.items.0.id', 7)

Parameters

  • path (string)
  • value (?any) — when given, what must be there. Left out, this only asserts that the path resolves to something other than nil.

Returns Expect

Expect.to_be_sorted()

test.Expect.to_be_sorted() -> Expect

Asserts the list is in ascending order.

Numbers compare numerically and strings compare by codepoint. A list mixing the two has no order to be in, and says so.

Returns Expect

Expect.to_have_unique_items()

test.Expect.to_have_unique_items() -> Expect

Asserts no two elements of the list are equal.

Returns Expect

Expect.to_raise()

test.Expect.to_raise() -> Expect

Asserts that calling the subject raises.

The subject must be a function taking no arguments; wrap the call being tested in one.

expect(@{ parse('') }).to_raise()

Returns Expect

Expect.to_raise_instance_of()

test.Expect.to_raise_instance_of(klass: type) -> Expect

Asserts that calling the subject raises an instance of klass, or of something inheriting from it.

expect(@{ parse('') }).to_raise_instance_of(ValueError)

Parameters

  • type — klass

Returns Expect

Expect.to_raise_with_message()

test.Expect.to_raise_with_message(part: string) -> Expect

Asserts that calling the subject raises something whose message contains part, or matches it when part is a regular expression.

expect(@{ withdraw(10, 5) }).to_raise_with_message('cannot withdraw')
expect(@{ withdraw(10, 5) }).to_raise_with_message('/withdraw [0-9]+/')

Parameters

  • part (string) — taken as a regular expression when it is delimited like one, and as a substring otherwise.

Returns Expect

Expect.to_not_raise()

test.Expect.to_not_raise() -> Expect

Asserts that calling the subject does not raise.

The same as .not.to_raise(), and there for the many tests that read better stating it directly.

Returns Expect

Expect.to_have_been_called()

test.Expect.to_have_been_called() -> Expect

Asserts the mock has been called at least once.

The subject may be the Mock or the function it handed out.

Returns Expect

Expect.to_have_been_called_times()

test.Expect.to_have_been_called_times(count: int) -> Expect

Asserts the mock has been called exactly count times.

Parameters

  • count (int)

Returns Expect

Expect.to_have_been_called_with()

test.Expect.to_have_been_called_with(...args: list) -> Expect

Asserts the mock was called at least once with exactly these arguments.

expect(send).to_have_been_called_with('hello', { retry: true })

Parameters

  • args (...any)

Returns Expect

Expect.to_have_been_last_called_with()

test.Expect.to_have_been_last_called_with(...args: list) -> Expect

Asserts the mock’s most recent call had exactly these arguments.

Parameters

  • args (...any)

Returns Expect

Expect.to_have_been_nth_called_with()

test.Expect.to_have_been_nth_called_with(index, ...args: list) -> Expect

Asserts the mock’s index-th call had exactly these arguments, counting from zero. A negative index counts back from the most recent.

Parameters

  • index (int)
  • args (...any)

Returns Expect

Expect.to_have_returned()

test.Expect.to_have_returned() -> Expect

Asserts at least one call returned rather than raised.

Returns Expect

Expect.to_have_returned_with()

test.Expect.to_have_returned_with(value) -> Expect

Asserts at least one call returned value.

Parameters

  • value (any)

Returns Expect

Expect.to_have_raised()

test.Expect.to_have_raised() -> Expect

Asserts at least one call raised.

Returns Expect

Expect.to_print()

test.Expect.to_print(part: string) -> Expect

Asserts that running the subject prints something containing part.

The subject must be a function taking no arguments. Everything Zuri writes to standard output while it runs is collected: echo, print() and io.stdout alike.

expect(@{ greet('Ada') }).to_print('Hello, Ada')

Parameters

  • part (string)

Returns Expect

Expect.to_print_exactly()

test.Expect.to_print_exactly(expected: string) -> Expect

Asserts that running the subject prints exactly expected, trailing newline and all.

Parameters

  • expected (string)

Returns Expect

Expect.to_print_nothing()

test.Expect.to_print_nothing() -> Expect

Asserts that running the subject prints nothing at all.

Returns Expect

Expect.to_match_snapshot()

test.Expect.to_match_snapshot(name: ?string) -> Expect

Asserts the value still looks the way it did when it was first recorded.

The first run writes the snapshot and passes. Later runs compare against it. See test.snapshot on where the files live, how to update them, and why a brand new snapshot fails on CI.

expect(render(invoice)).to_match_snapshot()
expect(headers).to_match_snapshot('response headers')

Parameters

  • name (?string) — distinguishes several snapshots in one test. Without one they are numbered in the order they are taken.

Returns Expect

Expect.to_satisfy()

test.Expect.to_satisfy(predicate: function, description: ?string) -> Expect

Asserts predicate(value) is truthy, for the assertion no built-in matcher makes.

expect(port).to_satisfy(@(n) { return n % 2 == 0 }, 'to be an even port')

Parameters

  • predicate (function)
  • description (?string) — completes the sentence Expected <value> .... Without one the message says only that a predicate was not satisfied, which is rarely enough.

Returns Expect

test.format

import test.format

test does not re-export this module, so it is reached only by importing it directly.

Turning any Zuri value into text a person can read in a failure message.

Two shapes are produced. inline() gives one line, abbreviated to fit in a sentence: this is what Expected 3 but received 4 is built from. block() gives a list of lines, indented, with nothing abbreviated away: this is what a diff and a snapshot are built from.

Both are safe on values that contain themselves. A list or a dictionary already being rendered further up the same call is printed as [Circular] rather than followed round again.

Limits

inline() shows at most 8 entries of a list or dictionary, nests at most 3 levels deep, and shortens a string past 60 characters. Anything cut off is marked, never silently dropped. Pass an options dictionary to change any of them:

import test.format

echo format.inline(big_list, { max_items: 40, max_depth: 6 })

block() has no item or depth limit at all, because the reader asked for the whole thing.

Constants

DEFAULT_MAX_ITEMS

test.format.DEFAULT_MAX_ITEMS = 8

DEFAULT_MAX_DEPTH

test.format.DEFAULT_MAX_DEPTH = 3

DEFAULT_MAX_STRING

test.format.DEFAULT_MAX_STRING = 60

Functions

type_name()

test.format.type_name(value) -> string

The name of value’s type: 'nil', 'bool', 'int', 'float', 'bigint', 'string', 'bytes', 'list', 'dict', 'range', 'function', 'class', 'instance', 'module' or 'file'.

Every instance is an 'instance', whatever it was built from. Use type_label() for the form that names the class.

Parameters

  • value (any)

Returns string

type_label()

test.format.type_label(value) -> string

How to name value’s type in a sentence a person reads.

The same as type_name(), except that an instance is called by its class: “expected a Point, received a Vector” beats hearing that both of them are instances.

Parameters

  • value (any)

Returns string

with_article()

test.format.with_article(word: string) -> string

word with the article that belongs in front of it.

Worth the three lines: “expected a int” is the kind of wording that makes a tool feel unfinished.

Parameters

  • word (string)

Returns string

properties_of()

test.format.properties_of(value) -> list

The names of an instance’s own properties, in declaration order.

The single place in the test module that knows how to look inside an instance, so equality, diffing and rendering never disagree about what an object’s contents are.

Parameters

  • value (instance)

Returns list

property_of()

test.format.property_of(value, name: string)

Reads one of an instance’s own properties by name.

Parameters

  • value (instance)
  • name (string)

Returns — any: nil when there is no such property.

class_name()

test.format.class_name(value) -> string

The name of the class value was built from, or nil when it is not an instance.

Parameters

  • value (any)

Returns string

inline()

test.format.inline(value, options: ?dict) -> string

value rendered as a single line, abbreviated to stay readable inside a sentence.

import test.format

echo format.inline({ name: 'Ada', tags: ['x', 'y'] })
{ name: 'Ada', tags: ['x', 'y'] }

Parameters

  • value (any)
  • options (?dict) — any of color, max_items, max_depth, max_string, sort_keys. Anything left out keeps its default.

Returns string

inline_plain()

test.format.inline_plain(value, options: ?dict) -> string

inline() with colour suppressed, whatever the terminal supports.

This is what a machine-readable reporter and a snapshot key are built from, since an escape sequence in either would be a correctness bug rather than a cosmetic one.

Parameters

  • value (any)
  • options (?dict)

Returns string

block()

test.format.block(value, options: ?dict)

value rendered over as many lines as it takes, with nothing abbreviated away.

A scalar comes back as a single-element list, so the caller never has to special-case it.

import test.format

for line in format.block({ id: 7, tags: ['a', 'b'] }) {
  echo line
}
{
  id: 7,
  tags: [
    'a',
    'b'
  ]
}

Parameters

  • value (any)
  • options (?dict) — as inline(), except that max_items and max_depth default to unlimited.

Returns — list: the lines, without trailing newlines.

serialize()

test.format.serialize(value)

block() with colour suppressed and dictionary keys sorted, which together make the output stable enough to commit to a repository.

This is the exact text a snapshot file holds.

Parameters

  • value (any)

Returns — string: the lines joined with \n, with no trailing one.

duration()

test.format.duration(milliseconds: number) -> string

A duration in milliseconds, rendered the way a reader wants to see it: whole milliseconds below a second, and seconds with one decimal place above.

Parameters

  • milliseconds (number)

Returns string

plural()

test.format.plural(count: int, singular: string, plural: ?string) -> string

count followed by singular or plural, whichever the count calls for.

Parameters

  • count (int)
  • singular (string)
  • plural (?string) — singular + 's' by default.

Returns string

test.mock

import test.mock

test lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled test.mock.* needs import test.mock.

Test doubles: functions that record how they were called, and stand-ins that replace a real one for the length of a test.

A recording function

import test { * }

describe('retry', @{
  it('calls the operation once per attempt', @{
    var operation = mock()
    operation.raises_once(Error('first attempt'))
    operation.returns('ok')

    expect(retry(operation.fn, 3)).to_be('ok')
    expect(operation).to_have_been_called_times(2)
  })
})

mock() hands back a Mock, and mock.fn is the plain function to pass wherever a callback is wanted. Matchers accept either, so expect(operation) and expect(operation.fn) mean the same thing.

Replacing something that already exists

spy_on() swaps a function out in place and gives back a Mock that both records and stands in for it:

var clock = { now: @{ return real_time() } }
var now = spy_on(clock, 'now')
now.returns(1000)

It works on a dictionary entry and on an instance property that holds a function. It does not work on a class method or a module function: Zuri classes are immutable once declared, and a module’s members cannot be assigned from outside it. Code that wants to be substitutable takes its collaborators as arguments or holds them in properties, which is the shape worth designing for anyway.

Lifetime

The runner calls restore_all() after every test, so a spy never outlives the test that installed it, even one that failed partway through. Nothing needs to be undone by hand.

Functions

mock()

test.mock(implementation: ?function, name: ?string) -> Mock

A new recording function.

var send = mock()
notify(send.fn)
expect(send).to_have_been_called_with('hello')

Parameters

  • implementation (?function) — what it does when called. Without one it records the call and returns nil.
  • name (?string) — used in failure messages; 'mock' by default.

Returns Mock

spy_on()

test.spy_on(target, key: string) -> Mock

Replaces target[key] with a recording stand-in that calls through to the original, and returns the Mock that now sits there.

Calling through is the default on purpose: a spy that changes behaviour as well as observing it is a different tool, and saying so is one .returns() away.

var gateway = { charge: @(amount) { return real_charge(amount) } }
var charge = spy_on(gateway, 'charge')

checkout(gateway)

expect(charge).to_have_been_called_times(1)

Parameters

  • target (dict|instance) — a dictionary, or an instance with a property holding a function.
  • key (string)

Returns Mock

Raises TestSetupError when target is not a dictionary or an instance, when it has no such entry, or when that entry does not hold something callable. A class method cannot be spied on: classes are immutable once declared.

mock_of()

test.mock.mock_of(value)

The Mock behind value, which may be a Mock already or the plain function one handed out.

Parameters

  • value (any)

Returns — Mock: nil when value is neither.

restore_all()

test.mock.restore_all()

Undoes every spy and forgets every mock built so far.

The runner calls this after each test, so a spy installed by a test that failed halfway through still comes back out.

reset_all()

test.mock.reset_all()

Forgets the calls recorded by every live mock, leaving the mocks themselves and their behaviours in place.

Classes

Call

class test.mock.Call

One recorded call.

Fields

FieldTypeDescription
argsThe arguments, in order.
resultWhat the call returned, or nil when it raised.
errorThe error it raised, or nil when it returned.
threwWhether the call raised rather than returned.

Constructor

test.mock.Call(args)

Mock

class test.Mock

A recording stand-in for a function.

Built by mock() and by spy_on(); there is no reason to construct one directly.

Fields

FieldTypeDescription
nameWhat the mock is called in failure messages.
fnThe plain function to pass around.
callsEvery call so far, oldest first.

Constructor

test.Mock(name, implementation, replacing)

Parameters

  • name (?string)
  • implementation (?function) — what the mock does when called. A mock with none returns nil.
  • replacing (?dict) — for a spy, { target, key, original } naming what this mock was installed over, so restore() knows what to put back.

Mock.returns()

test.Mock.returns(value)

Makes every call from now on return value.

Parameters

  • value (any)

Returns — Mock: itself, so calls chain.

Mock.returns_once()

test.Mock.returns_once(value) -> Mock

Makes the next call return value, once. Queue several to script a sequence; once the queue runs dry the standing behaviour takes over again.

Parameters

  • value (any)

Returns Mock

Mock.raises()

test.Mock.raises(value) -> Mock

Makes every call from now on raise value.

Parameters

  • value (Error)

Returns Mock

Mock.raises_once()

test.Mock.raises_once(value) -> Mock

Makes the next call raise value, once.

Parameters

  • value (Error)

Returns Mock

Mock.implements()

test.Mock.implements(implementation: function) -> Mock

Makes every call from now on run implementation and return what it returns. The implementation receives exactly the arguments the mock was called with.

Parameters

  • implementation (function)

Returns Mock

Mock.implements_once()

test.Mock.implements_once(implementation: function) -> Mock

Makes the next call run implementation, once.

Parameters

  • implementation (function)

Returns Mock

Mock.clear_behaviour()

test.Mock.clear_behaviour() -> Mock

Restores the original behaviour, dropping the standing one and anything still queued. Recorded calls are kept.

Returns Mock

Mock.call_count()

test.Mock.call_count() -> int

How many times the mock has been called.

Returns int

Mock.called()

test.Mock.called() -> bool

Whether the mock has been called at all.

Returns bool

Mock.call_args()

test.Mock.call_args() -> list

The arguments of every call, as a list of lists.

Returns list

Mock.last_call()

test.Mock.last_call() -> Call

The most recent call, or nil when there has not been one.

Returns Call

Mock.nth_call()

test.Mock.nth_call(index: int) -> Call

The index-th call, counting from zero, or nil when there have not been that many. A negative index counts back from the most recent.

Parameters

  • index (int)

Returns Call

Mock.reset()

test.Mock.reset() -> Mock

Forgets every recorded call. The behaviour is left alone.

Returns Mock

Mock.restore()

test.Mock.restore() -> Mock

Puts back whatever this mock replaced, if it replaced anything.

Safe to call more than once, and on a mock that was never installed over something.

Returns Mock

test.reporter

import test.reporter

test lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled test.reporter.* needs import test.reporter.

How a run is shown.

The runner knows nothing about output. It walks the tree and calls the methods below as things happen, and a reporter decides what, if anything, that looks like. Swapping one for another changes the report and nothing else.

The built-in reporters

NameWhat it is for
specthe default: an indented tree, with failures spelled out
dotone character per test, for a run too long to read
tapTAP version 14, for anything that already speaks TAP
junitJUnit XML, which is what most CI systems ingest
jsonone JSON document at the end, for a tool of your own
ndjsonone JSON object per event, as it happens
silentnothing at all

test.run({ reporter: 'dot' }) picks one by name, and test.run({ reporter: MyReporter() }) uses your own.

Writing one

Subclass Reporter and override what you care about. Every method has a do-nothing default, so a reporter that only reacts to failures is six lines:

import test.reporter { Reporter }

class Quiet < Reporter {
  test_finished(test) {
    if test.status == 'failed' {
      echo test.full_name()
    }
  }
}

Constants

PREFIX

test.reporter.PREFIX = '@@zuri-test@@'

What marks a line of ndjson output as protocol rather than as something the test file happened to print.

Functions

create()

test.reporter.create(name: string, options: ?dict) -> Reporter

Builds a reporter by name.

Parameters

  • name (string) — one of spec, dot, tap, junit, json, ndjson, silent.
  • options (?dict) — passed to the reporter’s constructor.

Returns Reporter

Raises TestSetupError when there is no such reporter.

Classes

Reporter

class test.Reporter

The interface the runner talks to, with every method doing nothing.

Subclass it and override what you want; there is no need to call parent() from an override, since nothing here does anything.

Reporter.run_started()

test.Reporter.run_started(plan)

Called once, before anything runs.

Parameters

  • plan (dict) — { cases, suites, files, options }, where cases is how many tests are going to run.

Reporter.suite_started()

test.Reporter.suite_started(suite)

Called when a suite is entered, before any of its tests.

Parameters

  • suite (Suite)

Reporter.suite_finished()

test.Reporter.suite_finished(suite)

Called when a suite is left, after every one of its tests.

Parameters

  • suite (Suite)

Reporter.test_started()

test.Reporter.test_started(test)

Called before a test’s body runs. Not called for a test that is skipped or is a todo.

Parameters

  • test (Case)

Reporter.test_finished()

test.Reporter.test_finished(test)

Called once per test, whatever became of it, including the ones that never ran.

Parameters

  • test (Case)

Reporter.run_finished()

test.Reporter.run_finished(summary, root)

Called once, after everything.

Parameters

  • summary (Summary)
  • root (Suite)

Spec

class test.reporter.Spec < Reporter

The default: the suite tree, one line per test, and every failure written out in full at the end.

Constructor

test.reporter.Spec(options)

Parameters

  • options (?dict) — slow is the millisecond threshold past which a test’s time is highlighted (300 by default, 0 to never highlight). verbose prints a passing test’s captured output as well as a failing one’s.

Spec.run_started()

test.reporter.Spec.run_started(plan)

Spec.test_finished()

test.reporter.Spec.test_finished(test)

Spec.run_finished()

test.reporter.Spec.run_finished(summary, root)

Dot

class test.reporter.Dot < Reporter

One character per test, wrapped to the terminal, then the same failure detail and summary the spec reporter gives.

For a run long enough that a line per test is more scrolling than information.

Constructor

test.reporter.Dot(options)

Dot.run_started()

test.reporter.Dot.run_started(plan)

Dot.test_finished()

test.reporter.Dot.test_finished(test)

Dot.run_finished()

test.reporter.Dot.run_finished(summary, root)

Tap

class test.reporter.Tap < Reporter

TAP version 14: one ok/not ok line per test, with failure detail in a YAML block underneath.

The output carries no colour, whatever the terminal supports, since something else is going to parse it.

Constructor

test.reporter.Tap(options)

Tap.run_started()

test.reporter.Tap.run_started(plan)

Tap.test_finished()

test.reporter.Tap.test_finished(test)

Tap.run_finished()

test.reporter.Tap.run_finished(summary, root)

Junit

class test.reporter.Junit < Reporter

JUnit XML, which is the format nearly every CI system knows how to turn into a test report page.

Each describe becomes a <testsuite> and each test a <testcase>. Written to standard output; redirect it to the file your CI is configured to collect.

Constructor

test.reporter.Junit(options)

Junit.run_finished()

test.reporter.Junit.run_finished(summary, root)

Json

class test.reporter.Json < Reporter

One JSON document at the end, holding the summary and every test.

For a tool of your own that wants the whole run rather than a stream of it.

Constructor

test.reporter.Json(options)

Json.test_finished()

test.reporter.Json.test_finished(test)

Json.run_finished()

test.reporter.Json.run_finished(summary, root)

Ndjson

class test.reporter.Ndjson < Reporter

One JSON object per line, emitted as each thing happens.

This is what a test file writes when it is being run by the conductor in a process of its own: the parent reads the stream as it arrives rather than waiting for the child to finish.

Every line carries the prefix below, so anything else the file prints can be told apart from the protocol and passed through.

Constructor

test.reporter.Ndjson(options)

Ndjson.run_started()

test.reporter.Ndjson.run_started(plan)

Ndjson.test_finished()

test.reporter.Ndjson.test_finished(test)

Ndjson.run_finished()

test.reporter.Ndjson.run_finished(summary, root)

Silent

class test.reporter.Silent < Reporter

No output at all, for a caller reading the returned Summary instead.

Constructor

test.reporter.Silent(options)

test.result

import test.result

test lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled test.result.* needs import test.result.

The shapes a test run is made of: the tree the declarations build, and what running it produced.

Every reporter is handed these objects and nothing else, so a custom reporter has the same view of a run that the built-in ones do. See test.reporter for what a reporter does with them.

Constants

PENDING

test.result.PENDING = 'pending'

A test was declared and not yet run.

PASSED

test.result.PASSED = 'passed'

It ran and every assertion held.

FAILED

test.result.FAILED = 'failed'

It ran and something did not hold.

SKIPPED

test.result.SKIPPED = 'skipped'

It was not run: skip, a filter, or another test’s only.

TODO

test.result.TODO = 'todo'

It was declared with no body, as a note to write it later.

FLAKY

test.result.FLAKY = 'flaky'

It failed, was retried, and then passed.

Classes

Failure

class test.Failure

One thing that went wrong.

A test can carry more than one: a failing body and then a failing after_each are two separate answers to “what went wrong”, and collapsing them loses the one that explains the other.

Fields

FieldTypeDescription
kindWhere it came from: 'assertion' for a matcher, 'error' for anything else raised by the test body,…
messageThe one-line summary.
matcherThe matcher that failed, for an assertion.
expectedWhat was expected, when that is a meaningful thing to show.
receivedWhat was received.
has_valuesWhether expected and received are worth printing.
detailsPre-rendered lines to print under the message.
stacktraceThe raw stack trace, innermost first.

Constructor

test.Failure(kind, message)

Failure.origin()

test.Failure.origin()

Where the failure happened, in the code under test.

Returns — source.Frame: nil when the trace holds nothing outside the test module.

Case

class test.Case

One declared test, and what became of it.

Fields

FieldTypeDescription
nameThe name given to it().
ownerThe suite it was declared in.
fileThe file it was declared in.
modeHow it was declared: 'normal', 'skip', 'only' or 'todo'.
bodyThe body, or nil for a todo.
optionsIts options, as given to it().
statusWhat became of it: one of the status constants.
failuresEverything that went wrong.
durationHow long the last attempt took, in milliseconds.
attemptsHow many times the body ran, counting retries.
outputWhat it printed, when output was being captured.
skip_reasonWhy it was skipped, when something other than skip decided.

Constructor

test.Case(name, owner, file, mode, body, options)

Case.depth()

test.Case.depth()

How deeply nested the test is. @returns int

Case.suite_path()

test.Case.suite_path()

The enclosing suite names, outermost first. @returns list

Case.full_name()

test.Case.full_name() -> string

The test’s full name, suite path included.

Returns string

Case.is_slow()

test.Case.is_slow(threshold: number)

Whether it ran longer than threshold milliseconds. @param number threshold @returns bool

Case.tags()

test.Case.tags()

The tags it was declared with. @returns list

Suite

class test.Suite

A describe() block: its tests, its nested suites, and its hooks.

Fields

FieldTypeDescription
nameThe name given to describe(), or '' for the implicit root.
ownerThe enclosing suite, or nil for the root.
fileThe file it was declared in.
modeHow it was declared: 'normal', 'skip' or 'only'.
childrenIts tests and nested suites, in declaration order.
before_allHooks registered inside it.
after_all
before_each
after_each
failuresAnything that went wrong in a hook belonging to this suite.

Constructor

test.Suite(name, owner, file, mode)

Suite.hooks_for()

test.Suite.hooks_for(kind: string) -> list

The hooks of one kind registered on this suite, in the order they were registered.

Parameters

  • kind (string) — 'before_all', 'after_all', 'before_each' or 'after_each'.

Returns list

Suite.depth()

test.Suite.depth()

How deeply nested the suite is; the root is 0. @returns int

Suite.path()

test.Suite.path() -> list

This suite’s name preceded by its ancestors’, skipping the unnamed root.

Returns list

Suite.cases()

test.Suite.cases() -> list

Every test in this suite and everything under it, in declaration order.

Returns list

Suite.suites()

test.Suite.suites() -> list

Every suite under this one, itself included.

Returns list

Summary

class test.Summary

What a whole run came to.

Fields

FieldTypeDescription
suitesSuites that contained at least one test that ran.
passed
failed
skipped
todo
flakyTests that passed only after a retry.
failuresEvery test that failed, for the report at the end.
slowTests that ran slower than the slow threshold.
durationTotal wall time, in milliseconds.
seedThe seed the order was shuffled with, or nil when it was not.
snapshotsWhat happened to snapshots, from test.snapshot.
bailedWhether the run stopped early because bail was reached.

Constructor

test.Summary()

Summary.ran()

test.Summary.ran()

Tests that actually ran. @returns int

Summary.total()

test.Summary.total()

Every test the run knew about. @returns int

Summary.ok()

test.Summary.ok()

Whether the run should be considered a success. @returns bool

Summary.exit_code()

test.Summary.exit_code()

0 when everything passed, 1 when anything did not. @returns int

Summary.to_dict()

test.Summary.to_dict() -> dict

The run as plain data, for a machine-readable reporter or for a caller driving the framework itself.

Returns dict

Summary.to_string()

test.Summary.to_string() -> string

Returns string

test.runner

import test.runner

test does not re-export this module, so it is reached only by importing it directly.

Collecting the tests a file declares, deciding which of them to run, and running them.

Two phases, not one

describe() and it() do not run anything. They build a tree, and run() walks it afterwards. That separation is what makes only, filtering, shuffling, bailing and an up-front test count possible at all: none of them can be decided while the file is still being read.

Declaring the first test registers an os.at_exit() handler that calls run() once the file is done, so a test file needs no closing line. An explicit run() marks the tree as run and the handler then does nothing.

Hook ordering

before_all runs once, immediately before the first test in its suite that is actually going to run, and not at all for a suite whose tests were all filtered out. after_all runs after the last one, and only if before_all ran; it runs even when before_all failed, so whatever the setup acquired before failing is still let go. before_each runs outermost first, after_each innermost first, and after_each runs even when the test failed.

Timeouts

Zuri runs synchronously, so a test runs to completion and is then timed: the timeout option is measured after the body returns. It catches a test that got too slow, not one that hangs. A hang needs a process boundary, which is what the conductor puts around each file; see test.conduct.

Functions

root()

test.runner.root() -> Suite

The tree as it stands, for a caller that wants to look at what was declared without running it.

Returns Suite

reset()

test.runner.reset()

Throws away every declaration and every snapshot store, so a second run in the same process starts from nothing.

describe()

test.runner.describe(name: string, body: function, mode: ?string) -> Suite

Opens a suite, runs body to collect what is inside it, and closes it again.

Parameters

  • name (string)
  • body (function)
  • mode (?string) — 'normal', 'skip' or 'only'.

Returns Suite

it()

test.runner.it(name: string, body: ?function, options: ?dict, mode: ?string) -> Case

Declares one test.

Parameters

  • name (string)
  • body (?function) — left out, the test is a todo.
  • options (?dict) — retries, failing, timeout, tags.
  • mode (?string) — 'normal', 'skip', 'only' or 'todo'.

Returns Case

before_all()

test.runner.before_all(body: function)

Parameters

  • body (function)

after_all()

test.runner.after_all(body: function)

Parameters

  • body (function)

before_each()

test.runner.before_each(body: function)

Parameters

  • body (function)

after_each()

test.runner.after_each(body: function)

Parameters

  • body (function)

run()

test.runner.run(options: ?dict) -> Summary

Runs everything declared so far and reports it.

import test { * }

describe('parser', @{
  it('reads an empty document', @{
    expect(parse('')).to_equal({})
  })
})

run()

Parameters

  • options (?dict) — see {test} for every option and its default.

Returns Summary

test.snapshot

import test.snapshot

test does not re-export this module, so it is reached only by importing it directly.

Snapshot testing: recording what a value looked like the first time and failing when it stops looking like that.

it('renders the invoice', @{
  expect(render(invoice)).to_match_snapshot()
})

The first run writes the value down. Every run after that compares against what was written and shows a diff when it differs. The file is meant to be committed and reviewed like any other code, because that review is the entire value of the technique: an unexplained change to a snapshot in a pull request is exactly the thing worth noticing.

Where they live

Beside the test file, in a __snapshots__ directory, named after it: tests/invoice.zu gets tests/__snapshots__/invoice.zu.snap.

The file format

Plain text, indented, one entry per snapshot:

# Zuri snapshot file v1

=== Invoice > renders the invoice 1 ===
  {
    total: 4200
  }

The heading is the test’s full name plus the position of the snapshot within that test, or the name given to to_match_snapshot('name'). Every body line is indented two spaces, which is what keeps a value that happens to contain === from being read back as a heading.

Updating

Run with ZURI_UPDATE_SNAPSHOTS=1, or pass { update_snapshots: true } to run(). Every mismatching snapshot is rewritten and reported as updated rather than failed, and entries nothing asked for any more are deleted.

On CI

Writing a brand new snapshot is a pass locally and a failure on CI, because a snapshot nobody has looked at asserts nothing. CI being set in the environment is what switches this on; { ci: false } turns it back off.

Constants

test.snapshot.HEADER = '# Zuri snapshot file v1'

BODY_INDENT

test.snapshot.BODY_INDENT = '  '

Functions

path_for()

test.snapshot.path_for(test_file: string) -> string

The .snap file that belongs to a given test file.

Parameters

  • test_file (string)

Returns string

store_for()

test.snapshot.store_for(test_file: string) -> Store

The store for a test file, read from disk the first time it is asked for and kept afterwards.

Parameters

  • test_file (string)

Returns Store

updating()

test.snapshot.updating() -> bool

Whether snapshots are being rewritten rather than checked.

Returns bool

set_updating()

test.snapshot.set_updating(on: bool)

Turns snapshot rewriting on or off.

Parameters

  • on (bool)

strict()

test.snapshot.strict() -> bool

Whether a brand new snapshot counts as a failure, which is what it should be anywhere nobody is going to look at it before it is committed.

Returns bool

set_strict()

test.snapshot.set_strict(on: bool)

Turns the strict, new-snapshots-fail behaviour on or off.

Parameters

  • on (bool)

check()

test.snapshot.check(test_file: string, key: string, serialized: string)

Compares one value against what the snapshot file holds for key, writing it down when there is nothing there yet.

Parameters

  • test_file (string) — the file the test was declared in.
  • key (string)
  • serialized (string) — the value, already rendered by format.serialize().

Returns — dict: { status, expected, received }, where status is one of 'match', 'written', 'updated', 'mismatch' or 'missing'. 'missing' is the strict-mode answer to what would otherwise have been 'written'.

recorded_lines()

test.snapshot.recorded_lines(test_file: string, key: string) -> list

The lines of the snapshot at key, as a list, for a reporter that wants to show what was recorded.

Parameters

  • test_file (string)
  • key (string)

Returns list

flush()

test.snapshot.flush(prune: ?bool)

Writes every changed store back to disk, and in update mode drops entries the run never asked about.

The runner calls this once, after everything has run. Pruning earlier would delete the snapshots of tests that a filter happened to skip.

Parameters

  • prune (?bool) — whether to drop unused entries. Defaults to whatever updating() says.

summary()

test.snapshot.summary()

What happened to snapshots over the whole run.

Returns — dict: matched, written, updated, obsolete (found and left alone), removed (found and deleted, in update mode), and missing when strict mode refused to write a new one.

reset()

test.snapshot.reset()

Forgets every loaded store and zeroes the counters, so a second run in the same process starts clean.

Classes

Store

class test.snapshot.Store

Every snapshot recorded for one test file, and the file they live in.

Fields

FieldTypeDescription
pathThe .snap file this reads and writes.
entriesSnapshot key to recorded text.
usedKeys this run actually asked about.
dirtyWhether anything has changed since it was read.

Constructor

test.snapshot.Store(path)

Store.read()

test.snapshot.Store.read()

Reads the file, if it is there. A missing file is not an error: it is what every snapshot’s first run looks like.

Store.save()

test.snapshot.Store.save()

Writes the file back, creating the __snapshots__ directory if it is not there yet. Does nothing when nothing changed.

An empty store deletes its file rather than leaving an unexplained stub behind.

Store.prune()

test.snapshot.Store.prune() -> int

Drops every entry this run never asked about, and reports how many there were.

Returns int

Store.obsolete()

test.snapshot.Store.obsolete() -> list

Keys the file holds that this run never asked about.

Returns list

test.source

import test.source

test does not re-export this module, so it is reached only by importing it directly.

Working out where a failure came from, and showing the code that was there.

Every Zuri error carries a stacktrace: a list of strings shaped like /path/to/file.zu:12 -> divide(). That is enough to point a reader at the exact line, provided the frames belonging to the test module itself are dropped first. Nobody needs to be told that the failure passed through _check() on its way out.

Functions

parse_frame()

test.source.parse_frame(line: string)

Splits one stack trace line into a Frame.

Parameters

  • line (string)

Returns — Frame: nil when the line is not shaped like a frame, which is what a trace from a future runtime might well look like.

user_frames()

test.source.user_frames(stacktrace: list)

Every frame of stacktrace that belongs to the code under test, innermost first.

The test module’s own frames are dropped, because a failure is never interesting for having gone through a matcher.

Parameters

  • stacktrace (list) — an error’s stacktrace.

Returns — list: a list of Frame.

origin()

test.source.origin(stacktrace: list)

The innermost frame of stacktrace outside the test module: where the failing line actually is.

Parameters

  • stacktrace (list)

Returns — Frame: nil when every frame belongs to the framework, which happens when a matcher is misused rather than failing.

short_path()

test.source.short_path(path: string) -> string

A path written relative to the working directory when it is under it, and left alone when it is not.

Parameters

  • path (string)

Returns string

code_frame()

test.source.code_frame(frame, context: ?int)

The source around a failure, with the offending line marked.

  10 |   var total = 0
  11 |
> 12 |   expect(total).to_be(1)
  13 | })

Parameters

  • frame (Frame)
  • context (?int) — lines of surrounding code either side, 2 by default.

Returns — list: the lines, already coloured. Empty when the file cannot be read, which is the normal answer for a frame from somewhere that no longer exists.

clear_cache()

test.source.clear_cache()

Forgets every file read for a code frame.

Only matters to a long-lived process that edits the files it is testing between runs.

Classes

Frame

class test.source.Frame

One frame of a stack trace, pulled apart.

Fields

FieldTypeDescription
fileThe file, as an absolute path.
lineThe line number.
nameThe function, without its trailing ().
rawThe frame exactly as the trace spelled it.

Constructor

test.source.Frame(file, line, name, raw)

Frame.to_string()

test.source.Frame.to_string() -> string

The frame written for a report: a path relative to the working directory, since an absolute one is mostly noise the reader already knows.

Returns string

test.style

import test.style

test does not re-export this module, so it is reached only by importing it directly.

Terminal presentation for the test module: colour, symbols, width, and the padding helpers that keep a report’s columns lined up once escape sequences are in the text.

Colour and symbols are decided once, when this module loads, from the environment the process actually has. Nothing else in the test module asks whether colour is on; it calls green() and gets plain text back when it isn’t.

Deciding whether to use colour

In order, the first of these that applies wins:

  1. NO_COLOR set to anything non-empty turns colour off, per https://no-color.org. 2. FORCE_COLOR set to anything other than 0 turns it on, which is how CI systems that capture output but still render ANSI ask for it. 3. Standard output being a terminal turns it on, and a pipe or a file turns it off.

set_enabled() overrides the lot, for a caller that has already made the decision itself.

Deciding whether to use Unicode

ZURI_TEST_ASCII set to anything non-empty forces the ASCII symbol set. Otherwise the box-drawing and tick/cross characters are used everywhere except a Windows console that is not Windows Terminal, which is the one common environment that still renders them as mojibake.

Functions

enabled()

test.style.enabled() -> bool

Whether colour is currently being emitted.

Returns bool

set_enabled()

test.style.set_enabled(on: bool)

Forces colour on or off, overriding what the environment said.

Parameters

  • on (bool)

unicode()

test.style.unicode() -> bool

Whether the Unicode symbol set is in use, as opposed to the ASCII fallbacks.

Returns bool

set_unicode()

test.style.set_unicode(on: bool)

Forces the Unicode symbol set on or off.

Parameters

  • on (bool)

bold()

test.style.bold(text: string) -> string

Emphasis. Returns text unchanged when colour is off.

Parameters

  • text (string)

Returns string

dim()

test.style.dim(text: string) -> string

De-emphasis, for detail that should recede: timings, counts, the suite path above a failure.

Parameters

  • text (string)

Returns string

inverse()

test.style.inverse(text: string) -> string

Reserved for text that has to be found instantly on a busy screen.

Parameters

  • text (string)

Returns string

red()

test.style.red(text: string)

Failure. @param string text @returns string

green()

test.style.green(text: string)

Success. @param string text @returns string

yellow()

test.style.yellow(text: string)

Skipped and other deliberate non-results. @param string text @returns string

cyan()

test.style.cyan(text: string)

Structure: suite names, headings. @param string text @returns string

blue()

test.style.blue(text: string)

Todo entries and other informational notes. @param string text @returns string

magenta()

test.style.magenta(text: string)

Values in a rendered diff or failure message. @param string text @returns string

grey()

test.style.grey(text: string)

Punctuation and separators. @param string text @returns string

white()

test.style.white(text: string)

Plain foreground, used to lift a key out of dimmed surroundings. @param string text @returns string

badge()

test.style.badge(kind: string, text: string) -> string

A filled, inverted label, the way a status banner reads in a CI log. kind is one of 'pass', 'fail', 'skip', 'todo' or 'flaky'; anything else gets the neutral grey background.

Parameters

  • kind (string)
  • text (string)

Returns string

symbols()

test.style.symbols() -> dict

The symbol set in use, keyed by role.

Every entry is a single display column wide in both sets, so a layout built on them does not shift when the ASCII fallbacks are in play. The exception is arrow, which is two columns in ASCII.

Returns dict

strip()

test.style.strip(text: string) -> string

text with every ANSI escape sequence removed.

Parameters

  • text (string)

Returns string

visible_length()

test.style.visible_length(text: string) -> int

How many columns text occupies once its escape sequences are discounted. This, not length(), is what padding and alignment have to measure.

Parameters

  • text (string)

Returns int

Note: counts codepoints, so a wide CJK character or an emoji is counted as one column even though a terminal draws it as two. Test names are the only user-supplied text this is applied to.

pad_right()

test.style.pad_right(text: string, width: int, fill: ?string) -> string

Pads text on the right to width visible columns, leaving it alone when it is already that wide or wider.

Parameters

  • text (string)
  • width (int)
  • fill (?string) — a single character, ' ' by default.

Returns string

pad_left()

test.style.pad_left(text: string, width: int, fill: ?string) -> string

Pads text on the left to width visible columns, leaving it alone when it is already that wide or wider.

Parameters

  • text (string)
  • width (int)
  • fill (?string) — a single character, ' ' by default.

Returns string

truncate()

test.style.truncate(text: string, width: int) -> string

Shortens text to at most width visible columns, marking the cut with an ellipsis. Returns text untouched when it already fits.

Parameters

  • text (string)
  • width (int)

Returns string

Note: only safe on text with no escape sequences in it, since it cuts by codepoint and would leave a colour sequence unterminated. Colour the result, not the input.

rule()

test.style.rule(character: string, count: int) -> string

count copies of character.

Parameters

  • character (string)
  • count (int)

Returns string

indent()

test.style.indent(depth: int) -> string

Two spaces per level, the indent every nested suite and test line in the report is built from.

Parameters

  • depth (int)

Returns string

width()

test.style.width() -> int

The width to lay the report out to.

Asks the terminal first, falls back to COLUMNS, and finally to a conventional 80. Clamped to 40..120 so neither a one-column pty nor a maximised ultrawide produces an unreadable layout.

Returns int

net

import net

The net module provides networking primitives for Transmission Control Protocol and User Datagram Protocol communication as well as IP address manipulation interfaces.

  • TcpStream communicates over TCP.
  • UdpSocket communicates over UDP.
  • UnixStream communicates over a unix domain socket, which is a path on the filesystem rather than an address on the network.
  • Ip parses, formats and classifies IP addresses.
  • SocketAddr pairs an IP address with a port.
  • net.tls wraps a TcpStream in Transport Layer Security.
  • net.dtls provides Datagram Transport Layer Security over UDP.
  • net.poll waits on many sockets at once.
  • net.resolver asks the domain name system questions of its own.

The net API

Every public name in net, wherever it is declared. Each links to the page that documents it.

NameKindSummary
net.AcceptorclassAn accept loop that never parks the thread inside accept().
net.ERRORconstantReadiness only: the socket has failed, or its handle can no longer supply a descriptor because it was closed,…
net.HANGUPconstantReadiness only: the peer has closed its end.
net.IpclassEither an Ipv4 or an Ipv6, for code that needs to accept and work with addresses of either family without…
net.Ipv4classAn IPv4 address, stored internally as four octets in network (i.e. big-endian, most significant octet first)…
net.Ipv6classAn IPv6 address, stored internally as eight 16-bit segments in network (i.e. big-endian, most significant…
net.PollerclassA set of sockets to watch, each under a token of your choosing.
net.READABLEconstantInterest: tell me when this socket has data to read, a connection to accept, or an end-of-stream to observe.
net.ShutdownclassUsed with TcpStream.shutdown() to select which half (or both halves) of a full-duplex TCP connection to…
net.SocketAddrclassEither a SocketAddrV4 or a SocketAddrV6, for code that needs to accept and work with socket addresses of…
net.SocketAddrV4classAn IPv4 address and port, e.g. 192.168.1.10:8080.
net.SocketAddrV6classAn IPv6 address and port, e.g. [2001:db8::1]:8080, optionally carrying a zone/scope identifier (RFC 4007)…
net.TcpStreamclassA TCP socket, in either its connected-stream or listening-server role.
net.UdpSocketclassA UDP socket, usable for both sending and receiving datagrams, either to/from a single fixed peer (after…
net.UnixStreamclassA unix domain socket, connected or listening.
net.WRITABLEconstantInterest: tell me when this socket can be written to without blocking.
net.dtls.DtlsConfigclassThe trust roots and optional certificate a DTLS handshake needs - the same idea as tls.TlsConfig, minus the…
net.dtls.DtlsSocketclassA DTLS socket, in either its connected (client or accepted-server) role or its listening role.
net.is_supportedfunctionWhether unix domain sockets work on this machine.
net.pairfunctionTwo sockets already connected to each other, with no path and nothing on the filesystem.
net.resolvefunctionResolves address - a host:port string whose host may be a name or an IP literal - into every socket…
net.resolver.CLASSESconstantThe classes a question can be asked in.
net.resolver.CertificationclassA CAA record: which certificate authorities a domain allows to issue for it.
net.resolver.MailExchangeclassOne mail server for a domain, as an MX record names it.
net.resolver.MalformedResponseclassSomething came back that is not a DNS message, or is one that contradicts itself.
net.resolver.NameErrorclassThe name does not exist.
net.resolver.NoServersErrorclassThe resolver has nowhere to send a query: no servers were given and the system has none configured.
net.resolver.QuestionclassThe question a message asks, echoed back in the answer so a reply can be matched to what it replies to.
net.resolver.RCODESconstantWhat a server said about the question, by name.
net.resolver.RecordclassOne resource record: a name, what kind of thing it holds, how long it may be cached, and the thing itself.
net.resolver.ResolverclassAsks questions of the domain name system and reads the answers.
net.resolver.ResolverErrorclassThe root of every error this module raises.
net.resolver.ResolverTimeoutclassNo server answered in time.
net.resolver.ResponseclassA whole answer: what was asked, what came back, and what the server said about it.
net.resolver.ServerErrorclassThe server answered, and the answer was a refusal or a failure of its own.
net.resolver.ServiceclassWhere a service lives, as an SRV record names it: the host and port, with the priority and weight that…
net.resolver.StartOfAuthorityclassThe authority record at the top of a zone, which says who publishes it and how long the rest of the world may…
net.resolver.TYPESconstantThe record types this module knows how to read, by name.
net.resolver.afunctionThe IPv4 addresses of a name.
net.resolver.aaaafunctionThe IPv6 addresses of a name.
net.resolver.addressesfunctionEvery address a name has, IPv4 first.
net.resolver.build_queryfunctionBuilds the bytes of a query.
net.resolver.caafunctionWhich certificate authorities a domain allows to issue for it.
net.resolver.cnamefunctionThe name this one is an alias for, or nil.
net.resolver.exchangefunctionAsks one server one question and returns what it said.
net.resolver.flushfunctionEmpties the shared resolver’s cache.
net.resolver.mxfunctionThe mail servers for a domain, lowest preference first.
net.resolver.nsfunctionThe nameservers a domain is delegated to.
net.resolver.parse_messagefunctionTurns the bytes of a message into a Response.
net.resolver.ptrfunctionThe names an address points back at.
net.resolver.queryfunctionAsks the shared resolver about one name and hands back the whole answer.
net.resolver.resolvefunctionThe records of one type published for a name, through the shared resolver.
net.resolver.reverse_namefunctionThe name an address’s records are published under.
net.resolver.sharedfunctionThe resolver the module-level functions use: one shared Resolver built from this machine’s own…
net.resolver.soafunctionThe authority record at the top of a name’s zone, or nil.
net.resolver.srvfunctionThe hosts running a service, by priority and then by weight.
net.resolver.system_searchfunctionThe domain suffixes to try against a name that has no dots in it.
net.resolver.system_serversfunctionThe nameservers this machine is configured to use, in the order the system lists them.
net.resolver.txtfunctionThe text records of a name.
net.resolver.usefunctionReplaces the shared resolver, so that every module-level function goes through the one given instead.
net.tls.PeerCertificateclassThe subset of a peer’s certificate that’s useful to inspect from Zuri code, without needing a full…
net.tls.TlsConfigclassThe handful of choices a TLS handshake needs: which certificate authorities to trust, an optional certificate…
net.tls.TlsStreamclassAn established TLS connection, wrapping an already-connected TcpStream.

Submodules

ModuleReached asSummary
net.addrnet.*Socket addresses: an IP address and a port.
net.dtlsnet.dtls.*Datagram Transport Layer Security over a UdpSocket - the same certificate-based encryption and…
net.ipnet.*This module provides types for parsing, formatting, comparing and classifying IP addresses in both their IPv4…
net.pollnet.*Readiness polling: given a set of sockets, which of them can be read or written right now, without blocking.
net.resolvernet.resolver.*A DNS resolver, written in Zuri from the wire format up.
net.tcpnet.*This module provides a complete implementation of the Transmission Control Protocol as specified in IETF RFC…
net.tlsnet.tls.*Transport Layer Security over a TcpStream, built on top of rustls.
net.udpnet.*This module provides a complete implementation of the User Datagram Protocol as specified in IETF RFC 768.
net.unixnet.*Unix domain sockets: the same stream interface as tcp, over a path on the filesystem rather than an address…

2026, Richard Ore and Zuri contributors

net.addr

import net

Everything here is re-exported by net, so import net is enough and the names are called as net.*. Importing net.addr on its own works too and reaches the same definitions.

Socket addresses: an IP address and a port. SocketAddrV4 and SocketAddrV6 each represent an address of their respective family, while SocketAddr is a small wrapper around either one for code that needs to accept and work with both interchangeably just like the same relationship Ip has to Ipv4/Ipv6.

An IPv6 address may also carry a zone/scope identifier (RFC 4007), used to disambiguate a link-local address between network interfaces, e.g. fe80::1%3.

TcpStream, UdpSocket, and DtlsSocket return these from local_address()/peer_address().

import net

var server = net.TcpStream()
server.bind('0.0.0.0:8080')

var local = server.local_address()
echo 'listening on ${local.ip()}, port ${local.port()}'

Classes

SocketAddrV4

class net.SocketAddrV4

An IPv4 address and port, e.g. 192.168.1.10:8080.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

net.SocketAddrV4(address, port)

Returns a new instance of a SocketAddrV4.

Parameters

  • address (Ipv4) — The IP address
  • port (number) — The port, 0-65535

Raises Error if address is not an {Ipv4}, or port is missing or outside 0-65535

SocketAddrV4.parse()

net.SocketAddrV4.parse(address) -> SocketAddrV4

Parses an address given as ip:port, e.g. '192.168.1.10:8080'.

Parameters

  • address (string) — The address to parse

Returns SocketAddrV4

Raises Error if address isn’t a well-formed IPv4 address followed by : and a port from 0 to 65535

SocketAddrV4.ip()

net.SocketAddrV4.ip() -> Ipv4

The IP address.

Returns Ipv4

SocketAddrV4.port()

net.SocketAddrV4.port() -> number

The port.

Returns number

SocketAddrV4.equals()

net.SocketAddrV4.equals(other) -> bool

Whether other refers to the same address and port as this one.

Parameters

  • other (SocketAddrV4) — The address to compare against

Returns bool

SocketAddrV4.to_string()

net.SocketAddrV4.to_string() -> string

This address as ip:port, e.g. '192.168.1.10:8080'.

Returns string

SocketAddrV6

class net.SocketAddrV6

An IPv6 address and port, e.g. [2001:db8::1]:8080, optionally carrying a zone/scope identifier (RFC 4007) that disambiguates a link-local address between network interfaces, e.g. [fe80::1%3]:8080.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

net.SocketAddrV6(address, port, scope_id)

Returns a new instance of a SocketAddrV6.

Parameters

  • address (Ipv6) — The IP address
  • port (number) — The port, 0-65535
  • scope_id (?number) — The zone/scope identifier, or 0 (the default) if this address isn’t scoped to a particular interface

Raises Error if address is not an {Ipv6}, port is missing or outside 0-65535, or scope_id is negative

SocketAddrV6.parse()

net.SocketAddrV6.parse(address) -> SocketAddrV6

Parses an address given as [ip]:port or, for a scoped address, [ip%scope_id]:port, e.g. '[::1]:8080' or '[fe80::1%3]:8080'.

Parameters

  • address (string) — The address to parse

Returns SocketAddrV6

Raises Error if address isn’t a well-formed bracketed IPv6 address followed by : and a port from 0 to 65535

SocketAddrV6.ip()

net.SocketAddrV6.ip() -> Ipv6

The IP address.

Returns Ipv6

SocketAddrV6.port()

net.SocketAddrV6.port() -> number

The port.

Returns number

SocketAddrV6.scope_id()

net.SocketAddrV6.scope_id() -> number

The zone/scope identifier, or 0 if this address isn’t scoped to a particular network interface.

Returns number

SocketAddrV6.equals()

net.SocketAddrV6.equals(other) -> bool

Whether other refers to the same address, port, and scope as this one.

Parameters

  • other (SocketAddrV6) — The address to compare against

Returns bool

SocketAddrV6.to_string()

net.SocketAddrV6.to_string() -> string

This address as [ip]:port, or [ip%scope_id]:port if it carries a non-zero scope id.

Returns string

SocketAddr

class net.SocketAddr

Either a SocketAddrV4 or a SocketAddrV6, for code that needs to accept and work with socket addresses of either family without committing to one upfront.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

net.SocketAddr(inner)

Wraps an already-constructed SocketAddrV4 or SocketAddrV6. Application code should generally prefer SocketAddr.parse(), SocketAddr.v4(), or SocketAddr.v6() over calling this directly.

Parameters

  • inner (SocketAddrV4|SocketAddrV6) — The address to wrap

SocketAddr.v4()

net.SocketAddr.v4(address, port) -> SocketAddr

Wraps an IPv4 address and port as a SocketAddr.

Parameters

  • address (Ipv4) — The IP address
  • port (number) — The port, 0-65535

Returns SocketAddr

SocketAddr.v6()

net.SocketAddr.v6(address, port, scope_id) -> SocketAddr

Wraps an IPv6 address and port as a SocketAddr.

Parameters

  • address (Ipv6) — The IP address
  • port (number) — The port, 0-65535
  • scope_id (?number) — The zone/scope identifier, or 0

Returns SocketAddr

SocketAddr.parse()

net.SocketAddr.parse(address) -> SocketAddr

Parses address as either an IPv4 or IPv6 socket address, choosing the family based on whether it starts with [.

Parameters

  • address (string) — The address to parse, e.g. '192.168.1.10:8080' or '[::1]:8080'

Returns SocketAddr

Raises Error if address is not a well-formed IPv4 or IPv6 socket address

SocketAddr.is_v4()

net.SocketAddr.is_v4() -> bool

Whether this is a SocketAddrV4.

Returns bool

SocketAddr.is_v6()

net.SocketAddr.is_v6() -> bool

Whether this is a SocketAddrV6.

Returns bool

SocketAddr.as_v4()

net.SocketAddr.as_v4() -> ?SocketAddrV4

Returns the wrapped SocketAddrV4, or nil if this is an IPv6 address.

Returns ?SocketAddrV4

SocketAddr.as_v6()

net.SocketAddr.as_v6() -> ?SocketAddrV6

Returns the wrapped SocketAddrV6, or nil if this is an IPv4 address.

Returns ?SocketAddrV6

SocketAddr.ip()

net.SocketAddr.ip() -> Ip

The IP address, wrapped as an Ip so it can be worked with without committing to a family.

Returns Ip

SocketAddr.port()

net.SocketAddr.port() -> number

The port.

Returns number

SocketAddr.equals()

net.SocketAddr.equals(other) -> bool

Whether other refers to the same address as this one. An IPv4 address and an IPv6 address are never equal, even if one is the IPv4-mapped form of the other.

Parameters

  • other (SocketAddr) — The address to compare against

Returns bool

SocketAddr.to_string()

net.SocketAddr.to_string() -> string

This address in its family’s standard notation.

Returns string


2026, Richard Ore and Zuri contributors

net.dtls

import net

net exposes this as net.dtls, so import net is enough and the names are called as net.dtls.*. import net.dtls reaches the same definitions directly.

Datagram Transport Layer Security over a UdpSocket - the same certificate-based encryption and authentication tls provides, for protocols built on UDP instead of TCP.

Unlike tls, DtlsSocket owns its own socket rather than wrapping one you’ve already opened: a UDP port has no OS-level concept of a per-peer connection, so a single bound DtlsSocket multiplexes every accepted peer itself, by remote address.

A client:

import net.dtls

var config = dtls.DtlsConfig()
var socket = dtls.DtlsSocket()
socket.connect(config, '203.0.113.10:5684')

socket.write('hello')
echo socket.read(1024)
socket.close()

A server, accepting many clients on one bound port:

import net.dtls

var config = dtls.DtlsConfig()
config.set_cert_chain(cert_chain_pem, private_key_pem)

var server = dtls.DtlsSocket()
server.bind('0.0.0.0:5684')

while true {
  var client = server.accept(config)
  echo 'connection from ${client.peer_address()}'
  client.write('hello over DTLS')
}

Classes

DtlsConfig

class net.dtls.DtlsConfig

The trust roots and optional certificate a DTLS handshake needs - the same idea as tls.TlsConfig, minus the TLS-specific ALPN/version options DTLS doesn’t have.

Constructor

net.dtls.DtlsConfig()

Returns a new DtlsConfig with sensible defaults: the bundled Mozilla root certificate list and no client/server certificate.

DtlsConfig.native_ptr()

net.dtls.DtlsConfig.native_ptr() -> Ptr

Hands out this config’s underlying native handle - DtlsSocket needs it and, like any _-prefixed field, _ptr isn’t otherwise reachable from outside this class.

Returns Ptr

DtlsConfig.set_root_store()

net.dtls.DtlsConfig.set_root_store(mode)

Chooses where trusted root certificate authorities come from. Same meaning as tls.TlsConfig.set_root_store().

Parameters

  • mode (string) — 'bundled' or 'native'

Raises Error if mode is neither, or (for 'native') if the OS trust store can’t be read

DtlsConfig.add_ca_pem()

net.dtls.DtlsConfig.add_ca_pem(pem)

Adds one or more PEM-encoded certificates to this config’s trusted roots. Same meaning as tls.TlsConfig.add_ca_pem().

Parameters

  • pem (string) — One or more PEM-encoded CA certificates

Raises Error if none of the certificates in pem could be parsed

DtlsConfig.set_cert_chain()

net.dtls.DtlsConfig.set_cert_chain(cert_chain_pem, key_pem)

Sets the certificate chain and private key this side presents. Required before DtlsSocket.accept() can be used with this config.

Parameters

  • cert_chain_pem (string) — The end-entity certificate followed by any intermediate certificates, all PEM-encoded
  • key_pem (string) — The PEM-encoded private key matching the end-entity certificate

Raises Error if either PEM can’t be parsed, or if the key doesn’t match the certificate

DtlsConfig.require_client_cert()

net.dtls.DtlsConfig.require_client_cert(required)

For a server config: whether to require the connecting peer to present a trusted certificate (mutual TLS).

Parameters

  • required (bool)

DtlsConfig.set_insecure()

net.dtls.DtlsConfig.set_insecure(insecure)

Disables certificate verification entirely. Same meaning, and same “development and testing only” caveat, as tls.TlsConfig.set_insecure().

Parameters

  • insecure (bool)

DtlsSocket

class net.dtls.DtlsSocket

A DTLS socket, in either its connected (client or accepted-server) role or its listening role.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

net.dtls.DtlsSocket(_ptr)

Returns a new, unbound DtlsSocket. Call bind() to listen for incoming connections, or connect() to dial out to one.

DtlsSocket.bind()

net.dtls.DtlsSocket.bind(address)

Binds this socket to address and prepares it to accept incoming DTLS associations via accept().

Parameters

  • address (string|SocketAddr) — The host:port to bind and listen on

Raises Error if binding fails (e.g. the address is already in use)

DtlsSocket.connect()

net.dtls.DtlsSocket.connect(config, address, server_name)

Opens a DTLS association with address, performing the full handshake before returning. server_name is used to verify the peer’s certificate, the same way tls.TlsStream.connect() uses it; omit it to fall back to verifying against the resolved IP address instead of a hostname.

Parameters

  • config (DtlsConfig) — The trust roots and options to handshake with
  • address (string|SocketAddr) — The host:port to connect to
  • server_name (?string) — The hostname to verify the peer’s certificate against

Raises Error if resolution or the handshake fails, or the peer’s certificate isn’t trusted or doesn’t match server_name

DtlsSocket.accept()

net.dtls.DtlsSocket.accept(config) -> DtlsSocket

Waits for the next peer to complete a handshake and returns a new DtlsSocket representing that specific peer’s association. Only valid on a bound socket. Blocks (subject to set_read_timeout()) until a new peer finishes handshaking; existing peers’ traffic keeps flowing to their own DtlsSocket instances in the meantime.

Parameters

  • config (DtlsConfig) — Must have a certificate chain set via DtlsConfig.set_cert_chain()

Returns DtlsSocket — a socket for the newly accepted peer

Raises Error if this socket isn’t bound, config has no certificate chain, or (with require_client_cert()) a peer’s handshake didn’t present a trusted certificate

DtlsSocket.read()

net.dtls.DtlsSocket.read(length) -> bytes

Reads the next decrypted application-data message, up to length bytes of it. DTLS preserves message boundaries the way UDP does: this always returns one whole message (truncated to length if it was longer), never a partial one glued to the next.

Parameters

  • length (number) — The maximum number of bytes to return

Returns bytes — the next message, up to length bytes

Raises Error on an underlying I/O or DTLS record error, or if the configured read timeout elapses first

DtlsSocket.write()

net.dtls.DtlsSocket.write(data) -> number

Encrypts and sends data as a single application-data message to the peer this socket is connected/accepted for.

Parameters

  • data (bytes|string) — The bytes (or UTF-8 text) to send

Returns number — the number of bytes sent

Raises Error on an underlying I/O or DTLS record error

DtlsSocket.local_address()

net.dtls.DtlsSocket.local_address() -> SocketAddr

The local socket address this instance is bound to.

Returns SocketAddr

Raises Error if this socket isn’t bound

DtlsSocket.peer_address()

net.dtls.DtlsSocket.peer_address() -> SocketAddr

The remote peer’s socket address.

Returns SocketAddr

Raises Error if this socket isn’t a connected/accepted association

DtlsSocket.set_read_timeout()

net.dtls.DtlsSocket.set_read_timeout(timeout)

Sets how long read() may block before raising a timeout error. Pass 0 (or any negative number) to block indefinitely, which is the default.

Parameters

  • timeout (number) — Milliseconds, or a non-positive number to disable the timeout

DtlsSocket.close()

net.dtls.DtlsSocket.close()

Releases this socket. Safe to call more than once. Only closes this particular peer association’s bookkeeping - if this is a listening socket, other already-accepted DtlsSocket instances sharing its underlying bound port are unaffected.

DtlsSocket.peer_certificate()

net.dtls.DtlsSocket.peer_certificate() -> ?PeerCertificate

The peer’s end-entity certificate, or nil if the peer didn’t present one.

Returns ?PeerCertificate

DtlsSocket.to_string()

net.dtls.DtlsSocket.to_string()

2026, Richard Ore and Zuri contributors

net.ip

import net

Everything here is re-exported by net, so import net is enough and the names are called as net.*. Importing net.ip on its own works too and reaches the same definitions.

This module provides types for parsing, formatting, comparing and classifying IP addresses in both their IPv4 and IPv6 forms.

Ipv4 and Ipv6 each represent an address of their respective family, while Ip is a small wrapper around either one for code that needs to accept and work with both interchangeably.

The example below parses an address of unknown family and reports a few things about it.

import net

var addr = net.Ip.parse('192.168.1.10')

if addr.is_private() {
  echo '${addr} is a private address'
}

Ipv4 and Ipv6 can also be worked with directly when the family is already known, as shown below.

import net

var a = net.Ipv4.parse('10.0.0.1')
var b = net.Ipv4(10, 0, 0, 2)

echo a.is_private()
echo a.equals(b)
echo a.octets()

Classes

Ipv4

class net.Ipv4

An IPv4 address, stored internally as four octets in network (i.e. big-endian, most significant octet first) order.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

net.Ipv4(a, b, c, d)

Returns a new instance of an Ipv4 from its four octets, given in the same left-to-right order they’d be written in dotted-decimal notation, e.g. Ipv4(192, 168, 1, 10) for 192.168.1.10.

Parameters

  • a (number) — The 1st octet, 0-255
  • b (number) — The 2nd octet, 0-255
  • c (number) — The 3rd octet, 0-255
  • d (number) — The 4th octet, 0-255

Raises Error if any octet is missing or outside 0-255

Ipv4.unspecified()

net.Ipv4.unspecified() -> Ipv4

The 0.0.0.0 address, used to mean “any local address” when binding a socket.

Returns Ipv4

Ipv4.localhost()

net.Ipv4.localhost() -> Ipv4

The 127.0.0.1 loopback address.

Returns Ipv4

Ipv4.broadcast()

net.Ipv4.broadcast() -> Ipv4

The 255.255.255.255 limited broadcast address.

Returns Ipv4

Ipv4.parse()

net.Ipv4.parse(address) -> Ipv4

Parses an address given in standard dotted-decimal notation, e.g. '192.168.1.10'.

Parameters

  • address (string) — The address to parse

Returns Ipv4

Raises Error if address is not four dot-separated octets, each a decimal number from 0 to 255

Note: Each octet must be written without a leading zero (other than the literal digit 0 itself) - '192.168.001.10' is rejected rather than guessed at, since a leading zero is read as an octal prefix by some other tools and silently accepting it here could make an address mean two different things depending on what reads it.

Ipv4.octets()

net.Ipv4.octets() -> number[]

Returns the four octets making up this address as a list, in the same left-to-right order they’d be written in dotted-decimal notation. Each call returns a fresh list, so mutating it does not affect this instance.

Returns number[] — [a, b, c, d]

Ipv4.is_unspecified()

net.Ipv4.is_unspecified() -> bool

Whether this is the 0.0.0.0 unspecified address.

Returns bool

Ipv4.is_loopback()

net.Ipv4.is_loopback() -> bool

Whether this address is in the 127.0.0.0/8 loopback range.

Returns bool

Ipv4.is_private()

net.Ipv4.is_private() -> bool

Whether this address is in one of the private-use ranges reserved by RFC 1918: 10.0.0.0/8, 172.16.0.0/12, or 192.168.0.0/16.

Returns bool

net.Ipv4.is_link_local() -> bool

Whether this address is in the 169.254.0.0/16 link-local range, used for address autoconfiguration when no other address is available.

Returns bool

Ipv4.is_multicast()

net.Ipv4.is_multicast() -> bool

Whether this address is in the 224.0.0.0/4 multicast range.

Returns bool

Ipv4.is_broadcast()

net.Ipv4.is_broadcast() -> bool

Whether this is the 255.255.255.255 limited broadcast address.

Returns bool

Ipv4.is_documentation()

net.Ipv4.is_documentation() -> bool

Whether this address falls in one of the ranges reserved by RFC 5737 for use in documentation and examples: 192.0.2.0/24, 198.51.100.0/24, or 203.0.113.0/24.

Returns bool

Ipv4.to_ipv6_mapped()

net.Ipv4.to_ipv6_mapped() -> Ipv6

Returns this address re-expressed as an IPv4-mapped IPv6 address, e.g. 192.168.1.10 becomes ::ffff:192.168.1.10.

Returns Ipv6

Ipv4.equals()

net.Ipv4.equals(other) -> bool

Whether other refers to the same address as this one.

Parameters

  • other (Ipv4) — The address to compare against

Returns bool

Ipv4.to_string()

net.Ipv4.to_string() -> string

This address in standard dotted-decimal notation, e.g. '192.168.1.10'.

Returns string

Ipv4.to_list()

net.Ipv4.to_list() -> list

Returns the octets of the Ipv4 address as a list

Returns list

Ipv6

class net.Ipv6

An IPv6 address, stored internally as eight 16-bit segments in network (i.e. big-endian, most significant segment first) order.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

net.Ipv6(a, b, c, d, e, f, g, h)

Returns a new instance of an Ipv6 from its eight segments, given in the same left-to-right order they’d be written in colon-hex notation, e.g. Ipv6(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1) for 2001:db8::1.

Parameters

  • a (number) — The 1st segment, 0-65535
  • b (number) — The 2nd segment, 0-65535
  • c (number) — The 3rd segment, 0-65535
  • d (number) — The 4th segment, 0-65535
  • e (number) — The 5th segment, 0-65535
  • f (number) — The 6th segment, 0-65535
  • g (number) — The 7th segment, 0-65535
  • h (number) — The 8th segment, 0-65535

Raises Error if any segment is missing or outside 0-65535

Ipv6.unspecified()

net.Ipv6.unspecified() -> Ipv6

The :: unspecified address, used to mean “any local address” when binding a socket.

Returns Ipv6

Ipv6.localhost()

net.Ipv6.localhost() -> Ipv6

The ::1 loopback address.

Returns Ipv6

Ipv6.parse()

net.Ipv6.parse(address) -> Ipv6

Parses an address given in standard colon-hex notation, including the :: shorthand for a run of zero segments (at most one :: is allowed per address) and the embedded dotted-decimal form used for IPv4-mapped/-compatible addresses, e.g. '::ffff:192.168.1.10'.

Parameters

  • address (string) — The address to parse

Returns Ipv6

Raises Error if address is not a well-formed IPv6 address

Note: Zone identifiers (e.g. the %eth0 suffix used to disambiguate link-local addresses on a particular interface) are not supported and will cause parsing to fail.

Ipv6.segments()

net.Ipv6.segments() -> number[]

Returns the eight segments making up this address as a list, in the same left-to-right order they’d be written in colon-hex notation. Each call returns a fresh list, so mutating it does not affect this instance.

Returns number[] — [a, b, c, d, e, f, g, h]

Ipv6.is_unspecified()

net.Ipv6.is_unspecified() -> bool

Whether this is the :: unspecified address.

Returns bool

Ipv6.is_loopback()

net.Ipv6.is_loopback() -> bool

Whether this is the ::1 loopback address.

Returns bool

Ipv6.is_multicast()

net.Ipv6.is_multicast() -> bool

Whether this address is in the ff00::/8 multicast range.

Returns bool

net.Ipv6.is_unicast_link_local() -> bool

Whether this address is in the fe80::/10 unicast link-local range.

Returns bool

Ipv6.to_ipv4_mapped()

net.Ipv6.to_ipv4_mapped() -> ?Ipv4

If this address is an IPv4-mapped address (::ffff:0:0/96), returns the IPv4 address it maps to; otherwise returns nil.

Returns ?Ipv4

Ipv6.equals()

net.Ipv6.equals(other) -> bool

Whether other refers to the same address as this one.

Parameters

  • other (Ipv6) — The address to compare against

Returns bool

Ipv6.to_string()

net.Ipv6.to_string() -> string

This address in its canonical, most-compressed colon-hex notation - the longest run of consecutive zero segments (if any run is at least two segments long) is collapsed to ::; if more than one such run exists, the leftmost is preferred.

Returns string

Ipv6.to_list()

net.Ipv6.to_list() -> list

Returns the segments of the Ipv6 address as a list

Returns list

Ip

class net.Ip

Either an Ipv4 or an Ipv6, for code that needs to accept and work with addresses of either family without committing to one upfront.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

net.Ip(inner)

Wraps an already-constructed Ipv4 or Ipv6. Application code should generally prefer Ip.parse(), Ip.v4(), or Ip.v6() over calling this directly.

Parameters

  • inner (Ipv4|Ipv6) — The address to wrap

Ip.v4()

net.Ip.v4(address) -> Ip

Wraps an Ipv4 as an Ip.

Parameters

  • address (Ipv4) — The address to wrap

Returns Ip

Ip.v6()

net.Ip.v6(address) -> Ip

Wraps an Ipv6 as an Ip.

Parameters

  • address (Ipv6) — The address to wrap

Returns Ip

Ip.parse()

net.Ip.parse(address) -> Ip

Parses address as either an IPv4 or IPv6 address, choosing the family based on whether address contains a : character.

Parameters

  • address (string) — The address to parse

Returns Ip

Raises Error if address is not a well-formed IPv4 or IPv6 address

Ip.is_ipv4()

net.Ip.is_ipv4() -> bool

Whether this address is an Ipv4.

Returns bool

Ip.is_ipv6()

net.Ip.is_ipv6() -> bool

Whether this address is an Ipv6.

Returns bool

Ip.as_ipv4()

net.Ip.as_ipv4() -> ?Ipv4

Returns the wrapped Ipv4, or nil if this is an IPv6 address.

Returns ?Ipv4

Ip.as_ipv6()

net.Ip.as_ipv6() -> ?Ipv6

Returns the wrapped Ipv6, or nil if this is an IPv4 address.

Returns ?Ipv6

Ip.is_unspecified()

net.Ip.is_unspecified() -> bool

Whether this is the unspecified address for its family (0.0.0.0 or ::).

Returns bool

Ip.is_loopback()

net.Ip.is_loopback() -> bool

Whether this is the loopback address for its family (127.0.0.0/8 or ::1).

Returns bool

Ip.is_multicast()

net.Ip.is_multicast() -> bool

Whether this is a multicast address for its family.

Returns bool

Ip.equals()

net.Ip.equals(other) -> bool

Whether other refers to the same address as this one. Two addresses of different families are never equal, even if one is the IPv4-mapped form of the other - compare their canonical forms explicitly if that’s the comparison you want.

Parameters

  • other (Ip) — The address to compare against

Returns bool

Ip.to_string()

net.Ip.to_string() -> string

This address in its family’s standard notation.

Returns string

Ip.to_list()

net.Ip.to_list() -> list

Returns the address in this IP as a list. If the address is an Ipv4 address, the list will contain four (4) elements. Otherwise, it will contain eight (8) elements.

Returns list


2026, Richard Ore and Zuri contributors

net.poll

import net

Everything here is re-exported by net, so import net is enough and the names are called as net.*. Importing net.poll on its own works too and reaches the same definitions.

Readiness polling: given a set of sockets, which of them can be read or written right now, without blocking.

Every other call in net blocks. TcpStream.read() waits until its own socket has something to say and the thread does nothing else in the meantime, which is right for a client and wrong for a server: a thread that commits to one connection cannot serve another until that connection lets it go. A server built only from blocking calls can hold exactly as many concurrent connections as it has threads.

A Poller is the way out. It watches many sockets at once and tells you which ones are ready, so a single thread can own hundreds of connections and spend its time only on the ones with work:

import net

var server = net.TcpStream()
server.bind('127.0.0.1:8080')

var poller = net.Poller()
var clients = {}
var next_token = 1

# Token 0 watches the listener itself; an incoming connection makes
# it readable exactly the way data does on a connected socket.
poller.add(server, 0, net.READABLE)

while true {
  for event in poller.wait(1000) {
    var token = event[0]

    if token == 0 {
      var client = server.accept()
      clients[next_token] = client
      poller.add(client, next_token, net.READABLE)
      next_token++
      continue
    }

    var client = clients[token]

    # A peer that has gone away is reported rather than left to be
    # discovered by a read that returns nothing, forever.
    if event[1] & (net.ERROR | net.HANGUP) != 0 {
      poller.remove(token)
      clients.remove(token)
      client.close()
      continue
    }

    echo client.read(1024).to_string()
  }
}

Isolates

A Poller watches the sockets of the isolate that owns it, and nothing about it crosses an isolate boundary. Several worker isolates each keeping their own poller over their own connections is the intended shape, and is exactly the shared-nothing model.

A socket that is closed, upgraded into a TlsStream, or moved to another isolate stops being able to supply a descriptor. Rather than this being a rule you have to remember, wait() reports such a socket as ERROR against its own token, because the descriptor is resolved fresh on every call and never remembered between them. It is not possible for a poller to end up watching a descriptor that the operating system has since handed to something else.

What polling does not do

Readiness is not a guarantee. A socket reported READABLE has data now, but a read can still come up short, and a large write can still be partial on a socket reported WRITABLE. Poll to decide what to work on; keep handling short reads and writes the way you would anyway.

Constants

READABLE

net.READABLE: int = 1

Interest: tell me when this socket has data to read, a connection to accept, or an end-of-stream to observe.

WRITABLE

net.WRITABLE: int = 2

Interest: tell me when this socket can be written to without blocking.

ERROR

net.ERROR: int = 4

Readiness only: the socket has failed, or its handle can no longer supply a descriptor because it was closed, upgraded, or moved to another isolate. Deregister it.

HANGUP

net.HANGUP: int = 8

Readiness only: the peer has closed its end. Any data already sent is still readable, so drain the socket before closing it.

Classes

Poller

class net.Poller

A set of sockets to watch, each under a token of your choosing.

The token is whatever number is convenient for finding the socket again

  • an index into your own table, a connection id. wait() hands back the tokens that are ready, never the sockets, so the mapping stays yours.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

net.Poller()

Returns a new, empty Poller.

Poller.add()

net.Poller.add(socket, token: number, interest: ?number)

Starts watching socket under token.

Registering a token that is already present replaces it, so a caller that loses track cannot end up with one token naming two sockets.

Parameters

  • socket (TcpStream|TlsStream) — connected or bound; a bound listener becomes readable when a connection is waiting
  • token (number) — your own identifier for this socket
  • interest (?number) — READABLE, WRITABLE, or both; defaults to READABLE

Returns — Poller: this same instance, for chaining

Raises ValueError if interest names neither readability nor writability

Poller.modify()

net.Poller.modify(token: number, interest: number)

Changes what token is watched for, leaving the socket alone.

This is how a connection with a reply still to write switches from waiting for a request to waiting for room to send, and back again.

Parameters

  • token (number)
  • interest (number)

Returns — bool: whether the token was registered

Raises ValueError if interest names neither readability nor writability

Poller.remove()

net.Poller.remove(token: number)

Stops watching token, and releases this poller’s reference to its socket. Removing a token that is not registered does nothing rather than failing.

Parameters

  • token (number)

Returns — bool: whether the token was registered

Poller.contains()

net.Poller.contains(token: number) -> bool

Whether token is currently registered.

Parameters

  • token (number)

Returns bool

Poller.interest()

net.Poller.interest(token: number) -> ?number

The interest currently registered for token, or nil.

Parameters

  • token (number)

Returns ?number

Poller.tokens()

net.Poller.tokens() -> list

Every registered token, in registration order.

Returns list

Poller.length()

net.Poller.length() -> number

How many sockets are being watched.

Returns number

Poller.is_empty()

net.Poller.is_empty() -> bool

Whether nothing is being watched.

Returns bool

Poller.clear()

net.Poller.clear()

Stops watching everything.

Returns — Poller: this same instance, for chaining

Poller.wait()

net.Poller.wait(timeout: ?number)

Waits until at least one registered socket is ready, and returns what happened.

Each entry is [token, flags], where flags is any combination of READABLE, WRITABLE, ERROR and HANGUP. An empty list means the timeout elapsed with nothing ready - and also that nothing is registered, since waiting on an empty set could only ever sleep for the full timeout to no purpose.

Parameters

  • timeout (?number) — milliseconds to wait; 0 polls without waiting, a negative number waits indefinitely. Defaults to waiting indefinitely.

Returns — list: [token, flags] pairs, one per ready socket

Raises Error on an underlying platform failure

Note: Test flags with &, never ==: a socket can be readable and hung up at the same time, which is what a peer that sent a final response and closed looks like.

Poller.to_string()

net.Poller.to_string()

Acceptor

class net.Acceptor

An accept loop that never parks the thread inside accept().

A blocking accept() waits inside the runtime, where nothing else on the thread gets a turn: a signal trapped with os.on_signal() is not delivered, and a flag telling the server to stop is not read, until a connection happens to arrive. On an idle server that is never, which is why Ctrl+C on one can appear to do nothing.

An Acceptor puts the listener into non-blocking mode and waits on a Poller instead. next() hands over a connection when there is one, and returns nil when the interval passes with nothing arriving, which is the loop’s chance to look at whatever else it has to look at.

import net
import os

var listener = net.TcpStream()
listener.bind('127.0.0.1:8080')

var running = [true]

os.on_signal('INT', @() {
  running[0] = false
  return true
})

var acceptor = net.Acceptor(listener)

while running[0] {
  var client = acceptor.next()

  if client == nil {
    continue
  }

  echo 'connection from ${client.peer_address()}'
  client.close()
}

Nothing is added while connections are arriving: next() tries accept() first, and only consults the poller when the queue is empty.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

net.Acceptor(listener, interval: ?number)

Takes over listener, which must already be bound.

The listener is put into non-blocking mode and left that way, so a caller that also accepts from it directly gets nil when nothing is waiting rather than a blocking wait.

Parameters

  • listener (TcpStream|UnixStream) — bound and listening
  • interval (?number) — Milliseconds next() waits before giving up on a round. Defaults to 100. A connection that arrives wakes the wait at once, so this is the idle interval rather than added latency: it decides how soon a stopped server and a trapped signal are noticed.

Raises ValueError if interval is negative, which would wait forever and defeat the point.

Raises Error if the listener is not bound, or cannot be put into non-blocking mode.

Acceptor.next()

net.Acceptor.next() -> ?TcpStream|?UnixStream

The next connection, or nil when the interval passed with nothing arriving.

nil means “go round again”, never “there will be no more”.

The connection comes back in blocking mode whatever mode the listener is in, since what usually follows is a request pipeline written against blocking reads bounded by timeouts.

Returns ?TcpStream|?UnixStream

Raises Error on an underlying accept failure, including the listener having been closed.

Acceptor.wait_for_one()

net.Acceptor.wait_for_one() -> TcpStream|UnixStream

Waits for a connection however long it takes, the way a blocking accept() would, while still coming up for air every interval.

Returns TcpStream|UnixStream

Raises Error on an underlying accept failure

Acceptor.listener()

net.Acceptor.listener() -> TcpStream|UnixStream

The listener this accepts from.

Returns TcpStream|UnixStream

Acceptor.to_string()

net.Acceptor.to_string()

2026, Richard Ore and Zuri contributors

net.resolver

import net

net exposes this as net.resolver, so import net is enough and the names are called as net.resolver.*. import net.resolver reaches the same definitions directly.

A DNS resolver, written in Zuri from the wire format up.

net.resolve() answers the one question the operating system will answer for you: what addresses does this name have. Everything else a name can carry, from the mail servers of a domain to the text record a service asks you to publish, needs a resolver that speaks DNS itself. This is that resolver.

import net.resolver

for record in resolver.mx('example.com') {
  echo '${record.preference} ${record.exchange}'
}

The module-level functions go through one shared Resolver built from this machine’s own configuration, which is what most programs want. Build your own when you need to ask a particular server, or ask it over TLS:

import net.resolver { Resolver }

var dns = Resolver({
  servers: ['1.1.1.1'],
  tls: true,
  server_name: 'cloudflare-dns.com',
})

echo dns.txt('example.com')

What it does

Queries go out over UDP and fall back to TCP when the answer does not fit, which is the behaviour every resolver is required to have and the reason a large TXT or DNSKEY record arrives intact. EDNS(0) is advertised so that fallback is rare. Each query carries a random identifier from a fresh ephemeral port, answers that do not match the question asked are discarded, and a server that does not answer in time gives way to the next one.

Answers are cached for as long as their own time-to-live says, and a name that does not exist is cached too, for as long as the zone’s own authority record allows. Caching is per resolver and can be turned off.

What it does not do

It does not validate DNSSEC. Signatures are carried through when a server sends them, and the dnssec option asks for them, but nothing here checks one. A program that needs a validated answer wants a validating server and a trusted path to it, which is what tls gives.

Constants

TYPES

net.resolver.TYPES: dict = {...}

The record types this module knows how to read, by name. A type it has no name for is still parsed, reported by its number, and its data handed over as raw bytes.

CLASSES

net.resolver.CLASSES: dict = {...}

The classes a question can be asked in. IN, the internet class, is the only one in general use and is the default everywhere here.

RCODES

net.resolver.RCODES: dict = {...}

What a server said about the question, by name. NOERROR means the query succeeded, which is not the same as it having found anything.

Functions

parse_message()

net.resolver.parse_message(data) -> Response

Turns the bytes of a message into a Response.

Parameters

  • data (bytes)

Returns Response

Raises MalformedResponse if the bytes are not a well-formed message

build_query()

net.resolver.build_query(id: number, name: string, type: number, klass: number, options: dict) -> bytes

Builds the bytes of a query.

Parameters

  • id (number) — The identifier to match the answer against.
  • name (string) — The name to ask about.
  • type (number)
  • klass (number)
  • options (dict) — recursion, edns, udp_size and dnssec.

Returns bytes

exchange()

net.resolver.exchange(server: string, query, options: dict) -> bytes

Asks one server one question and returns what it said.

Used by Resolver, and on its own when you want to talk to a particular server without any of the retrying, caching or search list a resolver puts around it.

Parameters

  • server (string) — An address, optionally with a port.
  • query (bytes) — The message to send.
  • options (dict) — timeout, port, tcp, tls, tls_config and server_name.

Returns bytes — the answer, unparsed

Raises Error on a network failure or a timeout

system_servers()

net.resolver.system_servers() -> list

The nameservers this machine is configured to use, in the order the system lists them.

Read from /etc/resolv.conf on unix and from the network stack on Windows. An address that is only meaningful together with an interface, which is how a link-local IPv6 server is configured, is left out, because nothing here can carry the interface along with it.

import net.resolver

echo resolver.system_servers()
# [127.0.0.53]

Returns list — of string, possibly empty

net.resolver.system_search() -> list

The domain suffixes to try against a name that has no dots in it.

Usually empty away from a managed network. Resolver uses this as the default for its search option.

Returns list — of string, possibly empty

reverse_name()

net.resolver.reverse_name(address: string) -> string

The name an address’s records are published under.

IPv4 addresses live under in-addr.arpa with their octets reversed, and IPv6 addresses under ip6.arpa with one label per nibble.

import net.resolver

echo resolver.reverse_name('192.0.2.7')
echo resolver.reverse_name('2001:db8::1')
7.2.0.192.in-addr.arpa
1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa

Parameters

  • address (string) — An IPv4 or IPv6 address.

Returns string

Raises Error if address is not an address.

shared()

net.resolver.shared() -> Resolver

The resolver the module-level functions use: one shared Resolver built from this machine’s own configuration.

Reach for it when you want the shared cache but need a method the module does not re-export.

Returns Resolver

use()

net.resolver.use(instance: instance) -> Resolver

Replaces the shared resolver, so that every module-level function goes through the one given instead.

import net.resolver { Resolver }
import net.resolver

resolver.use(Resolver({ servers: ['9.9.9.9'] }))

Parameters

  • instance (Resolver)

Returns Resolver — the one now in use.

query()

net.resolver.query(name: string, type, klass) -> Response

Asks the shared resolver about one name and hands back the whole answer.

Parameters

  • name (string)
  • type (string|number)
  • klass (?string|number)

Returns Response

resolve()

net.resolver.resolve(name: string, type, klass) -> list

The records of one type published for a name, through the shared resolver.

Parameters

  • name (string)
  • type (string|number)
  • klass (?string|number)

Returns list — of Record

addresses()

net.resolver.addresses(name: string) -> list

Every address a name has, IPv4 first.

Parameters

  • name (string)

Returns list — of string

a()

net.resolver.a(name: string) -> list

The IPv4 addresses of a name.

Parameters

  • name (string)

Returns list — of string

aaaa()

net.resolver.aaaa(name: string) -> list

The IPv6 addresses of a name.

Parameters

  • name (string)

Returns list — of string

cname()

net.resolver.cname(name: string) -> string|nil

The name this one is an alias for, or nil.

Parameters

  • name (string)

Returns string|nil

mx()

net.resolver.mx(name: string) -> list

The mail servers for a domain, lowest preference first.

Parameters

  • name (string)

Returns list — of MailExchange

ns()

net.resolver.ns(name: string) -> list

The nameservers a domain is delegated to.

Parameters

  • name (string)

Returns list — of string

txt()

net.resolver.txt(name: string) -> list

The text records of a name.

Parameters

  • name (string)

Returns list — of string

soa()

net.resolver.soa(name: string) -> StartOfAuthority|nil

The authority record at the top of a name’s zone, or nil.

Parameters

  • name (string)

Returns StartOfAuthority|nil

srv()

net.resolver.srv(name: string) -> list

The hosts running a service, by priority and then by weight.

Parameters

  • name (string)

Returns list — of Service

ptr()

net.resolver.ptr(address: string) -> list

The names an address points back at.

Parameters

  • address (string)

Returns list — of string

caa()

net.resolver.caa(name: string) -> list

Which certificate authorities a domain allows to issue for it.

Parameters

  • name (string)

Returns list — of Certification

flush()

net.resolver.flush() -> Resolver

Empties the shared resolver’s cache.

Returns Resolver — the shared resolver.

Classes

ResolverError

class net.resolver.ResolverError < Error

The root of every error this module raises.

NameError

class net.resolver.NameError < ResolverError

The name does not exist. This is the NXDOMAIN answer, and it is an answer rather than a failure: an authoritative server has said that nothing is published under this name at all.

ServerError

class net.resolver.ServerError < ResolverError

The server answered, and the answer was a refusal or a failure of its own. rcode carries the code it sent and rcode_name the name of that code.

Constructor

net.resolver.ServerError(message: string, rcode: number)

Parameters

  • message (string)
  • rcode (number) — The code the server replied with.

ResolverTimeout

class net.resolver.ResolverTimeout < ResolverError

No server answered in time. Every configured server was asked, as many times as attempts allows, and none of them replied.

MalformedResponse

class net.resolver.MalformedResponse < ResolverError

Something came back that is not a DNS message, or is one that contradicts itself. A truncated answer, a compression pointer that loops, a record whose length runs past the end of the message.

NoServersError

class net.resolver.NoServersError < ResolverError

The resolver has nowhere to send a query: no servers were given and the system has none configured.

MailExchange

class net.resolver.MailExchange

One mail server for a domain, as an MX record names it.

Mail is delivered to the exchange with the lowest preference that answers, so Resolver.mx() hands them back in that order.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

net.resolver.MailExchange(preference: number, exchange: string)

Parameters

  • preference (number) — Lower is tried first.
  • exchange (string) — The name of the mail server.

MailExchange.to_string()

net.resolver.MailExchange.to_string()

Service

class net.resolver.Service

Where a service lives, as an SRV record names it: the host and port, with the priority and weight that decide which of several to use.

Clients try the lowest priority first, and choose among equal priorities in proportion to their weight.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

net.resolver.Service(priority: number, weight: number, port: number, target: string)

Parameters

  • priority (number) — Lower is tried first.
  • weight (number) — The share of traffic among equal priorities.
  • port (number) — The port the service listens on.
  • target (string) — The name of the host running it.

Service.address()

net.resolver.Service.address() -> string

The host:port this record points at, ready to hand to TcpStream.connect().

Returns string

Service.to_string()

net.resolver.Service.to_string()

StartOfAuthority

class net.resolver.StartOfAuthority

The authority record at the top of a zone, which says who publishes it and how long the rest of the world may hold on to what it says.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

net.resolver.StartOfAuthority(primary, mailbox, serial, refresh, retry, expire, minimum)

Parameters

  • primary (string) — The name of the primary nameserver.
  • mailbox (string) — The address of whoever is responsible, with the first dot standing in for the @.
  • serial (number) — The version of the zone.
  • refresh (number) — Seconds a secondary waits between checks.
  • retry (number) — Seconds a secondary waits after a failed one.
  • expire (number) — Seconds a secondary may serve the zone without reaching the primary.
  • minimum (number) — Seconds a negative answer may be cached.

StartOfAuthority.to_string()

net.resolver.StartOfAuthority.to_string()

Certification

class net.resolver.Certification

A CAA record: which certificate authorities a domain allows to issue for it.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

net.resolver.Certification(flags: number, tag: string, value: string)

Parameters

  • flags (number) — Bit 7 set means a client must understand the tag or refuse to issue.
  • tag (string) — issue, issuewild or iodef in practice.
  • value (string) — What the tag names, usually an authority’s domain.

Certification.to_string()

net.resolver.Certification.to_string()

Record

class net.resolver.Record

One resource record: a name, what kind of thing it holds, how long it may be cached, and the thing itself.

What data holds depends on the type:

typedata
A, AAAAthe address, as a string
NS, CNAME, PTR, DNAMEthe name, as a string
MXa MailExchange
SRVa Service
SOAa StartOfAuthority
CAAa Certification
TXTa list of strings, one per character-string
anything elsethe record’s data, as bytes
  • printable — has a @to_string(), so echo and print() show something useful

Constructor

net.resolver.Record(name, type, klass, ttl, data)

Parameters

  • name (string) — The name this record is published under.
  • type (number) — The record type, as a number.
  • klass (number) — The record class, as a number.
  • ttl (number) — Seconds this record may be cached for.
  • data (any) — The record’s contents, per the table above.

Record.text()

net.resolver.Record.text() -> string

A TXT record’s strings joined into one, which is how a record split across several character-strings is meant to be read. Any other type raises.

Returns string

Raises ResolverError if this is not a TXT record.

Record.to_string()

net.resolver.Record.to_string() -> string

The record as a zone file would write it.

Returns string

Question

class net.resolver.Question

The question a message asks, echoed back in the answer so a reply can be matched to what it replies to.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

net.resolver.Question(name: string, type: number, klass: number)

Parameters

  • name (string)
  • type (number)
  • klass (number)

Question.to_string()

net.resolver.Question.to_string()

Response

class net.resolver.Response

A whole answer: what was asked, what came back, and what the server said about it.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

net.resolver.Response(id, flags, question, answers, authorities, additionals, extended)

Response.records_of()

net.resolver.Response.records_of(type) -> list

The answers of one type, leaving out the aliases and anything else the server sent along.

Parameters

  • type (string|number)

Returns list — of Record

Response.ok()

net.resolver.Response.ok() -> Response

Raises when the server reported anything other than success. A name that does not exist raises NameError; everything else raises ServerError.

Returns Response — itself, so this can be chained.

Raises NameError|ServerError

Response.to_string()

net.resolver.Response.to_string()

Resolver

class net.resolver.Resolver

Asks questions of the domain name system and reads the answers.

One resolver holds its own configuration and its own cache, so a program can keep several: one for the machine’s own servers, one for a particular provider over TLS, one with caching off for a test.

import net.resolver { Resolver }

var dns = Resolver()

echo dns.addresses('example.com')
echo dns.mx('example.com')
  • printable — has a @to_string(), so echo and print() show something useful

Constructor

net.resolver.Resolver(options: ?dict)

Builds a resolver.

Every option has a default, and the defaults describe this machine, so Resolver() behaves the way the rest of the system does.

optiondefaultwhat it does
serversthe system’s ownthe addresses to ask, in order
port53, or 853 with tlsthe port to ask them on
timeout5000milliseconds one server gets to answer
attempts2times to go round the whole list
tcpfalsesend over TCP rather than UDP
tlsfalsesend over TLS, which implies TCP
server_namenonethe name to verify the TLS certificate against
tls_configa default TlsConfigthe trust settings for TLS
searchthe system’s ownsuffixes to try against a short name
ndots1dots a name needs before it is tried unsuffixed first
recursiontrueask the server to do the work
ednstrueadvertise a larger UDP payload
udp_size1232how much larger
dnssecfalseask for signature records, without validating them
cachetrueremember answers for as long as they say
cache_size512entries to keep before evicting

Parameters

  • options (?dict)

Raises ResolverError if an option is not one of the above.

Resolver.servers()

net.resolver.Resolver.servers() -> list

The servers this resolver asks, in the order it asks them.

Returns list — of string

Raises NoServersError if none were given and the system has none.

Resolver.search()

net.resolver.Resolver.search() -> list

The suffixes this resolver tries against a name too short to be meant on its own.

Returns list — of string, possibly empty

Resolver.flush()

net.resolver.Resolver.flush() -> Resolver

Empties this resolver’s cache.

Returns Resolver — itself.

Resolver.query()

net.resolver.Resolver.query(name: string, type, klass) -> Response

Asks about one name and hands back the whole answer, including the records the server sent along with it.

The search list is applied, so a short name is tried with each configured suffix. The answer is not checked: a name that does not exist comes back as a Response whose rcode says so, which is what ok() is for.

import net.resolver { Resolver }

var response = Resolver().query('example.com', 'MX').ok()

echo response.answers

Parameters

  • name (string)
  • type (string|number) — A name from TYPES or a number.
  • klass (?string|number) — IN unless told otherwise.

Returns Response

Raises NoServersError if there is nowhere to send the query.

Raises ResolverTimeout if no server answered.

Raises MalformedResponse if what came back is not a valid answer.

Resolver.resolve()

net.resolver.Resolver.resolve(name: string, type, klass) -> list

The records of one type published for a name.

Aliases are followed: asking for the A records of a name that is a CNAME gives the addresses at the end of the chain. A name that exists but publishes nothing of this type gives an empty list, which is a different thing from the name not existing.

Parameters

  • name (string)
  • type (string|number)
  • klass (?string|number)

Returns list — of Record

Raises NameError if the name does not exist.

Raises ServerError if the server reported a failure.

Resolver.addresses()

net.resolver.Resolver.addresses(name: string) -> list

Every address a name has, IPv4 first and then IPv6.

Parameters

  • name (string)

Returns list — of string

Raises NameError if the name does not exist.

Resolver.a()

net.resolver.Resolver.a(name: string) -> list

The IPv4 addresses of a name.

Parameters

  • name (string)

Returns list — of string

Raises NameError if the name does not exist.

Resolver.aaaa()

net.resolver.Resolver.aaaa(name: string) -> list

The IPv6 addresses of a name.

Parameters

  • name (string)

Returns list — of string

Raises NameError if the name does not exist.

Resolver.cname()

net.resolver.Resolver.cname(name: string) -> string|nil

The name this one is an alias for, or nil when it is not one.

Parameters

  • name (string)

Returns string|nil

Raises NameError if the name does not exist.

Resolver.mx()

net.resolver.Resolver.mx(name: string) -> list

The mail servers for a domain, lowest preference first, which is the order they should be tried in.

import net.resolver { Resolver }

for server in Resolver().mx('example.com') {
  echo '${server.preference} ${server.exchange}'
}

Parameters

  • name (string)

Returns list — of MailExchange

Raises NameError if the name does not exist.

Resolver.ns()

net.resolver.Resolver.ns(name: string) -> list

The nameservers a domain is delegated to.

Parameters

  • name (string)

Returns list — of string

Raises NameError if the name does not exist.

Resolver.txt()

net.resolver.Resolver.txt(name: string) -> list

The text records of a name, one string per record.

A record split across several character-strings, which is how anything longer than 255 bytes is published, is joined back together, because the split is a limit of the wire format and not part of what was published.

Parameters

  • name (string)

Returns list — of string

Raises NameError if the name does not exist.

Resolver.soa()

net.resolver.Resolver.soa(name: string) -> StartOfAuthority|nil

The authority record at the top of the zone a name belongs to, or nil when the name is not the top of one.

Parameters

  • name (string)

Returns StartOfAuthority|nil

Raises NameError if the name does not exist.

Resolver.srv()

net.resolver.Resolver.srv(name: string) -> list

The hosts running a service, by priority and then by weight.

Among equal priorities the record with the larger weight should take proportionally more traffic; this returns them heaviest first, and choosing between them is the caller’s to make.

Parameters

  • name (string) — The full service name, such as _imap._tcp.example.com.

Returns list — of Service

Raises NameError if the name does not exist.

Resolver.ptr()

net.resolver.Resolver.ptr(address: string) -> list

The names an address points back at.

Takes the address itself rather than a reversed name, so there is no need to build 4.3.2.1.in-addr.arpa by hand.

import net.resolver { Resolver }

echo Resolver().ptr('8.8.8.8')
# [dns.google]

Parameters

  • address (string) — An IPv4 or IPv6 address.

Returns list — of string

Raises NameError if nothing is published for the address.

Resolver.caa()

net.resolver.Resolver.caa(name: string) -> list

Which certificate authorities a domain allows to issue for it.

Parameters

  • name (string)

Returns list — of Certification

Raises NameError if the name does not exist.

Resolver.to_string()

net.resolver.Resolver.to_string()

2026, Richard Ore and Zuri contributors

net.tcp

import net

Everything here is re-exported by net, so import net is enough and the names are called as net.*. Importing net.tcp on its own works too and reaches the same definitions.

This module provides a complete implementation of the Transmission Control Protocol as specified in IETF RFC 793. It provides an interface for working with connected socket and listening socket alike.

It is meant to be used to provide more controlled and specific TCP-based operating system features and for implementing various standard and custom network protocols and specifications.

The example below shows how to use this module to create a basic HTTP client.

import net

var stream = net.TcpStream()
stream.connect('example.com:80')
stream.write_all('GET / HTTP/1.0\r\nHost: example.com\r\n\r\n')

echo stream.read_as_string()
stream.close()

Like earlier said, this module provides functionally for both connected sockets and listening sockets. A simple demonstration of a listening socket is given below which implements a basic and foundational HTTP server that recieves requests on port 8080 and returns a message containig the address of the client to the said client.

import net

var server = net.TcpStream()
server.bind('0.0.0.0:8080')
echo 'listening on ${server.local_address()}'

while true {
  var client = server.accept()
  client.write_all('hello, ${client.peer_address()}\n')
  client.close()
}

Functions

resolve()

net.resolve(address)

Resolves address - a host:port string whose host may be a name or an IP literal - into every socket address it names, in the order the resolver returned them.

A hostname routinely maps to several addresses (an IPv6 and an IPv4 one at the very least), which is why this returns a list rather than a single address. connect() does its own resolution when handed a name, so this is mainly useful when you need the concrete address first: to connect with a timeout (which requires a literal), to prefer one address family, or simply to look up what a name points at.

import net

echo net.resolve('example.com:443')
# [93.184.215.14:443]

Parameters

  • address (string|SocketAddr) — The host:port to resolve

Returns — list: one or more SocketAddr

Raises Error if the name cannot be resolved, or resolves to nothing

Classes

Shutdown

class net.Shutdown

Used with TcpStream.shutdown() to select which half (or both halves) of a full-duplex TCP connection to shut down. Shutting down a stream only affects the local socket as it is a local operation and does not send anything resembling a FIN/EOF marker to the peer beyond what the underlying platform’s shutdown(2) implementation does.

TcpStream

class net.TcpStream

A TCP socket, in either its connected-stream or listening-server role.

  • printable — has a @to_string(), so echo and print() show something useful

Note: On Unix systems, writes to the underlying socket in SOCK_STREAM mode are made with MSG_NOSIGNAL flag. This suppresses the emission of the SIGPIPE signal when writing to disconnected socket.

Note: It’s a good practice to call close() explicitly once you are done with an instance instead of relying on Zuri to release the socket for you.

Constructor

net.TcpStream(_ptr)

Returns a new instance of a TcpStream.

TcpStream.native_ptr()

net.TcpStream.native_ptr() -> Ptr

Hands out this stream’s underlying native socket handle. _ptr is private the way every field prefixed with _ is, so a sibling module like tls - which needs to take an already-connected TcpStream and wrap it, without belonging to this class or inheriting from it - has no other way to reach it.

Returns Ptr

TcpStream.connect()

net.TcpStream.connect(address, timeout)

Opens a TCP connection to a remote host, turning this instance into a connected stream.

address may be a hostname or an IP literal, combined with a port, e.g. 'example.com:80' or '127.0.0.1:8080'. A hostname may resolve to several addresses; they are tried in turn and the first one that succeeds is used, meaning this call can succeed even though some of the resolved addresses were unreachable.

Pass timeout to bound how long the connection attempt may take before giving up. When a timeout is given, address must already be a concrete ip:port literal rather than a hostname, as no DNS resolution would be performed and no multi-address fallback is guaranteed.

Parameters

  • address (string|SocketAddr) — The host:port to connect to
  • timeout (?int) — Optional milliseconds to wait before giving up; omit for an untimed connection attempt

Raises Error if resolution fails, if every resolved address refuses the connection, or (when timeout is given) if address cannot be parsed as an ip:port literal or the timeout elapses first.

Note: a timeout of 0 is the same as specifying no timeout.

TcpStream.bind()

net.TcpStream.bind(address)

Binds this instance to address and starts listening for incoming connections, turning it into a listening server.

address may be a hostname or an IP literal combined with a port, e.g. '0.0.0.0:8080' or '[::]:8080'; port 0 asks the OS to assign an available ephemeral port, which can then be discovered via local_address(). If address resolves to multiple addresses, binding is attempted against each in turn until one succeeds.

Parameters

  • address (string|SocketAddr) — The host:port to bind and listen on

Raises Error if resolution fails or every resolved address fails to bind (e.g. because it is already in use)

TcpStream.accept()

net.TcpStream.accept() -> ?TcpStream

Accepts a new incoming connection. Only valid once this instance has been bind()-ed.

Blocks until a connection arrives, and returns a fully connected TcpStream for the new client. To find out who connected, call peer_address() on it.

In non-blocking mode (see set_non_blocking()) it never blocks: it returns nil when no connection is waiting. That is an answer, not a failure, so nothing is raised for it.

The accepted stream can carry its own blocking mode, which is a platform’s default rather than this listener’s. Set it explicitly with set_non_blocking() on the returned stream if it matters.

Returns ?TcpStream — a stream connected to the newly accepted client, or nil in non-blocking mode with nobody waiting

Raises Error on an underlying I/O error, or if this instance has not been bound

TcpStream.peer_address()

net.TcpStream.peer_address() -> SocketAddr

Returns the socket address of the remote peer this stream is connected to.

Returns SocketAddr — the peer’s address

Raises Error if this instance is not a connected stream

TcpStream.local_address()

net.TcpStream.local_address() -> SocketAddr

Returns the local socket address of this instance, i.e. the address it connected from if it is a stream, or the address it is listening on if it is a listener.

This is particularly useful for discovering the actual port chosen by the OS after binding to port 0.

Returns SocketAddr — the local address

Raises Error if this instance is neither connected nor bound

TcpStream.shutdown()

net.TcpStream.shutdown(how)

Shuts down the read, write, or both halves of this connection. See Shutdown for the meaning of each value; defaults to Shutdown.BOTH when how is omitted. Only valid on a connected stream. Calling this more than once may return an error on some platforms.

Parameters

  • how (?int) — One of the Shutdown constants

Raises Error if the underlying shutdown(2) call fails, or if this instance is not a connected stream

TcpStream.set_read_timeout()

net.TcpStream.set_read_timeout(timeout)

Sets the timeout for future read()/read_exact()/read_all()/ read_as_string() calls.

Pass a number of milliseconds greater than 0 to bound how long a read may block before raising a timeout error, or pass 0 (or any negative number) to remove the timeout entirely and block indefinitely.

Parameters

  • timeout (number) — Milliseconds, or a negative number to disable the timeout

Raises Error if the platform rejects the value

Note: A set timeout is an upper bound: a future read may still return before that bound is reached, and system timers mean the actual time elapsed before a timeout error can be slightly longer than what was requested.

TcpStream.get_read_timeout()

net.TcpStream.get_read_timeout() -> number

Returns the currently configured read timeout in milliseconds, or -1 if no timeout is set (i.e. reads block indefinitely).

Returns number — milliseconds, or -1 if unset

Note: Some platforms do not provide access to the current timeout.

TcpStream.set_write_timeout()

net.TcpStream.set_write_timeout(timeout)

Sets the timeout for future write()/write_all() calls. Same rules as set_read_timeout(): a number of milliseconds greater than 0 bounds how long a write may block, 0 (or any negative number) disables the timeout.

Parameters

  • timeout (number) — Milliseconds, or a negative number to disable the timeout

Raises Error if the platform rejects the value

TcpStream.get_write_timeout()

net.TcpStream.get_write_timeout() -> number

Returns the currently configured write timeout in milliseconds, or -1 if no timeout is set (i.e. writes block indefinitely).

Returns number — milliseconds, or -1 if unset

Note: Some platforms do not provide access to the current timeout.

TcpStream.peek()

net.TcpStream.peek(length) -> bytes

Peeks at up to length bytes of incoming data without consuming it such that a subsequent read()/read_exact() call will still see the peeked bytes. Defaults to peeking a single byte when length is omitted.

Successful peeks can, and often do, return fewer bytes than length.

Parameters

  • length (number) — The maximum number of bytes to peek

Returns bytes — up to length bytes currently in the receive buffer (nil if none are available yet)

Raises Error on an underlying I/O error

TcpStream.set_nodelay()

net.TcpStream.set_nodelay(nodelay)

Enables or disables TCP_NODELAY (i.e. disables or enables Nagle’s algorithm). When nodelay is enabled, small writes are sent immediately instead of being buffered in the hope of coalescing them with subsequent writes, trading throughput for lower latency.

Parameters

  • nodelay (bool) — true to disable Nagle’s algorithm

Raises Error on an underlying I/O error

TcpStream.get_nodelay()

net.TcpStream.get_nodelay() -> bool

Returns whether TCP_NODELAY is currently enabled on this stream.

Returns bool

TcpStream.set_ttl()

net.TcpStream.set_ttl(ttl)

Sets IP_TTL (the IPv4 time-to-live / IPv6 hop limit) for packets sent on this socket. For a listener, this is the TTL that will be used by sockets it accepts.

Parameters

  • ttl (number) — The time-to-live value, 0-255

Raises Error on an underlying I/O error

TcpStream.get_ttl()

net.TcpStream.get_ttl() -> number

Returns the currently configured IP_TTL value for this socket.

Returns number

Raises Error on an underlying I/O error

TcpStream.get_error()

net.TcpStream.get_error() -> ?string

Retrieves and clears the value of the socket’s SO_ERROR option - i.e. any pending error the OS has recorded for this socket without yet surfacing through a failed read, write, or accept().

Returns ?string — a description of the pending error, or nil if there is none

Raises Error on an underlying I/O error while querying the socket

TcpStream.set_non_blocking()

net.TcpStream.set_non_blocking(nonblocking)

Puts the socket into or out of non-blocking mode. In non-blocking mode, read()/write()/accept() and friends return immediately with an error instead of blocking when the operation would otherwise wait on I/O.

Parameters

  • nonblocking (bool) — true to enable non-blocking mode

Raises Error on an underlying I/O error

TcpStream.read()

net.TcpStream.read(length) -> bytes

Reads up to length bytes from the stream into a newly allocated byte buffer, without necessarily filling it and returns the buffer. It is entirely normal for this to return fewer bytes than length; even when more data will eventually be available. So, callers that need an exact amount of data should call this in a loop or use read_exact() instead.

An empty result means the peer has closed its write half (EOF).

Parameters

  • length (number) — The maximum number of bytes to read

Returns bytes — up to length bytes read from the stream (may be empty on EOF)

Raises Error on an underlying I/O error, or if the configured read timeout elapses first

TcpStream.read_exact()

net.TcpStream.read_exact(length) -> bytes

Reads exactly length bytes from the stream, blocking (subject to the read timeout) until that many bytes have arrived.

If the stream reaches EOF before length bytes have been read, this raises an error; the contents of any bytes already read are lost.

Parameters

  • length (number) — The exact number of bytes to read

Returns bytes — exactly length bytes

Raises Error if EOF is reached early, or on any other underlying I/O error

TcpStream.read_all()

net.TcpStream.read_all() -> bytes

Reads from the stream until EOF, returning everything received as a single byte stream. Since a stream is only closed by the peer (or by calling shutdown()/close()), this will block until the connection is closed. Therefore, do not call this on a stream you expect to stay open, or set a read timeout first.

Returns bytes — every byte read up to EOF

Raises Error on an underlying I/O error

TcpStream.read_as_string()

net.TcpStream.read_as_string() -> string

Reads from the stream until EOF, decoding everything received as UTF-8 text. Like read_all(), this blocks until the peer closes the connection. Fails if the received bytes are not valid UTF-8, in which case the data already read is discarded.

Returns string — every byte read up to EOF, decoded as UTF-8

Raises Error on an underlying I/O error, or if the data is not valid UTF-8

TcpStream.write()

net.TcpStream.write(data) -> number

Writes data to the stream, returning the number of bytes actually written. A successful call may write fewer bytes than data contains. Use write_all() if you need every byte written before continuing.

Parameters

  • data (bytes|string) — The bytes (or UTF-8 text) to write

Returns number — the number of bytes actually written

Raises Error on an underlying I/O error, or if the configured write timeout elapses first

TcpStream.write_all()

net.TcpStream.write_all(data)

Writes the entirety of data to the stream, blocking (subject to the write timeout) and retrying internally until every byte has been written.

Parameters

  • data (bytes|string) — The bytes (or UTF-8 text) to write

Raises Error on an underlying I/O error, or if the configured write timeout elapses first

TcpStream.flush()

net.TcpStream.flush()

Flushes any buffered output. TCP streams are unbuffered at this layer, so this is a no-op and doesn’t currently do anything for TcpStream. This is a no-op provided purely so this class satisfies the same interface as other writable streams in the standard library.

Raises Error on an underlying I/O error

TcpStream.is_connected()

net.TcpStream.is_connected() -> bool

Whether this instance currently wraps a connected stream (i.e. connect() has succeeded and close() has not been called since).

Returns bool

TcpStream.is_bound()

net.TcpStream.is_bound() -> bool

Whether this instance currently wraps a bound listener (i.e. bind() has succeeded and close() has not been called since).

Returns bool

TcpStream.close()

net.TcpStream.close()

Flushes, shuts down, and releases the underlying socket, whether it is a connected stream or a bound listener. It is advised to call this explicitly once you are done with the socket rather than relying on the Zuri garbage collector to do it for you eventually. Safe to call more than once, and safe to call on an instance that was never connected or bound.

TcpStream.to_string()

net.TcpStream.to_string() -> string

A human readable representation of this socket, showing whichever of the connected-stream or bound-listener addresses apply.

Returns string


2026, Richard Ore and Zuri contributors

net.tls

import net

net exposes this as net.tls, so import net is enough and the names are called as net.tls.*. import net.tls reaches the same definitions directly.

Transport Layer Security over a TcpStream, built on top of rustls. TlsStream.connect()/accept() take an already-connected TcpStream and run a handshake over it, turning it into an encrypted, certificate-verified stream. That works equally well right after connect() (HTTPS, IMAPS) or after a STARTTLS-style plaintext exchange (SMTP, FTPS) - see the second example below.

A basic HTTPS-style client:

import net
import net.tls

var tcp = net.TcpStream()
tcp.connect('example.com:443')

var config = tls.TlsConfig()
var stream = tls.TlsStream.connect(tcp, config, 'example.com')

stream.write_all('GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n')
echo stream.read_as_string()
stream.close()

A STARTTLS-style upgrade looks almost identical, except the wrap happens after a round of plaintext instead of right after connect:

import net
import net.tls

var tcp = net.TcpStream()
tcp.connect('mail.example.com:587')
echo tcp.read_as_string()          # server greeting
tcp.write_all('STARTTLS\r\n')
echo tcp.read_as_string()          # server's go-ahead

var stream = tls.TlsStream.connect(tcp, tls.TlsConfig(), 'mail.example.com')

A minimal server, reusing TcpStream’s own bind/accept for the plaintext side of things:

import net
import net.tls

var config = tls.TlsConfig()
config.set_cert_chain(cert_chain_pem, private_key_pem)

var server = net.TcpStream()
server.bind('0.0.0.0:8443')

while true {
  var client = server.accept()
  var tls_client = tls.TlsStream.accept(client, config)
  tls_client.write_all('hello over TLS\n')
  tls_client.close()
}

Classes

TlsConfig

class net.tls.TlsConfig

The handful of choices a TLS handshake needs: which certificate authorities to trust, an optional certificate and private key to present (as a server, or as a client doing mutual TLS), and which protocol versions and ALPN protocols are acceptable.

A single TlsConfig can be reused for many connections - build it once and pass it to every connect()/accept() call rather than constructing a fresh one per connection.

Constructor

net.tls.TlsConfig()

Returns a new TlsConfig with sensible defaults: the bundled Mozilla root certificate list, no client/server certificate, no ALPN protocols, and both TLS 1.2 and TLS 1.3 enabled.

TlsConfig.native_ptr()

net.tls.TlsConfig.native_ptr() -> Ptr

Hands out this config’s underlying native handle, the same way TcpStream.native_ptr() does - TlsStream’s connect()/accept() need it and, like any _-prefixed field, _ptr isn’t otherwise reachable from outside this class.

Returns Ptr

TlsConfig.set_root_store()

net.tls.TlsConfig.set_root_store(mode)

Chooses where trusted root certificate authorities come from when verifying a peer’s certificate chain.

'bundled' (the default) uses a compiled-in copy of Mozilla’s root list, which is portable and doesn’t depend on anything being installed on the machine. 'native' uses the operating system’s own trust store instead, which picks up locally-installed or enterprise-managed CAs that the bundled list doesn’t know about.

Parameters

  • mode (string) — 'bundled' or 'native'

Raises Error if mode is neither, or (for 'native') if the OS trust store can’t be read

TlsConfig.add_ca_pem()

net.tls.TlsConfig.add_ca_pem(pem)

Adds one or more PEM-encoded certificates to this config’s trusted roots, on top of whatever set_root_store() already selected. Call this more than once to add several custom CAs

  • useful for trusting a self-signed development certificate or a private/internal CA alongside the normal public trust store.

Parameters

  • pem (string) — One or more PEM-encoded CA certificates

Raises Error if none of the certificates in pem could be parsed

TlsConfig.set_cert_chain()

net.tls.TlsConfig.set_cert_chain(cert_chain_pem, key_pem)

Sets the certificate chain and private key this side of the connection presents to the other. Required before TlsStream.accept() can be used with this config (a server always needs a certificate); optional for TlsStream.connect(), where it’s only needed if the server requires a client certificate (mutual TLS).

Parameters

  • cert_chain_pem (string) — The end-entity certificate followed by any intermediate certificates, all PEM-encoded
  • key_pem (string) — The PEM-encoded private key matching the end-entity certificate

Raises Error if either PEM can’t be parsed, or if the key doesn’t match the certificate

TlsConfig.require_client_cert()

net.tls.TlsConfig.require_client_cert(required)

For a server config: whether to require the connecting client to present a trusted certificate (mutual TLS). Verified against the same root store set_root_store()/add_ca_pem() configured. Has no effect on a config only ever used with TlsStream.connect().

Parameters

  • required (bool)

TlsConfig.set_alpn()

net.tls.TlsConfig.set_alpn(protocols)

Sets the list of ALPN (Application-Layer Protocol Negotiation) protocol names this side is willing to speak, in preference order, e.g. ['h2', 'http/1.1']. The negotiated protocol - if any - is available afterwards via TlsStream.alpn_protocol().

Parameters

  • protocols (list) — Protocol names, most preferred first

TlsConfig.set_versions()

net.tls.TlsConfig.set_versions(min, max)

Restricts which TLS protocol versions are acceptable. Pass '1.2' or '1.3' for either bound, or nil to leave that bound open. Both versions are enabled by default.

Parameters

  • min (?string) — The lowest acceptable version, or nil
  • max (?string) — The highest acceptable version, or nil

Raises Error if min or max is neither '1.2', '1.3', nor nil

TlsConfig.set_insecure()

net.tls.TlsConfig.set_insecure(insecure)

Disables certificate verification entirely when insecure is true - no chain-of-trust check, no hostname check, no expiry check. Handshake signatures are still verified cryptographically, so this isn’t a total bypass, just an untrusted one.

Parameters

  • insecure (bool)

Note: This exists for local development and testing against self-signed certificates. Never enable it for a connection that handles real traffic.

PeerCertificate

class net.tls.PeerCertificate

The subset of a peer’s certificate that’s useful to inspect from Zuri code, without needing a full ASN.1/X.509 library on hand. Returned by TlsStream.peer_certificate().

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

net.tls.PeerCertificate(der, subject, issuer, sans, not_before, not_after)

PeerCertificate.der()

net.tls.PeerCertificate.der() -> bytes

The raw DER-encoded certificate, exactly as the peer presented it.

Returns bytes

PeerCertificate.subject()

net.tls.PeerCertificate.subject() -> string

The certificate’s subject, as an RFC 4514-style distinguished name string, e.g. 'CN=example.com,O=Example Inc,C=US'.

Returns string

PeerCertificate.issuer()

net.tls.PeerCertificate.issuer() -> string

The certificate’s issuer, in the same distinguished-name format as subject().

Returns string

PeerCertificate.sans()

net.tls.PeerCertificate.sans() -> list

The certificate’s Subject Alternative Names - the DNS names, IP addresses, email addresses, and URIs it’s actually valid for.

Returns list — a list of strings, possibly empty

PeerCertificate.not_before()

net.tls.PeerCertificate.not_before() -> number

The start of the certificate’s validity period, as seconds since the Unix epoch (UTC).

Returns number

PeerCertificate.not_after()

net.tls.PeerCertificate.not_after() -> number

The end of the certificate’s validity period, as seconds since the Unix epoch (UTC).

Returns number

PeerCertificate.to_string()

net.tls.PeerCertificate.to_string()

TlsStream

class net.tls.TlsStream

An established TLS connection, wrapping an already-connected TcpStream. Always created by wrapping an existing stream - via connect() on the client side or accept() on the server side - never constructed directly.

  • printable — has a @to_string(), so echo and print() show something useful

Note: The wrapped TcpStream is consumed: once connect()/accept() returns, the original TcpStream instance is dead (the same way TcpStream.close() leaves it dead) and every further read/write happens through the returned TlsStream instead.

Constructor

net.tls.TlsStream(_ptr)

TlsStream.connect()

net.tls.TlsStream.connect(tcp_stream, config, server_name) -> TlsStream

Performs a client-side TLS handshake over tcp_stream, verifying the peer’s certificate against config’s trusted roots and against server_name.

tcp_stream must already be connected - to the target host for an implicit-TLS protocol like HTTPS, or to a plaintext connection that’s already had some protocol-specific greeting exchanged over it for a STARTTLS-style protocol.

Parameters

  • tcp_stream (TcpStream) — An already-connected stream; consumed by this call
  • config (TlsConfig) — The trust roots and options to handshake with
  • server_name (string) — The hostname to verify the peer’s certificate against (also sent as the SNI extension)

Returns TlsStream

Raises Error if the handshake fails, or the peer’s certificate isn’t trusted or doesn’t match server_name

TlsStream.accept()

net.tls.TlsStream.accept(tcp_stream, config) -> TlsStream

Performs a server-side TLS handshake over tcp_stream, presenting config’s certificate chain to the connecting peer.

tcp_stream must already be connected - typically the result of TcpStream.accept(), either wrapped immediately (implicit TLS) or after exchanging a protocol-specific plaintext greeting first (STARTTLS-style).

Parameters

  • tcp_stream (TcpStream) — An already-accepted stream; consumed by this call
  • config (TlsConfig) — Must have a certificate chain set via TlsConfig.set_cert_chain()

Returns TlsStream

Raises Error if the handshake fails, config has no certificate chain, or (with require_client_cert()) the client didn’t present a trusted certificate

TlsStream.native_ptr()

net.tls.TlsStream.native_ptr() -> Ptr

Hands out this stream’s underlying native handle, the same way TcpStream.native_ptr() does. net.Poller needs it in order to watch the socket underneath the TLS session, and _ptr is not otherwise reachable from outside this class.

Returns Ptr

TlsStream.read()

net.tls.TlsStream.read(length) -> bytes

Reads up to length bytes of decrypted application data. Same blocking/timeout/EOF semantics as TcpStream.read().

Parameters

  • length (number) — The maximum number of bytes to read

Returns bytes — up to length bytes (may be empty on EOF)

Raises Error on an underlying I/O or TLS record error

TlsStream.read_exact()

net.tls.TlsStream.read_exact(length) -> bytes

Reads exactly length bytes, blocking until that many bytes have arrived.

Parameters

  • length (number) — The exact number of bytes to read

Returns bytes — exactly length bytes

Raises Error if EOF is reached early, or on any other I/O or TLS record error

TlsStream.read_all()

net.tls.TlsStream.read_all() -> bytes

Reads until EOF (i.e. until the peer sends close_notify and closes the connection), returning everything received.

Returns bytes — every byte read up to EOF

Raises Error on an underlying I/O or TLS record error

TlsStream.read_as_string()

net.tls.TlsStream.read_as_string() -> string

Reads until EOF, decoding everything received as UTF-8 text.

Returns string — every byte read up to EOF, decoded as UTF-8

Raises Error on an underlying I/O error, a TLS record error, or if the data isn’t valid UTF-8

TlsStream.write()

net.tls.TlsStream.write(data) -> number

Encrypts and writes data, returning the number of plaintext bytes actually written. A successful call may write fewer bytes than data contains; use write_all() if every byte needs to go out before continuing.

Parameters

  • data (bytes|string) — The bytes (or UTF-8 text) to write

Returns number — the number of bytes actually written

Raises Error on an underlying I/O or TLS record error

TlsStream.write_all()

net.tls.TlsStream.write_all(data)

Encrypts and writes the entirety of data, blocking and retrying internally until every byte has been written.

Parameters

  • data (bytes|string) — The bytes (or UTF-8 text) to write

Raises Error on an underlying I/O or TLS record error

TlsStream.flush()

net.tls.TlsStream.flush()

Flushes any buffered ciphertext out to the underlying socket.

Raises Error on an underlying I/O error

TlsStream.shutdown()

net.tls.TlsStream.shutdown()

Sends a close_notify alert, telling the peer this side is done writing. Unlike close(), the stream stays usable afterwards for reading whatever the peer still has left to send.

Raises Error on an underlying I/O error

TlsStream.close()

net.tls.TlsStream.close()

Sends close_notify (best-effort - a peer that’s already gone won’t stop this from succeeding) and releases the underlying socket. Safe to call more than once.

TlsStream.alpn_protocol()

net.tls.TlsStream.alpn_protocol() -> ?string

The ALPN protocol negotiated during the handshake, or nil if neither side offered one or none matched.

Returns ?string

TlsStream.peer_certificate()

net.tls.TlsStream.peer_certificate() -> ?PeerCertificate

The peer’s end-entity certificate, or nil if the peer didn’t present one (only possible on the server side, when TlsConfig.require_client_cert() wasn’t set).

Returns ?PeerCertificate

TlsStream.to_string()

net.tls.TlsStream.to_string()

2026, Richard Ore and Zuri contributors

net.udp

import net

Everything here is re-exported by net, so import net is enough and the names are called as net.*. Importing net.udp on its own works too and reaches the same definitions.

This module provides a complete implementation of the User Datagram Protocol as specified in IETF RFC 768. It provides an interface for sending and receiving individual datagrams, including support for broadcast and multicast delivery.

Unlike TCP, UDP is connectionless: there is no handshake, no guarantee of delivery or ordering, and no notion of a byte stream - every send or receive operates on a single, self-contained datagram.

It is meant to be used to provide more controlled and specific UDP-based operating system features and for implementing various standard and custom network protocols and specifications.

The example below shows how to use this module for a simple request/response exchange with a remote host.

import net

var socket = net.UdpSocket()
socket.connect('example.com:9000')
socket.send('ping')

echo socket.receive(512)
socket.close()

A socket can also be bound to a local address to receive datagrams from any sender, as shown below with a basic listener on port 9000.

import net

var server = net.UdpSocket()
server.bind('0.0.0.0:9000')
echo 'listening on ${server.local_address()}'

while true {
  echo 'received: ${server.receive_from(512)}'
}

Classes

UdpSocket

class net.UdpSocket

A UDP socket, usable for both sending and receiving datagrams, either to/from a single fixed peer (after connect()) or to/from arbitrary addresses (via send_to()/receive_from()/peek_from()).

Unlike a TCP stream, connect() and bind() are not mutually exclusive here: a socket may be bound to a local address and then separately connected to a default remote peer.

  • printable — has a @to_string(), so echo and print() show something useful

Note: UdpSocket.connect() only narrows which peer send()/recv() talk to and does not perform any handshake.

Note: It’s a good practice to call close() explicitly once you are done with an instance instead of relying on Zuri to release the socket for you.

Constructor

net.UdpSocket()

Returns a new instance of a UdpSocket, backed by a fresh, unbound and unconnected native socket. Use bind() to receive datagrams on a local address, and/or connect() to fix the peer used by send()/receive().

UdpSocket.connect()

net.UdpSocket.connect(address)

Connects this socket to a remote address. This does not perform a handshake - UDP is connectionless - it simply records address as the default peer for future send()/receive()/peek() calls, and filters out datagrams arriving from any other address. Calling this again re-targets the socket at a new peer.

address may be a hostname or an IP literal, combined with a port, e.g. 'example.com:9000' or '127.0.0.1:9000'. A hostname may resolve to several addresses; they are tried in turn and the first one that succeeds is used.

Parameters

  • address (string|SocketAddr) — The host:port to treat as the default peer

Raises Error if resolution fails or every resolved address is rejected

UdpSocket.bind()

net.UdpSocket.bind(address)

Binds this socket to address, allowing it to receive datagrams sent to that address.

address may be a hostname or an IP literal combined with a port, e.g. '0.0.0.0:9000' or '[::]:9000'; port 0 asks the OS to assign an available ephemeral port, which can then be discovered via local_address(). If address resolves to multiple addresses, binding is attempted against each in turn until one succeeds.

Parameters

  • address (string|SocketAddr) — The host:port to bind and receive on

Raises Error if resolution fails or every resolved address fails to bind (e.g. because it is already in use)

UdpSocket.peer_address()

net.UdpSocket.peer_address() -> SocketAddr

Returns the address of the remote peer this socket was connected to via connect().

Returns SocketAddr — the peer’s address

Raises Error if this socket has not been connected

UdpSocket.local_address()

net.UdpSocket.local_address() -> SocketAddr

Returns the local socket address of this instance. Particularly useful for discovering the actual port chosen by the OS after binding to port 0.

Returns SocketAddr — the local address

Raises Error if this socket has not been bound

UdpSocket.set_read_timeout()

net.UdpSocket.set_read_timeout(timeout)

Sets the timeout for future receive()/peek() calls.

Pass a number of milliseconds greater than 0 to bound how long a read may block before raising a timeout error, or pass 0 (or any negative number) to remove the timeout entirely and block indefinitely.

Parameters

  • timeout (number) — Milliseconds, or a negative number to disable the timeout

Raises Error if the platform rejects the value

Note: Some platforms do not provide access to the current timeout.

UdpSocket.get_read_timeout()

net.UdpSocket.get_read_timeout() -> number

Returns the currently configured read timeout in milliseconds, or -1 if no timeout is set (i.e. reads block indefinitely).

Returns number — milliseconds, or -1 if unset

UdpSocket.set_write_timeout()

net.UdpSocket.set_write_timeout(timeout)

Sets the timeout for future send() calls. Same rules as set_read_timeout(): a number of milliseconds greater than 0 bounds how long a send may block, 0 (or any negative number) disables the timeout.

Parameters

  • timeout (number) — Milliseconds, or a negative number to disable the timeout

Raises Error if the platform rejects the value

Note: A send only blocks in the first place when the OS’s outgoing socket buffer is full, which is uncommon for datagram sockets.

UdpSocket.get_write_timeout()

net.UdpSocket.get_write_timeout() -> number

Returns the currently configured write timeout in milliseconds, or -1 if no timeout is set (i.e. sends block indefinitely).

Returns number — milliseconds, or -1 if unset

UdpSocket.peek()

net.UdpSocket.peek(length) -> bytes

Peeks at the next incoming datagram from the connected peer without removing it from the socket’s receive queue - a subsequent receive()/peek() call will still see it. Defaults to peeking a single byte when length is omitted.

Requires a connected socket, since (like receive()) it only reads from the peer set via connect(). Use peek_from() on a socket that is only bound.

Parameters

  • length (number) — The maximum number of bytes to peek

Returns bytes — up to length bytes of the pending datagram

Raises Error on an underlying I/O error, or if this socket has not been connected

UdpSocket.peek_from()

net.UdpSocket.peek_from(length) -> data: bytes, address: SocketAddr

Peeks at the next incoming datagram from any sender without removing it from the socket’s receive queue - a subsequent receive_from()/peek_from() call will still see it. Defaults to peeking a single byte when length is omitted. Unlike peek(), this does not require a connected socket.

Parameters

  • length (number) — The maximum number of bytes to peek

Returns data: bytes, address: SocketAddr — the pending datagram’s bytes and the address it was sent from

Raises Error on an underlying I/O error

UdpSocket.set_ttl()

net.UdpSocket.set_ttl(ttl)

Sets IP_TTL (the IPv4 time-to-live / IPv6 hop limit) used for regular, non-multicast datagrams sent on this socket.

Parameters

  • ttl (number) — The time-to-live value, 0-255

Raises Error on an underlying I/O error

UdpSocket.get_ttl()

net.UdpSocket.get_ttl() -> number

Returns the currently configured IP_TTL value for this socket.

Returns number

Raises Error on an underlying I/O error

UdpSocket.get_error()

net.UdpSocket.get_error() -> ?string

Retrieves and clears the value of the socket’s SO_ERROR option - i.e. any pending error the OS has recorded for this socket without yet surfacing through a failed send or receive.

Returns ?string — a description of the pending error, or nil if there is none

Raises Error on an underlying I/O error while querying the socket

UdpSocket.set_non_blocking()

net.UdpSocket.set_non_blocking(nonblocking)

Puts the socket into or out of non-blocking mode. In non-blocking mode, receive()/receive_from()/send() and friends return immediately with an error instead of blocking when the operation would otherwise wait on I/O.

Parameters

  • nonblocking (bool) — true to enable non-blocking mode

Raises Error on an underlying I/O error

UdpSocket.set_broadcast()

net.UdpSocket.set_broadcast(broadcast)

Enables or disables SO_BROADCAST. This must be enabled before send_to() can deliver a datagram to a broadcast address, such as 255.255.255.255.

Parameters

  • broadcast (bool) — true to allow sending to broadcast addresses

Raises Error on an underlying I/O error

UdpSocket.get_broadcast()

net.UdpSocket.get_broadcast() -> bool

Returns whether SO_BROADCAST is currently enabled on this socket.

Returns bool

UdpSocket.set_multicast_loop_v4()

net.UdpSocket.set_multicast_loop_v4(loop)

Enables or disables IP_MULTICAST_LOOP for IPv4 multicast. When enabled (the default), multicast datagrams sent from this socket are also looped back and delivered to this same host if it has joined the destination group.

Parameters

  • loop (bool) — true to loop sent multicast datagrams back

Raises Error on an underlying I/O error

UdpSocket.get_multicast_loop_v4()

net.UdpSocket.get_multicast_loop_v4() -> bool

Returns whether IP_MULTICAST_LOOP is currently enabled for IPv4 multicast on this socket.

Returns bool

UdpSocket.set_multicast_ttl_v4()

net.UdpSocket.set_multicast_ttl_v4(ttl)

Sets IP_MULTICAST_TTL, the time-to-live used specifically for outgoing IPv4 multicast datagrams sent on this socket, independent of the regular IP_TTL set via set_ttl(). Defaults to 1, restricting multicast datagrams to the local network.

Parameters

  • ttl (number) — The multicast time-to-live value, 0-255

Raises Error on an underlying I/O error

UdpSocket.get_multicast_ttl_v4()

net.UdpSocket.get_multicast_ttl_v4() -> number

Returns the currently configured IP_MULTICAST_TTL value for IPv4 multicast on this socket.

Returns number

Raises Error on an underlying I/O error

UdpSocket.set_multicast_loop_v6()

net.UdpSocket.set_multicast_loop_v6(loop)

Enables or disables IPV6_MULTICAST_LOOP for IPv6 multicast. When enabled (the default), multicast datagrams sent from this socket are also looped back and delivered to this same host if it has joined the destination group.

Parameters

  • loop (bool) — true to loop sent multicast datagrams back

Raises Error on an underlying I/O error

UdpSocket.get_multicast_loop_v6()

net.UdpSocket.get_multicast_loop_v6() -> bool

Returns whether IPV6_MULTICAST_LOOP is currently enabled for IPv6 multicast on this socket.

Returns bool

UdpSocket.receive()

net.UdpSocket.receive(length) -> bytes

Receives a single datagram from the connected peer into a newly allocated byte buffer of up to length bytes. Requires a connected socket, since (like peek()) it only reads from the peer set via connect(). Use receive_from() on a socket that is only bound.

An empty result means an empty (zero-length) datagram was received, which is valid and distinct from there being nothing to receive.

Parameters

  • length (number) — The maximum number of bytes to read

Returns bytes — up to length bytes of the received datagram

Raises Error on an underlying I/O error, or if the configured read timeout elapses first

Note: A datagram read is all-or-nothing per message: if the incoming datagram is larger than length, the excess bytes are discarded rather than being returned on a subsequent call - this is unlike a TCP stream, where a short read just leaves the rest to be read later.

UdpSocket.receive_from()

net.UdpSocket.receive_from(length) -> bytes

Receives a single datagram from any sender into a newly allocated byte buffer of length bytes. Does not require a connected socket.

Parameters

  • length (number) — The size of the buffer to read into

Returns bytes — a length-byte buffer containing the received datagram

Raises Error on an underlying I/O error, or if the configured read timeout elapses first

Note: As with receive(), if the incoming datagram is larger than length, the excess bytes are discarded.

UdpSocket.send()

net.UdpSocket.send(data) -> number

Sends data as a single datagram to the connected peer. Requires a connected socket. Unlike a TCP stream’s write(), a successful call always sends the entirety of data as one datagram - there is no concept of a partial send here, though very large datagrams may be rejected outright depending on the platform and network path.

Parameters

  • data (bytes|string) — The bytes (or UTF-8 text) to send

Returns number — the number of bytes sent (equal to the size of data on success)

Raises Error on an underlying I/O error, or if the configured write timeout elapses first

UdpSocket.send_to()

net.UdpSocket.send_to(data, address) -> number

Sends data as a single datagram to address. Does not require a connected socket, and does not change the peer set by connect(), if any.

Parameters

  • data (bytes|string) — The bytes (or UTF-8 text) to send
  • address (string|SocketAddr) — The host:port to send the datagram to

Returns number — the number of bytes sent (equal to the size of data on success)

Raises Error on an underlying I/O error, or if the configured write timeout elapses first

UdpSocket.join_multicast_v4()

net.UdpSocket.join_multicast_v4(multiaddr, interface)

Joins the IPv4 multicast group multiaddr on the network interface identified by interface.

Parameters

  • multiaddr (string) — The multicast address to join, e.g. '239.0.0.1'
  • interface (string) — The local IPv4 address of the interface to join on, e.g. '0.0.0.0' for the default interface

Raises Error if multiaddr/interface cannot be parsed as IPv4 addresses, or on an underlying I/O error

UdpSocket.leave_multicast_v4()

net.UdpSocket.leave_multicast_v4(multiaddr, interface)

Leaves the IPv4 multicast group multiaddr on the network interface identified by interface. Both arguments must match the values originally passed to join_multicast_v4().

Parameters

  • multiaddr (string) — The multicast address to leave
  • interface (string) — The local IPv4 address of the interface to leave on

Raises Error if multiaddr/interface cannot be parsed as IPv4 addresses, or on an underlying I/O error

UdpSocket.join_multicast_v6()

net.UdpSocket.join_multicast_v6(multiaddr, interface)

Joins the IPv6 multicast group multiaddr on the network interface identified by interface.

Parameters

  • multiaddr (string) — The multicast address to join, e.g. 'ff02::1'
  • interface (number) — The index of the local interface to join on, or 0 to let the OS choose the default interface

Raises Error if multiaddr cannot be parsed as an IPv6 address, or on an underlying I/O error

UdpSocket.leave_multicast_v6()

net.UdpSocket.leave_multicast_v6(multiaddr, interface)

Leaves the IPv6 multicast group multiaddr on the network interface identified by interface. Both arguments must match the values originally passed to join_multicast_v6().

Parameters

  • multiaddr (string) — The multicast address to leave
  • interface (number) — The index of the local interface to leave on

Raises Error if multiaddr cannot be parsed as an IPv6 address, or on an underlying I/O error

UdpSocket.close()

net.UdpSocket.close()

Releases the underlying socket. It is advised to call this explicitly once you are done with the socket rather than relying on the Zuri garbage collector to do it for you eventually. Safe to call more than once, and safe to call on an instance that was never bound or connected.

UdpSocket.to_string()

net.UdpSocket.to_string() -> string

A human readable representation of this socket, showing whichever of the bound-local or connected-peer addresses apply.

Returns string


2026, Richard Ore and Zuri contributors

net.unix

import net

Everything here is re-exported by net, so import net is enough and the names are called as net.*. Importing net.unix on its own works too and reaches the same definitions.

Unix domain sockets: the same stream interface as tcp, over a path on the filesystem rather than an address on the network.

A unix socket is how two programs on one machine usually talk when one of them is a server. It skips the network stack, so there is no port to collide with and nothing to reach it from another host. And because the socket is a file, the filesystem decides who may connect: a socket in a directory only one user can enter is reachable by only that user, which is a stronger answer than binding to loopback and trusting everyone on the machine.

import net

var client = net.UnixStream()
client.connect('/var/run/mysqld/mysqld.sock')
client.write_all('hello')

echo client.read(5)
client.close()

A server binds a path and accepts on it:

import net
import os

var server = net.UnixStream()
server.bind('/run/app.sock')

while true {
  var client = server.accept()
  client.write_all('hello\n')
  client.close()
}

Binding leaves a file behind

bind() creates the path and refuses if something is already there, including a socket left by a process that has since died. Closing the socket frees the descriptor but does not remove the file, because by then another process may have bound the same path and removing it would break them. A server that expects to be restarted deletes a stale path itself before binding.

Windows

Windows has unix sockets, but they are not reachable from Zuri. Every call here raises there rather than the module being absent, so a program fails where it tries to connect, saying why, instead of failing at import. is_supported() answers the question in advance.

Functions

is_supported()

net.is_supported() -> bool

Whether unix domain sockets work on this machine.

False on Windows and true everywhere else. A program that can fall back to TCP tests this rather than catching the error.

Returns bool

pair()

net.pair() -> list[UnixStream]

Two sockets already connected to each other, with no path and nothing on the filesystem.

This is the way to hand one end to a child process, or to talk between threads over a real socket without choosing a name that something else might collide with.

Returns list[UnixStream] — The two ends, which are interchangeable.

Raises Error if the pair cannot be created.

Classes

UnixStream

class net.UnixStream

A unix domain socket, connected or listening.

One class covers both, as TcpStream does: connect() makes it a stream and bind() makes it a listener, and the methods that do not apply raise rather than doing something surprising.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

net.UnixStream(_ptr)

Parameters

  • _ptr (Ptr|nil) — An existing native handle, as accept() and pair() produce. A new socket when nil.

UnixStream.native_ptr()

net.UnixStream.native_ptr() -> Ptr

Hands out the underlying native handle, the same way TcpStream does, so a sibling module can wrap an already-connected socket without belonging to this class.

Returns Ptr

UnixStream.connect()

net.UnixStream.connect(path: string)

Connects to the socket at path.

There is no timeout: a unix socket connect either succeeds at once or fails at once, since there is no network in between. Use set_read_timeout() and set_write_timeout() to bound the conversation that follows.

Parameters

  • path (string)

Raises Error if nothing is listening there, if the path does not exist, or if this process may not open it.

UnixStream.bind()

net.UnixStream.bind(path: string)

Creates the socket at path and starts listening on it.

Parameters

  • path (string)

Raises Error if something already exists at that path, including a socket a dead process left behind, or if the directory is not writable.

UnixStream.accept()

net.UnixStream.accept() -> ?UnixStream

Waits for a connection and returns it.

Blocks until someone connects. In non-blocking mode (see set_non_blocking()) it never blocks: it returns nil when nobody is waiting, which is an answer rather than a failure.

The accepted stream can carry its own blocking mode, which is a platform’s default rather than this listener’s. Set it explicitly with set_non_blocking() on the returned stream if it matters.

Returns ?UnixStream — The connected end of the new conversation, or nil in non-blocking mode with nobody waiting.

Raises Error if this socket is not listening.

UnixStream.peer_address()

net.UnixStream.peer_address() -> string|nil

The path the other end is bound to.

nil for a socket that has no path, which is what both ends of a pair() are and what a client that never bound one is.

Returns string|nil

UnixStream.local_address()

net.UnixStream.local_address() -> string|nil

The path this socket is bound to, or nil where it has none.

Returns string|nil

UnixStream.shutdown()

net.UnixStream.shutdown()

Closes both directions without releasing the handle, so a read at the other end returns nothing rather than blocking.

Raises Error if this socket is not connected.

UnixStream.set_read_timeout()

net.UnixStream.set_read_timeout(timeout: number)

Gives up on a read that takes longer than timeout.

Parameters

  • timeout (number) — Milliseconds. Zero means wait forever.

UnixStream.get_read_timeout()

net.UnixStream.get_read_timeout() -> number

The read timeout in milliseconds, zero for none.

Returns number

UnixStream.set_write_timeout()

net.UnixStream.set_write_timeout(timeout: number)

Gives up on a write that takes longer than timeout.

Parameters

  • timeout (number) — Milliseconds. Zero means wait forever.

UnixStream.get_write_timeout()

net.UnixStream.get_write_timeout() -> number

The write timeout in milliseconds, zero for none.

Returns number

UnixStream.set_non_blocking()

net.UnixStream.set_non_blocking(nonblocking: bool)

Whether reads and writes return at once rather than waiting.

A non-blocking socket raises where it would have blocked, which is what net.poll is for: wait there, then read here.

Parameters

  • nonblocking (bool)

UnixStream.read()

net.UnixStream.read(length: number) -> bytes

Reads up to length bytes, returning what arrived.

A shorter result than asked for is normal and does not mean the conversation is over; an empty one does.

Parameters

  • length (number)

Returns bytes

UnixStream.read_exact()

net.UnixStream.read_exact(length: number) -> bytes

Reads exactly length bytes, waiting for as many as it takes.

Parameters

  • length (number)

Returns bytes

Raises Error if the other end closes before that many arrive.

UnixStream.read_all()

net.UnixStream.read_all() -> bytes

Reads until the other end closes.

Returns bytes

UnixStream.read_as_string()

net.UnixStream.read_as_string() -> string

Reads until the other end closes and decodes the result as text.

Returns string

Raises Error if what arrived is not valid UTF-8.

UnixStream.write()

net.UnixStream.write(data) -> number

Writes what it can and reports how much that was.

Parameters

  • data (bytes|string)

Returns number — Bytes written, which may be fewer than given.

UnixStream.write_all()

net.UnixStream.write_all(data)

Writes everything, however many attempts that takes.

Parameters

  • data (bytes|string)

UnixStream.flush()

net.UnixStream.flush()

Pushes out anything held back.

UnixStream.is_connected()

net.UnixStream.is_connected() -> bool

Whether this socket is connected to another.

Returns bool

UnixStream.is_bound()

net.UnixStream.is_bound() -> bool

Whether this socket is listening on a path.

Returns bool

UnixStream.close()

net.UnixStream.close()

Closes the socket. The handle cannot be used afterwards.

A path this socket was bound to stays on the filesystem, since by now another process may have bound it.

UnixStream.to_string()

net.UnixStream.to_string()

2026, Richard Ore and Zuri contributors

http

import http

A complete HTTP stack: a client, a server, and the pieces both are built from.

The server is meant to face the internet directly. Everything that is normally the reverse proxy’s job - TLS, static files with byte ranges and conditional requests, response compression, keep-alive, request size limits, timeouts, forwarding-header handling - is here rather than assumed to be somewhere in front.

Making a request

import http

echo http.get('https://example.com').as_text()

The module-level get(), post(), put(), patch(), delete(), head(), options() and trace() all go through one shared client, which keeps its connections open between calls. For anything that needs its own settings - a base URL, an authorization header, a cookie jar, a custom certificate authority

  • build a client of your own:
var api = http.client('https://api.example.com', {
  headers: { 'Authorization': 'Bearer ' + token },
})

var user = api.get('/users/me').raise_for_status().as_dict()
api.post('/posts', { title: 'Hello', body: 'World' })

Serving requests

import http

var server = http.server(3000)

server.get('/', @(request, response) {
  response.html('<h1>Hello</h1>')
})

server.get('/users/:id', @(request, response) {
  response.json({ id: request.param('id') })
})

server.serve_files('/static', './public', { cache_age: 86400 })

server.listen()

Routes match literal segments, :name parameters, and a trailing catch-all, with the most specific pattern winning regardless of registration order. use() adds middleware, which runs outermost first and controls whether the rest of the chain runs at all:

server.use(@(request, response, next) {
  if request.header('x-api-key') != key {
    response.json({ error: 'unauthorized' }, 401)
    return
  }
  next()
})

Serving over TLS

var server = http.server(443, '0.0.0.0')
server.load_certs('/etc/certs/site.crt', '/etc/certs/site.key')
server.listen()

Using more than one core

listen() serves connections on the calling thread. serve() runs the same pipeline across a pool of isolates instead, one accept loop handing connections to workers:

# app.zu
import http

def setup(server) {
  server.get('/', @(request, response) {
    response.text('hello from a worker')
  })
}
# main.zu
import http
import .app

http.serve(app.setup, { port: 3000, workers: 8 })

setup runs inside each worker, so everything it reaches for has to be something an isolate can be handed. A module is not: a setup that names an imported module belongs in a module itself, which the isolate resolves by name and whose own imports are resolved again on that side.

What the module handles for you

Requests are parsed strictly: a header name with whitespace before its colon, two disagreeing Content-Length values, a Transfer-Encoding alongside a Content-Length, an obsolete folded header line - each of these is a way to make a proxy and an origin server disagree about where one message ends and the next begins, and each is refused rather than guessed at.

Bodies are decompressed on the way in and compressed on the way out, cookies are parsed and rendered per RFC 6265, dates are read in all three formats HTTP allows and written in the one it requires, and content negotiation follows the quality weights the client actually sent.

Sessions

http.session keeps per-visitor state on the server and finds it again by a cookie:

import http.session

server.use(http.session.session())

server.get('/', @(request, response) {
  var seen = request.session().get('seen', 0)

  request.session().set('seen', seen + 1)
  response.text('visit ${seen + 1}')
})

Sessions are kept in files by default, in a database through http.session.sql, or anywhere else a SessionStore can reach.

The http API

Every public name in http, wherever it is declared. Each links to the page that documents it.

NameKindSummary
http.BodyReaderclassReads a message body off a connection according to whichever of HTTP/1.1’s framings applies.
http.ByteRangeclassOne byte range asked for by a Range header, already resolved against the size of the representation.
http.ConnectionclassA buffered, message-oriented view of a connected socket.
http.ConnectionErrorclassRaised when a connection could not be established, or when an established connection failed or was closed…
http.CookieclassA single cookie, in either direction: the name/value pair a client sends back in a Cookie header, or the…
http.CookieJarclassA client-side cookie store: keeps the cookies a server set, decides which of them a later request is entitled…
http.HeadersclassAn ordered, case-insensitive, multi-value collection of HTTP header fields.
http.HttpClientclassAn HTTP client.
http.HttpErrorclassBase class for every error the http module raises.
http.HttpRequestclassAn HTTP request, in both directions.
http.HttpResponseclassAn HTTP response, in both directions: the thing a server builds and sends, and the thing a client receives…
http.HttpServerclassAn HTTP/1.1 server.
http.LoadBalancerclassBalances requests across several upstreams.
http.MultipartBuilderclassBuilds a multipart/form-data request body.
http.MultipartDataclassThe result of parsing a multipart/form-data body.
http.ProtocolErrorclassRaised when a peer sends something that isn’t a well-formed HTTP message: a broken request line or status…
http.ReverseProxyclassA reverse proxy: takes a request this server received and passes it to an upstream, then passes the…
http.RouteclassOne registered route.
http.RouterclassMatches request paths to handlers.
http.StaticFilesclassServes files from a directory, with the conditional-request, range-request and caching behaviour a browser…
http.StatusErrorclassRaised by HttpResponse.raise_for_status() when the response carries a 4xx or 5xx status.
http.TimeoutErrorclassRaised when an operation exceeded its configured deadline: a connect, a read, a write, or the total time…
http.TooLargeErrorclassRaised when a message exceeds one of the configured size limits - request line, header block, or body.
http.TooManyRedirectsErrorclassRaised by the client when a redirect chain exceeds HttpClient.max_redirects, which usually means the chain…
http.UnsupportedProtocolErrorclassRaised when a URL names a scheme this module cannot speak, or when a peer insists on a protocol version that…
http.UploadedFileclassOne file received in a multipart/form-data body.
http.WebSocketclassAn open WebSocket connection (RFC 6455).
http.body.DEFAULT_BROTLI_QUALITYconstantThe brotli quality this module compresses a response with when the caller names no quality of its own.
http.body.decode_contentfunctionReverses the content codings named in a Content-Encoding field, innermost last, as RFC 9110 §8.4 requires.
http.body.encode_chunkfunctionWraps data as a single HTTP/1.1 chunk: the size in hexadecimal, a CRLF, the data, and a CRLF.
http.body.encode_contentfunctionApplies a content coding to data.
http.body.encode_last_chunkfunctionThe terminating 0\r\n chunk plus a trailer section.
http.body.parse_chunk_sizefunctionParses a chunk-size line, ignoring any chunk extensions after the ;.
http.body.parse_content_lengthfunctionParses a Content-Length field value.
http.body.reader_forfunctionWorks out how a message’s body is framed and returns a reader for it.
http.build_query_stringfunctionEncodes parameters as an application/x-www-form-urlencoded string.
http.clientfunctionBuilds a new HttpClient.
http.cookies.format_cookie_headerfunctionRenders a dictionary of name to value as a request Cookie header value.
http.cookies.is_valid_valuefunctionWhether value can be sent as a cookie value without quoting.
http.cookies.parse_cookie_headerfunctionParses a request’s Cookie header into a dictionary of name to value.
http.cookies.parse_set_cookiefunctionParses one Set-Cookie field value into a Cookie.
http.deletefunctionSends a DELETE request through the shared client.
http.files.etag_matchesfunctionWhether an If-None-Match value matches etag.
http.files.parse_rangefunctionParses a Range header against a representation of size bytes.
http.files.weak_etagfunctionA weak validator derived from a file’s size and modification time.
http.getfunctionSends a GET request through the shared client.
http.h1.ChunkWriterclassThe writer handed to a streaming response body.
http.h1.DEFAULT_LIMITSconstant
http.h1.SERVER_NAMEconstant
http.h1.USER_AGENTconstant
http.h1.parse_versionfunctionParses HTTP/1.1 into '1.1', rejecting anything that is not a version this module understands the framing…
http.h1.read_requestfunctionReads one request off a connection: the request line, the header section, and enough framing information to…
http.h1.read_responsefunctionReads a response off a connection.
http.h1.send_continuefunctionSends a 100 Continue interim response, telling a client that asked with Expect: 100-continue to go ahead…
http.h1.should_keep_alivefunctionWhether the connection should be kept open after this exchange.
http.h1.write_requestfunctionWrites a request to a connection.
http.h1.write_responsefunctionWrites a response to a connection and returns whether the connection is still usable afterwards.
http.h2.Http2ConnectionclassAn HTTP/2 connection, in either role.
http.h2.Http2StreamclassOne HTTP/2 stream: a request and its response, multiplexed with others over a single connection.
http.h2.Http2WriterclassThe writer a streaming response body gets on an HTTP/2 connection.
http.h2.frames.CANCELconstantCANCEL.
http.h2.frames.COMPRESSION_ERRORconstantCOMPRESSION_ERROR.
http.h2.frames.CONNECT_ERRORconstantCONNECT_ERROR.
http.h2.frames.CONTINUATIONconstantCONTINUATION frame.
http.h2.frames.DATAconstantDATA frame.
http.h2.frames.ENHANCE_YOUR_CALMconstantENHANCE_YOUR_CALM.
http.h2.frames.FLAG_ACKconstantACK flag, on SETTINGS and PING.
http.h2.frames.FLAG_END_HEADERSconstantEND_HEADERS flag, on HEADERS, PUSH_PROMISE and CONTINUATION.
http.h2.frames.FLAG_END_STREAMconstantEND_STREAM flag, on DATA and HEADERS.
http.h2.frames.FLAG_PADDEDconstantPADDED flag, on DATA, HEADERS and PUSH_PROMISE.
http.h2.frames.FLAG_PRIORITYconstantPRIORITY flag, on HEADERS.
http.h2.frames.FLOW_CONTROL_ERRORconstantFLOW_CONTROL_ERROR.
http.h2.frames.FRAME_SIZE_ERRORconstantFRAME_SIZE_ERROR.
http.h2.frames.FrameclassOne frame, as read off the wire.
http.h2.frames.GOAWAYconstantGOAWAY frame.
http.h2.frames.HEADERSconstantHEADERS frame.
http.h2.frames.HTTP_1_1_REQUIREDconstantHTTP_1_1_REQUIRED.
http.h2.frames.INADEQUATE_SECURITYconstantINADEQUATE_SECURITY.
http.h2.frames.INTERNAL_ERRORconstantINTERNAL_ERROR.
http.h2.frames.NO_ERRORconstantNO_ERROR.
http.h2.frames.PINGconstantPING frame.
http.h2.frames.PREFACEconstantThe connection preface every HTTP/2 client sends before anything else.
http.h2.frames.PRIORITYconstantPRIORITY frame; deprecated by RFC 9113 and ignored here.
http.h2.frames.PROTOCOL_ERRORconstantPROTOCOL_ERROR.
http.h2.frames.PUSH_PROMISEconstantPUSH_PROMISE frame.
http.h2.frames.REFUSED_STREAMconstantREFUSED_STREAM.
http.h2.frames.RST_STREAMconstantRST_STREAM frame.
http.h2.frames.SETTINGSconstantSETTINGS frame.
http.h2.frames.SETTINGS_ENABLE_PUSHconstantSETTINGS_ENABLE_PUSH.
http.h2.frames.SETTINGS_HEADER_TABLE_SIZEconstantSETTINGS_HEADER_TABLE_SIZE.
http.h2.frames.SETTINGS_INITIAL_WINDOW_SIZEconstantSETTINGS_INITIAL_WINDOW_SIZE.
http.h2.frames.SETTINGS_MAX_CONCURRENT_STREAMSconstantSETTINGS_MAX_CONCURRENT_STREAMS.
http.h2.frames.SETTINGS_MAX_FRAME_SIZEconstantSETTINGS_MAX_FRAME_SIZE.
http.h2.frames.SETTINGS_MAX_HEADER_LIST_SIZEconstantSETTINGS_MAX_HEADER_LIST_SIZE.
http.h2.frames.SETTINGS_TIMEOUTconstantSETTINGS_TIMEOUT.
http.h2.frames.STREAM_CLOSEDconstantSTREAM_CLOSED.
http.h2.frames.WINDOW_UPDATEconstantWINDOW_UPDATE frame.
http.h2.frames.decode_settingsfunctionParses a SETTINGS payload into a dictionary.
http.h2.frames.encode_errorfunctionBuilds an RST_STREAM payload.
http.h2.frames.encode_goawayfunctionBuilds a GOAWAY payload.
http.h2.frames.encode_settingsfunctionBuilds a SETTINGS payload from a dictionary of identifier to value.
http.h2.frames.read_framefunctionReads one frame off a connection.
http.h2.frames.read_u24functionReads a 24-bit big-endian integer from data at offset.
http.h2.frames.read_u32functionReads a 32-bit big-endian integer from data at offset.
http.h2.frames.write_framefunctionWrites one frame to a connection.
http.h2.frames.write_u32functionAppends a 32-bit big-endian integer to out.
http.h2.hpack.DecoderclassDecodes header blocks for one direction of one connection.
http.h2.hpack.DynamicTableclassThe dynamic table half of an HPACK context: the most recently inserted fields, evicted from the far end when…
http.h2.hpack.EncoderclassEncodes header blocks for one direction of one connection.
http.h2.hpack.decode_integerfunctionDecodes an integer with an prefix_bits-wide prefix, starting at offset.
http.h2.hpack.decode_stringfunctionDecodes a string literal starting at offset.
http.h2.hpack.encode_integerfunctionEncodes an integer with an prefix_bits-wide prefix, per RFC 7541 §5.1.
http.h2.hpack.encode_stringfunctionEncodes a string literal, Huffman-coding it when that comes out shorter.
http.h2.huffman.EOSconstantThe number of the symbol HPACK uses to pad the final byte of a Huffman-encoded string, and which must never…
http.h2.huffman.decodefunctionDecodes a Huffman-encoded string.
http.h2.huffman.encodefunctionHuffman-encodes data, padding the final byte with the leading bits of the EOS code (which are all ones) as…
http.h2.huffman.encoded_lengthfunctionThe number of bytes data would occupy once Huffman-encoded.
http.headfunctionSends a HEAD request through the shared client.
http.headers.canonical_namefunctionThe conventional spelling of a field name, e.g. 'content-type' becomes 'Content-Type' and 'etag'…
http.headers.is_never_foldedfunctionWhether a field with this name must be repeated rather than folded into one comma-separated value when…
http.headers.is_valid_namefunctionWhether name is a syntactically valid HTTP field name, i.e. a non-empty RFC 9110 token.
http.headers.is_valid_valuefunctionWhether value is a legal HTTP field value.
http.headers.parsefunctionParses a raw header block - everything between a start line and the blank line that ends the head - into a…
http.middleware.basic_authfunctionRequires HTTP Basic authentication.
http.middleware.bearer_authfunctionRequires a bearer token.
http.middleware.corsfunctionAnswers CORS preflights and adds the cross-origin headers a browser needs before it will let script read a…
http.middleware.etagfunctionComputes a weak ETag over a finished response body and answers 304 Not Modified when the client already…
http.middleware.force_httpsfunctionSends every request that arrived over cleartext to the same URL over HTTPS.
http.middleware.jwt_authfunctionRequires a valid JSON Web Token, verified by the jwt module.
http.middleware.loggerfunctionWrites one line per request once the response is finished.
http.middleware.parse_basicfunctionParses an HTTP Basic Authorization header into a username and password.
http.middleware.parse_bearerfunctionParses a Bearer Authorization header into its token.
http.middleware.rate_limitfunctionLimits how many requests one client may make in a window of time.
http.middleware.request_idfunctionAttaches a unique identifier to every request, echoing back one the client supplied so a trace can be…
http.middleware.security_headersfunctionAdds the response headers a browser acts on to harden a page.
http.multipart.parsefunctionParses a multipart/form-data body (RFC 7578).
http.negotiate.AcceptEntryclassOne entry of an Accept-style header: the value, its quality weight, and any other parameters it carried.
http.negotiate.best_matchfunctionPicks the entry of available the client would most like, or nil when it would accept none of them.
http.negotiate.names_explicitlyfunctionWhether header names value outright rather than covering it with a wildcard.
http.negotiate.parse_acceptfunctionParses an Accept, Accept-Encoding, Accept-Language or Accept-Charset header into entries, most…
http.negotiate.preferred_encodingfunctionPicks a content coding for a response, given the request’s Accept-Encoding.
http.negotiate.preferred_languagefunctionPicks a language from available using the request’s Accept-Language.
http.negotiate.quality_offunctionThe quality weight header assigns to candidate, honouring wildcards.
http.optionsfunctionSends an OPTIONS request through the shared client.
http.parse_query_stringfunctionDecodes an application/x-www-form-urlencoded string - a query string, or a form body - into `name ->…
http.patchfunctionSends a PATCH request through the shared client.
http.postfunctionSends a POST request through the shared client.
http.putfunctionSends a PUT request through the shared client.
http.router.RouteMatchclassThe result of asking a router about a request.
http.servefunctionRuns a server across a pool of isolates, one per core by default.
http.serverfunctionBuilds an HttpServer.
http.session.FORMATconstantThe payload format this module writes and reads.
http.session.FileStoreclassKeeps each session in its own file, named after the session’s storage key.
http.session.ID_LENGTHconstantHow many characters a session identifier has.
http.session.MemoryStoreclassKeeps sessions in a dictionary, for as long as the isolate that made the store lives.
http.session.SessionclassOne visitor’s session.
http.session.SessionErrorclassRaised when a session cannot be read, written or configured: a storage directory that cannot be created or is…
http.session.SessionStoreclassWhat every session store implements.
http.session.default_directoryfunctionThe directory sessions are kept in when a FileStore is not told where to put them: a private subdirectory…
http.session.file.DIRECTORY_MODEconstant
http.session.file.FILE_MODEconstant
http.session.file.SUFFIXconstant
http.session.sessionfunctionMiddleware that finds each request’s session and writes it back when the response goes out.
http.session.sql.DEFAULT_TABLEconstant
http.session.sql.SqlStoreclassKeeps sessions in one table of a relational database.
http.session.storage_keyfunctionThe key a store files a session under: the SHA-256 of the session identifier, as 64 lowercase hex characters.
http.set_headersfunctionSets the default headers on the shared client and returns it, so a call can be chained straight onto it.
http.shared_clientfunctionThe shared client the module-level request functions use.
http.sse.EventStreamclassThe writer a server-sent event stream hands to its producer.
http.sse.last_event_idfunctionThe Last-Event-ID a reconnecting client sent, or nil.
http.sse.parsefunctionParses a text/event-stream body into a list of events, each a dictionary with event, data, id and…
http.sse.streamfunctionTurns response into a server-sent event stream and runs producer against it.
http.status.ACCEPTEDconstant202 Accepted.
http.status.ALREADY_REPORTEDconstant208 Already Reported (WebDAV, RFC 5842).
http.status.BAD_GATEWAYconstant502 Bad Gateway.
http.status.BAD_REQUESTconstant400 Bad Request.
http.status.CONFLICTconstant409 Conflict.
http.status.CONTENT_TOO_LARGEconstant413 Content Too Large.
http.status.CONTINUEconstant100 Continue.
http.status.CREATEDconstant201 Created.
http.status.EARLY_HINTSconstant103 Early Hints (RFC 8297).
http.status.EXPECTATION_FAILEDconstant417 Expectation Failed.
http.status.FAILED_DEPENDENCYconstant424 Failed Dependency (WebDAV, RFC 4918).
http.status.FORBIDDENconstant403 Forbidden.
http.status.FOUNDconstant302 Found.
http.status.GATEWAY_TIMEOUTconstant504 Gateway Timeout.
http.status.GONEconstant410 Gone.
http.status.HTTP_VERSION_NOT_SUPPORTEDconstant505 HTTP Version Not Supported.
http.status.IM_A_TEAPOTconstant418 I’m a teapot (RFC 2324).
http.status.IM_USEDconstant226 IM Used (RFC 3229).
http.status.INSUFFICIENT_STORAGEconstant507 Insufficient Storage (WebDAV, RFC 4918).
http.status.INTERNAL_SERVER_ERRORconstant500 Internal Server Error.
http.status.LENGTH_REQUIREDconstant411 Length Required.
http.status.LOCKEDconstant423 Locked (WebDAV, RFC 4918).
http.status.LOOP_DETECTEDconstant508 Loop Detected (WebDAV, RFC 5842).
http.status.METHOD_NOT_ALLOWEDconstant405 Method Not Allowed.
http.status.MISDIRECTED_REQUESTconstant421 Misdirected Request.
http.status.MOVED_PERMANENTLYconstant301 Moved Permanently.
http.status.MULTIPLE_CHOICESconstant300 Multiple Choices.
http.status.MULTI_STATUSconstant207 Multi-Status (WebDAV, RFC 4918).
http.status.NETWORK_AUTHENTICATION_REQUIREDconstant511 Network Authentication Required (RFC 6585).
http.status.NON_AUTHORITATIVE_INFORMATIONconstant203 Non-Authoritative Information.
http.status.NOT_ACCEPTABLEconstant406 Not Acceptable.
http.status.NOT_EXTENDEDconstant510 Not Extended (RFC 2774).
http.status.NOT_FOUNDconstant404 Not Found.
http.status.NOT_IMPLEMENTEDconstant501 Not Implemented.
http.status.NOT_MODIFIEDconstant304 Not Modified.
http.status.NO_CONTENTconstant204 No Content.
http.status.OKconstant200 OK.
http.status.PARTIAL_CONTENTconstant206 Partial Content.
http.status.PAYMENT_REQUIREDconstant402 Payment Required.
http.status.PERMANENT_REDIRECTconstant308 Permanent Redirect.
http.status.PRECONDITION_FAILEDconstant412 Precondition Failed.
http.status.PRECONDITION_REQUIREDconstant428 Precondition Required (RFC 6585).
http.status.PROCESSINGconstant102 Processing (WebDAV, RFC 2518).
http.status.PROXY_AUTHENTICATION_REQUIREDconstant407 Proxy Authentication Required.
http.status.RANGE_NOT_SATISFIABLEconstant416 Range Not Satisfiable.
http.status.REQUEST_HEADER_FIELDS_TOO_LARGEconstant431 Request Header Fields Too Large (RFC 6585).
http.status.REQUEST_TIMEOUTconstant408 Request Timeout.
http.status.RESET_CONTENTconstant205 Reset Content.
http.status.SEE_OTHERconstant303 See Other.
http.status.SERVICE_UNAVAILABLEconstant503 Service Unavailable.
http.status.SWITCHING_PROTOCOLSconstant101 Switching Protocols.
http.status.TEMPORARY_REDIRECTconstant307 Temporary Redirect.
http.status.TOO_EARLYconstant425 Too Early (RFC 8470).
http.status.TOO_MANY_REQUESTSconstant429 Too Many Requests (RFC 6585).
http.status.UNAUTHORIZEDconstant401 Unauthorized.
http.status.UNAVAILABLE_FOR_LEGAL_REASONSconstant451 Unavailable For Legal Reasons (RFC 7725).
http.status.UNPROCESSABLE_CONTENTconstant422 Unprocessable Content.
http.status.UNSUPPORTED_MEDIA_TYPEconstant415 Unsupported Media Type.
http.status.UPGRADE_REQUIREDconstant426 Upgrade Required.
http.status.URI_TOO_LONGconstant414 URI Too Long.
http.status.USE_PROXYconstant305 Use Proxy.
http.status.VARIANT_ALSO_NEGOTIATESconstant506 Variant Also Negotiates (RFC 2295).
http.status.is_bodilessfunctionWhether a response carrying code is defined to have no body at all, regardless of what headers say.
http.status.is_client_errorfunctionWhether code is a 4xx client error.
http.status.is_errorfunctionWhether code is any kind of error, client or server.
http.status.is_informationalfunctionWhether code is a 1xx interim status.
http.status.is_redirectfunctionWhether code is a 3xx redirection status.
http.status.is_registeredfunctionWhether code is a registered status code with a canonical reason phrase of its own.
http.status.is_server_errorfunctionWhether code is a 5xx server error.
http.status.is_successfunctionWhether code is a 2xx success status.
http.status.preserves_methodfunctionWhether a redirect with code must keep the original method and body when followed.
http.status.reasonfunctionThe canonical reason phrase for code, e.g. 'Not Found' for 404.
http.stream.acceptfunctionTurns a freshly accepted TcpStream into a Connection, running a server-side TLS handshake first when…
http.stream.connectfunctionOpens a connection to host on port, optionally wrapping it in TLS.
http.stream.is_timeout_errorfunctionWhether a transport error message describes a timeout (or a would-block, which a socket with a receive…
http.stream.tunnelfunctionOpens a connection to host:port through an HTTP proxy’s CONNECT tunnel, running a TLS handshake with…
http.tls_serverfunctionBuilds an HttpServer already configured for TLS.
http.tracefunctionSends a TRACE request through the shared client.
http.util.find_bytesfunctionFinds the first occurrence of the byte sequence needle in haystack, at or after from, or -1 when it…
http.util.format_datefunctionFormats a Unix timestamp as an IMF-fixdate, the one date format RFC 9110 §5.6.7 requires every HTTP sender to…
http.util.is_valid_methodfunctionWhether value is a valid HTTP method: a non-empty token, per RFC 9110 §9.
http.util.normalize_pathfunctionResolves the . and .. segments of a path and collapses repeated slashes, returning a path that cannot…
http.util.parse_basicfunctionParses an HTTP Basic Authorization field value into a username and password.
http.util.parse_bearerfunctionParses a Bearer Authorization field value into its token.
http.util.parse_datefunctionParses any of the three date formats RFC 9110 §5.6.7 requires a recipient to accept, and returns the Unix…
http.util.parse_parametersfunctionSplits a header value that carries parameters - a media type, a Content-Disposition, a challenge - into its…
http.util.percent_decodefunctionPercent-decodes a URI component, turning + into a space only when plus_as_space is set - which is right…
http.util.percent_encodefunctionPercent-encodes every character of text that is not an RFC 3986 unreserved character, over the UTF-8…
http.util.quotefunctionWraps value in double quotes, escaping any quote or backslash it contains, so it can be used as an RFC 9110…
http.util.random_tokenfunctionA random lowercase-hex token of length characters, drawn from the platform’s cryptographically secure…
http.util.secure_equalsfunctionCompares two strings without leaking, through how long the comparison takes, where they first differ.
http.util.to_hexfunctionn as lowercase hexadecimal, with no 0x prefix and no padding.
http.util.unquotefunctionRemoves the surrounding double quotes from a header value and resolves its backslash escapes.
http.websocket.CLOSE_GOING_AWAYconstantThe endpoint is going away.
http.websocket.CLOSE_INTERNAL_ERRORconstantAn unexpected condition on the server.
http.websocket.CLOSE_INVALID_PAYLOADconstantA text message that was not valid UTF-8.
http.websocket.CLOSE_NORMALconstantNormal closure.
http.websocket.CLOSE_POLICY_VIOLATIONconstantA message that violates a policy.
http.websocket.CLOSE_PROTOCOL_ERRORconstantA protocol error was detected.
http.websocket.CLOSE_TOO_LARGEconstantA message too large to process.
http.websocket.CLOSE_UNSUPPORTEDconstantA message of a kind this endpoint cannot accept.
http.websocket.MessageclassOne complete WebSocket message, with any fragmentation already reassembled.
http.websocket.OPCODE_BINARYconstantBinary frame.
http.websocket.OPCODE_CLOSEconstantClose frame.
http.websocket.OPCODE_CONTINUATIONconstantContinuation frame.
http.websocket.OPCODE_PINGconstantPing frame.
http.websocket.OPCODE_PONGconstantPong frame.
http.websocket.OPCODE_TEXTconstantText frame.
http.websocket.acceptfunctionCompletes a WebSocket handshake and takes over the connection.
http.websocket.accept_keyfunctionThe value a server must return in Sec-WebSocket-Accept for a given client key.
http.websocket.connectfunctionOpens a WebSocket connection to target.
http.websocket.is_handshakefunctionWhether request is a well-formed WebSocket handshake.
http.worker.servefunctionBinds a listening socket and serves it across a pool of worker isolates.
http.worker.worker_mainfunctionThe loop each worker isolate runs: build a server of its own from setup, then serve whatever connections…

Submodules

ModuleReached asSummary
http.bodyhttp.body.*A request or response body, in whatever shape the wire delivered it: a fixed Content-Length, a chunked…
http.clienthttp.client.*HttpClient: the connection-pooling, redirect-following, cookie-aware side of the module.
http.cookieshttp.cookies.*Cookies, both halves of them: Cookie is one cookie with its attributes, CookieJar is a store that applies…
http.errorshttp.*Every error the HTTP stack raises, under one root.
http.fileshttp.files.*Serving files off disk, with the parts that make it correct rather than merely working: conditional requests,…
http.h1http.h1.*HTTP/1.1 on the wire (RFC 9110 and RFC 9112): reading a request line and its headers, writing a status line…
http.h2http.h2.*HTTP/2 (RFC 9113) and the header compression it uses (RFC 7541).
http.headershttp.headers.*Headers: a case-insensitive, order-preserving multimap, because HTTP header names do not compare…
http.middlewarehttp.middleware.*The middleware every public HTTP service ends up needing: CORS, access logging, the security headers a…
http.multiparthttp.multipart.*multipart/form-data, in both directions.
http.negotiatehttp.negotiate.*Content negotiation: choosing what to send when the client has said what it prefers.
http.proxyhttp.proxy.*ReverseProxy forwards a request to another server and streams the response back; LoadBalancer spreads…
http.requesthttp.request.*HttpRequest: one inbound request, with its method, target, headers and body, plus the query string and…
http.responsehttp.response.*HttpResponse: one response, whether it is being built by a handler or read back from a server.
http.routerhttp.router.*Matching a request to a handler.
http.serverhttp.server.*HttpServer: the server end of the module.
http.sessionhttp.session.*Server-side sessions: a small amount of state that belongs to one visitor, kept on the server and found again…
http.ssehttp.sse.*Server-sent events (the WHATWG text/event-stream format): a one-way stream of named, identified messages…
http.statushttp.status.*The HTTP status codes registered with IANA, their canonical reason phrases, and a handful of predicates for…
http.streamhttp.stream.*The transport underneath everything else: a byte stream with buffering, timeouts and optional TLS.
http.utilhttp.util.*The small, exact pieces of the HTTP specifications that several parts of the module need: date formatting,…
http.websockethttp.websocket.*WebSocket (RFC 6455), both ends of it.
http.workerhttp.worker.*The multi-process side of HttpServer: a pool of isolates, each accepting and serving connections from the…

Functions

shared_client()

http.shared_client() -> HttpClient

The shared client the module-level request functions use.

Reach for this to change a setting that should apply to every casual http.get() in a program - a proxy-wide certificate authority, a longer timeout - and build your own client() for anything more specific than that.

Returns HttpClient

client()

http.client(base_url: ?string, options: ?dict) -> HttpClient

Builds a new HttpClient.

Parameters

  • base_url (?string) — prefixed to any relative request target
  • options (?dict) — any HttpClient field, plus headers

Returns HttpClient

set_headers()

http.set_headers(values: dict) -> HttpClient

Sets the default headers on the shared client and returns it, so a call can be chained straight onto it.

echo http.set_headers({ 'Authorization': 'Bearer ' + token })
  .get('https://example.com/me')
  .as_dict()

Parameters

  • values (dict)

Returns HttpClient

get()

http.get(url: string, options: ?dict) -> HttpResponse

Sends a GET request through the shared client.

Parameters

  • url (string)
  • options (?dict) — see HttpClient.request()

Returns HttpResponse

post()

http.post(url: string, data, options: ?dict) -> HttpResponse

Sends a POST request through the shared client.

Parameters

  • url (string)
  • data (?any) — a string or bytes sent as-is, a dictionary or list sent as JSON, or a MultipartBuilder
  • options (?dict)

Returns HttpResponse

put()

http.put(url: string, data, options: ?dict) -> HttpResponse

Sends a PUT request through the shared client.

Parameters

  • url (string)
  • data (?any)
  • options (?dict)

Returns HttpResponse

patch()

http.patch(url: string, data, options: ?dict) -> HttpResponse

Sends a PATCH request through the shared client.

Parameters

  • url (string)
  • data (?any)
  • options (?dict)

Returns HttpResponse

delete()

http.delete(url: string, options: ?dict) -> HttpResponse

Sends a DELETE request through the shared client.

Parameters

  • url (string)
  • options (?dict)

Returns HttpResponse

http.head(url: string, options: ?dict) -> HttpResponse

Sends a HEAD request through the shared client. Redirects are not followed unless options says to.

Parameters

  • url (string)
  • options (?dict)

Returns HttpResponse

options()

http.options(url: string, options: ?dict) -> HttpResponse

Sends an OPTIONS request through the shared client.

Parameters

  • url (string)
  • options (?dict)

Returns HttpResponse

trace()

http.trace(url: string, options: ?dict) -> HttpResponse

Sends a TRACE request through the shared client.

Parameters

  • url (string)
  • options (?dict)

Returns HttpResponse

server()

http.server(port: ?number, host: ?string) -> HttpServer

Builds an HttpServer.

Parameters

  • port (?number) — defaults to 8000
  • host (?string) — defaults to '127.0.0.1'

Returns HttpServer

tls_server()

http.tls_server(port: number, cert_chain: string, private_key: string, host: ?string) -> HttpServer

Builds an HttpServer already configured for TLS.

Parameters

  • port (number)
  • cert_chain (string) — the PEM certificate chain, leaf first
  • private_key (string) — the PEM private key
  • host (?string)

Returns HttpServer

serve()

http.serve(setup, options: ?dict)

Runs a server across a pool of isolates, one per core by default.

setup is called once inside each worker with that worker’s own HttpServer, and registers the routes, middleware and settings the worker should serve with. It may be defined in the main script or in a module, and may use whatever it imports; an imported module is reloaded inside the worker rather than shared with it, so the worker gets its own copy of that module’s top-level state.

The calling isolate binds the socket and accepts connections, handing each one to a worker. It does not return until the server is stopped.

Parameters

  • setup (function(1)) — receives the worker’s HttpServer
  • options (?dict) — port (default 8000), host (default '127.0.0.1'), workers (default: the number of CPUs), backlog (how many accepted connections may wait for a free worker), plus cert_chain/private_key for TLS

Raises HttpError if the socket cannot be bound


2026, Richard Ore and Zuri contributors

http.body

import http.body

http lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled http.body.* needs import http.body.

A request or response body, in whatever shape the wire delivered it: a fixed Content-Length, a chunked stream, or nothing at all.

BodyReader hides that difference behind one interface, and decode_content()/encode_content() handle the Content-Encoding layer on top of it: gzip, deflate, brotli and zstd.

Constants

DEFAULT_BROTLI_QUALITY

http.body.DEFAULT_BROTLI_QUALITY: int = 5

The brotli quality this module compresses a response with when the caller names no quality of its own.

Brotli’s own default is 11, and that is a setting for compressing a file once at build time rather than for compressing a response per request. Measured against q5 on real response bodies, q11 costs 64-104x the CPU and, because of how brotli sizes its window at high quality, frequently produces a larger result:

bodyq11q5
1.5 KB JSON3.48 ms, 152 B0.054 ms, 152 B
15 KB JSON13.5 ms, 654 B0.19 ms, 591 B
150 KB JSON144 ms, 5119 B2.08 ms, 4831 B
120 KB HTML178 ms, 3322 B1.88 ms, 3350 B

A 178 ms compression on a single page is the entire time budget for a request, spent to make the response 0.8% smaller.

Functions

parse_content_length()

http.body.parse_content_length(value) -> number

Parses a Content-Length field value.

RFC 9112 §6.3 is strict here for good reason: a value that isn’t a plain run of digits, or a field carrying two different lengths, is the raw material of request smuggling, where a proxy and an origin read the same bytes as different numbers of messages.

Parameters

  • value (string)

Returns number

Raises ProtocolError if the value is not a bare decimal number

parse_chunk_size()

http.body.parse_chunk_size(line) -> number

Parses a chunk-size line, ignoring any chunk extensions after the ;.

Parameters

  • line (string)

Returns number

Raises ProtocolError on anything that isn’t hexadecimal

reader_for()

http.body.reader_for(connection, headers, is_request: bool, bodiless: ?bool) -> BodyReader

Works out how a message’s body is framed and returns a reader for it.

is_request matters because the two directions disagree about what “no framing headers” means: a request without them has no body, while a response without them runs until the connection closes.

Parameters

  • connection (Connection)
  • headers (Headers)
  • is_request (bool)
  • bodiless (?bool) — force an empty body regardless of the headers - true for a response to HEAD, and for 1xx, 204 and 304, all of which carry framing headers that describe a body they do not actually send

Returns BodyReader

Raises ProtocolError if the framing headers contradict each other

encode_chunk()

http.body.encode_chunk(data) -> bytes

Wraps data as a single HTTP/1.1 chunk: the size in hexadecimal, a CRLF, the data, and a CRLF.

Parameters

  • data (bytes)

Returns bytes

encode_last_chunk()

http.body.encode_last_chunk(trailers) -> bytes

The terminating 0\r\n chunk plus a trailer section.

Parameters

  • trailers (?Headers)

Returns bytes

decode_content()

http.body.decode_content(data, encoding) -> bytes

Reverses the content codings named in a Content-Encoding field, innermost last, as RFC 9110 §8.4 requires.

identity is accepted and does nothing. Anything else raises, because silently handing back still-encoded bytes would look like corrupt data at the call site.

Parameters

  • data (bytes)
  • encoding (string) — the raw field value, e.g. 'gzip'

Returns bytes

Raises ProtocolError on an unknown coding or undecodable data

encode_content()

http.body.encode_content(data, coding: string, quality: ?number) -> bytes

Applies a content coding to data.

quality is interpreted on the scale of whichever coding is being applied - brotli 0-11, deflate 0-9, zstd 1-22 - and is clamped into range rather than rejected. Omit it to get the coding’s own sensible default for a response compressed per request: 5 for brotli (see DEFAULT_BROTLI_QUALITY), and each of the others’ native default, which is already the right shape for dynamic content.

Parameters

  • data (bytes)
  • coding (string) — 'gzip', 'deflate', 'br', 'zstd' or 'identity'
  • quality (?number)

Returns bytes

Raises ProtocolError on an unknown coding

Note: gzip has no quality parameter at this layer. It compresses at zlib’s own default level of 6, which is what nginx and every CDN use for dynamic responses anyway.

Classes

BodyReader

class http.BodyReader

Reads a message body off a connection according to whichever of HTTP/1.1’s framings applies.

The framing is decided once, by for_message(), and never re-derived: Transfer-Encoding: chunked wins over Content-Length, a fixed length is next, and a response with neither is delimited by the connection closing. A request with neither has no body at all - a server may not wait for a close that a client has no reason to perform.

Read it incrementally with read(), or in one go with read_all(). Either way, call drain() before reusing the connection, or the next message will start parsing in the middle of this one’s body.

Fields

FieldTypeDescription
modestringOne of 'empty', 'length', 'chunked' or 'eof'.
lengthnumberThe declared body length for a 'length' body, or -1 when the length is not known in advance.
trailersHeadersTrailer fields, populated once a chunked body has been read to completion.

Constructor

http.BodyReader(connection, mode: string, length)

Parameters

  • connection (Connection)
  • mode (string)
  • length (?number)

BodyReader.is_finished()

http.BodyReader.is_finished() -> bool

Whether the body has been read to its end.

Returns bool

BodyReader.read()

http.BodyReader.read(length)

Reads up to length more bytes of the body, or all of what remains when length is omitted and the body is short.

Parameters

  • length (?number) — defaults to 64 KiB

Returns — bytes: empty once the body is exhausted

Raises ProtocolError on a malformed chunked body or a truncated fixed-length one

BodyReader.read_all()

http.BodyReader.read_all(limit) -> bytes

Reads the whole body into memory.

Parameters

  • limit (?number) — the most to accept, in bytes; nil for no limit

Returns bytes

Raises TooLargeError if the body exceeds limit

BodyReader.drain()

http.BodyReader.drain(limit)

Reads and throws away whatever is left, so the connection is positioned at the start of the next message and can be reused.

Returns false when the leftover exceeds limit - at which point draining costs more than a new connection would, and the caller should close instead.

Parameters

  • limit (?number) — the most worth draining; defaults to 1 MiB

Returns — bool: whether the connection is now reusable


2026, Richard Ore and Zuri contributors

http.client

import http.client

http lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled http.client.* needs import http.client.

HttpClient: the connection-pooling, redirect-following, cookie-aware side of the module.

It negotiates HTTP/2 over TLS when the server offers it, falls back to HTTP/1.1 otherwise, and reuses connections between requests. The module-level http.get()/http.post() shortcuts are thin wrappers around a shared instance of this.

Classes

HttpClient

class http.HttpClient

An HTTP client.

import http

var client = http.HttpClient()
var response = client.get('https://example.com')

echo response.status
echo response.as_text()

One client is meant to be kept and reused: it holds the connection pool, the cookie jar, and the TLS configuration, all of which are wasted if a fresh client is built per request. Reusing it also means the second request to a host skips the TCP and TLS handshakes entirely.

Redirects are followed, responses are decompressed, and cookies are carried between requests when a jar is attached - all of which can be turned off.

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
base_url?stringPrefixed to any request URL that is not already absolute, so a client can be pointed at one service and then…
headersHeadersHeaders sent with every request, unless a request overrides them.
user_agentstringThe User-Agent sent unless one is set in headers.
follow_redirectsboolWhether to follow 3xx responses.
max_redirectsnumberThe most redirects to follow before giving up.
connect_timeoutnumberHow long to wait for a connection to be established, in milliseconds.
read_timeoutnumberHow long to wait for data once connected, in milliseconds.
write_timeoutnumberHow long a write may block, in milliseconds.
verifyboolWhether to verify the server’s certificate chain and hostname.
max_body_sizenumberThe largest response body accepted, in bytes.
decode_contentboolWhether to decode a compressed response body automatically.
max_retriesnumberHow many times to retry a request that failed on a connection error before giving up.
http2boolWhether to negotiate HTTP/2 over TLS.
proxy?stringThe HTTP proxy every request goes through, as http://host:port, with user:password@ in front of the host…
trust_envboolWhether to take a proxy from the environment when proxy is not set: HTTPS_PROXY for https addresses,…
cookie_jar?CookieJarThe cookie jar, or nil to not track cookies at all.

Constructor

http.HttpClient(base_url: ?string, options: ?dict)

Parameters

  • base_url (?string)
  • options (?dict) — any of the fields above, plus headers

HttpClient.get()

http.HttpClient.get(target: string, options: ?dict) -> HttpResponse

Sends a GET request.

Parameters

  • target (string) — an absolute URL, or a path when base_url is set
  • options (?dict) — see request()

Returns HttpResponse

HttpClient.post()

http.HttpClient.post(target: string, data, options: ?dict) -> HttpResponse

Sends a POST request.

data may be a string, bytes, a dictionary (sent as JSON), or a MultipartBuilder. Use the form option instead to send a dictionary as application/x-www-form-urlencoded.

Parameters

  • target (string)
  • data (?any)
  • options (?dict) — see request()

Returns HttpResponse

HttpClient.put()

http.HttpClient.put(target: string, data, options: ?dict) -> HttpResponse

Sends a PUT request.

Parameters

  • target (string)
  • data (?any)
  • options (?dict)

Returns HttpResponse

HttpClient.patch()

http.HttpClient.patch(target: string, data, options: ?dict) -> HttpResponse

Sends a PATCH request.

Parameters

  • target (string)
  • data (?any)
  • options (?dict)

Returns HttpResponse

HttpClient.delete()

http.HttpClient.delete(target: string, options: ?dict) -> HttpResponse

Sends a DELETE request.

Parameters

  • target (string)
  • options (?dict)

Returns HttpResponse

HttpClient.head()

http.HttpClient.head(target: string, options: ?dict) -> HttpResponse

Sends a HEAD request. Redirects are not followed by default here, since the point of a HEAD is usually to inspect the very response a redirect would hide.

Parameters

  • target (string)
  • options (?dict)

Returns HttpResponse

HttpClient.options()

http.HttpClient.options(target: string, options: ?dict) -> HttpResponse

Sends an OPTIONS request.

Parameters

  • target (string)
  • options (?dict)

Returns HttpResponse

HttpClient.trace()

http.HttpClient.trace(target: string, options: ?dict) -> HttpResponse

Sends a TRACE request.

Parameters

  • target (string)
  • options (?dict)

Returns HttpResponse

Note: A TRACE echoes the request back, headers included, which is why servers that sit behind authenticating proxies usually refuse it.

HttpClient.request()

http.HttpClient.request(method: string, target: string, options: ?dict) -> HttpResponse

Sends a request and returns the response.

The options dictionary accepts:

OptionMeaning
headersa dictionary or Headers merged over the client’s
bodya string or bytes, sent as-is
jsonany value, JSON-encoded, with the matching content type
forma dictionary, form-urlencoded
multiparta MultipartBuilder
querya dictionary merged into the URL’s query string
auth['basic', user, password] or ['bearer', token]
follow_redirectsoverrides the client setting
timeoutread timeout for this request, in milliseconds
streamleave the body unread on response.body_reader
decode_contentoverrides the client setting

Parameters

  • method (string)
  • target (string)
  • options (?dict)

Returns HttpResponse

Raises ConnectionError, TimeoutError, ProtocolError, TooManyRedirectsError, UnsupportedProtocolError

HttpClient.finish()

http.HttpClient.finish(response)

Releases the connection behind a streamed response, once the caller has finished reading its body.

Anything left unread is drained so the connection can be reused; when there is too much left for that to be worth it, the connection is closed instead. Calling this on an ordinary (non-streamed) response does nothing.

Parameters

  • response (HttpResponse)

HttpClient.add_ca()

http.HttpClient.add_ca(pem: string)

Trusts an additional PEM-encoded certificate authority, on top of the platform’s own roots.

This is the right way to talk to a service with an internal or self-signed certificate; turning verify off instead trusts everyone, including whoever is between you and the server.

Parameters

  • pem (string)

Returns — HttpClient: this same instance, for chaining

HttpClient.set_tls_config()

http.HttpClient.set_tls_config(config)

Uses a net.tls.TlsConfig built elsewhere for every HTTPS connection this client makes.

Parameters

  • config (TlsConfig)

Returns — HttpClient: this same instance, for chaining

HttpClient.set_header()

http.HttpClient.set_header(name: string, value)

Sets a header sent with every request from this client.

Parameters

  • name (string)
  • value (string|number|bool)

Returns — HttpClient: this same instance, for chaining

HttpClient.set_headers()

http.HttpClient.set_headers(values)

Replaces the default headers wholesale.

Parameters

  • values (dict|Headers)

Returns — HttpClient: this same instance, for chaining

HttpClient.enable_cookies()

http.HttpClient.enable_cookies()

Starts tracking cookies, so a session survives across requests from this client.

Returns — HttpClient: this same instance, for chaining

HttpClient.close()

http.HttpClient.close()

Closes every pooled connection. A client is usable afterwards - the next request simply opens a fresh connection - but the sockets are released, which matters at the end of a long-running process or before forking.

HttpClient.pooled_connections()

http.HttpClient.pooled_connections() -> number

How many connections are currently idle in the pool.

Returns number

HttpClient.to_string()

http.HttpClient.to_string()

2026, Richard Ore and Zuri contributors

http.cookies

import http.cookies

http lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled http.cookies.* needs import http.cookies.

Cookies, both halves of them: Cookie is one cookie with its attributes, CookieJar is a store that applies the domain, path and expiry rules when deciding what to send.

The parsing and formatting functions handle the two header formats, which are not symmetric: a request sends many cookies in one Cookie header, a response sets one per Set-Cookie.

Functions

is_valid_value()

http.cookies.is_valid_value(value) -> bool

Whether value can be sent as a cookie value without quoting.

Parameters

  • value (string)

Returns bool

http.cookies.parse_cookie_header(header) -> dict

Parses a request’s Cookie header into a dictionary of name to value.

A cookie header is a flat a=1; b=2 list with no attributes - everything the server set is gone by the time it comes back, which is why a server that needs to know a cookie’s path or expiry has to have stored that itself.

Malformed pairs are skipped rather than raising: a browser will cheerfully send whatever junk any script on the page stored, and refusing the whole request over one bad pair loses the good ones too.

Parameters

  • header (string)

Returns dict

http.cookies.parse_set_cookie(header)

Parses one Set-Cookie field value into a Cookie.

Parameters

  • header (string)

Returns — ?Cookie: nil when the field carries no usable name/value pair

http.cookies.format_cookie_header(cookies: dict) -> string

Renders a dictionary of name to value as a request Cookie header value.

Parameters

  • cookies (dict)

Returns string

Classes

class http.Cookie

A single cookie, in either direction: the name/value pair a client sends back in a Cookie header, or the full attribute set a server sends in Set-Cookie.

The attributes follow RFC 6265bis, including SameSite and the __Host-/__Secure- name prefixes, whose rules are enforced by to_header() rather than left to the caller to remember.

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
namestringThe cookie’s name.
valuestringThe cookie’s value.
domain?stringThe domain the cookie is sent to, or nil to scope it to exactly the host that set it.
path?stringThe path prefix the cookie is sent for.
expires?numberAbsolute expiry, as seconds since the epoch.
max_age?numberLifetime in seconds from now.
secureboolWhether the cookie is only ever sent over HTTPS.
http_onlyboolWhether the cookie is hidden from client-side scripts.
same_site?string'Strict', 'Lax' or 'None'.
partitionedboolWhether the cookie is partitioned by top-level site (CHIPS).

Constructor

http.Cookie(name: string, value: string, attributes: ?dict)

Parameters

  • name (string)
  • value (string)
  • attributes (?dict) — any of domain, path, expires, max_age, secure, http_only, same_site, partitioned

Raises ProtocolError if the name is not a token, or the value contains a character that cannot appear in a cookie

Cookie.to_header()

http.Cookie.to_header() -> string

Renders the cookie as a Set-Cookie field value.

The name prefixes defined in RFC 6265bis §4.1.3 are honoured here: a __Secure- cookie is forced Secure, and a __Host- cookie is forced Secure, pinned to Path=/, and stripped of any Domain. Browsers reject cookies that carry a prefix without meeting its conditions, so fixing them up beats silently emitting a cookie that never gets stored.

SameSite=None likewise implies Secure, without which the cookie is dropped.

Returns string

Cookie.is_expired()

http.Cookie.is_expired(now) -> bool

Whether the cookie’s own attributes say it has already expired, as of now (default: the current time). A session cookie - one with neither expires nor max_age - is never expired by this test.

Parameters

  • now (?number)

Returns bool

Cookie.to_string()

http.Cookie.to_string()

CookieJar

class http.CookieJar

A client-side cookie store: keeps the cookies a server set, decides which of them a later request is entitled to see, and forgets the ones that have expired.

The matching rules are RFC 6265 §5.4’s: domain-match (an exact host match, or a suffix match when the cookie carried a Domain), path-match, and the Secure flag against the request’s scheme. Getting this wrong in either direction is a real problem - too strict and sessions break, too loose and a cookie leaks to a host that never should have seen it.

Constructor

http.CookieJar()

CookieJar.store()

http.CookieJar.store(cookie, host: string, request_path: ?string)

Records a cookie as having been set by host.

A cookie whose Domain is not a suffix of host is rejected outright - that is a server trying to set a cookie for a domain it does not control.

Parameters

  • cookie (Cookie)
  • host (string) — the host of the response that set it
  • request_path (?string) — used to derive a default path

Returns — bool: whether the cookie was accepted

CookieJar.cookies_for()

http.CookieJar.cookies_for(host: string, path: string, secure: ?bool) -> dict

The cookies that should be sent with a request to host and path, as a dictionary of name to value.

Parameters

  • host (string)
  • path (string)
  • secure (?bool) — whether the request is over HTTPS

Returns dict

CookieJar.all()

http.CookieJar.all() -> list

Every cookie currently held, expired ones included.

Returns list

CookieJar.clear()

http.CookieJar.clear()

Drops every stored cookie.

CookieJar.length()

http.CookieJar.length() -> number

How many cookies are held.

Returns number


2026, Richard Ore and Zuri contributors

http.errors

import http

Everything here is re-exported by http, so import http is enough and the names are called as http.*. Importing http.errors on its own works too and reaches the same definitions.

Every error the HTTP stack raises, under one root.

HttpError is that root, so a caller that only wants to know whether the request failed can catch it and ignore the rest. The subclasses separate the cases worth handling differently: a connection that never opened, one that timed out, a response too large to accept, a redirect loop, and a status the caller asked to be treated as an error.

Classes

HttpError

class http.HttpError < Error

Base class for every error the http module raises.

Catching HttpError catches everything this module can throw on its own; the more specific subclasses below let a caller tell a connection failure apart from a malformed response without matching on message text.

  • printable — has a @to_string(), so echo and print() show something useful

HttpError.to_string()

http.HttpError.to_string()

ProtocolError

class http.ProtocolError < HttpError

Raised when a peer sends something that isn’t a well-formed HTTP message: a broken request line or status line, a header field that doesn’t parse, an impossible framing combination, or a chunked body whose chunk sizes don’t add up.

A server answers these with 400 Bad Request and closes the connection, since a stream whose framing is in doubt cannot safely be reused.

  • printable — has a @to_string(), so echo and print() show something useful

ProtocolError.to_string()

http.ProtocolError.to_string()

ConnectionError

class http.ConnectionError < HttpError

Raised when a connection could not be established, or when an established connection failed or was closed before the message being read or written was complete.

  • printable — has a @to_string(), so echo and print() show something useful

ConnectionError.to_string()

http.ConnectionError.to_string()

TimeoutError

class http.TimeoutError < HttpError

Raised when an operation exceeded its configured deadline: a connect, a read, a write, or the total time budget for a request.

  • printable — has a @to_string(), so echo and print() show something useful

TimeoutError.to_string()

http.TimeoutError.to_string()

TooLargeError

class http.TooLargeError < HttpError

Raised when a message exceeds one of the configured size limits - request line, header block, or body. Servers turn this into a 431 or 413 response rather than reading an unbounded amount of attacker-controlled data into memory.

  • printable — has a @to_string(), so echo and print() show something useful

TooLargeError.to_string()

http.TooLargeError.to_string()

TooManyRedirectsError

class http.TooManyRedirectsError < HttpError

Raised by the client when a redirect chain exceeds HttpClient.max_redirects, which usually means the chain is a loop.

  • printable — has a @to_string(), so echo and print() show something useful

TooManyRedirectsError.to_string()

http.TooManyRedirectsError.to_string()

StatusError

class http.StatusError < HttpError

Raised by HttpResponse.raise_for_status() when the response carries a 4xx or 5xx status. The response itself stays reachable through response, so the body (which for an API is usually where the actual explanation lives) isn’t lost.

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
responseHttpResponseThe response that carried the failing status.

Constructor

http.StatusError(message, response)

StatusError.to_string()

http.StatusError.to_string()

UnsupportedProtocolError

class http.UnsupportedProtocolError < HttpError

Raised when a URL names a scheme this module cannot speak, or when a peer insists on a protocol version that was not offered.

  • printable — has a @to_string(), so echo and print() show something useful

UnsupportedProtocolError.to_string()

http.UnsupportedProtocolError.to_string()

2026, Richard Ore and Zuri contributors

http.files

import http.files

http lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled http.files.* needs import http.files.

Serving files off disk, with the parts that make it correct rather than merely working: conditional requests, entity tags, and byte ranges.

StaticFiles is the handler. ByteRange and the range parser implement Range/206 Partial Content, which is what lets a browser seek within a video without downloading it whole.

Functions

parse_range()

http.files.parse_range(header, size: number) -> ?list

Parses a Range header against a representation of size bytes.

Returns a list of ByteRange, an empty list when the header names only ranges that fall outside the representation (a 416), or nil when the header is not a byte range at all - which RFC 9110 §14.2 says to ignore and answer with the whole thing.

Parameters

  • header (?string)
  • size (number)

Returns ?list

weak_etag()

http.files.weak_etag(stats) -> string

A weak validator derived from a file’s size and modification time.

Weak rather than strong because two writes within the same second are indistinguishable to it, which is exactly the condition RFC 9110 §8.8.1 reserves the W/ prefix for. It costs one stat, where a strong one costs a full read of the file.

Parameters

  • stats (dict) — a file’s stats() dictionary

Returns string

etag_matches()

http.files.etag_matches(header, etag) -> bool

Whether an If-None-Match value matches etag.

Comparison is weak, per RFC 9110 §13.1.2: W/"x" and "x" match each other, because a conditional GET only needs the two representations to be equivalent, not byte-identical.

Parameters

  • header (string)
  • etag (?string)

Returns bool

Classes

ByteRange

class http.ByteRange

One byte range asked for by a Range header, already resolved against the size of the representation.

Fields

FieldTypeDescription
startnumberThe first byte of the range, inclusive.
lastnumberThe last byte of the range, inclusive - as Content-Range states it, not as a length.

Constructor

http.ByteRange(start, last)

ByteRange.length()

http.ByteRange.length() -> number

How many bytes the range covers.

Returns number

ByteRange.to_content_range()

http.ByteRange.to_content_range(total) -> string

The Content-Range field value for this range against a representation of total bytes.

Parameters

  • total (number)

Returns string

StaticFiles

class http.StaticFiles

Serves files from a directory, with the conditional-request, range-request and caching behaviour a browser and a CDN both expect.

Every request path is percent-decoded and normalised before it is joined to the root, and the result is checked to still be inside the root afterwards - the decode-then-normalise-then-verify sequence, because doing any two of the three is not enough.

var files = http.StaticFiles('./public', { cache_age: 3600 })
server.handle('GET', '/static/' + '*path', @(request, response) {
  files.serve(request, response, request.param('path'))
})

HttpServer.serve_files() wires all of that up for you; this class is what it uses, and is here for applications that want to serve files from somewhere the router does not reach.

Fields

FieldTypeDescription
rootstringThe directory files are served from, as an absolute path.
index_fileslistFilenames tried when the request names a directory.
cache_agenumbermax-age, in seconds, for served files.
etagboolWhether to send an ETag.
precompressedboolWhether to answer a request for a file with a .gz or .br sibling by serving that instead, when the client…
allow_dotfilesboolWhether to serve dotfiles.
fallback?stringA file served in place of a missing one, relative to the root.

Constructor

http.StaticFiles(directory: string, options: ?dict)

Parameters

  • directory (string)
  • options (?dict) — index_files, cache_age, etag, precompressed, allow_dotfiles, fallback

Raises HttpError if the directory does not exist

StaticFiles.resolve()

http.StaticFiles.resolve(relative_path: string)

Resolves relative_path inside the root, or returns nil when it escapes, names a dotfile that is not allowed, or does not exist.

Parameters

  • relative_path (string)

Returns — ?string: an absolute path

StaticFiles.serve()

http.StaticFiles.serve(request, response, relative_path: string)

Serves relative_path from the root into response.

Returns false when there is nothing to serve, leaving the response untouched so the caller can fall through to its own not-found handling.

Parameters

  • request (HttpRequest)
  • response (HttpResponse)
  • relative_path (string)

Returns — bool: whether the response was filled in


2026, Richard Ore and Zuri contributors

http.h1

import http

http exposes this as http.h1, so import http is enough and the names are called as http.h1.*. import http.h1 reaches the same definitions directly.

HTTP/1.1 on the wire (RFC 9110 and RFC 9112): reading a request line and its headers, writing a status line and its headers, and the chunked transfer encoding.

This is the layer HttpServer and HttpClient sit on when the connection is not HTTP/2. DEFAULT_LIMITS is what stops a malformed or hostile peer from making the parser allocate without bound.

Constants

DEFAULT_LIMITS

http.h1.DEFAULT_LIMITS = {...}

SERVER_NAME

http.h1.SERVER_NAME = 'Zuri'

USER_AGENT

http.h1.USER_AGENT = 'Zuri/1.0'

Functions

parse_version()

http.h1.parse_version(text: string) -> string

Parses HTTP/1.1 into '1.1', rejecting anything that is not a version this module understands the framing of.

Parameters

  • text (string)

Returns string

Raises ProtocolError

read_request()

http.h1.read_request(connection, options: ?dict)

Reads one request off a connection: the request line, the header section, and enough framing information to read the body.

The body is not read. request.body_reader is left positioned at its first byte, so the caller decides whether to materialise it, stream it, or refuse it - which is the only way to answer a 10 GiB upload with a 413 instead of accepting it first.

Parameters

  • connection (Connection)
  • options (?dict) — any of max_line_size, max_header_size, max_header_count

Returns — ?HttpRequest: nil when the peer closed the connection cleanly between messages

Raises ProtocolError on a malformed message

Raises TooLargeError if a limit is exceeded

send_continue()

http.h1.send_continue(connection)

Sends a 100 Continue interim response, telling a client that asked with Expect: 100-continue to go ahead and send its body.

Parameters

  • connection (Connection)

should_keep_alive()

http.h1.should_keep_alive(request, response) -> bool

Whether the connection should be kept open after this exchange.

HTTP/1.1 keeps connections open unless told otherwise; HTTP/1.0 closes them unless told otherwise. Either side can ask to close, and either side asking is enough.

Parameters

  • request (HttpRequest)
  • response (HttpResponse)

Returns bool

write_response()

http.h1.write_response(connection, request, response, options: ?dict)

Writes a response to a connection and returns whether the connection is still usable afterwards.

Framing is decided here rather than by the caller, because it has to agree with the status, the method, and the kind of body:

  • a 1xx, 204 or 304, and any response to HEAD, sends no body at all no matter what the body holds;
  • a body of known size gets a Content-Length;
  • a streamed body gets chunked encoding on HTTP/1.1, and on HTTP/1.0 - which has no chunked encoding - is delimited by closing the connection.

Parameters

  • connection (Connection)
  • request (HttpRequest)
  • response (HttpResponse)
  • options (?dict) — server_name for the Server header, and keep_alive to force the connection decision

Returns — bool: whether the connection may be reused

Raises ConnectionError, TimeoutError

write_request()

http.h1.write_request(connection, request, options: ?dict)

Writes a request to a connection.

Parameters

  • connection (Connection)
  • request (HttpRequest)
  • options (?dict) — body (bytes), and origin_form (default true) to control whether the target is written as a path or as an absolute URL, which is what a request to a forward proxy needs

Raises ConnectionError, TimeoutError

read_response()

http.h1.read_response(connection, request, options: ?dict) -> HttpResponse

Reads a response off a connection.

Interim 1xx responses are consumed and skipped, since they are part of the exchange rather than its result. A 101 is returned as it is: that one is the result, and the caller is switching protocols on the strength of it.

Parameters

  • connection (Connection)
  • request (HttpRequest) — the request being answered; its method decides the response’s framing
  • options (?dict) — max_line_size, max_header_size, max_header_count, max_body_size, decode_content (default true), and stream (default false) to leave the body unread

Returns HttpResponse

Raises ProtocolError, TooLargeError, ConnectionError

Classes

ChunkWriter

class http.h1.ChunkWriter

The writer handed to a streaming response body.

write() sends data as it is produced, framed as chunked transfer encoding when that is how the response was framed, and raw otherwise. flush() pushes what has been written all the way to the socket, which is what makes a progress feed or a server-sent-event stream actually arrive rather than sit in a buffer.

Constructor

http.h1.ChunkWriter(connection, chunked)

Parameters

  • connection (Connection)
  • chunked (bool)

ChunkWriter.write()

http.h1.ChunkWriter.write(data)

Writes part of the body.

A zero-length write is dropped rather than sent, since a zero-length chunk is the end-of-body marker and sending one early would truncate the response.

Parameters

  • data (bytes|string)

ChunkWriter.flush()

http.h1.ChunkWriter.flush()

Pushes everything written so far out to the socket.

ChunkWriter.finish()

http.h1.ChunkWriter.finish(trailers)

Ends the body, writing the terminating chunk when the response is chunked. Called for you when the streaming handler returns.

Parameters

  • trailers (?Headers) — trailer fields to send after the last chunk; only meaningful for a chunked body, and only ever read by a client that announced TE: trailers

ChunkWriter.abort()

http.h1.ChunkWriter.abort()

2026, Richard Ore and Zuri contributors

http.h2

import http

http exposes this as http.h2, so import http is enough and the names are called as http.h2.*. import http.h2 reaches the same definitions directly.

HTTP/2 (RFC 9113) and the header compression it uses (RFC 7541).

HttpServer speaks this automatically: over TLS when ALPN negotiates h2, and over cleartext when a client opens with the HTTP/2 connection preface. HttpClient uses it when a server negotiates h2 for an HTTPS connection.

The pieces are here for anything that needs them directly - a gateway, a test harness, or a protocol that borrows HPACK.

Submodules

ModuleReached asSummary
http.h2.connectionhttp.h2.connection.*One HTTP/2 connection and the streams multiplexed over it.
http.h2.frameshttp.h2.frames.*The HTTP/2 frame layer (RFC 9113 §4 and §6): a nine-octet header - a 24-bit length, a type, a flags byte, and…
http.h2.hpackhttp.h2.hpack.*HPACK, the header compression HTTP/2 uses (RFC 7541).
http.h2.huffmanhttp.h2.huffman.*The static Huffman code HPACK uses to compress header field names and values, from RFC 7541 Appendix B.

2026, Richard Ore and Zuri contributors

http.h2.connection

import http.h2.connection

http lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled http.h2.connection.* needs import http.h2.connection.

One HTTP/2 connection and the streams multiplexed over it.

Http2Connection owns the connection-level state: the settings exchange, flow-control windows, and the HPACK contexts shared by every stream. Http2Stream is one request/response pair within it, and Http2Writer serialises frames so that concurrent streams cannot interleave mid-frame.

Classes

Http2Stream

class http.h2.Http2Stream

One HTTP/2 stream: a request and its response, multiplexed with others over a single connection.

Fields

FieldTypeDescription
idintThe stream identifier.
fieldslistThe decoded header fields, in the order they arrived.
databytesThe request or response body received so far.
endedboolWhether the peer has finished sending on this stream.
headers_doneboolWhether the header block is complete - that is, whether an END_HEADERS flag has been seen.
closedboolWhether this endpoint has finished sending on this stream.
send_windownumberHow many bytes may still be sent on this stream before the peer has to grant more.
consumednumberHow much of this stream’s receive window has been consumed since the last WINDOW_UPDATE.
trailerslistThe trailer fields, if any arrived after the body.
fragment

Constructor

http.h2.Http2Stream(id, send_window)

Http2Connection

class http.h2.Http2Connection

An HTTP/2 connection, in either role.

On the server side, serve() reads the preface, exchanges settings, and then runs the frame loop, handing each completed request to the HttpServer it was given and writing the response back. On the client side, start_client() performs the same handshake and send_request()/await_response() carry one exchange.

Streams are multiplexed on the wire but handled in the order they complete: this is one thread reading one socket, so a request that arrives while another is being handled is read, buffered, and answered next rather than in parallel. That is a throughput property, not a correctness one - the framing, flow control and header state are all maintained exactly as the protocol requires.

Fields

FieldTypeDescription
is_serverboolWhether this endpoint is the server.
max_body_sizenumberThe largest request body accepted on one stream, in bytes.

Constructor

http.h2.Http2Connection(connection, is_server: bool)

Parameters

  • connection (Connection) — the transport, already connected and (for h2) already through its TLS handshake
  • is_server (bool)

Http2Connection.start_server()

http.h2.Http2Connection.start_server(already_read)

Reads the client connection preface and sends this endpoint’s SETTINGS. Server side.

Parameters

  • already_read (?bytes) — preface bytes a caller consumed while working out which protocol this was

Raises ProtocolError if the preface is not what HTTP/2 requires

Http2Connection.start_client()

http.h2.Http2Connection.start_client()

Sends the client connection preface and this endpoint’s SETTINGS. Client side.

Http2Connection.serve()

http.h2.Http2Connection.serve(server)

Runs the connection until the peer goes away, handing every completed request to server.

Parameters

  • server (HttpServer)

Http2Connection.send_data()

http.h2.Http2Connection.send_data(stream, data, end_stream)

Sends data on stream as DATA frames, respecting both the stream’s and the connection’s send windows and the peer’s maximum frame size.

When a window is exhausted this reads frames until the peer grants more, which is what makes flow control actually work rather than simply be accounted for.

Parameters

  • stream (Http2Stream)
  • data (bytes)
  • end_stream (bool)

Http2Connection.send_request()

http.h2.Http2Connection.send_request(request, payload) -> Http2Stream

Opens a stream and sends request on it. Client side.

Parameters

  • request (HttpRequest)
  • payload (?bytes) — the request body

Returns Http2Stream

Http2Connection.await_response()

http.h2.Http2Connection.await_response(stream, request) -> HttpResponse

Reads frames until stream is finished and returns the response it carried. Client side.

Parameters

  • stream (Http2Stream)
  • request (HttpRequest)

Returns HttpResponse

Raises ProtocolError if the response is malformed

Raises ConnectionError if the connection ends first

Http2Connection.is_usable()

http.h2.Http2Connection.is_usable() -> bool

Whether the connection is still usable for another request.

Returns bool

Http2Connection.close()

http.h2.Http2Connection.close(error_code)

Sends GOAWAY and closes the transport.

Parameters

  • error_code (?int) — defaults to NO_ERROR

Http2Writer

class http.h2.Http2Writer

The writer a streaming response body gets on an HTTP/2 connection. Same shape as the HTTP/1.1 one, so a handler never has to know which version it is answering.

Constructor

http.h2.Http2Writer(connection, stream)

Http2Writer.write()

http.h2.Http2Writer.write(data)

Sends part of the body.

Parameters

  • data (bytes|string)

Http2Writer.flush()

http.h2.Http2Writer.flush()

Pushes what has been written out to the socket. DATA frames are already flushed as they go, so this is here for interface parity with the HTTP/1.1 writer.

Http2Writer.finish()

http.h2.Http2Writer.finish()

Ends the body with an empty END_STREAM frame.


2026, Richard Ore and Zuri contributors

http.h2.frames

import http

http exposes this as http.h2.frames, so import http is enough and the names are called as http.h2.frames.*. import http.h2.frames reaches the same definitions directly.

The HTTP/2 frame layer (RFC 9113 §4 and §6): a nine-octet header - a 24-bit length, a type, a flags byte, and a 31-bit stream identifier - followed by a payload.

Constants

PREFACE

http.h2.frames.PREFACE: string = 'PRI * HTTP/2.0

SM

'

The connection preface every HTTP/2 client sends before anything else. A server that reads these exact bytes knows it is speaking HTTP/2 and not an HTTP/1.1 request that happens to start with PRI.

DATA

http.h2.frames.DATA = 0

DATA frame. @type int

HEADERS

http.h2.frames.HEADERS = 1

HEADERS frame. @type int

PRIORITY

http.h2.frames.PRIORITY = 2

PRIORITY frame; deprecated by RFC 9113 and ignored here. @type int

RST_STREAM

http.h2.frames.RST_STREAM = 3

RST_STREAM frame. @type int

SETTINGS

http.h2.frames.SETTINGS = 4

SETTINGS frame. @type int

PUSH_PROMISE

http.h2.frames.PUSH_PROMISE = 5

PUSH_PROMISE frame. @type int

PING

http.h2.frames.PING = 6

PING frame. @type int

GOAWAY

http.h2.frames.GOAWAY = 7

GOAWAY frame. @type int

WINDOW_UPDATE

http.h2.frames.WINDOW_UPDATE = 8

WINDOW_UPDATE frame. @type int

CONTINUATION

http.h2.frames.CONTINUATION = 9

CONTINUATION frame. @type int

FLAG_END_STREAM

http.h2.frames.FLAG_END_STREAM = 1

END_STREAM flag, on DATA and HEADERS. @type int

FLAG_ACK

http.h2.frames.FLAG_ACK = 1

ACK flag, on SETTINGS and PING. @type int

FLAG_END_HEADERS

http.h2.frames.FLAG_END_HEADERS = 4

END_HEADERS flag, on HEADERS, PUSH_PROMISE and CONTINUATION. @type int

FLAG_PADDED

http.h2.frames.FLAG_PADDED = 8

PADDED flag, on DATA, HEADERS and PUSH_PROMISE. @type int

FLAG_PRIORITY

http.h2.frames.FLAG_PRIORITY = 32

PRIORITY flag, on HEADERS. @type int

SETTINGS_HEADER_TABLE_SIZE

http.h2.frames.SETTINGS_HEADER_TABLE_SIZE = 1

SETTINGS_HEADER_TABLE_SIZE. @type int

SETTINGS_ENABLE_PUSH

http.h2.frames.SETTINGS_ENABLE_PUSH = 2

SETTINGS_ENABLE_PUSH. @type int

SETTINGS_MAX_CONCURRENT_STREAMS

http.h2.frames.SETTINGS_MAX_CONCURRENT_STREAMS = 3

SETTINGS_MAX_CONCURRENT_STREAMS. @type int

SETTINGS_INITIAL_WINDOW_SIZE

http.h2.frames.SETTINGS_INITIAL_WINDOW_SIZE = 4

SETTINGS_INITIAL_WINDOW_SIZE. @type int

SETTINGS_MAX_FRAME_SIZE

http.h2.frames.SETTINGS_MAX_FRAME_SIZE = 5

SETTINGS_MAX_FRAME_SIZE. @type int

SETTINGS_MAX_HEADER_LIST_SIZE

http.h2.frames.SETTINGS_MAX_HEADER_LIST_SIZE = 6

SETTINGS_MAX_HEADER_LIST_SIZE. @type int

NO_ERROR

http.h2.frames.NO_ERROR = 0

NO_ERROR. @type int

PROTOCOL_ERROR

http.h2.frames.PROTOCOL_ERROR = 1

PROTOCOL_ERROR. @type int

INTERNAL_ERROR

http.h2.frames.INTERNAL_ERROR = 2

INTERNAL_ERROR. @type int

FLOW_CONTROL_ERROR

http.h2.frames.FLOW_CONTROL_ERROR = 3

FLOW_CONTROL_ERROR. @type int

SETTINGS_TIMEOUT

http.h2.frames.SETTINGS_TIMEOUT = 4

SETTINGS_TIMEOUT. @type int

STREAM_CLOSED

http.h2.frames.STREAM_CLOSED = 5

STREAM_CLOSED. @type int

FRAME_SIZE_ERROR

http.h2.frames.FRAME_SIZE_ERROR = 6

FRAME_SIZE_ERROR. @type int

REFUSED_STREAM

http.h2.frames.REFUSED_STREAM = 7

REFUSED_STREAM. @type int

CANCEL

http.h2.frames.CANCEL = 8

CANCEL. @type int

COMPRESSION_ERROR

http.h2.frames.COMPRESSION_ERROR = 9

COMPRESSION_ERROR. @type int

CONNECT_ERROR

http.h2.frames.CONNECT_ERROR = 10

CONNECT_ERROR. @type int

ENHANCE_YOUR_CALM

http.h2.frames.ENHANCE_YOUR_CALM = 11

ENHANCE_YOUR_CALM. @type int

INADEQUATE_SECURITY

http.h2.frames.INADEQUATE_SECURITY = 12

INADEQUATE_SECURITY. @type int

HTTP_1_1_REQUIRED

http.h2.frames.HTTP_1_1_REQUIRED = 13

HTTP_1_1_REQUIRED. @type int

Functions

read_u32()

http.h2.frames.read_u32(data, offset: number) -> number

Reads a 32-bit big-endian integer from data at offset.

Parameters

  • data (bytes)
  • offset (number)

Returns number

write_u32()

http.h2.frames.write_u32(out, value: number)

Appends a 32-bit big-endian integer to out.

Parameters

  • out (bytes)
  • value (number)

read_u24()

http.h2.frames.read_u24(data, offset: number) -> number

Reads a 24-bit big-endian integer from data at offset.

Parameters

  • data (bytes)
  • offset (number)

Returns number

read_frame()

http.h2.frames.read_frame(connection, max_frame_size: number) -> Frame

Reads one frame off a connection.

Padding is stripped here, so a caller never has to think about it, and a pad length that does not fit inside the frame is rejected - that mismatch is a connection error, not a frame to salvage.

Parameters

  • connection (Connection)
  • max_frame_size (number) — refuse anything larger

Returns Frame

Raises ProtocolError on a malformed or over-large frame

write_frame()

http.h2.frames.write_frame(connection, type: int, flags: int, stream_id: int, payload)

Writes one frame to a connection.

Parameters

  • connection (Connection)
  • type (int)
  • flags (int)
  • stream_id (int)
  • payload (?bytes)

encode_settings()

http.h2.frames.encode_settings(values: dict) -> bytes

Builds a SETTINGS payload from a dictionary of identifier to value.

Parameters

  • values (dict)

Returns bytes

decode_settings()

http.h2.frames.decode_settings(payload) -> dict

Parses a SETTINGS payload into a dictionary.

Unknown identifiers are kept rather than dropped - an endpoint must ignore settings it does not understand, and keeping them makes that the caller’s decision rather than a silent one here.

Parameters

  • payload (bytes)

Returns dict

Raises ProtocolError if the payload is not a whole number of six-octet entries

encode_goaway()

http.h2.frames.encode_goaway(last_stream_id: int, error_code: int, debug_message: ?string) -> bytes

Builds a GOAWAY payload.

Parameters

  • last_stream_id (int) — the highest stream this endpoint acted on
  • error_code (int)
  • debug_message (?string) — for humans reading a packet capture; never interpreted by the peer

Returns bytes

encode_error()

http.h2.frames.encode_error(error_code: int) -> bytes

Builds an RST_STREAM payload.

Parameters

  • error_code (int)

Returns bytes

Classes

Frame

class http.h2.frames.Frame

One frame, as read off the wire.

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
typeintThe frame type - one of the constants above.
flagsintThe flags byte.
stream_idintThe stream this frame belongs to; 0 for connection-level frames.
payloadbytesThe payload, with any padding already removed.

Constructor

http.h2.frames.Frame(type, flags, stream_id, payload)

Frame.has_flag()

http.h2.frames.Frame.has_flag(flag: int) -> bool

Whether flag is set.

Parameters

  • flag (int)

Returns bool

Frame.to_string()

http.h2.frames.Frame.to_string()

2026, Richard Ore and Zuri contributors

http.h2.hpack

import http

http exposes this as http.h2.hpack, so import http is enough and the names are called as http.h2.hpack.*. import http.h2.hpack reaches the same definitions directly.

HPACK, the header compression HTTP/2 uses (RFC 7541).

A header block is a sequence of representations, each of which either names an entry of a table both endpoints maintain, or spells a field out. The static table below is fixed by the RFC; the dynamic table is built as a connection runs, which is what makes the decoder stateful - a block cannot be decoded without every block that came before it on the same connection.

The encoder here uses the static table where it can and literals everywhere else, and never adds to its own dynamic table. That gives up some compression and buys a great deal: an encoder with no dynamic table cannot desynchronise from a peer’s decoder, and it cannot leak one request’s secrets into another’s compression ratio, which is the whole of the CRIME family of attacks.

Functions

encode_integer()

http.h2.hpack.encode_integer(out, value: number, prefix_bits: number, first: number)

Encodes an integer with an prefix_bits-wide prefix, per RFC 7541 §5.1. first is whatever flag bits share that first byte.

Parameters

  • out (bytes)
  • value (number)
  • prefix_bits (number)
  • first (number) — the flag bits, already shifted into place

decode_integer()

http.h2.hpack.decode_integer(data, offset: number, prefix_bits: number)

Decodes an integer with an prefix_bits-wide prefix, starting at offset.

Parameters

  • data (bytes)
  • offset (number)
  • prefix_bits (number)

Returns — list: [value, next offset]

Raises ProtocolError if the encoding runs off the end or is absurdly long

encode_string()

http.h2.hpack.encode_string(out, text: string)

Encodes a string literal, Huffman-coding it when that comes out shorter.

Parameters

  • out (bytes)
  • text (string)

decode_string()

http.h2.hpack.decode_string(data, offset: number, max_length: number)

Decodes a string literal starting at offset.

Parameters

  • data (bytes)
  • offset (number)
  • max_length (number) — refuse a literal longer than this

Returns — list: [text, next offset]

Raises ProtocolError on a truncated or over-long literal

Classes

DynamicTable

class http.h2.hpack.DynamicTable

The dynamic table half of an HPACK context: the most recently inserted fields, evicted from the far end when the table would exceed the size its owner has been told to keep.

Entries are indexed from the newest, and continue the numbering where the static table stops, so index 62 is always the most recently added entry.

Constructor

http.h2.hpack.DynamicTable(capacity)

Parameters

  • capacity (?number) — the maximum table size in bytes; defaults to HPACK’s own default of 4096

DynamicTable.add()

http.h2.hpack.DynamicTable.add(name: string, value: string)

Adds a field, evicting from the oldest end until it fits.

An entry larger than the whole table is not an error: RFC 7541 §4.4 says to empty the table and add nothing, which is what happens here.

Parameters

  • name (string)
  • value (string)

DynamicTable.get()

http.h2.hpack.DynamicTable.get(index: number)

The entry at HPACK index index, which counts the static table first.

Parameters

  • index (number)

Returns — list: [name, value]

Raises ProtocolError if the index names nothing

DynamicTable.set_capacity()

http.h2.hpack.DynamicTable.set_capacity(capacity: number)

Changes the table’s maximum size, evicting whatever no longer fits.

Parameters

  • capacity (number)

DynamicTable.capacity()

http.h2.hpack.DynamicTable.capacity() -> number

The table’s current maximum size in bytes.

Returns number

DynamicTable.size()

http.h2.hpack.DynamicTable.size() -> number

How many bytes the entries currently occupy, by HPACK’s accounting (name plus value plus 32 per entry).

Returns number

DynamicTable.length()

http.h2.hpack.DynamicTable.length() -> number

How many entries the table holds.

Returns number

Decoder

class http.h2.hpack.Decoder

Decodes header blocks for one direction of one connection.

A decoder is stateful and order-dependent: it must see every header block on its connection, in order, or its dynamic table drifts out of step with the peer’s encoder and every block after that decodes to nonsense.

Fields

FieldTypeDescription
table
max_string_lengthnumberThe largest single string literal accepted.
max_header_list_sizenumberThe largest decoded header list accepted, by HPACK’s own size accounting.

Constructor

http.h2.hpack.Decoder(capacity)

Parameters

  • capacity (?number) — the initial dynamic table size

Decoder.decode()

http.h2.hpack.Decoder.decode(block) -> list

Decodes a complete header block into a list of [name, value] pairs, in the order they appeared - which HTTP/2 depends on, as repeated fields keep their order and Set-Cookie may appear many times.

Parameters

  • block (bytes)

Returns list

Raises ProtocolError on a malformed block or one that exceeds a limit

Encoder

class http.h2.hpack.Encoder

Encodes header blocks for one direction of one connection.

Fields are matched against the static table, and anything not found there is written out as a literal without indexing - see the module note above for why the dynamic table is left empty.

Constructor

http.h2.hpack.Encoder(capacity)

Encoder.set_capacity()

http.h2.hpack.Encoder.set_capacity(capacity: number)

Notes the maximum dynamic table size the peer will accept.

Parameters

  • capacity (number)

Encoder.encode()

http.h2.hpack.Encoder.encode(fields: list) -> bytes

Encodes a list of [name, value] pairs into a header block.

Names must already be lowercase; HTTP/2 has no other kind.

Parameters

  • fields (list)

Returns bytes


2026, Richard Ore and Zuri contributors

http.h2.huffman

import http

http exposes this as http.h2.huffman, so import http is enough and the names are called as http.h2.huffman.*. import http.h2.huffman reaches the same definitions directly.

The static Huffman code HPACK uses to compress header field names and values, from RFC 7541 Appendix B.

Only the code lengths are written out below. The code itself is canonical - codes are assigned in increasing symbol order within each length - so every one of the 257 codes can be derived from its length alone, which is both far less to state and far less to get wrong. _build() does the derivation at load time and also checks the Kraft sum, which comes to exactly one for a complete prefix code and to something else the moment a single length is wrong.

Constants

EOS

http.h2.huffman.EOS: int = 256

The number of the symbol HPACK uses to pad the final byte of a Huffman-encoded string, and which must never appear as a decoded value.

Functions

encoded_length()

http.h2.huffman.encoded_length(data) -> number

The number of bytes data would occupy once Huffman-encoded.

The encoder uses this to decide whether encoding is worth it at all: HPACK lets a string go out as-is, and for a string of mostly high-entropy bytes the encoded form is the longer one.

Parameters

  • data (bytes)

Returns number

encode()

http.h2.huffman.encode(data) -> bytes

Huffman-encodes data, padding the final byte with the leading bits of the EOS code (which are all ones) as RFC 7541 §5.2 requires.

Parameters

  • data (bytes)

Returns bytes

decode()

http.h2.huffman.decode(data, start, end) -> bytes

Decodes a Huffman-encoded string.

The padding at the end must be the leading bits of EOS and no longer than seven bits; anything else is a connection-level error per RFC 7541 §5.2, since it is the shape a smuggled second header takes.

Parameters

  • data (bytes)
  • start (?number)
  • end (?number)

Returns bytes

Raises ProtocolError on invalid padding or an encoded EOS

http.headers

import http.headers

http lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled http.headers.* needs import http.headers.

Headers: a case-insensitive, order-preserving multimap, because HTTP header names do not compare case-sensitively and some headers may legitimately appear more than once.

The validation functions enforce what a name and a value may contain, which is what keeps a header value carrying a newline from splitting one response into two.

Functions

is_valid_name()

http.headers.is_valid_name(name) -> bool

Whether name is a syntactically valid HTTP field name, i.e. a non-empty RFC 9110 token.

Parameters

  • name (string)

Returns bool

is_valid_value()

http.headers.is_valid_value(value) -> bool

Whether value is a legal HTTP field value.

The rule that actually matters here is that a value may not contain CR, LF, or NUL. Letting any of those through is response splitting: a value carrying \r\n ends the field early and lets whoever supplied it inject headers, or a whole second response, into the stream.

Leading and trailing whitespace is not part of the value and is stripped before the check rather than rejected.

Parameters

  • value (string)

Returns bool

canonical_name()

http.headers.canonical_name(name: string) -> string

The conventional spelling of a field name, e.g. 'content-type' becomes 'Content-Type' and 'etag' becomes 'ETag'.

Field names are case-insensitive on the wire, so this is purely cosmetic

  • but it is the casing every other HTTP implementation emits, and some badly written clients do compare case-sensitively.

Parameters

  • name (string)

Returns string

parse()

http.headers.parse(block: string, strict: ?bool) -> Headers

Parses a raw header block - everything between a start line and the blank line that ends the head - into a Headers.

Obsolete line folding (a continuation line starting with a space) is rejected outright rather than unfolded: RFC 9112 §5.2 deprecated it, and treating it as data is the safer reading given how differently intermediaries handle it.

Parameters

  • block (string) — the header lines, without the trailing blank line
  • strict (?bool) — when true (the default), a field appearing more than once that is defined to appear at most once raises instead of being folded

Returns Headers

Raises ProtocolError on a malformed field, a folded line, or a duplicated singleton field

is_never_folded()

http.headers.is_never_folded(name: string) -> bool

Whether a field with this name must be repeated rather than folded into one comma-separated value when written out.

Parameters

  • name (string)

Returns bool

Classes

Headers

class http.Headers

An ordered, case-insensitive, multi-value collection of HTTP header fields.

Field names are matched without regard to case, so headers.get('content-type') and headers.get('Content-Type') are the same lookup, while the spelling a field was first added under is what gets written back out. Insertion order is preserved.

A field may legally appear more than once. set() replaces every occurrence, add() appends another one, get() returns the first, and get_all() returns them all:

var h = http.Headers()
h.set('Accept', 'text/html')
h.add('Accept', 'application/json')

h.get('accept')      # 'text/html'
h.get_all('accept')  # ['text/html', 'application/json']

Every name and value is validated on the way in. A name that isn’t a token, or a value carrying CR, LF or NUL, raises rather than being quietly sanitised, because a value that can inject a newline is a response-splitting bug wherever it eventually lands.

  • printable — has a @to_string(), so echo and print() show something useful
  • serializable — has a @to_json(), so it can be handed straight to json.encode()

Constructor

http.Headers(fields)

Builds a Headers collection, optionally seeded from a dictionary of name -> value (or name -> [value, ...]) pairs.

Parameters

  • fields (?dict)

Headers.get()

http.Headers.get(name: string, fallback) -> any

The first value recorded for name, or fallback (default nil) when the field is absent.

Parameters

  • name (string)
  • fallback (?any)

Returns any

Headers.get_all()

http.Headers.get_all(name: string) -> list

Every value recorded for name, in the order they were added, or an empty list when the field is absent.

Parameters

  • name (string)

Returns list

Headers.get_joined()

http.Headers.get_joined(name: string, fallback) -> any

All values for name joined with ', ' - the form RFC 9110 §5.3 defines as equivalent to repeating the field - or fallback when the field is absent.

Never use this for Set-Cookie; commas are legal inside a cookie value and joining them corrupts the result. get_all() is the right call there.

Parameters

  • name (string)
  • fallback (?any)

Returns any

Headers.set()

http.Headers.set(name: string, value)

Replaces every value of name with value. The name keeps whatever spelling it is given here.

Parameters

  • name (string)
  • value (string|number|bool)

Returns — Headers: this same instance, for chaining

Raises ProtocolError if the name is not a token or the value contains CR, LF, or NUL

Headers.add()

http.Headers.add(name: string, value)

Records another value for name, keeping any already there.

Parameters

  • name (string)
  • value (string|number|bool)

Returns — Headers: this same instance, for chaining

Raises ProtocolError if the name is not a token or the value contains CR, LF, or NUL

Headers.set_default()

http.Headers.set_default(name: string, value)

Sets name to value only if the field is not already present. Used throughout this module for defaults a caller is free to override - Date, Server, User-Agent and friends.

Parameters

  • name (string)
  • value (string|number|bool)

Returns — Headers: this same instance, for chaining

Headers.remove()

http.Headers.remove(name: string)

Removes every value of name. Removing an absent field does nothing rather than failing.

Parameters

  • name (string)

Returns — Headers: this same instance, for chaining

Headers.contains()

http.Headers.contains(name: string) -> bool

Whether name is present at all.

Parameters

  • name (string)

Returns bool

Headers.contains_token()

http.Headers.contains_token(name: string, token: string) -> bool

Whether name is present and one of its comma-separated tokens equals token, compared case-insensitively.

This is the correct way to ask about the list-valued fields that drive protocol decisions - Connection: keep-alive, Upgrade, Transfer-Encoding: gzip, chunked - where a naive substring test would happily match keep-alive-ish and a naive equality test would miss the second token entirely.

Parameters

  • name (string)
  • token (string)

Returns bool

Headers.tokens()

http.Headers.tokens(name: string) -> list

Every comma-separated token across every occurrence of name, lowercased and trimmed, in order. Empty elements are dropped, as RFC 9110 §5.6.1 permits.

Parameters

  • name (string)

Returns list

Headers.extend()

http.Headers.extend(other)

Copies every field from other into this collection, replacing fields of the same name. other may be another Headers or a plain dictionary whose values are strings or lists of strings.

Parameters

  • other (Headers|dict)

Returns — Headers: this same instance, for chaining

Headers.each()

http.Headers.each(callback)

Calls callback(name, value) once per field occurrence, in insertion order - so a field present three times triggers three calls, which is exactly what writing the block back out needs.

Parameters

  • callback (function(2))

Headers.names()

http.Headers.names() -> list

The field names present, in insertion order and in the spelling they were added under.

Returns list

Headers.length()

http.Headers.length() -> number

The number of distinct field names present. A field repeated three times counts once.

Returns number

Headers.is_empty()

http.Headers.is_empty() -> bool

Whether there are no fields at all.

Returns bool

Headers.size()

http.Headers.size() -> number

The number of bytes this block would occupy once serialised, counting the ': ' and CRLF around every value. Servers use this to enforce a header-size limit as fields arrive.

Returns number

Headers.clear()

http.Headers.clear()

Drops every field. Cheaper than building a new instance when a connection is being reused for the next message.

Returns — Headers: this same instance, for chaining

Headers.clone()

http.Headers.clone() -> Headers

An independent copy. Mutating the copy never affects the original.

Returns Headers

Headers.canonicalize()

http.Headers.canonicalize()

Rewrites every field name to its conventional casing, e.g. content-type to Content-Type. Purely cosmetic; names remain case-insensitive either way.

Returns — Headers: this same instance, for chaining

Headers.strip_hop_by_hop()

http.Headers.strip_hop_by_hop()

Removes the hop-by-hop fields listed in RFC 9110 §7.6.1, which describe one connection rather than the message itself and must never be forwarded on to another connection. The set includes anything named by this message’s own Connection field, which is how an endpoint declares an extra hop-by-hop field.

A reverse proxy that forgets this is how a client-supplied Transfer-Encoding reaches an upstream that frames it differently - the classic smuggling setup.

Returns — Headers: this same instance, for chaining

Headers.to_wire()

http.Headers.to_wire() -> string

Serialises the block in wire format: Name: value\r\n per occurrence, with no terminating blank line (the caller decides where the block ends, since a trailer section ends differently to a head).

Returns string

Headers.to_dict()

http.Headers.to_dict() -> dict

The block as a plain dictionary of name -> value, folding repeated fields into a list. Handy for logging and for handing headers to code that has no reason to know about this class.

Returns dict

Headers.to_string()

http.Headers.to_string()

2026, Richard Ore and Zuri contributors

http.middleware

import http

http exposes this as http.middleware, so import http is enough and the names are called as http.middleware.*. import http.middleware reaches the same definitions directly.

The middleware every public HTTP service ends up needing: CORS, access logging, the security headers a browser acts on, authentication, and a request rate limit.

Each function here returns a middleware - a function of (request, response, next)

  • ready to hand to HttpServer.use():
import http
import http.middleware

var server = http.server(3000)

server.use(middleware.logger())
server.use(middleware.security_headers())
server.use(middleware.cors({ origins: ['https://app.example.com'] }))

Order matters: middleware run outermost first, so a logger added first sees the final status of everything added after it.

Functions

cors()

http.middleware.cors(options: ?dict) -> function(3)

Answers CORS preflights and adds the cross-origin headers a browser needs before it will let script read a response from another origin.

origins is the list of origins allowed, matched exactly. The literal '*' allows any origin, but only for requests that carry no credentials: a wildcard together with Access-Control-Allow-Credentials is rejected by every browser, so setting credentials reflects the request’s own origin instead - which means the origins list becomes the only thing standing between an attacker’s page and an authenticated response, and it needs to be an explicit list rather than a wildcard.

Parameters

  • options (?dict) — origins (default ['*']), methods, headers (allowed request headers; defaults to reflecting what was asked for), expose (response headers script may read), credentials (default false), max_age (seconds a preflight may be cached, default 86400)

Returns function(3)

logger()

http.middleware.logger(options: ?dict) -> function(3)

Writes one line per request once the response is finished.

The default format is the Common Log Format extended with the response time, which every log analyser already understands. Pass format to build the line yourself, or sink to send it somewhere other than standard output.

Parameters

  • options (?dict) — sink (a function taking the line), format (a function taking (request, response, milliseconds) and returning the line), trust_proxy (whether to log the forwarded client address rather than the peer)

Returns function(3)

security_headers()

http.middleware.security_headers(options: ?dict) -> function(3)

Adds the response headers a browser acts on to harden a page.

Every one of them is opt-out, because the right value depends on the application:

HeaderDefaultWhat it does
X-Content-Type-Optionsnosniffstops the browser second-guessing a declared content type
X-Frame-OptionsDENYrefuses to be framed, which is what clickjacking needs
Referrer-Policystrict-origin-when-cross-originkeeps paths and queries out of outbound referrers
Strict-Transport-Securityone yearonly sent over HTTPS, where it is meaningful
Content-Security-Policynot settoo application-specific to guess at

Parameters

  • options (?dict) — any of the header values above by lowercase name (content_security_policy, frame_options, referrer_policy, hsts_max_age, hsts_subdomains, content_type_options), each nil to omit

Returns function(3)

basic_auth()

http.middleware.basic_auth(verify, realm: ?string) -> function(3)

Requires HTTP Basic authentication.

verify is called with the username and password and returns whether they are acceptable; it should compare secrets with http.util.secure_equals() rather than ==, so that a wrong guess and a nearly-right one take the same time.

A nil verifier disables the middleware: it passes every request straight through rather than refusing one. That makes blanking the verifier a way to switch authentication off without unpicking the chain around it.

Parameters

  • verify (?function(2)) — nil to disable
  • realm (?string) — the realm named in the challenge; defaults to 'Restricted'

Returns function(3)

bearer_auth()

http.middleware.bearer_auth(verify, realm: ?string) -> function(3)

Requires a bearer token.

A nil verifier disables the middleware, as with basic_auth().

Parameters

  • verify (?function(1)) — called with the token; returns whether it is acceptable, or a value to attach to request.context as 'user'. nil to disable
  • realm (?string)

Returns function(3)

parse_basic()

http.middleware.parse_basic(header: string)

Parses an HTTP Basic Authorization header into a username and password.

Parameters

  • header (string)

Returns — ?list: [username, password], or nil if the header is not a well-formed Basic credential

parse_bearer()

http.middleware.parse_bearer(header: string) -> ?string

Parses a Bearer Authorization header into its token.

Parameters

  • header (string)

Returns ?string

jwt_auth()

http.middleware.jwt_auth(verifier, options: ?dict) -> function(3)

Requires a valid JSON Web Token, verified by the jwt module.

verifier is either a jwt.Verifier, or any function taking the token and returning its claims:

import http.middleware
import jwt

server.use(middleware.jwt_auth(
  jwt.Verifier(secret, { algorithms: ['HS256'], audience: 'api' })
))

The function form is what covers a key set resolved by kid, or anything else the jwt module can do that a fixed verifier cannot:

server.use(middleware.jwt_auth(@(token) {
  return jwt.verify_with_jwks(token, keys, { audience: 'api' })
}))

On success the claims land on request.context['claims'], and the sub claim - the usual place an issuer puts the account the token speaks for

  • on request.context['user'].

Failures follow RFC 6750 §3: a request with no token is answered 401 with a bare Bearer challenge, one whose token does not verify is answered 401 with error="invalid_token", and one whose token is valid but lacks a required scope is answered 403 with error="insufficient_scope".

A nil verifier disables the middleware: every request passes straight through, unauthenticated. Nothing here raises, so registering it never needs a catch block around it.

Parameters

  • verifier (Verifier|function(1)|nil) — nil to disable
  • options (?dict) — realm (default 'api'), optional (attach the claims when a valid token is present but do not refuse a request without one), and scopes (a list every token must carry)

Returns function(3)

Note: Like HttpRequest.validate(), this deliberately does not import the jwt module. The verifier is built by the caller, which keeps the token format entirely the application’s business and means a server that authenticates nothing never pays to load it.

rate_limit()

http.middleware.rate_limit(options: ?dict) -> function(3)

Limits how many requests one client may make in a window of time.

The counter lives in memory, so it is per worker: with workers isolates the effective limit is limit times workers. That is a deliberate trade - a shared counter would need shared state, and this is meant to blunt a runaway client rather than to meter billing.

Parameters

  • options (?dict) — limit (requests per window, default 60), window (seconds, default 60), key (a function of the request returning the bucket key; defaults to the client address), trust_proxy

Returns function(3)

request_id()

http.middleware.request_id(header: ?string) -> function(3)

Attaches a unique identifier to every request, echoing back one the client supplied so a trace can be followed across services.

The identifier lands on request.context['request_id'] and in the response’s X-Request-Id.

Parameters

  • header (?string) — the header to read and write; defaults to 'X-Request-Id'

Returns function(3)

force_https()

http.middleware.force_https(options: ?dict) -> function(3)

Sends every request that arrived over cleartext to the same URL over HTTPS.

Parameters

  • options (?dict) — status (default 308, which preserves the method), port (the HTTPS port, when it is not 443)

Returns function(3)

etag()

http.middleware.etag(min_size: ?number) -> function(3)

Computes a weak ETag over a finished response body and answers 304 Not Modified when the client already has that version.

A handler that already set its own ETag is left alone - it knows something about the resource that hashing the bytes does not.

Parameters

  • min_size (?number) — bodies smaller than this are not tagged; defaults to 128 bytes

Returns function(3)


2026, Richard Ore and Zuri contributors

http.multipart

import http.multipart

http lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled http.multipart.* needs import http.multipart.

multipart/form-data, in both directions.

MultipartData and UploadedFile are what a parsed upload arrives as; MultipartBuilder constructs one for sending. Parsing streams rather than buffering, so an upload larger than memory is still an upload.

Functions

parse()

http.multipart.parse(body, boundary: string, options: ?dict) -> MultipartData

Parses a multipart/form-data body (RFC 7578).

The boundary comes from the Content-Type header’s boundary parameter and must be given; a body cannot be parsed without it.

Parameters

  • body (bytes)
  • boundary (string)
  • options (?dict) — max_parts (default 1000) and max_file_size (default: unlimited)

Returns MultipartData

Raises ProtocolError if the body is not a well-formed multipart message

Raises TooLargeError if a limit is exceeded

Classes

UploadedFile

class http.UploadedFile

One file received in a multipart/form-data body.

The content is held in memory. HttpServer’s max_body_size is what bounds how much that can be, so a service accepting large uploads should raise that limit deliberately rather than by accident.

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
namestringThe form field name the file arrived under.
filename?stringThe filename the client claimed, exactly as sent.
content_typestringThe Content-Type the client declared for the file, or 'application/octet-stream' when it declared none.
headersHeadersEvery header that came with this part.
contentbytesThe file’s contents.

Constructor

http.UploadedFile(name, filename, content_type, headers, content)

UploadedFile.size()

http.UploadedFile.size() -> number

The size of the uploaded content in bytes.

Returns number

UploadedFile.safe_name()

http.UploadedFile.safe_name() -> string

The client’s filename reduced to a single path-safe segment: any directory component is dropped, and anything outside letters, digits, ., - and _ becomes an underscore. A name that reduces to nothing, or to a leading run of dots, comes back as 'unnamed'.

Returns string

UploadedFile.save_to()

http.UploadedFile.save_to(path: string)

Writes the content to path.

Parameters

  • path (string)

Returns — number: the number of bytes written

UploadedFile.to_text()

http.UploadedFile.to_text() -> string

The content decoded as UTF-8 text.

Returns string

UploadedFile.to_string()

http.UploadedFile.to_string()

MultipartData

class http.MultipartData

The result of parsing a multipart/form-data body.

Fields

FieldTypeDescription
fieldsdictPlain form values, as name -> [value, ...].
filesdictUploaded files, as name -> [UploadedFile, ...].

Constructor

http.MultipartData()

MultipartData.field()

http.MultipartData.field(name: string, fallback) -> any

The first value submitted for the field name, or fallback.

Parameters

  • name (string)
  • fallback (?any)

Returns any

MultipartData.file()

http.MultipartData.file(name: string) -> ?UploadedFile

The first file submitted under name, or nil.

Parameters

  • name (string)

Returns ?UploadedFile

MultipartData.to_dict()

http.MultipartData.to_dict() -> dict

The plain fields as a flat name -> value dictionary, keeping the first value of any repeated name. Convenient when a form is known not to repeat names.

Returns dict

MultipartBuilder

class http.MultipartBuilder

Builds a multipart/form-data request body.

The boundary is generated from the platform’s secure random source rather than a counter or a timestamp, since a boundary a peer can predict is a boundary a peer can inject into a field value and thereby forge extra parts.

var form = http.MultipartBuilder()
form.add_field('title', 'Holiday')
form.add_file('photo', 'beach.jpg', photo_bytes, 'image/jpeg')

client.post(url, form.build(), { 'Content-Type': form.content_type() })

Constructor

http.MultipartBuilder(boundary)

MultipartBuilder.content_type()

http.MultipartBuilder.content_type() -> string

The Content-Type header value this body must be sent with, boundary parameter included.

Returns string

MultipartBuilder.boundary()

http.MultipartBuilder.boundary() -> string

The boundary string in use.

Returns string

MultipartBuilder.add_field()

http.MultipartBuilder.add_field(name: string, value)

Adds a plain form field.

Parameters

  • name (string)
  • value (string|number|bool)

Returns — MultipartBuilder: this same instance, for chaining

MultipartBuilder.add_file()

http.MultipartBuilder.add_file(name: string, filename: string, content, content_type: ?string)

Adds a file part.

A filename that is not plain ASCII is additionally sent as an RFC 5987 filename* parameter, which is how a non-ASCII name survives the trip; the plain filename is kept alongside it for recipients that only understand that one.

Parameters

  • name (string) — the form field name
  • filename (string)
  • content (bytes|string)
  • content_type (?string) — defaults to 'application/octet-stream'

Returns — MultipartBuilder: this same instance, for chaining

MultipartBuilder.build()

http.MultipartBuilder.build() -> bytes

Serialises every part added so far into a complete body.

Returns bytes

MultipartBuilder.length()

http.MultipartBuilder.length() -> number

How many parts have been added.

Returns number


2026, Richard Ore and Zuri contributors

http.negotiate

import http

http exposes this as http.negotiate, so import http is enough and the names are called as http.negotiate.*. import http.negotiate reaches the same definitions directly.

Content negotiation: choosing what to send when the client has said what it prefers.

Accept, Accept-Encoding and Accept-Language all use the same quality-value grammar, which parse_accept() reads and best_match() scores against what the server can actually produce.

Functions

parse_accept()

http.negotiate.parse_accept(header)

Parses an Accept, Accept-Encoding, Accept-Language or Accept-Charset header into entries, most preferred first.

Ordering follows RFC 9110 §12.5.1: by quality weight descending, then by specificity, then by the order they were written. An entry with q=0 is kept rather than dropped, because q=0 is an explicit refusal (“anything but this”) and a caller needs to see it to honour it.

Parameters

  • header (?string)

Returns — list: AcceptEntry, most preferred first

quality_of()

http.negotiate.quality_of(header, candidate: string) -> number

The quality weight header assigns to candidate, honouring wildcards.

Returns -1 when the header names nothing that covers candidate at all, which is different from a weight of 0 (an explicit refusal) and lets a caller apply the different defaults the two cases call for.

Parameters

  • header (?string)
  • candidate (string)

Returns number

best_match()

http.negotiate.best_match(header, available: list) -> ?string

Picks the entry of available the client would most like, or nil when it would accept none of them.

available is in server preference order, which decides ties - the usual and correct behaviour, since the server is the one that knows which representation is cheapest or best.

negotiate.best_match(request.header('accept'), ['application/json', 'text/html'])

Parameters

  • header (?string)
  • available (list)

Returns ?string

preferred_encoding()

http.negotiate.preferred_encoding(header, available: list) -> ?string

Picks a content coding for a response, given the request’s Accept-Encoding.

identity - sending the body uncompressed - is acceptable unless the client explicitly refused it, so this falls back to 'identity' rather than to nil whenever it can, and only returns nil when the client has refused identity too and offered nothing else this server has.

Parameters

  • header (?string)
  • available (list) — the codings the server can produce, in preference order

Returns ?string

preferred_language()

http.negotiate.preferred_language(header, available: list) -> ?string

Picks a language from available using the request’s Accept-Language.

A tag matches a request for its prefix, so en-GB satisfies a request for en (RFC 4647’s basic filtering). The reverse is not true: a request for en-GB is not satisfied by plain en, though en will still be picked if nothing better is on offer.

Parameters

  • header (?string)
  • available (list)

Returns ?string

names_explicitly()

http.negotiate.names_explicitly(header, value: string) -> bool

Whether header names value outright rather than covering it with a wildcard.

This is the difference between a client that asked for JSON and one that sent Accept: followed by a bare wildcard and would take anything - a distinction that matters when deciding what to give a client that expressed no real preference.

Parameters

  • header (?string)
  • value (string)

Returns bool

Classes

AcceptEntry

class http.negotiate.AcceptEntry

One entry of an Accept-style header: the value, its quality weight, and any other parameters it carried.

Fields

FieldTypeDescription
valuestringThe value itself, lowercased - a media type, a coding, a language tag, a charset.
qualitynumberThe quality weight from the q parameter, 0.0 to 1.0.
paramsdictAny parameters other than q, e.g. the level of an old text/html;level=1.

Constructor

http.negotiate.AcceptEntry(value, quality, params)

AcceptEntry.specificity()

http.negotiate.AcceptEntry.specificity() -> number

How specific this entry is, used to break ties between entries of equal quality: an exact type outranks a subtype wildcard, which in turn outranks the bare wildcard.

Returns number

AcceptEntry.to_string()

http.negotiate.AcceptEntry.to_string()

2026, Richard Ore and Zuri contributors

http.proxy

import http.proxy

http lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled http.proxy.* needs import http.proxy.

ReverseProxy forwards a request to another server and streams the response back; LoadBalancer spreads those forwards across several backends and stops sending to one that is failing.

Hop-by-hop headers are stripped on the way through, and forwarding headers are added, so the backend can still tell who the original client was.

Classes

ReverseProxy

class http.ReverseProxy

A reverse proxy: takes a request this server received and passes it to an upstream, then passes the upstream’s response back.

var api = http.ReverseProxy('http://127.0.0.1:9000')

server.any('/api/' + '*path', @(request, response) {
  api.handle(request, response)
})

Hop-by-hop headers are stripped in both directions, the forwarding headers an upstream needs to know who the real client is are added, and the response body is streamed rather than buffered - a proxy that holds a whole response in memory is a proxy that falls over on the first large download.

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
upstreamstringThe upstream base URL, e.g. 'http://127.0.0.1:9000'.
strip_prefix?stringA path prefix stripped from the incoming request before it is passed upstream, so /api/users can reach an…
host_header?stringWhat to send upstream as Host.
forward_headersboolWhether to add X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Host and RFC 7239’s Forwarded.
trust_proxyboolWhether this proxy itself sits behind another one, and so should extend the forwarding chain it was given…
timeoutnumberHow long to wait for the upstream, in milliseconds.
on_request?functionCalled with (request, upstream_request) before the request goes out, to adjust it.
on_response?functionCalled with (request, upstream_response) once the upstream’s head has arrived and before it is passed on.

Constructor

http.ReverseProxy(upstream: string, options: ?dict)

Parameters

  • upstream (string) — the base URL to forward to
  • options (?dict) — any of the fields above, plus client to supply an HttpClient of your own

ReverseProxy.handle()

http.ReverseProxy.handle(request, response)

Forwards request upstream and writes the upstream’s response into response.

A connection failure becomes 502 Bad Gateway and a timeout 504 Gateway Timeout, which is what those statuses are for - they tell the client the failure was between this server and the next, not in the request.

Parameters

  • request (HttpRequest)
  • response (HttpResponse)

ReverseProxy.to_string()

http.ReverseProxy.to_string()

LoadBalancer

class http.LoadBalancer

Balances requests across several upstreams.

Selection is round-robin, and an upstream that fails is taken out of rotation for recovery_time seconds rather than retried on every request - which is what stops one dead backend from adding its full connect timeout to a share of all traffic.

var pool = http.LoadBalancer([
  'http://10.0.0.1:9000',
  'http://10.0.0.2:9000',
])

server.any('/' + '*path', @(request, response) {
  pool.handle(request, response)
})
  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
recovery_timenumberHow long a failed upstream stays out of rotation, in seconds.
max_attemptsnumberHow many upstreams to try before giving up on a request.

Constructor

http.LoadBalancer(upstreams: list, options: ?dict)

Parameters

  • upstreams (list) — base URLs
  • options (?dict) — passed to every ReverseProxy, plus recovery_time and max_attempts

LoadBalancer.handle()

http.LoadBalancer.handle(request, response)

Forwards request to the next healthy upstream.

Parameters

  • request (HttpRequest)
  • response (HttpResponse)

LoadBalancer.healthy_count()

http.LoadBalancer.healthy_count() -> number

How many upstreams are currently in rotation.

Returns number

LoadBalancer.to_string()

http.LoadBalancer.to_string()

2026, Richard Ore and Zuri contributors

http.request

import http.request

http lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled http.request.* needs import http.request.

HttpRequest: one inbound request, with its method, target, headers and body, plus the query string and route parameters already parsed.

The query-string functions are separate because they are useful on their own: build_query_string() is what constructs a URL to request, not only what reads one.

Functions

parse_query_string()

http.parse_query_string(text) -> dict

Decodes an application/x-www-form-urlencoded string - a query string, or a form body - into name -> [value, ...].

+ decodes to a space and %xx to the byte it names, both of which apply to names as well as values. A parameter with no = maps to an empty string rather than to nil, matching what every server-side form API does with ?debug.

Parameters

  • text (?string)

Returns dict

build_query_string()

http.build_query_string(parameters: dict) -> string

Encodes parameters as an application/x-www-form-urlencoded string. Values may be lists, in which case the name repeats.

Parameters

  • parameters (dict)

Returns string

Classes

HttpRequest

class http.HttpRequest

An HTTP request, in both directions.

On the server side an instance arrives fully parsed: method, path, query, headers, cookies and body are all populated, with form(), json_body() and files() decoding the body on first use according to its Content-Type.

On the client side, HttpClient builds one of these for every request it sends, so anything you can inspect on the server you can also set before sending.

  • printable — has a @to_string(), so echo and print() show something useful
  • serializable — has a @to_json(), so it can be handed straight to json.encode()

Fields

FieldTypeDescription
methodstringThe request method, uppercased: 'GET', 'POST', and so on.
targetstringThe request target exactly as it appeared on the request line, before any decoding - '/search?q=a%20b'.
pathstringThe path, percent-decoded and with its . and .. segments resolved.
query_stringstringThe query string, without the leading ?, or ''.
querydictThe decoded query parameters, as name -> [value, ...].
versionstringThe protocol version: '1.0', '1.1', '2' or '3'.
headersHeadersThe request header fields.
cookiesdictThe cookies the client sent, as name -> value.
paramsdictPath parameters captured by the route that matched, e.g. id for a route registered as /users/:id.
bodybytesThe request body.
remote_address?stringThe address of the peer at the other end of the socket.
secureboolWhether the request arrived over TLS.
schemestringThe scheme the request was made with: 'http' or 'https'.
authority?stringThe authority the request was addressed to, from HTTP/2’s :authority or HTTP/1.1’s Host header, port…
contextdictFree-form storage for whatever middleware wants to attach to a request - a resolved user, a request id, a…
body_reader?BodyReaderThe reader for the body, when the route asked to stream it rather than have it materialised.

Constructor

http.HttpRequest(method, target, headers)

Parameters

  • method (?string)
  • target (?string)
  • headers (?Headers)

HttpRequest.set_target()

http.HttpRequest.set_target(target: string)

Sets the request target and re-derives path, query_string and query from it.

The path is percent-decoded and then normalised, in that order. Doing it the other way round is the classic traversal hole: %2e%2e%2f looks like an ordinary segment until it is decoded.

Parameters

  • target (string)

HttpRequest.header()

http.HttpRequest.header(name: string, fallback) -> any

A request header value, or fallback when absent.

Parameters

  • name (string)
  • fallback (?any)

Returns any

HttpRequest.query_param()

http.HttpRequest.query_param(name: string, fallback) -> any

The first value of the query parameter name, or fallback.

Parameters

  • name (string)
  • fallback (?any)

Returns any

HttpRequest.param()

http.HttpRequest.param(name: string, fallback) -> any

A path parameter captured by the matched route, or fallback.

Parameters

  • name (string)
  • fallback (?any)

Returns any

HttpRequest.cookie()

http.HttpRequest.cookie(name: string, fallback) -> any

A cookie value, or fallback.

Parameters

  • name (string)
  • fallback (?any)

Returns any

HttpRequest.session()

http.HttpRequest.session() -> Session

This request’s session.

The session is put here by http.session.session(), so a server that never registered that middleware has none, and asking for one says so rather than handing back nil for a handler to misread as an empty session.

request.session().set('account', account.id)

Returns Session

Raises SessionError if the session middleware is not registered

HttpRequest.content_type()

http.HttpRequest.content_type() -> ?string

The media type of the body, without parameters, or nil.

Returns ?string

HttpRequest.content_length()

http.HttpRequest.content_length() -> number

The declared body length, or -1 when the request carries no Content-Length (which for a chunked body it will not).

Returns number

HttpRequest.text()

http.HttpRequest.text() -> string

The body decoded as UTF-8 text.

Returns string

HttpRequest.json_body()

http.HttpRequest.json_body() -> any

The body parsed as JSON.

Parses once and caches, so calling this in both a middleware and a handler costs one parse.

Returns any

Raises HttpError if the body is not valid JSON

HttpRequest.form()

http.HttpRequest.form() -> dict

The submitted form fields, as name -> value, from either an application/x-www-form-urlencoded or a multipart/form-data body. A body of neither type gives an empty dictionary rather than raising.

Repeated names keep their first value here; use form_all() when a field can legitimately repeat.

Returns dict

HttpRequest.form_all()

http.HttpRequest.form_all() -> dict

The submitted form fields as name -> [value, ...], keeping every value of a repeated name.

Returns dict

HttpRequest.form_field()

http.HttpRequest.form_field(name: string, fallback) -> any

A single form field’s first value, or fallback.

Parameters

  • name (string)
  • fallback (?any)

Returns any

HttpRequest.files()

http.HttpRequest.files() -> dict

The files uploaded in a multipart/form-data body, as name -> [UploadedFile, ...]. Empty for any other body type.

Returns dict

HttpRequest.file()

http.HttpRequest.file(name: string) -> ?UploadedFile

The first file uploaded under name, or nil.

Parameters

  • name (string)

Returns ?UploadedFile

HttpRequest.input()

http.HttpRequest.input(source: ?string) -> dict

The request’s input as one dictionary, ready to hand to a validator.

With no argument, three sources are merged, each overriding the one before it:

  1. route parameters, from the pattern that matched 2. query string parameters 3. the body - a JSON object’s keys, or the submitted form fields

Pass source to take exactly one of them instead: 'params', 'query' or 'body'.

A query or form field is a list on the wire, since a name may legally repeat. A name carrying exactly one value is flattened to that value here, so a rule expecting a string sees a string; a name carrying several stays a list. A field that must always be a list, however many values arrived, is better read through form_all() or query directly.

Uploaded files are not included - nothing a schema can say about a file is expressible as a rule over its bytes. Reach them with file() and check them by hand.

A JSON body that is not an object (an array, a bare string) contributes nothing, since there are no names to merge; read it with json_body() instead.

Parameters

  • source (?string) — 'params', 'query', 'body', or 'all' (the default)

Returns dict

Raises ValueError if source names something else

HttpRequest.validate()

http.HttpRequest.validate(schema, source: ?string)

Validates the request’s input against schema and returns the data that was validated.

schema is anything with a check_or_raise() method - a validate.Schema in practice:

import validate

var create_user = validate.schema({
  name:  validate.required().string().max_length(100),
  email: validate.required().string().email(),
})

server.post('/users', @(request, response) {
  var data = request.validate(create_user)
  response.json(create_account(data), 201)
})

Failure raises the schema’s own error - validate.ValidationError

  • carrying an errors list of { field, message }. Catch it to turn it into whatever your API answers with:
catch {
  var data = request.validate(create_user)
  response.json(create_account(data), 201)
} as error {
  response.json({ errors: create_user.group_errors(error.errors) }, 422)
}

Parameters

  • schema (Schema)
  • source (?string) — as input(); defaults to 'all'

Returns — dict: the input that was validated

Raises ValidationError when the input does not satisfy the schema

Raises TypeError if schema is not an object that can validate

Note: This deliberately does not import the validate module. Any object exposing check_or_raise(data) works, and a server that never validates anything never pays to load it.

HttpRequest.host()

http.HttpRequest.host() -> ?string

The host the request was addressed to, without any port.

Returns ?string

HttpRequest.port()

http.HttpRequest.port() -> number

The port the request was addressed to, from the authority, or the scheme’s default when the authority named none.

Returns number

HttpRequest.url()

http.HttpRequest.url() -> string

The full absolute URL this request names.

Returns string

HttpRequest.client_ip()

http.HttpRequest.client_ip(trust_proxy: ?bool, trusted: ?list) -> ?string

The originating client’s IP address.

Directly served, that is simply the peer’s address. Behind a reverse proxy the peer is the proxy, and the real client is in X-Forwarded-For or RFC 7239’s Forwarded - but those are request headers, which is to say anyone can write anything in them.

So they are only consulted when trust_proxy says to, and the value taken is the rightmost entry that is not itself one of trusted, walking in from the proxy end. Taking the leftmost entry - the common shortcut - hands an attacker whatever client IP they care to claim, which matters the moment an IP is used for rate limiting, allowlisting or audit.

Parameters

  • trust_proxy (?bool) — whether to consult forwarding headers at all; defaults to false
  • trusted (?list) — proxy addresses to skip over when walking the chain

Returns ?string

HttpRequest.accepts()

http.HttpRequest.accepts(type: string) -> bool

Whether the client would accept a response of media type type, per its Accept header. A request with no Accept accepts anything.

Parameters

  • type (string)

Returns bool

HttpRequest.wants_json()

http.HttpRequest.wants_json() -> bool

Whether the client would rather have JSON than HTML - the usual way to decide whether an error should be rendered as a page or returned as an object.

Returns bool

HttpRequest.bearer_token()

http.HttpRequest.bearer_token() -> ?string

The token from an Authorization: Bearer ... header, or nil when there is no such header or it carries a different scheme.

Returns ?string

HttpRequest.is_ajax()

http.HttpRequest.is_ajax() -> bool

Whether the request was made by client-side script, as reported by the X-Requested-With header that the major JavaScript libraries set.

Returns bool

HttpRequest.is_upgrade()

http.HttpRequest.is_upgrade(protocol: ?string) -> bool

Whether the request asks to switch to another protocol - a WebSocket handshake, or an HTTP/2 upgrade.

Parameters

  • protocol (?string) — check for one specific protocol

Returns bool

HttpRequest.expects_continue()

http.HttpRequest.expects_continue() -> bool

Whether the client asked the server to acknowledge before it sends the body (Expect: 100-continue).

Returns bool

HttpRequest.is_safe()

http.HttpRequest.is_safe() -> bool

Whether this method is defined to be safe: read-only, with no side effects the client is responsible for (RFC 9110 §9.2.1).

Returns bool

HttpRequest.is_idempotent()

http.HttpRequest.is_idempotent() -> bool

Whether this method is idempotent - repeating it has the same effect as making it once (RFC 9110 §9.2.2). This is what decides whether a client may retry a request after a connection failure.

Returns bool

HttpRequest.to_wire()

http.HttpRequest.to_wire() -> string

The full request head in wire format, as it would be sent on an HTTP/1.1 connection. Useful for logging and for debugging what actually went out.

Returns string

HttpRequest.to_string()

http.HttpRequest.to_string()

HttpRequest.to_json()

http.HttpRequest.to_json()

2026, Richard Ore and Zuri contributors

http.response

import http.response

http lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled http.response.* needs import http.response.

HttpResponse: one response, whether it is being built by a handler or read back from a server.

It carries the status, the headers and the body, and the helpers on it cover what a handler nearly always wants: JSON, a file, a redirect, or a stream written a chunk at a time.

Classes

HttpResponse

class http.HttpResponse

An HTTP response, in both directions: the thing a server builds and sends, and the thing a client receives and reads.

On the server side the useful surface is the writers - text(), json(), html(), file(), redirect(), render() - each of which sets a sensible Content-Type alongside the body, plus set_cookie() and the caching helpers.

On the client side it is as_text(), as_dict(), is_ok() and raise_for_status().

A body can be bytes held in memory, a file streamed from disk, or a callback that produces bytes as it goes. The last two matter for a server that has to serve something larger than it would like to hold in memory; see file() and stream().

  • printable — has a @to_string(), so echo and print() show something useful
  • serializable — has a @to_json(), so it can be handed straight to json.encode()

Fields

FieldTypeDescription
statusnumberThe status code.
reason?stringThe reason phrase.
versionstringThe protocol version this response was received over, or will be sent with: '1.0', '1.1', '2' or '3'.
headersHeadersThe response header fields.
bodybytesThe body, as bytes.
cookieslistCookies to be sent with this response.
time_takennumberHow long the request that produced this response took, in milliseconds.
redirectsnumberHow many redirects were followed to reach it.
responder?stringThe URL that finally answered, which differs from the requested one whenever a redirect was followed.
certificate?PeerCertificateThe peer’s TLS certificate, on a response received over HTTPS.
body_reader?BodyReaderThe reader for the body, when the client was asked to stream the response rather than materialise it.
request_method?stringThe method of the request this answers.

Constructor

http.HttpResponse(body, status, headers)

Parameters

  • body (?any) — bytes or a string to start the body with
  • status (?number) — defaults to 200
  • headers (?Headers)

HttpResponse.write()

http.HttpResponse.write(data)

Appends to the response body.

Parameters

  • data (string|bytes)

Returns — HttpResponse: this same instance, for chaining

HttpResponse.text()

http.HttpResponse.text(content: string, status: ?number)

Replaces the body with content and sets Content-Type to text/plain; charset=utf-8.

Parameters

  • content (string)
  • status (?number)

Returns — HttpResponse: this same instance, for chaining

HttpResponse.html()

http.HttpResponse.html(content: string, status: ?number)

Replaces the body with content and sets Content-Type to text/html; charset=utf-8.

Parameters

  • content (string)
  • status (?number)

Returns — HttpResponse: this same instance, for chaining

HttpResponse.json()

http.HttpResponse.json(data, status: ?number)

Replaces the body with the JSON encoding of data and sets Content-Type to application/json.

Parameters

  • data (any)
  • status (?number)

Returns — HttpResponse: this same instance, for chaining

HttpResponse.xml()

http.HttpResponse.xml(content: string, status: ?number)

Replaces the body with content and sets Content-Type to application/xml; charset=utf-8.

Parameters

  • content (string)
  • status (?number)

Returns — HttpResponse: this same instance, for chaining

HttpResponse.file()

http.HttpResponse.file(path: string, offset: ?number, length: ?number)

Serves the file at path, streamed from disk rather than read into memory, with Content-Type guessed from the extension.

offset and length serve part of the file, which is what a Range request needs; the caller is responsible for setting the 206 status and Content-Range header that go with it.

Parameters

  • path (string)
  • offset (?number) — byte offset to start from; defaults to 0
  • length (?number) — how many bytes to send; defaults to the rest of the file

Returns — HttpResponse: this same instance, for chaining

Raises HttpError if the file does not exist or cannot be read

HttpResponse.download()

http.HttpResponse.download(path: string, name: ?string)

Serves the file at path as an attachment, so a browser saves it rather than displaying it.

Parameters

  • path (string)
  • name (?string) — the filename to offer; defaults to the file’s own base name

Returns — HttpResponse: this same instance, for chaining

HttpResponse.stream()

http.HttpResponse.stream(handler)

Produces the body from a callback instead of holding it in memory.

handler is called with a writer that has write(data) and flush(). Anything it writes goes out as it is written, which is what makes server-sent events, a progress feed, or a large generated export possible without buffering the whole thing first.

Unless a Content-Length is set beforehand, the body is framed with chunked transfer encoding on HTTP/1.1 and as an ordinary DATA stream on HTTP/2.

server.handle('GET', '/export', @(request, response) {
  response.content_type('text/csv')
  response.stream(@(writer) {
    for row in rows {
      writer.write(row.join(',') + '\n')
    }
  })
})

Parameters

  • handler (function(1))

Returns — HttpResponse: this same instance, for chaining

HttpResponse.render()

http.HttpResponse.render(path: string, variables: ?dict)

Renders a Wire template and appends the result to the body, setting Content-Type to HTML if it is not already set.

Templates are resolved against Wire’s shared instance, whose root defaults to a templates directory beside the working directory. Use wire.shared().set_root() to point it elsewhere.

Parameters

  • path (string) — the template path, without the .html extension
  • variables (?dict)

Returns — HttpResponse: this same instance, for chaining

HttpResponse.redirect()

http.HttpResponse.redirect(location: string, status: ?number)

Sends the client to location, setting both the Location header and a 3xx status.

The default is 302 Found, which browsers follow with a GET regardless of the original method. Use 307 or 308 when the method and body must be preserved, and 303 after a form submission that should not be replayed on refresh.

Parameters

  • location (string)
  • status (?number) — must be a 3xx; defaults to 302

Returns — HttpResponse: this same instance, for chaining

Raises ValueError if status is not a redirect status

HttpResponse.content_type()

http.HttpResponse.content_type(mimetype: string)

Sets the Content-Type.

Parameters

  • mimetype (string)

Returns — HttpResponse: this same instance, for chaining

HttpResponse.header()

http.HttpResponse.header(name: string, value)

Sets a response header.

Parameters

  • name (string)
  • value (string|number|bool)

Returns — HttpResponse: this same instance, for chaining

http.HttpResponse.set_cookie(name: string, value: string, attributes: ?dict)

Adds a cookie to the response.

http_only defaults to true and same_site to 'Lax', which are the settings a session cookie should have; pass them explicitly to opt out. secure defaults to true for any cookie whose name carries the __Secure- or __Host- prefix.

Parameters

  • name (string)
  • value (string)
  • attributes (?dict) — domain, path, expires, max_age, secure, http_only, same_site, partitioned

Returns — HttpResponse: this same instance, for chaining

http.HttpResponse.clear_cookie(name: string, attributes: ?dict)

Expires a cookie on the client by re-sending it empty with a past expiry.

The domain and path must match the ones the cookie was set with, or the client will keep the original and simply store a second, differently-scoped, empty one.

Parameters

  • name (string)
  • attributes (?dict) — domain and path

Returns — HttpResponse: this same instance, for chaining

HttpResponse.cache_for()

http.HttpResponse.cache_for(seconds: number, public_cache: ?bool)

Marks the response as cacheable for seconds, for both private and shared caches.

Parameters

  • seconds (number)
  • public_cache (?bool) — whether shared caches (a CDN, a proxy) may store it too; defaults to true

Returns — HttpResponse: this same instance, for chaining

HttpResponse.no_cache()

http.HttpResponse.no_cache()

Tells every cache, including the browser’s, not to store this response. The three headers below are the combination that actually works across the caches still in service.

Returns — HttpResponse: this same instance, for chaining

HttpResponse.as_text()

http.HttpResponse.as_text() -> string

The body decoded as UTF-8 text, or '' for an empty body.

Returns string

HttpResponse.as_dict()

http.HttpResponse.as_dict() -> any

The body parsed as JSON.

Returns any

Raises HttpError if the body is empty or is not valid JSON

HttpResponse.as_bytes()

http.HttpResponse.as_bytes() -> bytes

The raw body bytes.

Returns bytes

HttpResponse.media_type()

http.HttpResponse.media_type() -> ?string

The media type from Content-Type, without its parameters, e.g. 'application/json' for 'application/json; charset=utf-8'.

Returns ?string

HttpResponse.charset()

http.HttpResponse.charset() -> ?string

The charset named by Content-Type, or nil.

Returns ?string

HttpResponse.is_ok()

http.HttpResponse.is_ok() -> bool

Whether the status is a 2xx.

Returns bool

HttpResponse.is_redirect()

http.HttpResponse.is_redirect() -> bool

Whether the status is a 3xx.

Returns bool

HttpResponse.is_error()

http.HttpResponse.is_error() -> bool

Whether the status is a 4xx or 5xx.

Returns bool

HttpResponse.raise_for_status()

http.HttpResponse.raise_for_status() -> HttpResponse

Raises StatusError when the status is a 4xx or 5xx, and returns the response otherwise, so it can be used inline:

var data = http.get(url).raise_for_status().as_dict()

The response stays reachable on the raised error, so the body - where an API usually explains what went wrong - is not lost.

Returns HttpResponse

Raises StatusError

HttpResponse.reason_phrase()

http.HttpResponse.reason_phrase() -> string

The reason phrase, either the one received or the canonical one for the status code.

Returns string

HttpResponse.is_bodiless()

http.HttpResponse.is_bodiless() -> bool

Whether the response is defined to carry no body: a 1xx, 204 or 304 status, or any response to a HEAD request.

Returns bool

HttpResponse.source()

http.HttpResponse.source() -> ?list

The body source, when the body is not held in body: ['file', path, offset, length] or ['stream', handler]. nil for an ordinary in-memory body.

Returns ?list

HttpResponse.set_carrier()

http.HttpResponse.set_carrier(carrier)

Records the connection a streamed response is still being read from. Set by HttpClient; hand the response to HttpClient.finish() when you are done reading it.

Parameters

  • carrier (?list)

HttpResponse.carrier()

http.HttpResponse.carrier() -> ?list

The connection a streamed response is still being read from, or nil.

Returns ?list

HttpResponse.is_committed()

http.HttpResponse.is_committed() -> bool

Whether the head has already been written to the wire, after which changing a header or the status has no effect.

Returns bool

HttpResponse.commit()

http.HttpResponse.commit()

Marks the response as committed. Called by the protocol writer; applications have no reason to.

HttpResponse.on_finish()

http.HttpResponse.on_finish(callback: function)

Registers callback to run once the response is final: after the handler, every middleware and any error handling have had their say, so status, the headers and the body are what the client receives.

Middleware that reports on a response, such as an access log, registers here rather than reading the response when its next() returns. A failure further in never returns there, and the status the client is sent for it is only decided afterwards.

Callbacks run once each, in the order they were registered. One that raises is skipped over, so a broken reporter never costs the client its response.

Parameters

  • callback (function) — called with no arguments.

HttpResponse.finish()

http.HttpResponse.finish()

Runs every callback on_finish() registered, once. Called by the server when the response is final; applications have no reason to.

HttpResponse.cookie_headers()

http.HttpResponse.cookie_headers() -> list

Every Set-Cookie field value this response will send.

Returns list

HttpResponse.to_string()

http.HttpResponse.to_string()

HttpResponse.to_json()

http.HttpResponse.to_json()

2026, Richard Ore and Zuri contributors

http.router

import http.router

http lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled http.router.* needs import http.router.

Matching a request to a handler.

Router holds the route table, Route is one pattern, and RouteMatch is the result of matching including whatever the path parameters captured. Patterns support named parameters and wildcards, and the router answers 405 rather than 404 when a path exists under a different method.

Classes

Route

class http.Route

One registered route.

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
methodstringThe method this route answers, uppercased.
patternstringThe pattern it was registered with, e.g. '/users/:id'.
handlerfunctionThe function called when the route matches, taking the request and the response.
name?stringAn optional name, so a URL can be built from the route rather than written out again at every call site.

Constructor

http.Route(method, pattern, handler, name)

Route.to_string()

http.Route.to_string()

RouteMatch

class http.router.RouteMatch

The result of asking a router about a request.

Fields

FieldTypeDescription
route?RouteThe matched route, or nil when nothing matched.
paramsdictPath parameters captured from the pattern.
allowedlistWhen a path matched but the method did not, the methods that path does answer - which is exactly what a…

Constructor

http.router.RouteMatch(route, params, allowed)

RouteMatch.is_match()

http.router.RouteMatch.is_match() -> bool

Whether a route was found.

Returns bool

RouteMatch.is_method_mismatch()

http.router.RouteMatch.is_method_mismatch() -> bool

Whether the path exists but not for the requested method, which calls for a 405 rather than a 404.

Returns bool

Router

class http.Router

Matches request paths to handlers.

Patterns are made of literal segments, named parameters, and an optional trailing catch-all:

PatternMatchesCaptures
/users/users
/users/:id/users/42id = '42'
/users/:id/posts/users/42/postsid = '42'
/static/ + *path/static/css/site.csspath = 'css/site.css'

A literal segment always beats a parameter, and a parameter always beats a catch-all, so /users/new and /users/:id can coexist and the specific one wins - regardless of which was registered first, which is a property a list-of-patterns router cannot offer.

Matching happens over a trie, so a router with a thousand routes costs the same per request as one with ten.

Constructor

http.Router()

Router.add()

http.Router.add(method: string, pattern: string, handler, name: ?string) -> Route

Registers handler for method requests to pattern.

Parameters

  • method (string)
  • pattern (string)
  • handler (function(2)) — called with the request and response
  • name (?string) — a name to look the route up by later

Returns Route

Raises HttpError if the pattern is malformed, or if the same method and pattern were already registered

Router.match()

http.Router.match(method: string, path: string) -> RouteMatch

Finds the route for method and path.

When the path matches but the method does not, the returned match carries the methods that path does answer, so the caller can send a 405 with a correct Allow header instead of a misleading 404.

HEAD falls back to the GET route when no HEAD route was registered, since RFC 9110 §9.3.2 defines HEAD as GET without the body, and the response writer drops the body anyway.

Parameters

  • method (string)
  • path (string)

Returns RouteMatch

Router.url_for()

http.Router.url_for(name: string, params: ?dict) -> string

Builds the path for a named route, substituting params into its pattern.

router.add('GET', '/users/:id', show, 'user.show')
router.url_for('user.show', { id: 42 })   # '/users/42'

Parameters

  • name (string)
  • params (?dict)

Returns string

Raises HttpError if no route has that name, or a parameter the pattern needs was not supplied

Router.routes()

http.Router.routes() -> list

Every registered route.

Returns list

Router.length()

http.Router.length() -> number

How many routes are registered.

Returns number


2026, Richard Ore and Zuri contributors

http.server

import http.server

http lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled http.server.* needs import http.server.

HttpServer: the server end of the module.

It accepts connections, speaks HTTP/1.1 or HTTP/2 as negotiated, runs the middleware chain around the matched handler, and serves static files when configured to. It can run single-threaded or hand connections to a pool of workers.

Classes

HttpServer

class http.HttpServer

An HTTP/1.1 server.

import http

var server = http.server(3000)

server.get('/', @(request, response) {
  response.html('<h1>Hello</h1>')
})

server.get('/users/:id', @(request, response) {
  response.json({ id: request.param('id') })
})

server.listen()

The pieces a server put in front of an application usually provides are here rather than assumed: TLS with use_tls(), static files with serve_files(), response compression, byte ranges, conditional requests, keep-alive with bounded reuse, and limits on every part of a request that a peer controls the size of.

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
hoststringThe address to bind to.
portnumberThe port to bind to.
read_timeoutnumberHow long to wait for a request to arrive, in milliseconds.
write_timeoutnumberHow long a write may block before the client is treated as gone.
keep_alive_timeoutnumberHow long an idle keep-alive connection is held open, in milliseconds.
max_keep_alive_requestsnumberThe most requests one connection may serve before it is closed.
max_line_sizenumberThe longest request line, in bytes.
max_header_sizenumberThe largest header section, in bytes.
max_header_countnumberThe most header fields one request may carry.
header_timeoutnumberHow long the whole request head may take to arrive, in seconds, once its first byte has.
max_body_sizenumberThe largest request body accepted, in bytes.
server_namestringThe value sent in the Server header.
compressionboolWhether to compress responses whose media type benefits from it and whose client asked for it.
compression_min_sizenumberThe smallest response worth compressing, in bytes.
compression_quality?numberHow hard to compress, on the scale of whichever coding content negotiation lands on - brotli 0-11,…
trust_proxyboolWhether HttpRequest.client_ip() should consult forwarding headers.
trusted_proxieslistThe proxy addresses to skip when walking a forwarding chain.
socket?TcpStreamThe listening socket, once listen() has bound it.
http2boolWhether to speak HTTP/2 when a client asks for it - through ALPN on a TLS connection, or through the…

Constructor

http.HttpServer(port: ?number, host: ?string)

Parameters

  • port (?number) — defaults to 8000
  • host (?string) — defaults to '127.0.0.1'

HttpServer.handle()

http.HttpServer.handle(method: string, pattern: string, handler, name: ?string)

Registers handler for method requests matching pattern.

See Router for what a pattern may contain: literal segments, :name parameters, and a trailing catch-all.

Parameters

  • method (string)
  • pattern (string)
  • handler (function(2)) — called with the request and response
  • name (?string) — a route name, for url_for()

Returns — HttpServer: this same instance, for chaining

HttpServer.get()

http.HttpServer.get(pattern: string, handler, name: ?string)

Registers a GET handler. A HEAD request for the same path is answered by it too, with the body dropped.

Parameters

  • pattern (string)
  • handler (function(2))
  • name (?string)

Returns — HttpServer: this same instance, for chaining

HttpServer.post()

http.HttpServer.post(pattern: string, handler, name: ?string)

Registers a POST handler.

Parameters

  • pattern (string)
  • handler (function(2))
  • name (?string)

Returns — HttpServer: this same instance, for chaining

HttpServer.put()

http.HttpServer.put(pattern: string, handler, name: ?string)

Registers a PUT handler.

Parameters

  • pattern (string)
  • handler (function(2))
  • name (?string)

Returns — HttpServer: this same instance, for chaining

HttpServer.patch()

http.HttpServer.patch(pattern: string, handler, name: ?string)

Registers a PATCH handler.

Parameters

  • pattern (string)
  • handler (function(2))
  • name (?string)

Returns — HttpServer: this same instance, for chaining

HttpServer.delete()

http.HttpServer.delete(pattern: string, handler, name: ?string)

Registers a DELETE handler.

Parameters

  • pattern (string)
  • handler (function(2))
  • name (?string)

Returns — HttpServer: this same instance, for chaining

HttpServer.options()

http.HttpServer.options(pattern: string, handler, name: ?string)

Registers an OPTIONS handler, replacing the automatic one for this path.

Parameters

  • pattern (string)
  • handler (function(2))
  • name (?string)

Returns — HttpServer: this same instance, for chaining

HttpServer.head()

http.HttpServer.head(pattern: string, handler, name: ?string)

Registers a HEAD handler, replacing the automatic GET fallback for this path.

Parameters

  • pattern (string)
  • handler (function(2))
  • name (?string)

Returns — HttpServer: this same instance, for chaining

HttpServer.any()

http.HttpServer.any(pattern: string, handler)

Registers one handler for every method a route can carry.

Parameters

  • pattern (string)
  • handler (function(2))

Returns — HttpServer: this same instance, for chaining

HttpServer.use()

http.HttpServer.use(middleware)

Adds a middleware, called for every request before the route handler.

A middleware takes (request, response, next) and must call next() to let the rest of the chain run; not calling it is how a middleware short-circuits, which is exactly what an authentication or rate-limiting layer wants to do.

server.use(@(request, response, next) {
  var started = time()
  next()
  echo '${request.method} ${request.path} ${response.status} ' +
    '${(time() - started) * 1000}ms'
})

Middleware run in the order they were added, outermost first.

Parameters

  • middleware (function(3))

Returns — HttpServer: this same instance, for chaining

HttpServer.not_found()

http.HttpServer.not_found(handler)

Sets the handler called when no route matches. The default sends a plain 404, or a JSON one when the client asked for JSON.

Parameters

  • handler (function(2))

Returns — HttpServer: this same instance, for chaining

HttpServer.error_handler()

http.HttpServer.error_handler(handler)

Sets the handler called when a route handler raises.

It takes (request, response, error) and is responsible for producing the response. Without one, the server sends a bare 500 and reports the error through on_error() listeners - it never puts an exception message in a response body, since that is how internal paths and query fragments end up on a user’s screen.

Parameters

  • handler (function(3))

Returns — HttpServer: this same instance, for chaining

HttpServer.serve_files()

http.HttpServer.serve_files(prefix: string, directory: string, options: ?dict)

Serves the files under directory at URLs beginning with prefix.

server.serve_files('/static', './public', { cache_age: 86400 })

Parameters

  • prefix (string)
  • directory (string)
  • options (?dict) — passed through to StaticFiles - index_files, cache_age, etag, precompressed, allow_dotfiles, fallback

Returns — HttpServer: this same instance, for chaining

HttpServer.routes()

http.HttpServer.routes() -> Router

The router, for anything the convenience methods above do not cover - listing routes, or building a URL from a route name.

Returns Router

HttpServer.on_connect()

http.HttpServer.on_connect(listener)

Adds a listener called with the Connection each time a client connects.

Parameters

  • listener (function(1))

Returns — HttpServer: this same instance, for chaining

HttpServer.on_disconnect()

http.HttpServer.on_disconnect(listener)

Adds a listener called with the Connection when a client disconnects.

Parameters

  • listener (function(1))

Returns — HttpServer: this same instance, for chaining

HttpServer.on_receive()

http.HttpServer.on_receive(listener)

Adds a listener called with (request, response) after a request has been parsed and before it is routed.

Parameters

  • listener (function(2))

Returns — HttpServer: this same instance, for chaining

HttpServer.on_reply()

http.HttpServer.on_reply(listener)

Adds a listener called with (request, response) after a response has been sent. This is where an access log belongs.

Parameters

  • listener (function(2))

Returns — HttpServer: this same instance, for chaining

HttpServer.on_error()

http.HttpServer.on_error(listener)

Adds a listener called with (error, connection) whenever serving a connection fails.

Without at least one such listener, connection-level errors are swallowed: one client sending a malformed request must not take the server down with it.

Parameters

  • listener (function(2))

Returns — HttpServer: this same instance, for chaining

HttpServer.use_tls()

http.HttpServer.use_tls(cert_chain: string, private_key: string, options: ?dict)

Turns on TLS, using the given PEM-encoded certificate chain and private key.

cert_chain must be the server certificate followed by any intermediates - leaving the intermediates out is the single most common TLS misconfiguration, and it fails only for the clients that do not happen to have them cached.

Parameters

  • cert_chain (string) — PEM, leaf first
  • private_key (string) — PEM
  • options (?dict) — alpn (a list of protocol names), client_ca (PEM roots for mutual TLS), require_client_cert, min_version and max_version

Returns — HttpServer: this same instance, for chaining

Note: The certificate and key are parsed when the first handshake runs, not here, so a malformed or mismatched pair surfaces as a failed connection rather than as a failed call to this method.

HttpServer.load_certs()

http.HttpServer.load_certs(cert_path: string, key_path: ?string, options: ?dict)

Loads the certificate and key from files rather than strings.

Parameters

  • cert_path (string)
  • key_path (?string) — defaults to cert_path, for a combined PEM file
  • options (?dict) — as use_tls()

Returns — HttpServer: this same instance, for chaining

HttpServer.set_tls_config()

http.HttpServer.set_tls_config(config)

Uses a net.tls.TlsConfig built elsewhere, for anything use_tls() does not expose.

Parameters

  • config (TlsConfig)

Returns — HttpServer: this same instance, for chaining

HttpServer.is_secure()

http.HttpServer.is_secure() -> bool

Whether this server is configured to serve over TLS.

Returns bool

HttpServer.listen()

http.HttpServer.listen()

Binds the socket and serves connections until close() is called.

Connections are served one at a time on the calling thread. For a server that uses more than one core, see http.serve(), which runs this same request pipeline across a pool of isolates.

Raises HttpError if the socket cannot be bound

HttpServer.bind()

http.HttpServer.bind()

Binds and starts listening without accepting anything, so a caller can drive the accept loop itself.

The listener is left in non-blocking mode, since accept() and listen() here go through a net.Acceptor. A caller driving its own loop over socket directly gets nil from accept() whenever nothing is waiting.

Raises HttpError if the socket cannot be bound

HttpServer.accept()

http.HttpServer.accept() -> ?TcpStream

Accepts one connection, blocking until one arrives. Only valid after bind().

Returns nil if the server is stopped while this is waiting, since there is then nothing left to wait for.

Returns ?TcpStream

HttpServer.address()

http.HttpServer.address() -> ?SocketAddr

The address the server is listening on. After binding to port 0, this is how to discover the port that was actually chosen.

Returns ?SocketAddr

HttpServer.is_listening()

http.HttpServer.is_listening() -> bool

Whether the server is currently listening.

Returns bool

HttpServer.close()

http.HttpServer.close()

Stops the server and closes the listening socket. The accept loop reads the stopped flag on its next round and ends, within one net.Acceptor interval of this call.

HttpServer.serve_connection()

http.HttpServer.serve_connection(client)

Serves every request on one already-accepted client socket, then closes it.

This is the whole per-connection pipeline - TLS handshake, keep-alive loop, parsing, dispatch, response - and is public so that a caller running its own accept loop (or handing sockets to worker isolates) can reuse it exactly as listen() does.

Parameters

  • client (TcpStream)

HttpServer.accept_connection()

http.HttpServer.accept_connection(client)

Turns an accepted socket into a Connection, running the TLS handshake when one is configured and notifying on_connect() listeners.

Split out of serve_connection() so that a caller driving its own event loop can take a connection on without also committing to serving it to completion on the spot: see serve_next().

Parameters

  • client (TcpStream)

Returns — ?Connection: nil when the handshake failed, in which case the socket has already been closed and the failure reported to on_error() listeners

HttpServer.serve_next()

http.HttpServer.serve_next(connection, served: ?number)

Reads and answers exactly one request, and reports whether the connection may be used again.

This is the unit an event loop works in. _serve_requests() calls it in a loop for one connection at a time, which is what listen() does; a polled worker calls it once per connection that has actually become readable, which is what lets one thread hold many connections at once.

Parameters

  • connection (Connection)
  • served (?number) — how many requests this connection has already answered, so the keep-alive budget and the shorter idle read timeout apply from the second request onwards

Returns — bool: whether to keep the connection open

HttpServer.serve_http2()

http.HttpServer.serve_http2(connection)

Runs an HTTP/2 session on connection until the peer goes away.

Unlike HTTP/1.1, this owns the connection for its whole life: an HTTP/2 connection is already multiplexed internally, so the streams on it are interleaved even though the connection itself is not interleaved with any other.

Parameters

  • connection (Connection)

HttpServer.is_http2_connection()

http.HttpServer.is_http2_connection(connection) -> bool

Whether a connection that has just been taken on is going to speak HTTP/2. Reading this costs a peek at the first bytes on a cleartext connection, so it is asked once and the answer kept.

Parameters

  • connection (Connection)

Returns bool

HttpServer.report_error()

http.HttpServer.report_error(error, connection)

Hands error to this server’s on_error() listeners.

Public so that a caller running its own connection loop reports failures through the same listeners the built-in loops use, instead of inventing a second place errors can appear.

Parameters

  • error (Error)
  • connection (?Connection)

HttpServer.finish_connection()

http.HttpServer.finish_connection(connection)

Notifies on_disconnect() listeners and closes the connection.

Parameters

  • connection (Connection)

HttpServer.handle_request()

http.HttpServer.handle_request(request, response) -> HttpResponse

Runs one already-parsed request through the middleware chain, the router and the error handling, and returns the response.

This is the whole application-facing half of the server, with no reference to how the request arrived. That is what lets the same routes and middleware serve an HTTP/1.1 connection and an HTTP/2 stream without either protocol knowing about the other.

Parameters

  • request (HttpRequest)
  • response (?HttpResponse) — an existing response to fill in

Returns HttpResponse

HttpServer.to_string()

http.HttpServer.to_string()

2026, Richard Ore and Zuri contributors

http.session

import http

http exposes this as http.session, so import http is enough and the names are called as http.session.*. import http.session reaches the same definitions directly.

Server-side sessions: a small amount of state that belongs to one visitor, kept on the server and found again by a cookie the browser sends back.

Only the identifier travels. Whatever the session holds stays in a store the server controls, so a visitor can neither read it nor change it, and the cookie is worth nothing to anyone who cannot present the exact value that was issued.

Using it

import http
import http.session

var server = http.server(3000)

server.use(session.session())

server.post('/login', @(request, response) {
  var account = authenticate(request.form())

  # A new identifier, so a session identifier planted beforehand is
  # not the one that ends up logged in.
  request.session().regenerate()
  request.session().set('account', account.id)

  response.redirect('/')
})

server.get('/', @(request, response) {
  var id = request.session().get('account', nil)

  response.html(id == nil ? render_guest() : render_account(id))
})

server.listen()

session() is middleware, so it is registered once and every handler below it can reach request.session().

Nothing happens until something uses it

A request that never touches its session costs nothing: no store read, no store write, and no Set-Cookie. A store record is created the first time something is written, which is what keeps a crawler working through a public site from filling the store with empty sessions.

A request that only reads an existing session writes nothing back either, beyond moving the idle expiry along at most once every touch_interval seconds.

Where it is kept

FileStore is the default, in a private directory under the platform’s temporary directory. It needs no setup and it is shared between the workers http.serve() starts, which a store held in memory is not.

server.use(session.session({
  store: session.FileStore('/var/lib/app/sessions'),
}))

A relational database works as well, from http.session.sql, which is a separate import so that a program using the default store never loads the sql module:

import http.session.sql { SqlStore }
import sql

var store = SqlStore(sql.pool('postgres://localhost/app'))
store.migrate()

server.use(session.session({ store }))

Anything else - Redis, a key-value service, a table of your own shape - is a subclass of SessionStore.

Options

OptionDefaultMeaning
storea FileStore in the default directorywhere sessions are kept
name'zuri_session'the cookie’s name
path'/'the cookie’s path
domainnilthe cookie’s domain; nil scopes it to the exact host
securenilnil follows the request’s own scheme
http_onlytruehides the cookie from scripts
same_site'Lax''Strict', 'Lax' or 'None'
persistentfalsewhether the cookie outlives the browser
idle_timeout7200seconds of inactivity before a session ends
lifetime86400seconds a session may live however active
touch_interval60, or half idle_timeout when that is lesshow often a read-only request moves the idle expiry
max_size65536the largest payload a session may serialise to
secretnilsigns the cookie, so a forged one is refused without a store read
gc_probability0.01passed to a default FileStore; ignored when store is given

Two clocks

idle_timeout rolls forward while the visitor is active; lifetime does not. A session ends at whichever comes first, so a tab left open overnight is still asked to sign in again. Either may be nil to remove that limit, but not both.

An identifier is 32 bytes from the platform’s cryptographic generator, which is far beyond guessing. Signing adds nothing against that, and everything against volume: with secret set, a cookie that was not issued by this server is thrown out after one HMAC, rather than after a read from disk or a query to the database. Set it on anything exposed to the open internet.

import env

server.use(session.session({ secret: env.require('SESSION_SECRET') }))

Every worker must be given the same secret, or a cookie issued by one is refused by the next.

What a session may hold

Whatever JSON holds: strings, numbers, booleans, nil, lists and dictionaries of those. A class instance is not JSON, and storing one raises when the session is written rather than coming back as something else.

Sessions are for identity and small state - who is signed in, which steps of a form are done, what to say on the next page. A payload over max_size raises SessionError; the answer is a row in a database with the session holding its key.

Submodules

ModuleReached asSummary
http.session.filehttp.session.file.*The session store that keeps one file per session.
http.session.memoryhttp.session.memory.*The session store that keeps everything in the isolate’s own heap.
http.session.sqlimport http.session.sqlThe session store that keeps sessions in a relational database.
http.session.storehttp.session.store.*What a session store is, and the error every one of them raises.

Constants

FORMAT

http.session.FORMAT = 1

The payload format this module writes and reads.

A session written by a different version is ignored rather than guessed at, so an upgrade signs people out instead of handing a handler fields that are not what it expects.

ID_LENGTH

http.session.ID_LENGTH = 43

How many characters a session identifier has.

Thirty-two bytes from the platform’s cryptographic generator, in base64url without padding.

Functions

session()

http.session.session(options: ?dict) -> function(3)

Middleware that finds each request’s session and writes it back when the response goes out.

Register it once, above anything that reads a session:

server.use(session.session({ secret: env.require('SESSION_SECRET') }))

The session lands on request.context['session'], which request.session() reads.

A handler that raises still has its session written, and the failure carries on to the error handler afterwards. That is what lets an error page show a flash the handler set before it failed.

Parameters

  • options (?dict) — see the table at the top of this module

Returns function(3)

Raises SessionError if an option is not one this module has, or holds a value it cannot mean

Classes

Session

class http.session.Session

One visitor’s session.

A handler reaches its own through request.session() rather than building one. The middleware makes it, hands it to the request, and writes it back afterwards.

Nothing is read from the store until the first method that needs the contents is called, and nothing is written back unless something changed.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

http.session.Session(store, config: dict, presented: ?string, secure: ?bool)

Parameters

  • store (SessionStore)
  • config (dict) — as session() resolved it
  • presented (?string) — the cookie value the client sent, if any
  • secure (?bool) — whether the request arrived over TLS

Session.id()

http.session.Session.id() -> ?string

The session’s identifier, or nil when it has none yet.

A session that has never been written to has no identifier, because nothing has been stored for one to name.

Returns ?string

Session.is_new()

http.session.Session.is_new() -> bool

Whether this request arrived without a session the store recognised - no cookie, a cookie that did not verify, or one naming a session that has expired or been destroyed.

Returns bool

Session.get()

http.session.Session.get(name: string, fallback) -> any

The value stored under name, or fallback when there is none.

Parameters

  • name (string)
  • fallback (?any) — defaults to nil

Returns any

Session.set()

http.session.Session.set(name: string, value)

Stores value under name.

This is what brings a session into existence: the first set() on a new session assigns an identifier, and the middleware writes the record and sends the cookie when the response goes out.

Parameters

  • name (string)
  • value (any) — anything JSON can carry

Returns — Session: this same instance, for chaining

Session.has()

http.session.Session.has(name: string) -> bool

Whether anything is stored under name.

Parameters

  • name (string)

Returns bool

Session.remove()

http.session.Session.remove(name: string)

Removes whatever is stored under name. Removing something that is not there changes nothing.

Parameters

  • name (string)

Returns — Session: this same instance, for chaining

Session.all()

http.session.Session.all() -> dict

Everything the session holds, as a dictionary.

The dictionary is a copy: writing to it does not change the session.

Returns dict

Session.clear()

http.session.Session.clear()

Empties the session without ending it. The identifier, the cookie and the record all survive; only the contents go.

Use destroy() to end the session itself, which is what signing out wants.

Returns — Session: this same instance, for chaining

Session.regenerate()

http.session.Session.regenerate()

Gives the session a new identifier, keeping everything it holds, and removes the record the old identifier named.

Call this the moment an account signs in. Without it, an attacker who can set a cookie in the victim’s browser beforehand - through a stray subdomain, an open redirect, a shared machine - knows the identifier the victim will be signed in under, and can simply use it. That is session fixation, and a new identifier is the whole of the defence.

It is also worth doing whenever what the session means changes: an elevation to administrator, a step-up authentication.

The absolute lifetime restarts here, since a new identifier begins a new session; the contents carry across.

Returns — Session: this same instance, for chaining

Session.destroy()

http.session.Session.destroy()

Ends the session: removes the record, empties the contents, and has the response expire the cookie.

This is what signing out does. A handler may keep using the session object afterwards, and doing so starts a fresh session with a new identifier.

Returns — Session: this same instance, for chaining

Session.flash()

http.session.Session.flash(name: string, value)

Stores value under name for exactly the next request.

This is the message a redirect needs to carry: the handler that did the work knows what happened, and the page the visitor is sent to is the one that has to say so.

server.post('/posts', @(request, response) {
  create_post(request.form())

  request.session().flash('notice', 'Your post is up.')
  response.redirect('/posts')
})

server.get('/posts', @(request, response) {
  response.html(render(request.session().take_flash('notice')))
})

A flash is spent by the next request that touches the session at all, whether or not that request asks for this one. A page that looked at the session and did not read the message does not leave it for the page after. A request that never touched its session - a static file, an image - leaves it waiting, which is what keeps the message for the page it was meant for.

Parameters

  • name (string)
  • value (any)

Returns — Session: this same instance, for chaining

Session.take_flash()

http.session.Session.take_flash(name: string, fallback) -> any

Reads the flash stored under name by the previous request and removes it, so a second call in the same request does not get it again.

Parameters

  • name (string)
  • fallback (?any) — defaults to nil

Returns any

Session.flashes()

http.session.Session.flashes() -> dict

Every flash the previous request left, as a dictionary, without taking any of them.

The dictionary is a copy.

Returns dict

Session.started_at()

http.session.Session.started_at() -> ?number

When the session was created, as epoch seconds, or nil when it has not been created yet.

regenerate() restarts this.

Returns ?number

Session.touched_at()

http.session.Session.touched_at() -> ?number

When the session was last written or touched, as epoch seconds, or nil when it does not exist yet.

A read-only request moves this at most once every touch_interval seconds, so it is a coarse measure of activity rather than the time of the last request.

Returns ?number

Session.expires_at()

http.session.Session.expires_at() -> ?number

When the session stops being valid, as epoch seconds, or nil when it does not exist yet.

Returns ?number

Session.save()

http.session.Session.save()

Writes the session to the store now, if anything changed.

The middleware does this as the response goes out, so a handler rarely calls it. It is here for one that is about to do something long - stream a large body, wait on a slow service - and would rather not hold the change until afterwards.

Returns — bool: whether anything was written

Raises SessionError if the session serialises to more than max_size bytes

Session.commit()

http.session.Session.commit(response)

Writes the session and puts whatever the client needs on the response: the cookie for a session that is new or has a new identifier, an expired cookie for one that was destroyed.

The middleware calls this once, after the rest of the chain has run. Calling it again does no harm and writes nothing more.

Parameters

  • response (HttpResponse)

Note: A response that has already started going out cannot carry a Set-Cookie. A session whose identifier has just changed is then not written at all, since nothing could present it; an existing session is still written, and only its cookie is skipped. Start a session before streaming begins.

Session.to_string()

http.session.Session.to_string()

2026, Richard Ore and Zuri contributors

http.session.file

import http.session.file

http lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled http.session.file.* needs import http.session.file.

The session store that keeps one file per session.

It is the default because it needs nothing set up: no database, no daemon, no schema. It is also shared, which matters the moment a server runs on more than one core - http.serve() puts every worker in its own isolate, and a store held in memory would give each of them a different set of sessions.

Constants

SUFFIX

http.session.file.SUFFIX = '.session'

DIRECTORY_MODE

http.session.file.DIRECTORY_MODE = 448

FILE_MODE

http.session.file.FILE_MODE = 384

Functions

default_directory()

http.session.default_directory() -> string

The directory sessions are kept in when a FileStore is not told where to put them: a private subdirectory of the platform’s temporary directory, named after the working directory of the program that asked.

The temporary directory itself is deliberately not used. It is world-writable, and on a shared machine world-readable, so a session file dropped straight into it can be read by every other account on the host - which is the oldest way there is to be logged in as somebody else. The subdirectory is created 0700 and checked before it is used.

Naming it after the working directory keeps two programs on one machine from sharing a session space by accident, and keeps the same program’s sessions across a restart.

Returns string

Note: The platform’s temporary directory is cleared on a schedule the platform decides, and on most of them at every reboot. Sessions kept here therefore survive a restart of the server but not necessarily a restart of the machine. Anything that has to outlive the host names its own directory.

Classes

FileStore

class http.session.FileStore < SessionStore

Keeps each session in its own file, named after the session’s storage key.

import http.session

server.use(session.session({
  store: session.FileStore('/var/lib/app/sessions'),
}))

A file holds its expiry on the first line and the payload on the rest, so a sweep can decide whether to keep a session without understanding what is in it.

Writing

A write goes to a uniquely named file in the same directory and is then renamed over the real one, which the filesystem does atomically. A reader therefore sees either the previous session or the new one, never a half-written mixture, and a server killed mid-write leaves the previous session intact.

Two requests writing the same session at the same moment - the usual cause being parallel requests from one browser tab - both succeed, and the one that renames last is the one that survives. Nothing here serialises them.

Permissions

A session file is a bearer credential in the same way the cookie is. The directory is created 0700 and each file 0600, and a directory that anyone outside the owner can reach is refused outright rather than used. Pass strict_permissions: false to accept one anyway, which is occasionally what a deployment with its own access control wants and is never what a default should do.

On Windows the mode check does not run, because the mode a file reports there does not describe who can open it.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

http.session.FileStore(directory: ?string, options: ?dict)

Parameters

  • directory (?string) — where to keep the files; defaults to default_directory(). Created, with every parent it needs, if it is not already there
  • options (?dict) — gc_probability (default 0.01) and strict_permissions (default true)

Raises SessionError if the directory cannot be created, is not a directory, or is reachable by users other than its owner

FileStore.directory()

http.session.FileStore.directory() -> string

The directory the sessions are in.

Returns string

FileStore.read()

http.session.FileStore.read(key: string)

FileStore.write()

http.session.FileStore.write(key: string, payload: string, expires_at: number)

FileStore.destroy()

http.session.FileStore.destroy(key: string)

FileStore.gc()

http.session.FileStore.gc(now: number)

FileStore.to_string()

http.session.FileStore.to_string()

2026, Richard Ore and Zuri contributors

http.session.memory

import http.session.memory

http lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled http.session.memory.* needs import http.session.memory.

The session store that keeps everything in the isolate’s own heap.

Classes

MemoryStore

class http.session.MemoryStore < SessionStore

Keeps sessions in a dictionary, for as long as the isolate that made the store lives.

It is the right store for a test, where a session has to survive a handful of requests and nothing more, and for a single-process development server. It is the wrong store for anything serving real traffic:

  • Nothing survives a restart. Every session in flight ends when the process does, deploy included.
  • Nothing is shared. http.serve() runs each worker in its own isolate with its own heap, so a browser whose next request lands on a different worker arrives with a session that worker has never heard of.

FileStore has neither problem and needs no more setup than this does, which is why it, and not this, is the default.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

http.session.MemoryStore(options: ?dict)

Parameters

  • options (?dict) — gc_probability (default 0.01)

MemoryStore.length()

http.session.MemoryStore.length() -> number

How many sessions are held, expired ones that have not yet been swept included.

Returns number

MemoryStore.clear()

http.session.MemoryStore.clear()

Drops every session at once.

MemoryStore.read()

http.session.MemoryStore.read(key: string)

MemoryStore.write()

http.session.MemoryStore.write(key: string, payload: string, expires_at: number)

MemoryStore.touch()

http.session.MemoryStore.touch(key: string, expires_at: number)

MemoryStore.destroy()

http.session.MemoryStore.destroy(key: string)

MemoryStore.gc()

http.session.MemoryStore.gc(now: number)

MemoryStore.to_string()

http.session.MemoryStore.to_string()

2026, Richard Ore and Zuri contributors

http.session.sql

import http.session.sql

http does not re-export this module, so it is reached only by importing it directly.

The session store that keeps sessions in a relational database.

It lives in its own submodule, rather than alongside the other stores, so that a program using the default file store never loads the sql module or any of its adapters.

import http
import http.session
import http.session.sql { SqlStore }
import sql

var store = SqlStore(sql.pool('postgres://localhost/app'))
store.migrate()

server.use(session.session({ store }))

Constants

DEFAULT_TABLE

http.session.sql.DEFAULT_TABLE = 'sessions'

Classes

SqlStore

class http.session.sql.SqlStore < SessionStore

Keeps sessions in one table of a relational database.

The table is three columns - the storage key, the payload, and when the session stops being valid - and migrate() creates it on whichever engine is in use.

What it takes

Either a sql.Connection or a sql.Pool. A server wants the pool: a connection is used by one isolate at a time, and the pool is what hands each request one and takes it back.

A connection belongs to the isolate that opened it and cannot be shared, so under http.serve() each worker builds its own pool and its own store inside setup, rather than being handed one from outside.

Concurrency

Every write is a single statement, so a reader never sees half a session. Two requests writing the same session at the same moment both succeed and the later one wins. Nothing here takes a lock.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

http.session.sql.SqlStore(database, options: ?dict)

Parameters

  • database (Connection|Pool) — where the table lives
  • options (?dict) — table (default 'sessions') and gc_probability (default 0.01)

Raises SessionError if the table name is not a plain identifier

SqlStore.table()

http.session.sql.SqlStore.table() -> string

The table sessions are kept in.

Returns string

SqlStore.migrate()

http.session.sql.SqlStore.migrate()

Creates the table and its index if they are not already there.

Safe to call on every start: an existing table is left exactly as it is, and nothing in it is touched.

The columns are portable rather than ideal for any one engine. An application that wants a different shape - a partitioned table, a different collation, an engine-specific TTL - creates the table itself and skips this.

ColumnType
idvarchar(64)the storage key, primary key
payloadtextwhat the session holds
expires_atbigintepoch seconds

Returns — SqlStore: this same instance, for chaining

SqlStore.read()

http.session.sql.SqlStore.read(key: string)

SqlStore.write()

http.session.sql.SqlStore.write(key: string, payload: string, expires_at: number)

SqlStore.touch()

http.session.sql.SqlStore.touch(key: string, expires_at: number)

SqlStore.destroy()

http.session.sql.SqlStore.destroy(key: string)

SqlStore.gc()

http.session.sql.SqlStore.gc(now: number)

SqlStore.to_string()

http.session.sql.SqlStore.to_string()

2026, Richard Ore and Zuri contributors

http.session.store

import http.session.store

http lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled http.session.store.* needs import http.session.store.

What a session store is, and the error every one of them raises.

A store is the only part of a session that touches durable state. It never sees a session identifier, only the key derived from one, and it never sees the session’s contents, only the opaque string the session layer serialised them into. That split is what lets a store be swapped without the rest of the stack knowing, and what lets a leaked store - a directory listing, a database dump, a backup tape - hand over nothing that can be replayed as a cookie.

Implementing one means five methods: read(), write(), destroy(), gc(), and optionally touch(), which has a working default built on the first two.

class RedisStore < session.SessionStore {
  @new(client) {
    self._client = client
  }

  read(key) {
    return self._client.get('session:' + key)
  }

  write(key, payload, expires_at) {
    var ttl = (expires_at - time()).ceil()

    self._client.set_with_ttl('session:' + key, payload, ttl)
  }

  destroy(key) {
    self._client.remove('session:' + key)
  }

  gc(now) {
    # Redis expires keys itself.
    return 0
  }
}

Functions

storage_key()

http.session.storage_key(id: string) -> string

The key a store files a session under: the SHA-256 of the session identifier, as 64 lowercase hex characters.

Storing the digest rather than the identifier means the contents of a store cannot be turned back into cookies. Someone who reads the directory, the table, or a backup of either learns what is in the sessions but cannot resume one, because the value the browser presents is the preimage.

It also settles the shape of a key once and for all: 64 hex characters make a safe filename and a fixed-width primary key, whatever a client put in its cookie.

Parameters

  • id (string)

Returns string

Classes

SessionError

class http.session.SessionError < HttpError

Raised when a session cannot be read, written or configured: a storage directory that cannot be created or is open to other users on the machine, a payload larger than the configured ceiling, an unknown option, or a store method an implementation forgot.

It is an HttpError, so a server already catching those catches this as well.

  • printable — has a @to_string(), so echo and print() show something useful

SessionError.to_string()

http.session.SessionError.to_string()

SessionStore

class http.session.SessionStore

What every session store implements.

The methods here raise SessionError, so a store that forgets one fails where the gap is rather than somewhere further down.

Every timestamp is epoch seconds, as time() reports them.

Fields

FieldTypeDescription
gc_probabilitynumberThe chance, between 0 and 1, that a write also sweeps expired sessions.

SessionStore.read()

http.session.SessionStore.read(key: string) -> ?string

The payload stored under key, or nil when there is none or the one there has expired.

A store that finds an expired entry removes it rather than leaving it for the sweep.

Parameters

  • key (string)

Returns ?string

SessionStore.write()

http.session.SessionStore.write(key: string, payload: string, expires_at: number)

Stores payload under key, replacing whatever was there, and records when it stops being valid.

Parameters

  • key (string)
  • payload (string)
  • expires_at (number)

SessionStore.touch()

http.session.SessionStore.touch(key: string, expires_at: number)

Moves the expiry of an existing entry without rewriting its contents, which is what a rolling idle timeout does on a request that only read the session.

The default reads the payload and writes it back. A store whose backend can change the expiry on its own - a single-column UPDATE, a TTL command - overrides this with that.

Parameters

  • key (string)
  • expires_at (number)

Returns — bool: whether an entry was there to touch

SessionStore.destroy()

http.session.SessionStore.destroy(key: string)

Removes the entry stored under key. Removing one that is not there is not an error.

Parameters

  • key (string)

SessionStore.gc()

http.session.SessionStore.gc(now: number)

Removes every entry that expired on or before now.

Parameters

  • now (number)

Returns — number: how many entries were removed

SessionStore.maybe_gc()

http.session.SessionStore.maybe_gc()

Runs gc() with probability gc_probability, and otherwise does nothing. A store calls this from its own write().

A sweep that fails raises out of the write that triggered it: a store that cannot sweep is a store that will not be able to write for much longer either, and swallowing that hides a full disk or a dead connection until the next outage.

Returns — bool: whether a sweep ran

SessionStore.close()

http.session.SessionStore.close()

Releases whatever the store holds open. The default does nothing, which is right for a store that holds nothing.


2026, Richard Ore and Zuri contributors

http.sse

import http

http exposes this as http.sse, so import http is enough and the names are called as http.sse.*. import http.sse reaches the same definitions directly.

Server-sent events (the WHATWG text/event-stream format): a one-way stream of named, identified messages that a browser’s EventSource reconnects to on its own.

Where a WebSocket gives you a duplex connection and a framing protocol to go with it, this gives you a long-lived response body and nothing else - which is all a live feed of updates actually needs, and it survives proxies that would refuse an upgrade.

import http
import http.sse

server.get('/events', @(request, response) {
  sse.stream(response, @(events) {
    iter var i = 0; i < 10; i++ {
      events.send({ tick: i }, 'tick')
      os.sleep(1)
    }
  })
})

Functions

stream()

http.sse.stream(response, producer)

Turns response into a server-sent event stream and runs producer against it.

The headers that matter are set here: text/event-stream, no caching, and X-Accel-Buffering: no for the benefit of any proxy in front that would otherwise hold the stream in a buffer and defeat the whole exercise.

Parameters

  • response (HttpResponse)
  • producer (function(1)) — called with an EventStream

last_event_id()

http.sse.last_event_id(request) -> ?string

The Last-Event-ID a reconnecting client sent, or nil.

A browser sends this automatically when EventSource reconnects, and it is how a stream resumes where it left off instead of replaying from the beginning.

Parameters

  • request (HttpRequest)

Returns ?string

parse()

http.sse.parse(text: string) -> list

Parses a text/event-stream body into a list of events, each a dictionary with event, data, id and retry.

Useful for a client consuming a stream, and for testing one.

Parameters

  • text (string)

Returns list

Classes

EventStream

class http.sse.EventStream

The writer a server-sent event stream hands to its producer.

Every method flushes, because an event that sits in a buffer is an event that has not been sent.

Constructor

http.sse.EventStream(writer)

EventStream.send()

http.sse.EventStream.send(data, event: ?string, id: ?string)

Sends one event.

data may be a string, or any value, in which case it is JSON-encoded - which is what a browser’s EventSource handler almost always parses it back out of anyway. A multi-line string is split across several data: lines, as the format requires.

Parameters

  • data (any)
  • event (?string) — the event name, which a browser dispatches under; omitted means the default message event
  • id (?string) — the event id, which the browser sends back as Last-Event-ID when it reconnects

Returns — EventStream: this same instance, for chaining

EventStream.comment()

http.sse.EventStream.comment(text: ?string)

Sends a comment line, which a client ignores.

This is the standard way to keep an idle stream alive: a proxy that closes connections after a quiet minute cannot tell a comment from real traffic.

Parameters

  • text (?string)

Returns — EventStream: this same instance, for chaining

EventStream.set_retry()

http.sse.EventStream.set_retry(milliseconds: number)

Tells the client how long to wait before reconnecting, in milliseconds.

Parameters

  • milliseconds (number)

Returns — EventStream: this same instance, for chaining

EventStream.close()

http.sse.EventStream.close()

Ends the stream. The connection closes, and a browser will reconnect unless the response said otherwise.

EventStream.is_closed()

http.sse.EventStream.is_closed() -> bool

Whether the stream has been closed.

Returns bool


2026, Richard Ore and Zuri contributors

http.status

import http

http exposes this as http.status, so import http is enough and the names are called as http.status.*. import http.status reaches the same definitions directly.

The HTTP status codes registered with IANA, their canonical reason phrases, and a handful of predicates for asking what class a code belongs to.

The names are the constants; reason() turns a code into the phrase a response line carries.

import http

echo http.status.NOT_FOUND          # 404
echo http.status.reason(404)        # Not Found
echo http.status.is_redirect(308)   # true

Constants

CONTINUE

http.status.CONTINUE: int = 100

100 Continue. The client may proceed to send the request body it announced with an Expect: 100-continue header.

SWITCHING_PROTOCOLS

http.status.SWITCHING_PROTOCOLS: int = 101

101 Switching Protocols. The server has accepted an Upgrade request; everything after the response head speaks the new protocol. This is how a WebSocket handshake completes.

PROCESSING

http.status.PROCESSING: int = 102

102 Processing (WebDAV, RFC 2518). An interim response telling a client the request is still being worked on.

EARLY_HINTS

http.status.EARLY_HINTS: int = 103

103 Early Hints (RFC 8297). Carries Link headers the client can act on (preload, preconnect) before the final response arrives.

OK

http.status.OK: int = 200

200 OK.

CREATED

http.status.CREATED: int = 201

201 Created. A new resource exists; Location names it.

ACCEPTED

http.status.ACCEPTED: int = 202

202 Accepted. The request was valid and queued, but not acted on yet, and may still ultimately fail.

NON_AUTHORITATIVE_INFORMATION

http.status.NON_AUTHORITATIVE_INFORMATION: int = 203

203 Non-Authoritative Information. A transforming proxy modified the origin server’s response.

NO_CONTENT

http.status.NO_CONTENT: int = 204

204 No Content. Success, and deliberately no body. A response with this status must never carry Content-Length or a body.

RESET_CONTENT

http.status.RESET_CONTENT: int = 205

205 Reset Content. Success; the client should reset the document view that sent the request.

PARTIAL_CONTENT

http.status.PARTIAL_CONTENT: int = 206

206 Partial Content. The body is the byte range(s) the request’s Range header asked for.

MULTI_STATUS

http.status.MULTI_STATUS: int = 207

207 Multi-Status (WebDAV, RFC 4918). The body is an XML document carrying a separate status per sub-request.

ALREADY_REPORTED

http.status.ALREADY_REPORTED: int = 208

208 Already Reported (WebDAV, RFC 5842).

IM_USED

http.status.IM_USED: int = 226

226 IM Used (RFC 3229). The response is the result of applying one or more instance manipulations to the current resource.

MULTIPLE_CHOICES

http.status.MULTIPLE_CHOICES: int = 300

300 Multiple Choices.

MOVED_PERMANENTLY

http.status.MOVED_PERMANENTLY: int = 301

301 Moved Permanently. Clients and caches may rewrite the request method to GET when following this, which is why 308 exists.

FOUND

http.status.FOUND: int = 302

302 Found. Historically (and still, in practice) followed by rewriting the method to GET.

SEE_OTHER

http.status.SEE_OTHER: int = 303

303 See Other. Follow with GET, always. This is the correct answer to a POST that should not be re-submitted on refresh.

NOT_MODIFIED

http.status.NOT_MODIFIED: int = 304

304 Not Modified. The cached representation the client already holds is still fresh. Carries no body.

USE_PROXY

http.status.USE_PROXY: int = 305

305 Use Proxy. Deprecated; clients are required to ignore it.

TEMPORARY_REDIRECT

http.status.TEMPORARY_REDIRECT: int = 307

307 Temporary Redirect. Like 302, but the method and body must be preserved when following.

PERMANENT_REDIRECT

http.status.PERMANENT_REDIRECT: int = 308

308 Permanent Redirect. Like 301, but the method and body must be preserved when following.

BAD_REQUEST

http.status.BAD_REQUEST: int = 400

400 Bad Request. The message could not be understood - malformed syntax, invalid framing, a header the server refuses to guess at.

UNAUTHORIZED

http.status.UNAUTHORIZED: int = 401

401 Unauthorized. Authentication is required or was rejected. A response with this status must carry WWW-Authenticate.

PAYMENT_REQUIRED

http.status.PAYMENT_REQUIRED: int = 402

402 Payment Required.

FORBIDDEN

http.status.FORBIDDEN: int = 403

403 Forbidden. Understood and refused; re-authenticating won’t help. Use 401 when it would.

NOT_FOUND

http.status.NOT_FOUND: int = 404

404 Not Found.

METHOD_NOT_ALLOWED

http.status.METHOD_NOT_ALLOWED: int = 405

405 Method Not Allowed. The path exists but not for this method. A response with this status must carry Allow.

NOT_ACCEPTABLE

http.status.NOT_ACCEPTABLE: int = 406

406 Not Acceptable. Nothing the server can produce matches the request’s Accept headers.

PROXY_AUTHENTICATION_REQUIRED

http.status.PROXY_AUTHENTICATION_REQUIRED: int = 407

407 Proxy Authentication Required.

REQUEST_TIMEOUT

http.status.REQUEST_TIMEOUT: int = 408

408 Request Timeout. The client took too long to send a complete request.

CONFLICT

http.status.CONFLICT: int = 409

409 Conflict. The request conflicts with the current state of the resource.

GONE

http.status.GONE: int = 410

410 Gone. Like 404, but deliberately permanent.

LENGTH_REQUIRED

http.status.LENGTH_REQUIRED: int = 411

411 Length Required.

PRECONDITION_FAILED

http.status.PRECONDITION_FAILED: int = 412

412 Precondition Failed. A conditional header (If-Match, If-Unmodified-Since, …) did not hold.

CONTENT_TOO_LARGE

http.status.CONTENT_TOO_LARGE: int = 413

413 Content Too Large. The body exceeds what the server will accept.

URI_TOO_LONG

http.status.URI_TOO_LONG: int = 414

414 URI Too Long.

UNSUPPORTED_MEDIA_TYPE

http.status.UNSUPPORTED_MEDIA_TYPE: int = 415

415 Unsupported Media Type. The body’s Content-Type isn’t one this resource handles.

RANGE_NOT_SATISFIABLE

http.status.RANGE_NOT_SATISFIABLE: int = 416

416 Range Not Satisfiable. None of the ranges asked for overlap the resource.

EXPECTATION_FAILED

http.status.EXPECTATION_FAILED: int = 417

417 Expectation Failed. The request’s Expect header names something the server cannot do.

IM_A_TEAPOT

http.status.IM_A_TEAPOT: int = 418

418 I’m a teapot (RFC 2324). Reserved, and not to be taken seriously, but registered often enough to be worth naming.

MISDIRECTED_REQUEST

http.status.MISDIRECTED_REQUEST: int = 421

421 Misdirected Request. The connection this request arrived on cannot serve the authority it names. Relevant to HTTP/2 connection coalescing.

UNPROCESSABLE_CONTENT

http.status.UNPROCESSABLE_CONTENT: int = 422

422 Unprocessable Content. Syntactically fine, semantically impossible - the usual answer to a validation failure.

LOCKED

http.status.LOCKED: int = 423

423 Locked (WebDAV, RFC 4918).

FAILED_DEPENDENCY

http.status.FAILED_DEPENDENCY: int = 424

424 Failed Dependency (WebDAV, RFC 4918).

TOO_EARLY

http.status.TOO_EARLY: int = 425

425 Too Early (RFC 8470). The server won’t risk processing a request that arrived in TLS early data.

UPGRADE_REQUIRED

http.status.UPGRADE_REQUIRED: int = 426

426 Upgrade Required. Carries an Upgrade header naming what the client must switch to.

PRECONDITION_REQUIRED

http.status.PRECONDITION_REQUIRED: int = 428

428 Precondition Required (RFC 6585). The server insists the request be conditional, to avoid a lost update.

TOO_MANY_REQUESTS

http.status.TOO_MANY_REQUESTS: int = 429

429 Too Many Requests (RFC 6585). Rate limited; Retry-After says for how long.

REQUEST_HEADER_FIELDS_TOO_LARGE

http.status.REQUEST_HEADER_FIELDS_TOO_LARGE: int = 431

431 Request Header Fields Too Large (RFC 6585).

http.status.UNAVAILABLE_FOR_LEGAL_REASONS: int = 451

451 Unavailable For Legal Reasons (RFC 7725).

INTERNAL_SERVER_ERROR

http.status.INTERNAL_SERVER_ERROR: int = 500

500 Internal Server Error.

NOT_IMPLEMENTED

http.status.NOT_IMPLEMENTED: int = 501

501 Not Implemented. The server does not recognise the method at all - not to be confused with 405, which is per-resource.

BAD_GATEWAY

http.status.BAD_GATEWAY: int = 502

502 Bad Gateway. An upstream this server proxies to returned something invalid.

SERVICE_UNAVAILABLE

http.status.SERVICE_UNAVAILABLE: int = 503

503 Service Unavailable. Temporary, by definition; pair it with Retry-After.

GATEWAY_TIMEOUT

http.status.GATEWAY_TIMEOUT: int = 504

504 Gateway Timeout. An upstream did not answer in time.

HTTP_VERSION_NOT_SUPPORTED

http.status.HTTP_VERSION_NOT_SUPPORTED: int = 505

505 HTTP Version Not Supported.

VARIANT_ALSO_NEGOTIATES

http.status.VARIANT_ALSO_NEGOTIATES: int = 506

506 Variant Also Negotiates (RFC 2295).

INSUFFICIENT_STORAGE

http.status.INSUFFICIENT_STORAGE: int = 507

507 Insufficient Storage (WebDAV, RFC 4918).

LOOP_DETECTED

http.status.LOOP_DETECTED: int = 508

508 Loop Detected (WebDAV, RFC 5842).

NOT_EXTENDED

http.status.NOT_EXTENDED: int = 510

510 Not Extended (RFC 2774).

NETWORK_AUTHENTICATION_REQUIRED

http.status.NETWORK_AUTHENTICATION_REQUIRED: int = 511

511 Network Authentication Required (RFC 6585). The captive-portal status.

Functions

reason()

http.status.reason(code: int) -> string

The canonical reason phrase for code, e.g. 'Not Found' for 404.

An unregistered code is still perfectly legal on the wire - a client is required to treat it as the generic x00 of its class - so rather than failing, this falls back to the class name ('Informational', 'Success', 'Redirection', 'Client Error', 'Server Error'), or 'Unknown' for a code outside 100-599.

Parameters

  • code (int)

Returns string

is_registered()

http.status.is_registered(code: int) -> bool

Whether code is a registered status code with a canonical reason phrase of its own.

Parameters

  • code (int)

Returns bool

is_informational()

http.status.is_informational(code: int) -> bool

Whether code is a 1xx interim status. These are consumed by the protocol layer and never surface as the final response.

Parameters

  • code (int)

Returns bool

is_success()

http.status.is_success(code: int) -> bool

Whether code is a 2xx success status.

Parameters

  • code (int)

Returns bool

is_redirect()

http.status.is_redirect(code: int) -> bool

Whether code is a 3xx redirection status.

Parameters

  • code (int)

Returns bool

is_client_error()

http.status.is_client_error(code: int) -> bool

Whether code is a 4xx client error.

Parameters

  • code (int)

Returns bool

is_server_error()

http.status.is_server_error(code: int) -> bool

Whether code is a 5xx server error.

Parameters

  • code (int)

Returns bool

is_error()

http.status.is_error(code: int) -> bool

Whether code is any kind of error, client or server.

Parameters

  • code (int)

Returns bool

is_bodiless()

http.status.is_bodiless(code: int) -> bool

Whether a response carrying code is defined to have no body at all, regardless of what headers say. RFC 9110 puts 1xx, 204 and 304 in this category, and framing a body onto any of them is a protocol violation rather than a stylistic choice.

Parameters

  • code (int)

Returns bool

preserves_method()

http.status.preserves_method(code: int) -> bool

Whether a redirect with code must keep the original method and body when followed. True for 307 and 308; 301, 302 and 303 are all conventionally rewritten to GET.

Parameters

  • code (int)

Returns bool


2026, Richard Ore and Zuri contributors

http.stream

import http.stream

http lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled http.stream.* needs import http.stream.

The transport underneath everything else: a byte stream with buffering, timeouts and optional TLS.

Connection is what both the client and the server read and write through, so the protocol layers above never have to know whether they are talking over TCP or over TLS.

Functions

is_timeout_error()

http.stream.is_timeout_error(message) -> bool

Whether a transport error message describes a timeout (or a would-block, which a socket with a receive timeout reports as the same condition) rather than a genuine failure.

Parameters

  • message (string)

Returns bool

connect()

http.stream.connect(host: string, port: int, options: ?dict) -> Connection

Opens a connection to host on port, optionally wrapping it in TLS.

Timeouts are applied to the raw socket before any TLS handshake runs, which is both the only place they can be applied and what keeps a stalled handshake from hanging forever.

Parameters

  • host (string)
  • port (int)
  • options (?dict) — secure (bool), tls_config (a net.tls.TlsConfig), server_name (the SNI/verification name, defaulting to host), connect_timeout, read_timeout and write_timeout (all milliseconds)

Returns Connection

Raises ConnectionError, TimeoutError

tunnel()

http.stream.tunnel(proxy: dict, host: string, port: int, options: ?dict) -> Connection

Opens a connection to host:port through an HTTP proxy’s CONNECT tunnel, running a TLS handshake with host inside it when secure is set.

The proxy sees only where the tunnel goes. Everything sent through it after the handshake is between this end and host, encrypted, and the certificate checked is host’s own.

Parameters

  • proxy (dict) — host and port of an HTTP proxy, and authorization, the Proxy-Authorization value to send, or nil.
  • host (string)
  • port (int)
  • options (?dict) — as connect() takes them.

Returns Connection

Raises ConnectionError when the proxy cannot be reached or refuses the tunnel, or the handshake through it fails

accept()

http.stream.accept(client, options: ?dict) -> Connection

Turns a freshly accepted TcpStream into a Connection, running a server-side TLS handshake first when tls_config is given.

Parameters

  • client (TcpStream) — the stream TcpStream.accept() returned
  • options (?dict) — tls_config (a net.tls.TlsConfig; when present the connection is wrapped in TLS), read_timeout and write_timeout (milliseconds)

Returns Connection

Raises ConnectionError if the TLS handshake fails

Classes

Connection

class http.Connection

A buffered, message-oriented view of a connected socket.

HTTP/1.1 is parsed a line at a time and then a body at a time, and HTTP/2 a frame at a time; both want to read a few bytes without paying for a syscall each time, and to write a head made of many small pieces as one write. Connection sits between the protocol code and either a plain net.TcpStream or a net.tls.TlsStream and provides exactly that, plus the peer information the transports stop reporting once TLS has consumed the underlying socket.

Instances are created by connect() and accept() below, or by from_transport() when the socket already exists.

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
peer_address?SocketAddrThe remote peer’s address, captured before any TLS wrap consumed the underlying socket.
local_address?SocketAddrThis side’s address.
secureboolWhether the connection is TLS-protected.
alpn?stringThe ALPN protocol negotiated during the TLS handshake ('h2', 'http/1.1'), or nil on a plaintext…

Constructor

http.Connection(transport, peer, local)

Wraps an already-connected transport.

Parameters

  • transport (TcpStream|TlsStream)
  • peer (?SocketAddr) — the peer address, which a TlsStream can no longer report on its own
  • local (?SocketAddr)

Connection.transport()

http.Connection.transport() -> TcpStream|TlsStream

The underlying TcpStream or TlsStream. Needed by the protocol upgrade paths (WebSocket, HTTP/2) that take a connection over wholesale.

Returns TcpStream|TlsStream

Connection.peer_certificate()

http.Connection.peer_certificate() -> ?PeerCertificate

The peer’s certificate on a TLS connection, or nil on a plaintext one or when the peer presented none.

Returns ?PeerCertificate

Connection.buffered()

http.Connection.buffered() -> number

How many bytes are already buffered and available without touching the socket.

Returns number

Connection.bytes_read()

http.Connection.bytes_read() -> number

How many bytes have been read off the transport over this connection’s whole life.

Returns number

Connection.is_eof()

http.Connection.is_eof() -> bool

Whether the peer has closed its side and the buffer is drained.

Returns bool

Connection.is_closed()

http.Connection.is_closed() -> bool

Whether close() has been called on this connection.

Returns bool

Connection.read_line()

http.Connection.read_line(limit)

Reads one CRLF-terminated line and returns it as a string with the terminator removed.

A bare LF is accepted as a line terminator as well. RFC 9112 §2.2 permits a recipient to do so, and enough real clients send one that refusing would break them; a bare CR, on the other hand, is left in the data where it belongs.

Parameters

  • limit (?number) — the longest line to tolerate, in bytes; defaults to 8192

Returns — ?string: the line, or nil at a clean EOF before any bytes of a line arrived

Raises TooLargeError if the line exceeds limit

Raises ProtocolError if EOF arrives mid-line

Raises TimeoutError, ConnectionError

Connection.read_exactly()

http.Connection.read_exactly(length: number) -> bytes

Reads exactly length bytes.

Parameters

  • length (number)

Returns bytes

Raises ProtocolError if EOF arrives first

Raises TimeoutError, ConnectionError

Connection.read_some()

http.Connection.read_some(length: number)

Reads whatever is available, up to length bytes, blocking only until the first byte arrives.

Parameters

  • length (number)

Returns — bytes: empty at EOF

Raises TimeoutError, ConnectionError

Connection.read_to_eof()

http.Connection.read_to_eof(limit) -> bytes

Reads until the peer closes its write half.

This is the framing of last resort - a response with neither Content-Length nor chunked encoding is delimited by the close itself (RFC 9112 §6.3). limit bounds how much will be accumulated so a peer that never closes cannot exhaust memory.

Parameters

  • limit (?number) — the most to accept, in bytes; nil for no limit

Returns bytes

Raises TooLargeError if limit is exceeded

Connection.discard()

http.Connection.discard(length: number)

Discards exactly length bytes without materialising them. Used to drain the body of a request whose response has already been decided, so the connection stays usable.

Parameters

  • length (number)

Raises ProtocolError if EOF arrives first

Connection.unread()

http.Connection.unread(data)

Pushes bytes back to the front of the read buffer so the next read sees them again. Used by protocol detection, which has to look at the first bytes of a connection before deciding which parser gets them.

Parameters

  • data (bytes)

Connection.write()

http.Connection.write(data)

Queues data for sending. Nothing reaches the socket until the buffer fills or flush() is called.

Parameters

  • data (bytes|string)

Connection.flush()

http.Connection.flush()

Sends everything queued and blocks until the transport has taken all of it.

Raises TimeoutError, ConnectionError

Connection.set_read_timeout()

http.Connection.set_read_timeout(milliseconds)

Sets how long a read may block before raising TimeoutError. Only effective on a plaintext connection; a TlsStream no longer owns a socket whose timeout can be changed, so a TLS connection’s timeouts have to be set on the TcpStream before it is wrapped (which connect() and accept() below both do).

Parameters

  • milliseconds (number) — 0 or negative to block indefinitely

Connection.set_write_timeout()

http.Connection.set_write_timeout(milliseconds)

Sets how long a write may block before raising TimeoutError. Carries the same TLS caveat as set_read_timeout().

Parameters

  • milliseconds (number)

Connection.set_nodelay()

http.Connection.set_nodelay(enabled)

Turns off Nagle’s algorithm, so a response head goes out immediately rather than waiting for more data to coalesce with. Every HTTP server wants this; a request is answered by a burst that is then followed by silence, which is the exact case Nagle handles badly.

Parameters

  • enabled (bool)

Connection.close()

http.Connection.close()

Flushes anything still queued and closes the transport. Safe to call more than once. A failure while flushing is swallowed - the connection is going away regardless, and the caller has usually already handled whatever went wrong.

Connection.to_string()

http.Connection.to_string()

2026, Richard Ore and Zuri contributors

http.util

import http

http exposes this as http.util, so import http is enough and the names are called as http.util.*. import http.util reaches the same definitions directly.

The small, exact pieces of the HTTP specifications that several parts of the module need: date formatting, percent-encoding, quoted strings, header parameter lists, path normalisation and authorization headers.

secure_equals() and random_token() are here because they are used where timing matters: comparing a token in constant time, and generating one that cannot be guessed.

Functions

find_bytes()

http.util.find_bytes(haystack, needle, from) -> number

Finds the first occurrence of the byte sequence needle in haystack, at or after from, or -1 when it does not occur.

The built-in bytes.index_of() searches for a single byte, which is not enough for the delimiters HTTP is full of - CRLF, the blank line ending a head, a multipart boundary. This finds candidate positions with that fast single-byte search and only then compares the rest.

Parameters

  • haystack (bytes)
  • needle (bytes)
  • from (?number)

Returns number

to_hex()

http.util.to_hex(n: number) -> string

n as lowercase hexadecimal, with no 0x prefix and no padding.

Parameters

  • n (number)

Returns string

format_date()

http.util.format_date(timestamp) -> string

Formats a Unix timestamp as an IMF-fixdate, the one date format RFC 9110 §5.6.7 requires every HTTP sender to produce: Sun, 06 Nov 1994 08:49:37 GMT.

Always UTC, always fixed-width, never localised. The two obsolete formats are accepted by parse_date() but never emitted.

Parameters

  • timestamp (?number) — seconds since the epoch; defaults to now

Returns string

parse_date()

http.util.parse_date(text: string)

Parses any of the three date formats RFC 9110 §5.6.7 requires a recipient to accept, and returns the Unix timestamp.

The two obsolete ones (RFC 850’s Sunday, 06-Nov-94 08:49:37 GMT and asctime’s Sun Nov 6 08:49:37 1994) still appear in the wild from old servers and caches, so a client that only understands IMF-fixdate will silently mishandle their Expires and Last-Modified.

Two-digit years follow the usual rule: a year that would place the date more than fifty years in the future is read as the previous century.

Parameters

  • text (string)

Returns — ?number: the timestamp, or nil if the date is unparseable

parse_parameters()

http.util.parse_parameters(value: string)

Splits a header value that carries parameters - a media type, a Content-Disposition, a challenge - into its leading value and a dictionary of lowercased parameter names to values.

Quoted strings are unquoted and their backslash escapes resolved. RFC 5987 extended parameters (filename*=UTF-8''na%C3%AFve.txt) are decoded and take precedence over the plain form of the same name, which is what lets a non-ASCII upload filename survive.

parse_parameters('text/html; charset=utf-8')
# ['text/html', {charset: 'utf-8'}]

Parameters

  • value (string)

Returns — list: [value, parameters]

unquote()

http.util.unquote(value: string) -> string

Removes the surrounding double quotes from a header value and resolves its backslash escapes. A value that isn’t quoted comes back unchanged.

Parameters

  • value (string)

Returns string

quote()

http.util.quote(value: string) -> string

Wraps value in double quotes, escaping any quote or backslash it contains, so it can be used as an RFC 9110 quoted-string.

Parameters

  • value (string)

Returns string

percent_decode()

http.util.percent_decode(text: string, plus_as_space: ?bool) -> string

Percent-decodes a URI component, turning + into a space only when plus_as_space is set - which is right for a query string or a form body, and wrong for a path, where + is a literal plus.

Invalid escapes are left as-is rather than raising: a malformed % in a path is a 404 waiting to happen, not a reason to drop the connection.

Parameters

  • text (string)
  • plus_as_space (?bool)

Returns string

percent_encode()

http.util.percent_encode(text: string) -> string

Percent-encodes every character of text that is not an RFC 3986 unreserved character, over the UTF-8 encoding of the input.

Parameters

  • text (string)

Returns string

normalize_path()

http.util.normalize_path(path: string)

Resolves the . and .. segments of a path and collapses repeated slashes, returning a path that cannot climb above /.

This is the check that stands between a static file handler and GET /static/../../etc/passwd, and it has to happen after percent-decoding, since %2e%2e%2f is the same request written to survive a naive check.

Parameters

  • path (string)

Returns — string: always starting with /

secure_equals()

http.util.secure_equals(a, b) -> bool

Compares two strings without leaking, through how long the comparison takes, where they first differ.

Use this for anything an attacker can submit repeatedly and tune - a session token, an API key, an HMAC - where a byte-at-a-time comparison hands over the secret one guess at a time.

Parameters

  • a (string)
  • b (string)

Returns bool

random_token()

http.util.random_token(length) -> string

A random lowercase-hex token of length characters, drawn from the platform’s cryptographically secure generator.

Used for multipart boundaries, WebSocket keys, and anywhere else this module needs a value an attacker must not be able to predict.

Parameters

  • length (?number) — defaults to 32

Returns string

parse_basic()

http.util.parse_basic(header)

Parses an HTTP Basic Authorization field value into a username and password.

The scheme name is matched case-insensitively, as RFC 9110 §11.1 requires. A header that is not a well-formed Basic credential - wrong scheme, undecodable base64, no colon in the decoded pair - comes back as nil rather than raising, since a malformed credential is a 401, not a crash.

Parameters

  • header (?string)

Returns — ?list: [username, password], or nil

parse_bearer()

http.util.parse_bearer(header)

Parses a Bearer Authorization field value into its token.

Parameters

  • header (?string)

Returns — ?string: the token, or nil when the header is absent or is not a bearer credential

is_valid_method()

http.util.is_valid_method(value) -> bool

Whether value is a valid HTTP method: a non-empty token, per RFC 9110 §9. Methods are case-sensitive, so no normalisation happens here.

Parameters

  • value (string)

Returns bool


2026, Richard Ore and Zuri contributors

http.websocket

import http.websocket

http lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled http.websocket.* needs import http.websocket.

WebSocket (RFC 6455), both ends of it.

accept() upgrades an inbound request, connect() opens an outbound connection, and WebSocket is the resulting full-duplex channel. Message is one complete message, reassembled from however many frames carried it, and the CLOSE_* constants are the status codes a close may carry.

Constants

OPCODE_CONTINUATION

http.websocket.OPCODE_CONTINUATION = 0

Continuation frame. @type int

OPCODE_TEXT

http.websocket.OPCODE_TEXT = 1

Text frame. @type int

OPCODE_BINARY

http.websocket.OPCODE_BINARY = 2

Binary frame. @type int

OPCODE_CLOSE

http.websocket.OPCODE_CLOSE = 8

Close frame. @type int

OPCODE_PING

http.websocket.OPCODE_PING = 9

Ping frame. @type int

OPCODE_PONG

http.websocket.OPCODE_PONG = 10

Pong frame. @type int

CLOSE_NORMAL

http.websocket.CLOSE_NORMAL = 1000

Normal closure. @type int

CLOSE_GOING_AWAY

http.websocket.CLOSE_GOING_AWAY = 1001

The endpoint is going away. @type int

CLOSE_PROTOCOL_ERROR

http.websocket.CLOSE_PROTOCOL_ERROR = 1002

A protocol error was detected. @type int

CLOSE_UNSUPPORTED

http.websocket.CLOSE_UNSUPPORTED = 1003

A message of a kind this endpoint cannot accept. @type int

CLOSE_INVALID_PAYLOAD

http.websocket.CLOSE_INVALID_PAYLOAD = 1007

A text message that was not valid UTF-8. @type int

CLOSE_POLICY_VIOLATION

http.websocket.CLOSE_POLICY_VIOLATION = 1008

A message that violates a policy. @type int

CLOSE_TOO_LARGE

http.websocket.CLOSE_TOO_LARGE = 1009

A message too large to process. @type int

CLOSE_INTERNAL_ERROR

http.websocket.CLOSE_INTERNAL_ERROR = 1011

An unexpected condition on the server. @type int

Functions

accept_key()

http.websocket.accept_key(key: string) -> string

The value a server must return in Sec-WebSocket-Accept for a given client key.

Parameters

  • key (string) — the client’s Sec-WebSocket-Key

Returns string

is_handshake()

http.websocket.is_handshake(request) -> bool

Whether request is a well-formed WebSocket handshake.

Parameters

  • request (HttpRequest)

Returns bool

accept()

http.websocket.accept(request, response, options: ?dict) -> WebSocket

Completes a WebSocket handshake and takes over the connection.

Call this from an ordinary route handler. It writes the 101 response itself and hands back a WebSocket; the server will not write anything more on that connection afterwards.

server.get('/ws', @(request, response) {
  var socket = websocket.accept(request, response)
  # ... talk to the socket from here
})

Parameters

  • request (HttpRequest)
  • response (HttpResponse)
  • options (?dict) — protocols (the subprotocols this server supports, most preferred first), max_message_size

Returns WebSocket

Raises HttpError if the request is not a WebSocket handshake, or arrived on a connection that cannot be taken over

connect()

http.websocket.connect(target: string, options: ?dict) -> WebSocket

Opens a WebSocket connection to target.

Parameters

  • target (string) — a ws:// or wss:// URL
  • options (?dict) — headers (extra request headers), protocols (subprotocols to offer), tls_config, connect_timeout, max_message_size

Returns WebSocket

Raises HttpError if the server does not complete the handshake

Classes

Message

class http.websocket.Message

One complete WebSocket message, with any fragmentation already reassembled.

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
opcodeintThe opcode of the message: OPCODE_TEXT, OPCODE_BINARY, OPCODE_CLOSE, OPCODE_PING or OPCODE_PONG.
databytesThe payload.

Constructor

http.websocket.Message(opcode, data)

Message.is_text()

http.websocket.Message.is_text() -> bool

Whether this is a text message.

Returns bool

Message.is_binary()

http.websocket.Message.is_binary() -> bool

Whether this is a binary message.

Returns bool

Message.is_close()

http.websocket.Message.is_close() -> bool

Whether this is a close frame, meaning the peer has begun the closing handshake.

Returns bool

Message.text()

http.websocket.Message.text() -> string

The payload decoded as UTF-8 text.

Returns string

Message.close_code()

http.websocket.Message.close_code() -> ?number

The close code carried by a close frame, or nil when the frame carried no code (which RFC 6455 §7.1.5 says to read as 1005, “no status received”).

Returns ?number

Message.close_reason()

http.websocket.Message.close_reason() -> string

The human-readable reason carried by a close frame.

Returns string

Message.to_string()

http.websocket.Message.to_string()

WebSocket

class http.WebSocket

An open WebSocket connection (RFC 6455).

Created by accept() on the server side or connect() on the client side, never directly.

server.get('/ws', @(request, response) {
  var socket = websocket.accept(request, response)

  while socket.is_open() {
    var message = socket.receive()
    if message == nil {
      break
    }
    socket.send('you said: ' + message.text())
  }
})

Ping frames are answered automatically, and a close frame from the peer is answered and then reported to the caller, so the closing handshake completes without the application having to know the rules.

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
is_clientboolWhether this endpoint is the client, and so must mask every frame it sends.
protocol?stringThe subprotocol negotiated during the handshake, or nil.
max_message_sizenumberThe largest message this endpoint will assemble, in bytes.

Constructor

http.WebSocket(connection, is_client, protocol)

WebSocket.is_open()

http.WebSocket.is_open() -> bool

Whether the connection is still open.

Returns bool

WebSocket.connection()

http.WebSocket.connection() -> Connection

The underlying transport, for anything this class does not expose - a read timeout, the peer’s address, its certificate.

Returns Connection

WebSocket.send()

http.WebSocket.send(text: string)

Sends a text message.

Parameters

  • text (string)

WebSocket.send_binary()

http.WebSocket.send_binary(data)

Sends a binary message.

Parameters

  • data (bytes)

WebSocket.ping()

http.WebSocket.ping(data)

Sends a ping. A conforming peer answers with a pong carrying the same payload, which is how an idle connection is kept alive through a NAT or a proxy that would otherwise time it out.

Parameters

  • data (?bytes) — at most 125 bytes

WebSocket.pong()

http.WebSocket.pong(data)

Sends a pong. Only needed to answer a ping this class did not answer for you, or as an unsolicited heartbeat.

Parameters

  • data (?bytes)

WebSocket.close()

http.WebSocket.close(code: ?number, reason: ?string)

Begins the closing handshake and closes the transport.

Parameters

  • code (?number) — a close code; defaults to CLOSE_NORMAL
  • reason (?string) — at most 123 bytes once encoded

WebSocket.receive()

http.WebSocket.receive() -> ?Message

Reads the next message, reassembling fragments and answering pings along the way.

Returns nil when the connection closes without a close frame. A close frame is returned as a message, and answered first, so the caller can see the code and reason the peer sent.

Returns ?Message

Raises ProtocolError if the peer violates the framing rules

WebSocket.to_string()

http.WebSocket.to_string()

2026, Richard Ore and Zuri contributors

http.worker

import http

http exposes this as http.worker, so import http is enough and the names are called as http.worker.*. import http.worker reaches the same definitions directly.

The multi-process side of HttpServer: a pool of isolates, each accepting and serving connections from the same listening socket.

Each worker polls the connections it holds rather than blocking on one, so a slow client occupies a descriptor and not a thread. This is what HttpServer uses when it is configured with more than one worker.

Functions

worker_main()

http.worker.worker_main(connections, setup, options)

The loop each worker isolate runs: build a server of its own from setup, then serve whatever connections the acceptor hands it.

This lives in a module because it names imported ones of its own (HttpServer, net). A module value cannot be handed to an isolate, but a function belonging to a module is resolved there by name, with its imports resolved again on that side.

Parameters

  • connections (Channel) — accepted sockets arrive here
  • setup (function(1)) — called once with this worker’s server
  • options (dict)

serve()

http.worker.serve(setup, options: ?dict)

Binds a listening socket and serves it across a pool of worker isolates.

The calling isolate does nothing but accept, which is cheap; every accepted socket is moved across a channel to whichever worker takes it next. Because isolates share no memory, each worker builds its own routes by calling setup.

backlog is how many accepted-but-unclaimed connections may queue before the acceptor blocks. Bounding it is deliberate: an unbounded queue under load means accepting connections faster than they can be served and answering all of them late, rather than letting the kernel’s own listen backlog apply back-pressure.

on_ready is called once in this isolate with the bound address and a function that stops the server, which is what a signal handler needs in order to shut down cleanly:

http.serve(app.setup, {
  port: 3000,
  on_ready: @(address, stop) {
    echo 'listening on ${address}'
    os.on_signal('INT', @() {
      stop()
      return true
    })
  },
})

Parameters

  • setup (function(1))
  • options (?dict) — port, host, workers, backlog, on_ready, max_connections (stop after serving that many), cert_chain, private_key, plus any HttpServer field to apply to every worker

Raises HttpError if the socket cannot be bound

Note: Each worker holds an isolate pool thread for as long as the server runs. serve() sizes the pool for that when it is the first thing in the process to use an isolate; when it is not - because something already spawned one, fixing the pool’s size - it raises rather than deadlocking behind workers that never finish. Call isolate.configure() yourself, first thing, if the process needs isolates for anything besides serving.


2026, Richard Ore and Zuri contributors

rpc

import rpc

JSON-RPC 2.0: calling methods on another program, and answering its calls, over HTTP, WebSockets, sockets, pipes, or any stream of bytes.

JSON-RPC is a small, stateless protocol for calling methods in another program. It defines the messages and nothing more: which methods exist is up to the programs using it, and the messages travel over whatever connects them. A message is a small JSON object: a request names a method and carries its parameters and an id, the response carries the same id and a result or an error, and a notification is a request nobody answers. Either side may send any of them, so the protocol has no fixed client and server.

The module is built in layers, each usable on its own:

  • Request, Notification, Response and RpcError are the messages, and encode(), decode() and read() turn them to and from JSON, checking every rule of the specification.
  • A Service holds the handler for each method a program serves, and answers every message it is given with them.
  • Over HTTP, http_handler() serves a service from a route of an http server, and HttpClient calls one.
  • HeaderFraming, LineFraming and MessageFraming tell messages apart in a stream: by a Content-Length header in front of each, one to a line, or one to each read of a transport that keeps them apart itself.
  • A transport carries the bytes of a connection: StdioTransport over the program’s own standard streams, SocketTransport over a socket, ProcessTransport to a child process, WebSocketTransport over a WebSocket, and the two ends of a pipe() between isolates.
  • An Endpoint is a service on a connection: it answers what arrives, and calls the other side with requests and notifications of its own.
  • serve() gives every connection to a listening socket an endpoint of its own, across a pool of isolates.
import rpc
import isolate

var ends = rpc.pipe()

isolate.spawn(@(transport) {
  import rpc

  rpc.endpoint(transport)
    .on_request('greet', @(params) => 'Hello, ${params.name}!')
    .serve()
}, ends[1])

var client = rpc.endpoint(ends[0])

echo client.request('greet', { name: 'Zuri' })  # Hello, Zuri!
client.close()

The rpc API

Every public name in rpc, wherever it is declared. Each links to the page that documents it.

NameKindSummary
rpc.ChannelTransportclassTalks over two isolate channels: what it reads arrives on inbox and what it writes goes to outbox.
rpc.ContextclassWhat a handler is told about the message it is handling: the request’s id, its method, the endpoint…
rpc.DEFAULT_BATCH_LIMITconstantThe most messages a batch may hold before it is refused whole, 1000, until Service.set_batch_limit() says…
rpc.DEFAULT_MAX_SIZEconstantThe largest message a framing accepts unless told otherwise: 64 MiB.
rpc.EndpointclassOne side of a JSON-RPC connection over a transport.
rpc.HeaderFramingclassFrames each message with a header block giving its length:
rpc.HttpClientclassCalls a JSON-RPC service over HTTP: each call is one POST to url, and its answer is the response.
rpc.INTERNAL_ERRORconstantThe code for a request that failed while it was being handled, -32603.
rpc.INVALID_PARAMSconstantThe code for a request whose parameters the method cannot take, -32602.
rpc.INVALID_REQUESTconstantThe code for valid JSON that is not a valid JSON-RPC message, -32600.
rpc.LineFramingclassFrames each message as one line, ended by \n, the framing of newline-delimited JSON.
rpc.METHOD_NOT_FOUNDconstantThe code for a request naming a method the other side does not have, -32601.
rpc.MessageFramingclassTakes each piece it is fed as one whole message, for a transport that keeps messages apart itself, such as a…
rpc.NotificationclassA call that expects no answer: the method to run and its params.
rpc.PARSE_ERRORconstantThe code for a message that is not valid JSON, -32700.
rpc.ProcessTransportclassTalks to a child process over its standard input and output, from os.spawn() with stdin and stdout both…
rpc.RequestclassA call that expects an answer: the method to run, its params, and the id the answer comes back with.
rpc.ResponseclassThe answer to a Request: its id, and either the result the method returned or the error it failed…
rpc.RpcClosedErrorclassRaised by Endpoint.request() when the connection ends before the answer arrives, and by sending on an…
rpc.RpcErrorclassA JSON-RPC error: what a request failed with, on either side of a connection.
rpc.RpcFramingErrorclassRaised when the framing of a stream cannot be read: a header block that is not ASCII, a header without a…
rpc.RpcHttpErrorclassRaised by an HttpClient when the server answers with an HTTP status that carries no JSON-RPC answer:…
rpc.RpcTimeoutErrorclassRaised by Endpoint.request() when its timeout runs out before the answer arrives.
rpc.SERVER_ERROR_MAXconstantThe highest code JSON-RPC sets aside for errors a server defines itself, -32000.
rpc.SERVER_ERROR_MINconstantThe lowest code JSON-RPC sets aside for errors a server defines itself, -32099.
rpc.ServiceclassThe methods a program serves, and the answering of every message sent to them.
rpc.SocketTransportclassTalks over a connected stream from net: a TcpStream, a UnixStream or a TlsStream, or anything else…
rpc.StdioTransportclassTalks over the program’s own standard input and output: what it reads arrives on stdin, and what it writes…
rpc.WebSocketTransportclassTalks over a WebSocket from http.websocket, on either end of it: one websocket.accept() returned in a…
rpc.decodefunctionDecodes the JSON text of a message, or of a batch, and reads it as read() does.
rpc.encodefunctionThe JSON text of a message, or of a list of messages sent as a batch.
rpc.endpointfunctionA new Endpoint over transport, framing its messages with a HeaderFraming.
rpc.http_handlerfunctionA route handler for an http server that answers JSON-RPC with service: register it for POST on whatever…
rpc.pipefunctionTwo ChannelTransports joined to each other: what one writes, the other reads.
rpc.processfunctionA transport to a child process, over its standard input and output.
rpc.readfunctionReads a value decoded from JSON as a message: a Request, a Notification or a Response.
rpc.servefunctionServes JSON-RPC on a listening socket, across a pool of worker isolates, until it is stopped.
rpc.socketfunctionA transport over a connected socket from net.
rpc.stdiofunctionA transport over the program’s own standard input and output.
rpc.websocketfunctionA transport over a WebSocket from http.websocket.

Submodules

ModuleReached asSummary
rpc.endpointrpc.*Endpoint: one side of a JSON-RPC connection.
rpc.errorrpc.*The error codes JSON-RPC defines and the errors rpc raises, kept in a module of their own so every other…
rpc.framingrpc.*How messages are told apart in a stream of bytes.
rpc.httprpc.*JSON-RPC over HTTP, both ends of it.
rpc.messagerpc.*The messages JSON-RPC 2.0 exchanges, a Request, a Notification and a Response, and turning them to and…
rpc.serverrpc.*Serving JSON-RPC to many connections at once, over TCP, TLS or a Unix domain socket.
rpc.servicerpc.*Service: what a program answers.
rpc.transportrpc.*Where an endpoint’s bytes come from and go to.

Functions

endpoint()

rpc.endpoint(transport) -> Endpoint

A new Endpoint over transport, framing its messages with a HeaderFraming.

Parameters

  • transport (any) — A transport; see {rpc.transport}.

Returns Endpoint

stdio()

rpc.stdio() -> StdioTransport

A transport over the program’s own standard input and output.

Returns StdioTransport

socket()

rpc.socket(socket) -> SocketTransport

A transport over a connected socket from net.

Parameters

  • socket (any) — A connected TcpStream, UnixStream or TlsStream.

Returns SocketTransport

process()

rpc.process(child) -> ProcessTransport

A transport to a child process, over its standard input and output.

Parameters

  • child (os.Process) — A child spawned with stdin and stdout both 'pipe'.

Returns ProcessTransport

websocket()

rpc.websocket(socket) -> WebSocketTransport

A transport over a WebSocket from http.websocket.

Parameters

  • socket (http.websocket.WebSocket) — An open WebSocket, from websocket.accept() or websocket.connect().

Returns WebSocketTransport


2026, Richard Ore and Zuri contributors

rpc.endpoint

import rpc

Everything here is re-exported by rpc, so import rpc is enough and the names are called as rpc.*. Importing rpc.endpoint on its own works too and reaches the same definitions.

Endpoint: one side of a JSON-RPC connection. It is a Service on a connection: it answers the requests and notifications the other side sends, and sends its own. JSON-RPC makes no difference between a client and a server, so the same endpoint can do both on one connection.

Re-exported by rpc.

Classes

Endpoint

class rpc.Endpoint < Service

One side of a JSON-RPC connection over a transport.

An endpoint is a Service, so its handlers, its fallback and its batch limit work exactly as a service’s do. The connection adds the other direction: an endpoint calls the other side with request(), request_batch(), send_request() and notify(), and a handler on one can defer() its answer and send it later.

import rpc
import isolate

var ends = rpc.pipe()

# The server runs on an isolate of its own, answering until the
# client closes its end.
isolate.spawn(@(transport) {
  import rpc

  rpc.endpoint(transport)
    .on_request('add', @(params) => params[0] + params[1])
    .serve()
}, ends[1])

var client = rpc.endpoint(ends[0])

echo client.request('add', [2, 3])  # 5
client.close()

An endpoint reads its transport in one of three ways. serve() reads and answers until the connection ends, on the isolate that calls it. listen() reads on an isolate of its own and returns a Channel of what arrives, for a program that waits on other things as well; it passes each one to dispatch() when it is ready to handle it. A program that reads the transport itself, such as one polling many connections, hands what it reads to feed(). Every handler runs on the isolate that owns the endpoint, and everything is sent from there.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

rpc.Endpoint(transport)

Returns a new Endpoint over transport. Its messages are framed the way the transport keeps them apart: with the transport’s own framing() when it has one, as a WebSocket does, and with a HeaderFraming otherwise.

Parameters

  • transport (any) — A transport; see rpc.transport.

Endpoint.set_framing()

rpc.Endpoint.set_framing(framing) -> Endpoint

Sets how messages are told apart in the stream. Set it before listen(), which hands the framing to the isolate it reads on.

Parameters

  • framing (HeaderFraming|LineFraming|MessageFraming) — Default the transport’s own framing(), or HeaderFraming().

Returns Endpoint — itself.

Endpoint.notify()

rpc.Endpoint.notify(method: string, params)

Sends a notification: calls method on the other side and expects nothing back.

Parameters

  • method (string)
  • params (list|dict|nil) — Default nil, which sends none.

Raises RpcClosedError when the endpoint has been closed.

Endpoint.send_request()

rpc.Endpoint.send_request(method: string, params, callback: function)

Sends a request and returns at once; callback is called with the answer when it arrives, on the isolate that owns the endpoint, while it serves, waits on a request, or dispatches.

Parameters

  • method (string)
  • params (list|dict|nil)
  • callback (function(2)) — Called with the result and nil, or with nil and the error: an {RpcError} the other side answered with, or an {RpcClosedError} when the connection ended first.

Returns — int: The request’s id.

Raises RpcClosedError when the endpoint has been closed.

Endpoint.request()

rpc.Endpoint.request(method: string, params, timeout: ?number)

Sends a request and waits for its answer, handling whatever else arrives in the meantime as serve() would.

var sum = endpoint.request('add', [2, 3])

An error answer that names no request, which the other side sends for a message it cannot read at all, goes to on_error() rather than to any one request.

Parameters

  • method (string)
  • params (list|dict|nil)
  • timeout (?number) — The most seconds to wait. Default nil, which waits until the answer comes or the connection ends. A timeout needs the endpoint to be listening; one reading its transport itself waits as long as the transport’s reads do.

Returns — any: The result the other side answered with.

Raises RpcError when the other side answers with an error.

Raises RpcClosedError when the endpoint has been closed, or the connection ends before the answer.

Raises RpcTimeoutError when timeout runs out first.

Endpoint.request_batch()

rpc.Endpoint.request_batch(calls: list, timeout: ?number)

Sends several requests as one batch and waits for every answer, handling whatever else arrives in the meantime as request() does. The other side may answer them in any order; they come back in the order of calls.

var sums = endpoint.request_batch([['add', [1, 2]], ['add', [3, 4]]])

Parameters

  • calls (list) — Each call as a pair, [method, params].
  • timeout (?number) — The most seconds to wait for every answer, as for request().

Returns — list: For each call, in order, the result the other side answered with, or the {RpcError} it answered with.

Raises ValueError when calls is empty, or a call is not a [method, params] pair.

Raises RpcClosedError when the endpoint has been closed, or the connection ends before every answer.

Raises RpcTimeoutError when timeout runs out first.

Endpoint.send()

rpc.Endpoint.send(message)

Sends a message as it is: a Request, a Notification, a Response, or a list of them as a batch.

Parameters

  • message (Request|Notification|Response|list)

Raises RpcClosedError when the endpoint has been closed.

Endpoint.serve()

rpc.Endpoint.serve()

Reads and answers messages until the connection ends, stop() is called, or the endpoint is closed. On a listening endpoint, it takes what arrives from the Channel listen() returned.

A message that is not valid UTF-8, or not valid JSON, is answered with PARSE_ERROR and an id of nil.

Raises RpcFramingError when the stream cannot be read past a message.

Endpoint.stop()

rpc.Endpoint.stop()

Makes serve() return once the message it is handling is done.

Endpoint.listen()

rpc.Endpoint.listen() -> isolate.Channel

Starts reading the transport on an isolate of its own, and returns the Channel each message is put on as it arrives, already decoded from JSON. Pass each item taken from it to dispatch(), on the isolate that owns the endpoint, when the program is ready to handle it.

var inbox = endpoint.listen()

while !endpoint.is_closed() {
  var ready = isolate.select([inbox, work], 0.5)

  if ready[0] == inbox {
    endpoint.dispatch(ready[1])
  }
}

Each item is a dictionary whose kind says what arrived:

  • { kind: 'message', text, value }: a message, as its JSON text and the value decoded from it.
  • { kind: 'unreadable', text, message }: a message that is not valid JSON, with text nil when it is not valid UTF-8 either, and the parse error it is answered with.
  • { kind: 'closed' }: the end of the connection.
  • { kind: 'failed', message }: a failure reading the transport, which ends the connection.

Calling listen() again returns the same Channel.

Returns isolate.Channel

Raises ValueError when the transport cannot be read from another isolate; see can_listen() in rpc.transport.

Endpoint.dispatch()

rpc.Endpoint.dispatch(item: dict)

Handles one item taken from the Channel listen() returned: answers a message, or marks the connection ended. A failed connection is reported to on_error() with an RpcClosedError.

Parameters

  • item (dict)

Endpoint.handle()

rpc.Endpoint.handle(value)

Handles one message already decoded from JSON, or a batch of them, exactly as if it had just arrived, and sends whatever answer it has.

Parameters

  • value (any) — A value as json.decode() returns it.

Endpoint.feed()

rpc.Endpoint.feed(data: bytes)

Handles every message data completes, for a program that reads the transport itself rather than through serve() or listen(), such as one polling many connections. Empty data ends the connection, as an empty read does.

Parameters

  • data (bytes) — The next bytes read from the transport.

Raises RpcFramingError when the stream cannot be read past a message.

Endpoint.close()

rpc.Endpoint.close()

Closes the transport. Nothing more can be sent, and every request still waiting is answered with an RpcClosedError.

Endpoint.is_closed()

rpc.Endpoint.is_closed() -> bool

True once the endpoint is closed or the connection has ended.

Returns bool

Endpoint.to_string()

rpc.Endpoint.to_string() -> string

This endpoint as Endpoint(transport).

Returns string


2026, Richard Ore and Zuri contributors

rpc.error

import rpc

Everything here is re-exported by rpc, so import rpc is enough and the names are called as rpc.*. Importing rpc.error on its own works too and reaches the same definitions.

The error codes JSON-RPC defines and the errors rpc raises, kept in a module of their own so every other part of rpc can use them. Re-exported by rpc.

Constants

PARSE_ERROR

rpc.PARSE_ERROR

The code for a message that is not valid JSON, -32700.

INVALID_REQUEST

rpc.INVALID_REQUEST

The code for valid JSON that is not a valid JSON-RPC message, -32600.

METHOD_NOT_FOUND

rpc.METHOD_NOT_FOUND

The code for a request naming a method the other side does not have, -32601.

INVALID_PARAMS

rpc.INVALID_PARAMS

The code for a request whose parameters the method cannot take, -32602.

INTERNAL_ERROR

rpc.INTERNAL_ERROR

The code for a request that failed while it was being handled, -32603.

SERVER_ERROR_MIN

rpc.SERVER_ERROR_MIN

The lowest code JSON-RPC sets aside for errors a server defines itself, -32099. The range runs up to SERVER_ERROR_MAX.

SERVER_ERROR_MAX

rpc.SERVER_ERROR_MAX

The highest code JSON-RPC sets aside for errors a server defines itself, -32000.

Classes

RpcError

class rpc.RpcError < Error

A JSON-RPC error: what a request failed with, on either side of a connection.

Raised from a request handler, it answers the request: its code, message and data go back to the caller as they are. Endpoint.request() raises one when the other side answers a request with an error.

JSON-RPC reserves the codes from -32768 to -32000 for itself; the constants in this module name the ones it defines. An application’s own errors take any code outside that range.

import rpc

var error = rpc.RpcError(rpc.INVALID_PARAMS, 'a name is required', { field: 'name' })

echo error.code       # -32602
echo error.to_dict()  # {code: -32602, message: a name is required, data: {field: name}}

Constructor

rpc.RpcError(code: int, message: string, data)

Returns a new RpcError.

Parameters

  • code (int) — The error’s code.
  • message (string) — A short description of what went wrong.
  • data (any) — Anything more the other side may need, sent along with the error. Default nil, which sends nothing.

RpcError.to_dict()

rpc.RpcError.to_dict() -> dict

The error as JSON-RPC sends it: { code, message }, and data as well when the error carries any.

Returns dict

RpcFramingError

class rpc.RpcFramingError < Error

Raised when the framing of a stream cannot be read: a header block that is not ASCII, a header without a length, a length that is not a whole number, a header or message larger than the framing allows, or a charset other than UTF-8. Nothing after it in the stream can be read.

Constructor

rpc.RpcFramingError(message: string)

Returns a new RpcFramingError.

Parameters

  • message (string) — What was wrong with the stream.

RpcClosedError

class rpc.RpcClosedError < Error

Raised by Endpoint.request() when the connection ends before the answer arrives, and by sending on an endpoint that has been closed.

Constructor

rpc.RpcClosedError(message: string)

Returns a new RpcClosedError.

Parameters

  • message (string) — What could not be done.

RpcTimeoutError

class rpc.RpcTimeoutError < Error

Raised by Endpoint.request() when its timeout runs out before the answer arrives.

Constructor

rpc.RpcTimeoutError(message: string)

Returns a new RpcTimeoutError.

Parameters

  • message (string) — What timed out.

RpcHttpError

class rpc.RpcHttpError < Error

Raised by an HttpClient when the server answers with an HTTP status that carries no JSON-RPC answer: anything other than 200, or 202 and 204 for a message that needs no answer, unless the body is a JSON-RPC answer all the same.

Constructor

rpc.RpcHttpError(status: int, message: string, body: string)

Returns a new RpcHttpError.

Parameters

  • status (int) — The HTTP status the server answered with.
  • message (string) — What went wrong.
  • body (string) — The body the server sent with it, as text; '' for none.

2026, Richard Ore and Zuri contributors

rpc.framing

import rpc

Everything here is re-exported by rpc, so import rpc is enough and the names are called as rpc.*. Importing rpc.framing on its own works too and reaches the same definitions.

How messages are told apart in a stream of bytes. A stream carries one message after another with nothing to mark where one ends, so each is framed: HeaderFraming puts a header giving its length in front of it, and LineFraming ends it with a line break. A transport that already keeps messages apart, as a WebSocket does, needs no framing of its own, and MessageFraming takes each of its reads as one whole message.

A framing is fed the bytes as they arrive, in whatever pieces they arrive in, and hands back each message once the whole of it is there. It frames the other way too, turning a message’s text into the bytes to send.

Re-exported by rpc.

Constants

DEFAULT_MAX_SIZE

rpc.DEFAULT_MAX_SIZE = 67108864

The largest message a framing accepts unless told otherwise: 64 MiB.

Classes

HeaderFraming

class rpc.HeaderFraming

Frames each message with a header block giving its length:

Content-Length: 42\r\n
\r\n
{"jsonrpc":"2.0","id":1,"method":"status"}

The header block is ASCII: lines of Name: value, each ended by \r\n, and a blank line after them. Content-Length gives the length of the message after the blank line, in bytes, and is required. Content-Type may be given, and if it names a charset that charset must be UTF-8. Header names are read without regard to case, and any other header is ignored. A message is sent with a Content-Length header alone.

import rpc

var framing = rpc.HeaderFraming()
var stream = framing.frame('{"a":1}') + framing.frame('{"b":2}')

# However the bytes are split, the same two messages come out.
var first = framing.feed(stream[0, 30])
var second = framing.feed(stream[30, stream.length()])

echo first.map(@(m) => m.to_string())   # [{"a":1}]
echo second.map(@(m) => m.to_string())  # [{"b":2}]
  • printable — has a @to_string(), so echo and print() show something useful

Constructor

rpc.HeaderFraming()

Returns a new HeaderFraming with nothing buffered and a maximum message size of DEFAULT_MAX_SIZE.

HeaderFraming.set_max_size()

rpc.HeaderFraming.set_max_size(size: int) -> HeaderFraming

Sets the largest message, in bytes, that feed() accepts.

Parameters

  • size (int) — Default DEFAULT_MAX_SIZE, 64 MiB.

Returns HeaderFraming — itself.

HeaderFraming.max_size()

rpc.HeaderFraming.max_size() -> int

The largest message, in bytes, that feed() accepts.

Returns int

HeaderFraming.pending()

rpc.HeaderFraming.pending() -> int

How many bytes are held waiting for the rest of a message.

Returns int

HeaderFraming.feed()

rpc.HeaderFraming.feed(data: bytes) -> list[bytes]

Takes the next bytes of the stream and returns every message they complete, as the bytes of its text, in the order they arrived. A message cut off at the end of data is held until the rest of it is fed.

Parameters

  • data (bytes) — The next bytes of the stream.

Returns list[bytes]

Raises RpcFramingError when the header block is not ASCII, has no Content-Length, or runs past 8 KiB, its length is not a whole number of bytes, a header line has no :, the charset is not UTF-8, or the message is larger than max_size(). Nothing after it in the stream can be read.

HeaderFraming.frame()

rpc.HeaderFraming.frame(message) -> bytes

The bytes that send message: its text with a Content-Length header in front.

Parameters

  • message (string|bytes) — The message’s text.

Returns bytes

HeaderFraming.to_string()

rpc.HeaderFraming.to_string() -> string

This framing as HeaderFraming(n bytes pending).

Returns string

LineFraming

class rpc.LineFraming

Frames each message as one line, ended by \n, the framing of newline-delimited JSON. JSON text holds no line breaks of its own, so a line is always exactly one message.

A line may end in \r\n as well, and an empty line is skipped.

import rpc

var framing = rpc.LineFraming()
var lines = framing.feed('{"a":1}\n{"b":'.to_bytes())

echo lines.length()  # 1
echo framing.feed('2}\n'.to_bytes())[0].to_string()  # {"b":2}
  • printable — has a @to_string(), so echo and print() show something useful

Constructor

rpc.LineFraming()

Returns a new LineFraming with nothing buffered and a maximum message size of DEFAULT_MAX_SIZE.

LineFraming.set_max_size()

rpc.LineFraming.set_max_size(size: int) -> LineFraming

Sets the longest line, in bytes, that feed() accepts.

Parameters

  • size (int) — Default DEFAULT_MAX_SIZE, 64 MiB.

Returns LineFraming — itself.

LineFraming.max_size()

rpc.LineFraming.max_size() -> int

The longest line, in bytes, that feed() accepts.

Returns int

LineFraming.pending()

rpc.LineFraming.pending() -> int

How many bytes are held waiting for the end of a line.

Returns int

LineFraming.feed()

rpc.LineFraming.feed(data: bytes) -> list[bytes]

Takes the next bytes of the stream and returns every line they complete, without its line break, as bytes, in the order they arrived. A line cut off at the end of data is held until the rest of it is fed.

Parameters

  • data (bytes) — The next bytes of the stream.

Returns list[bytes]

Raises RpcFramingError when a line runs past max_size(). Nothing after it in the stream can be read.

LineFraming.frame()

rpc.LineFraming.frame(message) -> bytes

The bytes that send message: its text and a \n.

Parameters

  • message (string|bytes) — The message’s text, which must hold no line break.

Returns bytes

LineFraming.to_string()

rpc.LineFraming.to_string() -> string

This framing as LineFraming(n bytes pending).

Returns string

MessageFraming

class rpc.MessageFraming

Takes each piece it is fed as one whole message, for a transport that keeps messages apart itself, such as a WebSocket, whose every read returns exactly one message. Framing a message adds nothing to it.

import rpc

var framing = rpc.MessageFraming()

echo framing.feed('{"a":1}'.to_bytes())[0].to_string()  # {"a":1}
echo framing.frame('{"b":2}').to_string()               # {"b":2}
  • printable — has a @to_string(), so echo and print() show something useful

Constructor

rpc.MessageFraming()

Returns a new MessageFraming with a maximum message size of DEFAULT_MAX_SIZE.

MessageFraming.set_max_size()

rpc.MessageFraming.set_max_size(size: int) -> MessageFraming

Sets the largest message, in bytes, that feed() accepts.

Parameters

  • size (int) — Default DEFAULT_MAX_SIZE, 64 MiB.

Returns MessageFraming — itself.

MessageFraming.max_size()

rpc.MessageFraming.max_size() -> int

The largest message, in bytes, that feed() accepts.

Returns int

MessageFraming.pending()

rpc.MessageFraming.pending() -> int

How many bytes are held waiting for the rest of a message: always 0, since every piece is a message of its own.

Returns int

MessageFraming.feed()

rpc.MessageFraming.feed(data: bytes) -> list[bytes]

Takes one whole message and returns it, alone in a list. Empty data holds no message and returns an empty list.

Parameters

  • data (bytes) — One whole message.

Returns list[bytes]

Raises RpcFramingError when the message is larger than max_size().

MessageFraming.frame()

rpc.MessageFraming.frame(message) -> bytes

The bytes that send message: its text, as it is.

Parameters

  • message (string|bytes) — The message’s text.

Returns bytes

MessageFraming.to_string()

rpc.MessageFraming.to_string() -> string

This framing as MessageFraming().

Returns string


2026, Richard Ore and Zuri contributors

rpc.http

import rpc

Everything here is re-exported by rpc, so import rpc is enough and the names are called as rpc.*. Importing rpc.http on its own works too and reaches the same definitions.

JSON-RPC over HTTP, both ends of it. Each message, or batch, is the body of a POST, and its answer is the body of the response, so every exchange stands on its own: no connection is kept between calls, and nothing is framed.

http_handler() serves a Service from a route of an http server, and HttpClient calls a JSON-RPC service over HTTP.

The statuses on the wire follow common practice:

StatusWhen
200an answer, whether it is a result or a JSON-RPC error
204nothing to answer: a notification, or a batch of them
405a method other than POST
415a body that is not application/json in UTF-8

A JSON-RPC error never changes the status: METHOD_NOT_FOUND comes back as a 200 whose body says so. The http server’s own limits apply first, so a body over its max_body_size is refused with 413 before it reaches the service.

Re-exported by rpc.

Functions

http_handler()

rpc.http_handler(service) -> function(2):

A route handler for an http server that answers JSON-RPC with service: register it for POST on whatever path the service lives at.

import http
import rpc

def setup(server) {
  var calculator = rpc.Service()
    .on_request('add', @(params) => params[0] + params[1])

  server.post('/rpc', rpc.http_handler(calculator))
}

http.serve(setup, { port: 8545 })

Every handler of the service is given the http request in context.request, for its headers, its client’s address, or what a middleware put in its context. A request cannot be deferred, since its answer is the response to the HTTP request it came in.

Registered for every method with server.any(), the handler answers anything other than POST with 405 and an Allow header, as the other methods should be answered.

Parameters

  • service (Service) — What answers the messages.

Returns function(2): — A handler taking an HttpRequest and an HttpResponse.

Classes

HttpClient

class rpc.HttpClient

Calls a JSON-RPC service over HTTP: each call is one POST to url, and its answer is the response.

import rpc

var node = rpc.HttpClient('https://node.example.com', {
  headers: { Authorization: 'Bearer ${token}' },
  timeout: 10,
})

echo node.request('eth_blockNumber', [])

var answers = node.request_batch([
  ['eth_blockNumber', []],
  ['eth_gasPrice', []],
])

An answer is matched to its call by id, never by position, so a batch’s answers come back in the order of its calls whatever order the server sent them in.

Some servers send a JSON-RPC error with a status other than 200. When the body of such a response is a JSON-RPC answer, it is read like any other answer, and its error raises from the call. Any other status raises an RpcHttpError.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

rpc.HttpClient(url: string, options: ?dict)

Returns a new HttpClient calling the service at url.

OptionMeaning
headersheaders sent with every call, such as Authorization
timeoutthe most seconds to wait for an answer
clientthe http.HttpClient to send through

headers defaults to none. timeout defaults to nil, which leaves the wait to the http client’s own read timeout. client defaults to a new http.HttpClient; pass one for its proxy, TLS and connection settings.

Parameters

  • url (string) — Where the service is, an http: or https: URL.
  • options (?dict) — Default nil, every option at its default.

HttpClient.request()

rpc.HttpClient.request(method: string, params, timeout: ?number)

Calls method and returns its result.

Parameters

  • method (string)
  • params (list|dict|nil)
  • timeout (?number) — The most seconds to wait for the answer. Default nil, the client’s own timeout.

Returns — any: The result the service answered with.

Raises RpcError when the service answers with an error, or its answer is not a valid answer to the call.

Raises RpcHttpError when the server answers with an HTTP status that carries no answer.

HttpClient.notify()

rpc.HttpClient.notify(method: string, params)

Sends a notification: calls method and expects nothing back.

Parameters

  • method (string)
  • params (list|dict|nil) — Default nil, which sends none.

Raises RpcHttpError when the server answers with an HTTP status that carries no answer.

HttpClient.request_batch()

rpc.HttpClient.request_batch(calls: list, timeout: ?number)

Sends several requests as one batch, in one POST, and returns every answer in the order of calls.

Parameters

  • calls (list) — Each call as a pair, [method, params].
  • timeout (?number) — The most seconds to wait for the answer. Default nil, the client’s own timeout.

Returns — list: For each call, in order, the result the service answered with, or the {RpcError} it answered with. A call the service left unanswered holds an {RpcError} with code INTERNAL_ERROR saying so.

Raises ValueError when calls is empty, or a call is not a [method, params] pair.

Raises RpcError when the service refuses the batch whole, or its answer is not a batch of answers.

Raises RpcHttpError when the server answers with an HTTP status that carries no answer.

HttpClient.send()

rpc.HttpClient.send(message, timeout: ?number) -> Response|list|nil

Sends a message as it is, a Request, a Notification, or a list of them as a batch, and returns the answer as it came: a Response, a list of what the batch’s answer holds, or nil when the server answered with nothing.

Parameters

  • message (Request|Notification|list)
  • timeout (?number) — The most seconds to wait for the answer. Default nil, the client’s own timeout.

Returns Response|list|nil

Raises RpcError when the answer is not valid JSON-RPC.

Raises RpcHttpError when the server answers with an HTTP status that carries no answer.

HttpClient.to_string()

rpc.HttpClient.to_string() -> string

This client as HttpClient(url).

Returns string


2026, Richard Ore and Zuri contributors

rpc.message

import rpc

Everything here is re-exported by rpc, so import rpc is enough and the names are called as rpc.*. Importing rpc.message on its own works too and reaches the same definitions.

The messages JSON-RPC 2.0 exchanges, a Request, a Notification and a Response, and turning them to and from JSON.

Every message is a JSON object whose jsonrpc member is the string '2.0'. A request names a method, may carry params, and has an id its response is matched to. A notification is a request without an id, and nothing answers it. A response carries the id of the request it answers and either a result or an error, never both. A message may also be a batch: a JSON array of messages sent and answered together.

Re-exported by rpc.

Functions

read()

rpc.read(value) -> Request|Notification|Response|list

Reads a value decoded from JSON as a message: a Request, a Notification or a Response.

A batch, a list, comes back as a list holding, for each of its entries in order, the message it holds or the RpcError saying why it does not hold one, so every entry can be answered on its own. An empty batch is not a message.

Parameters

  • value (any) — A value as json.decode() returns it.

Returns Request|Notification|Response|list

Raises RpcError with code INVALID_REQUEST when value is not a message, or is an empty batch.

decode()

rpc.decode(text: string) -> Request|Notification|Response|list

Decodes the JSON text of a message, or of a batch, and reads it as read() does.

import rpc

var call = rpc.decode('{"jsonrpc": "2.0", "id": 7, "method": "ping"}')
echo call.method  # ping
echo call.id      # 7

Parameters

  • text (string) — The message’s JSON text.

Returns Request|Notification|Response|list

Raises RpcError with code PARSE_ERROR when text is not valid JSON, or code INVALID_REQUEST when it is not a message.

encode()

rpc.encode(message) -> string

The JSON text of a message, or of a list of messages sent as a batch.

Parameters

  • message (Request|Notification|Response|list)

Returns string

Classes

Request

class rpc.Request

A call that expects an answer: the method to run, its params, and the id the answer comes back with.

import rpc

var call = rpc.Request('add', [2, 3], 1)
echo rpc.encode(call)  # {"jsonrpc":"2.0","id":1,"method":"add","params":[2,3]}
  • printable — has a @to_string(), so echo and print() show something useful

Constructor

rpc.Request(method: string, params, id)

Returns a new Request.

Parameters

  • method (string) — The method to call.
  • params (list|dict|nil) — Its parameters, by position as a list or by name as a dictionary; nil sends none.
  • id (string|number) — What the answer will be matched by.

Raises ValueError when params is not a list, a dictionary or nil.

Request.to_dict()

rpc.Request.to_dict() -> dict

The request as the dictionary JSON-RPC sends.

Returns dict

Request.to_string()

rpc.Request.to_string() -> string

This request as Request(method #id).

Returns string

Notification

class rpc.Notification

A call that expects no answer: the method to run and its params. Nothing comes back from a notification, not even an error.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

rpc.Notification(method: string, params)

Returns a new Notification.

Parameters

  • method (string) — The method to call.
  • params (list|dict|nil) — Its parameters, by position as a list or by name as a dictionary; nil sends none.

Raises ValueError when params is not a list, a dictionary or nil.

Notification.to_dict()

rpc.Notification.to_dict() -> dict

The notification as the dictionary JSON-RPC sends.

Returns dict

Notification.to_string()

rpc.Notification.to_string() -> string

This notification as Notification(method).

Returns string

Response

class rpc.Response

The answer to a Request: its id, and either the result the method returned or the error it failed with.

The id is nil only for an answer to a message whose id could not be read, such as one that was not valid JSON.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

rpc.Response(id, result, error)

Returns a new Response. success() and failure() say which kind more plainly.

Parameters

  • id (string|number|nil) — The id of the request answered.
  • result (any) — What the method returned; ignored when error is given.
  • error (?RpcError) — What the method failed with, or nil for a success.

Response.success()

rpc.Response.success(id, result) -> Response

A response answering request id with result.

Parameters

  • id (string|number)
  • result (any)

Returns Response

Response.failure()

rpc.Response.failure(id, error) -> Response

A response answering request id with error.

Parameters

  • id (string|number|nil)
  • error (RpcError)

Returns Response

Response.is_error()

rpc.Response.is_error() -> bool

True when the response carries an error rather than a result.

Returns bool

Response.to_dict()

rpc.Response.to_dict() -> dict

The response as the dictionary JSON-RPC sends.

Returns dict

Response.to_string()

rpc.Response.to_string() -> string

This response as Response(#id ok) or Response(#id error code).

Returns string


2026, Richard Ore and Zuri contributors

rpc.server

import rpc

Everything here is re-exported by rpc, so import rpc is enough and the names are called as rpc.*. Importing rpc.server on its own works too and reaches the same definitions.

Serving JSON-RPC to many connections at once, over TCP, TLS or a Unix domain socket.

serve() binds a listening socket and spreads the connections it accepts across a pool of worker isolates. Every connection gets an Endpoint of its own, and setup gives it its handlers, so each connection is a full JSON-RPC peer: it answers what its client asks, and it can call its client and notify it in return.

Each worker polls the connections it holds rather than blocking on one, so a quiet client occupies a descriptor and not a thread, and one worker serves many long-lived connections side by side.

Re-exported by rpc.

Functions

serve()

rpc.serve(setup, options: ?dict)

Serves JSON-RPC on a listening socket, across a pool of worker isolates, until it is stopped.

setup is called inside a worker for every connection accepted, with the connection’s own Endpoint and the client’s address, and gives the endpoint its handlers. A worker shares nothing with the others or with this isolate, so setup builds whatever state a connection needs, and is a function of a module, or a function that uses nothing but its own imports.

import rpc

def setup(endpoint, peer) {
  endpoint
    .on_request('add', @(params) => params[0] + params[1])
    .on_request('whoami', @() => peer.to_string())
}

rpc.serve(setup, { port: 4444, framing: 'line' })
OptionMeaningDefault
hostthe address to listen on'127.0.0.1'
portthe port to listen on; 0 picks a free one8000
patha Unix domain socket to listen on, in place of host and portnil
workershow many worker isolates serve connectionsthe number of CPUs
backloghow many accepted connections may wait for a workerworkers * 4
framing'header' for Content-Length headers, 'line' for lines'header'
max_message_sizethe largest message accepted, in bytes64 MiB
max_connections_per_workerthe most connections one worker holds256
idle_timeoutseconds a connection may stay silent before it is closednil, never
read_timeoutseconds a read may wait once a message has begun30
write_timeoutseconds a write may wait30
cert_chain, private_keyPEM strings that put every connection behind TLSnil
on_readycalled here once bound, with the address and a stop functionnil
max_connectionsstop after accepting this many connectionsnil, never

on_ready is called on this isolate once the socket is bound, with the address it is bound to, as a SocketAddr or the socket’s path, and a function that stops the server. Stopping it stops accepting connections; every worker then finishes the message it is handling, closes its connections and ends, and serve() returns. Reaching max_connections stops accepting too, but serves the connections already accepted until each of them closes, and serve() returns after the last.

A connection whose TLS handshake fails, or whose setup raises, is closed and nothing else is affected. A connection that cannot be read is reported to its endpoint’s on_error() handler and closed.

A handler that calls its client with request() holds its worker until the answer arrives, and every other connection on that worker waits with it.

The socket file of a Unix domain socket is removed when the server stops. A file already at path makes binding fail.

A failure accepting connections stops the server as stop would, and raises from serve() once it has.

Parameters

  • setup (function(2)) — Called with each connection’s {Endpoint} and the client’s address.
  • options (?dict) — Default nil, every option at its default.

Raises ValueError when an option holds a value it cannot take.

Raises Error when the socket cannot be bound.


2026, Richard Ore and Zuri contributors

rpc.service

import rpc

Everything here is re-exported by rpc, so import rpc is enough and the names are called as rpc.*. Importing rpc.service on its own works too and reaches the same definitions.

Service: what a program answers. A service holds the handler for each method the program serves, and answers every message it is given with them, whatever carried the message there.

A service on its own answers messages that each arrive in an exchange of their own, such as the body of an HTTP request, and whose answer goes back in that same exchange. An Endpoint is a service on a connection: it answers what arrives on the connection, and calls the other side of it as well.

Re-exported by rpc.

Constants

DEFAULT_BATCH_LIMIT

rpc.DEFAULT_BATCH_LIMIT = 1000

The most messages a batch may hold before it is refused whole, 1000, until Service.set_batch_limit() says otherwise.

Classes

Context

class rpc.Context

What a handler is told about the message it is handling: the request’s id, its method, the endpoint handling it, and the request it arrived in.

A request handler normally answers by returning. One on an Endpoint that cannot answer before it returns, because the answer comes from work still to be done, calls defer(), keeps the context, and answers through it with reply() or fail() once the answer is ready.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

rpc.Context(endpoint, id, method: string, is_request: bool, request, can_defer: bool)

Returns a new Context. A service makes one for each message it hands to a handler.

Parameters

  • endpoint (Service) — The {Service} or {Endpoint} handling the message.
  • id (string|number|nil) — The request’s id; nil for a notification.
  • method (string) — The method called.
  • is_request (bool) — Whether the message is a request, which is answered, rather than a notification, which is not.
  • request (any) — What the message arrived in, such as the http request whose body it was; nil for a message from a connection.
  • can_defer (bool) — Whether the request can be answered after its handler returns, which needs a connection to answer on.

Context.is_notification()

rpc.Context.is_notification() -> bool

True when the message is a notification, which nothing answers.

Returns bool

Context.defer()

rpc.Context.defer() -> Context

Takes over answering the request: whatever the handler returns is ignored, and the request is answered when reply() or fail() is called, from the isolate that owns the endpoint. Calling it again changes nothing.

Only a request that arrived on an Endpoint’s connection can be deferred. One answered in its own exchange, such as an HTTP request, is answered when its handler returns.

A request deferred from inside a batch is answered on its own, after the batch’s other answers.

Returns Context — itself.

Raises ValueError for a notification, which is never answered, or for a request with no connection to answer it on later.

Context.is_deferred()

rpc.Context.is_deferred() -> bool

True once defer() has been called.

Returns bool

Context.reply()

rpc.Context.reply(result)

Answers the deferred request with result.

Parameters

  • result (any)

Raises ValueError when the request was not deferred, or has already been answered.

Raises RpcClosedError when the endpoint has been closed.

Context.fail()

rpc.Context.fail(error)

Answers the deferred request with error.

Parameters

  • error (RpcError)

Raises ValueError when the request was not deferred, or has already been answered.

Raises RpcClosedError when the endpoint has been closed.

Context.is_answered()

rpc.Context.is_answered() -> bool

True once a deferred request has been answered.

Returns bool

Context.to_string()

rpc.Context.to_string() -> string

This context as Context(method #id).

Returns string

Service

class rpc.Service

The methods a program serves, and the answering of every message sent to them.

Handlers say what it answers. A request handler is called with the request’s params and a Context, and what it returns is the result sent back; an RpcError it raises is sent back as that error, and any other error as an INTERNAL_ERROR carrying its message. A notification handler is called the same way and nothing is sent back. A message for a method with no handler goes to the on_unhandled() fallback when one is set; without one, a request is answered with METHOD_NOT_FOUND and a notification is dropped. A message that is not valid UTF-8, or not valid JSON, is answered with PARSE_ERROR.

import rpc

var calculator = rpc.Service()
  .on_request('add', @(params) => params[0] + params[1])

echo calculator.answer('{"jsonrpc": "2.0", "id": 1, "method": "add", "params": [2, 3]}')
# {"jsonrpc":"2.0","id":1,"result":5}

answer() is the whole of a service’s work: one message or batch in, its answer out. rpc.http_handler() puts a service behind an http route, and an Endpoint puts one on a connection.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

rpc.Service()

Returns a new Service with no handlers and a batch limit of DEFAULT_BATCH_LIMIT.

Service.on_request()

rpc.Service.on_request(method: string, handler: function) -> Service

Answers requests for method with handler, in place of any handler set for it before.

Parameters

  • method (string) — The method’s name. Names starting with rpc. are reserved by JSON-RPC.
  • handler (function(2)) — Called with the request’s params and a {Context}; what it returns is the result sent back.

Returns Service — itself.

Raises ValueError for a name starting with rpc..

Service.on_notification()

rpc.Service.on_notification(method: string, handler: function) -> Service

Handles notifications for method with handler, in place of any handler set for it before.

Parameters

  • method (string) — The method’s name. Names starting with rpc. are reserved by JSON-RPC.
  • handler (function(2)) — Called with the notification’s params and a {Context}; what it returns is ignored.

Returns Service — itself.

Raises ValueError for a name starting with rpc..

Service.on_unhandled()

rpc.Service.on_unhandled(handler: function) -> Service

Handles every request and notification whose method has no handler of its own, in place of any fallback set before. It is called as the other handlers are, with context.method naming the method. For a request, what it returns is the result sent back, and raising an RpcError with METHOD_NOT_FOUND refuses the method as a service without a fallback would.

A method whose name starts with rpc. never reaches it: JSON-RPC reserves those names, so such a request is answered with METHOD_NOT_FOUND and such a notification is dropped.

Parameters

  • handler (function(2)) — Called with the message’s params and a {Context}.

Returns Service — itself.

Service.on_error()

rpc.Service.on_error(handler: function) -> Service

Called with every problem the other side is not told about: an error a notification handler raised, an error other than an RpcError a request handler raised, a response nothing was waiting for, and, on an Endpoint, an error a send_request() callback raised and a connection that failed.

Parameters

  • handler (function(2)) — Called with the error and the message it concerns, as a dictionary, or nil when there is no one message.

Returns Service — itself.

Service.on_trace()

rpc.Service.on_trace(handler: function) -> Service

Called with the text of every message, as it arrives and as it is sent, for logging a conversation.

Parameters

  • handler (function(2)) — Called with 'in' or 'out' and the message’s JSON text.

Returns Service — itself.

Service.set_batch_limit()

rpc.Service.set_batch_limit(limit: ?int) -> Service

Sets the most messages a batch may hold. A larger batch is refused whole with one INVALID_REQUEST error and an id of nil, before any of it is handled, so one message cannot make a service do an unbounded amount of work.

Parameters

  • limit (?int) — Default DEFAULT_BATCH_LIMIT, 1000. nil takes a batch of any size.

Returns Service — itself.

Raises ValueError for a limit below 1.

Service.batch_limit()

rpc.Service.batch_limit() -> ?int

The most messages a batch may hold, or nil for no limit.

Returns ?int

Service.answer()

rpc.Service.answer(message, request) -> ?string

Answers one message, or a batch of them, and returns the JSON text of the answer, or nil when there is nothing to answer: a notification, or a batch of nothing but notifications.

import rpc

var greeter = rpc.Service()
  .on_request('greet', @(params) => 'Hello, ${params.name}!')

var call = '{"jsonrpc": "2.0", "id": 7, "method": "greet", "params": {"name": "Ada"}}'
var note = '{"jsonrpc": "2.0", "method": "greet", "params": {"name": "Ada"}}'

echo greeter.answer(call)  # {"jsonrpc":"2.0","id":7,"result":"Hello, Ada!"}
echo greeter.answer(note)  # nil

Parameters

  • message (string|bytes) — The message’s JSON text, or the bytes of it, which must be UTF-8.
  • request (any) — What the message arrived in, handed to every handler as context.request, such as the http request whose body it was. Default nil.

Returns ?string

Service.to_string()

rpc.Service.to_string() -> string

This service as Service(n methods).

Returns string


2026, Richard Ore and Zuri contributors

rpc.transport

import rpc

Everything here is re-exported by rpc, so import rpc is enough and the names are called as rpc.*. Importing rpc.transport on its own works too and reaches the same definitions.

Where an endpoint’s bytes come from and go to. A transport is any object with three methods:

  • read(max) returns the next bytes that arrive, at most max of them, waiting until at least one does. It returns empty bytes once the stream has ended.
  • write(data) sends all of data, a bytes.
  • close() ends the stream in the direction it writes.

A transport may also have can_listen(), true when another isolate can read it while the one that made it writes to it, which Endpoint.listen() relies on. Without the method, it is false. And a transport that keeps messages apart itself has framing(), returning the framing an endpoint over it uses unless told otherwise.

StdioTransport talks over the program’s own standard input and output, SocketTransport over a connected socket, ProcessTransport over the standard input and output of a child process, WebSocketTransport over a WebSocket, and ChannelTransport over isolate channels, two of which pipe() joins into a connection between isolates. Any other object with the three methods works the same.

Re-exported by rpc.

Functions

pipe()

rpc.pipe() -> list[ChannelTransport]:

Two ChannelTransports joined to each other: what one writes, the other reads. Hand one end to another isolate and the two can talk JSON-RPC across the isolate boundary.

import rpc

var ends = rpc.pipe()

ends[0].write('ping'.to_bytes())
echo ends[1].read(4096).to_string()  # ping

Returns list[ChannelTransport]: — The two ends.

Classes

StdioTransport

class rpc.StdioTransport

Talks over the program’s own standard input and output: what it reads arrives on stdin, and what it writes goes to stdout. This is the transport of a program that another one starts and talks to.

Every write is flushed at once. Anything else the program prints to stdout lands in the middle of the stream, so a program serving over stdio writes everything else to stderr.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

rpc.StdioTransport()

Returns a new StdioTransport.

StdioTransport.read()

rpc.StdioTransport.read(max: int) -> bytes

The next bytes on stdin, at most max of them, or empty bytes once stdin has ended.

Parameters

  • max (int)

Returns bytes

StdioTransport.write()

rpc.StdioTransport.write(data: bytes)

Writes data to stdout and flushes it.

Parameters

  • data (bytes)

StdioTransport.close()

rpc.StdioTransport.close()

Flushes stdout. The program’s standard streams stay open.

StdioTransport.can_listen()

rpc.StdioTransport.can_listen() -> bool

True: every isolate reads the same stdin, so one can read it while another writes.

Returns bool

StdioTransport.to_string()

rpc.StdioTransport.to_string() -> string

This transport as StdioTransport().

Returns string

SocketTransport

class rpc.SocketTransport

Talks over a connected stream from net: a TcpStream, a UnixStream or a TlsStream, or anything else with read(), write_all() and close().

A socket is held by one isolate at a time, so an endpoint serving one reads it with serve() rather than listen().

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

rpc.SocketTransport(socket)

Returns a new SocketTransport over socket.

Parameters

  • socket (any) — A connected socket.

SocketTransport.read()

rpc.SocketTransport.read(max: int) -> bytes

The next bytes from the socket, at most max of them, or empty bytes once the other side has closed its end.

Parameters

  • max (int)

Returns bytes

Raises Error when the socket fails, or its read timeout runs out.

SocketTransport.write()

rpc.SocketTransport.write(data: bytes)

Writes all of data to the socket.

Parameters

  • data (bytes)

Raises Error when the socket fails.

SocketTransport.close()

rpc.SocketTransport.close()

Closes the socket.

SocketTransport.can_listen()

rpc.SocketTransport.can_listen() -> bool

False: a socket is held by one isolate at a time.

Returns bool

SocketTransport.to_string()

rpc.SocketTransport.to_string() -> string

This transport as SocketTransport().

Returns string

ProcessTransport

class rpc.ProcessTransport

Talks to a child process over its standard input and output, from os.spawn() with stdin and stdout both 'pipe': what the child prints is read, and what is written goes to its stdin.

import os
import rpc

var child = os.spawn('zuri', ['run', 'calculator.zu'], {
  stdin: 'pipe',
  stdout: 'pipe'
})
var calculator = rpc.endpoint(rpc.process(child))

echo calculator.request('add', [2, 3])

The child’s streams are held by the isolate that spawned it, so an endpoint talking to it reads with serve() and request() rather than listen().

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

rpc.ProcessTransport(process)

Returns a new ProcessTransport over process.

Parameters

  • process (os.Process) — A child whose stdin and stdout are pipes.

ProcessTransport.read()

rpc.ProcessTransport.read(max: int) -> bytes

The next bytes the child printed, at most max of them, or empty bytes once its stdout has ended.

Parameters

  • max (int)

Returns bytes

ProcessTransport.write()

rpc.ProcessTransport.write(data: bytes)

Writes all of data to the child’s stdin.

Parameters

  • data (bytes)

ProcessTransport.close()

rpc.ProcessTransport.close()

Closes the child’s stdin, so it reads the end of its input. The child itself keeps running until it exits.

ProcessTransport.can_listen()

rpc.ProcessTransport.can_listen() -> bool

False: a child process is read by the isolate that spawned it.

Returns bool

ProcessTransport.to_string()

rpc.ProcessTransport.to_string() -> string

This transport as ProcessTransport().

Returns string

WebSocketTransport

class rpc.WebSocketTransport

Talks over a WebSocket from http.websocket, on either end of it: one websocket.accept() returned in a route handler, or one websocket.connect() opened. Each message is one WebSocket message, sent as text, so an endpoint over it uses a MessageFraming.

import http.websocket
import rpc

var node = rpc.endpoint(rpc.websocket(websocket.connect('ws://127.0.0.1:8546')))

echo node.request('eth_blockNumber', [])

A WebSocket is held by one isolate at a time, so an endpoint over one reads with serve() and request() rather than listen().

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

rpc.WebSocketTransport(socket)

Returns a new WebSocketTransport over socket.

Parameters

  • socket (http.websocket.WebSocket) — An open WebSocket.

WebSocketTransport.read()

rpc.WebSocketTransport.read(max: int) -> bytes

The next message that arrives, as its bytes, or empty bytes once the connection has closed. An empty message is skipped, since it holds no JSON-RPC message.

Parameters

  • max (int) — Ignored; each read returns one message whole.

Returns bytes

WebSocketTransport.write()

rpc.WebSocketTransport.write(data: bytes)

Sends data as one text message.

Parameters

  • data (bytes) — One message’s UTF-8 text.

WebSocketTransport.close()

rpc.WebSocketTransport.close()

Closes the WebSocket, with the closing handshake.

WebSocketTransport.can_listen()

rpc.WebSocketTransport.can_listen() -> bool

False: a WebSocket is held by one isolate at a time.

Returns bool

WebSocketTransport.framing()

rpc.WebSocketTransport.framing() -> MessageFraming

A MessageFraming: a WebSocket keeps messages apart itself.

Returns MessageFraming

WebSocketTransport.to_string()

rpc.WebSocketTransport.to_string() -> string

This transport as WebSocketTransport().

Returns string

ChannelTransport

class rpc.ChannelTransport

Talks over two isolate channels: what it reads arrives on inbox and what it writes goes to outbox. Channels cross isolates, so a ChannelTransport can be handed to another isolate and talk to it from there; pipe() makes the two ends of such a connection.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

rpc.ChannelTransport(inbox, outbox)

Returns a new ChannelTransport.

Parameters

  • inbox (isolate.Channel) — Where the bytes it reads arrive.
  • outbox (isolate.Channel) — Where the bytes it writes go.

ChannelTransport.read()

rpc.ChannelTransport.read(max: int) -> bytes

The next bytes sent to this end, whatever their length, or empty bytes once the other end has closed.

Parameters

  • max (int) — Ignored; each read returns one write from the other end whole.

Returns bytes

ChannelTransport.write()

rpc.ChannelTransport.write(data: bytes)

Sends data to the other end.

Parameters

  • data (bytes)

Raises RpcClosedError when this end has been closed.

ChannelTransport.close()

rpc.ChannelTransport.close()

Closes this end, so the other end reads the end of its input.

ChannelTransport.can_listen()

rpc.ChannelTransport.can_listen() -> bool

True: channels are shared between isolates, so one can read this end while another writes to it.

Returns bool

ChannelTransport.to_string()

rpc.ChannelTransport.to_string() -> string

This transport as ChannelTransport().

Returns string


2026, Richard Ore and Zuri contributors

crypto

import crypto

Comprehensive cryptographic primitives for Zuri applications.

This module exposes symmetric encryption, authenticated encryption, asymmetric key operations, digital signatures, key exchange, and password hashing through a clean, opinionated API.


Algorithm summary

FamilyAlgorithms
SymmetricAES-128/192/256-GCM, AES-128/192/256-CBC
Stream AEADChaCha20-Poly1305
AsymmetricRSA-2048/4096 (OAEP + PSS-SHA256/384/512)
SignaturesECDSA P-256/P-384, Ed25519
Key exchangeX25519 (Diffie-Hellman)
Password KDFArgon2id
Key stretchHKDF-SHA256
Key importjwk.to_pem(): RSA/EC/OKP JWK → PEM
RandomnessOS CSPRNG

Choosing the right primitive

Encrypting data at rest or in transit: prefer aes_gcm (authenticated encryption with associated data). Use chacha20 on platforms where AES hardware acceleration is absent. Avoid aes_cbc in new designs; it is included for interoperability with legacy systems.

Signing and verifying messages: use ed25519 for modern systems (fastest, no hash to choose, deterministic). Use ecdsa when interoperating with systems that require NIST curves. Use rsa only when constrained by a protocol that demands it.

Asymmetric encryption: use rsa with OAEP. For larger payloads, use a hybrid scheme: exchange a symmetric key with RSA-OAEP, then encrypt the payload with aes_gcm.

Key agreement: use x25519_exchange followed by hkdf to derive a symmetric key from the shared secret.

Password storage: always use argon2_hash. Never use raw hashes (SHA-*, MD5) to store passwords.


Quick start

import crypto

# --- Symmetric (AES-GCM) -----------------------------------------------
var key = crypto.random_bytes(32)              # AES-256
var iv  = crypto.random_bytes(12)              # 96-bit GCM nonce
var ct  = crypto.aes_gcm.encrypt(key, iv, bytes('hello world'))
var pt  = crypto.aes_gcm.decrypt(key, iv, ct)
echo string(pt)  # hello world

# --- Password hashing (Argon2id) ----------------------------------------
var salt = crypto.random_bytes(16)
var hash = crypto.argon2.hash('my password', salt)
echo crypto.argon2.verify(hash, 'my password')  # true

# --- Ed25519 signature --------------------------------------------------
var kp  = crypto.ed25519.generate()
var sig = crypto.ed25519.sign(kp.private_pem, bytes('payload'))
echo crypto.ed25519.verify(kp.public_pem, bytes('payload'), sig)  # true

The crypto API

Every public name in crypto, wherever it is declared. Each links to the page that documents it.

NameKindSummary
crypto.CryptoErrorclassRaised by crypto operations that fail due to invalid input, key material errors, or authentication failures.
crypto.aes_cbcconstantAES-CBC encryption namespace.
crypto.aes_gcmconstantAES-GCM authenticated encryption namespace.
crypto.argon2constantArgon2id password hashing namespace.
crypto.chacha20constantChaCha20-Poly1305 AEAD namespace.
crypto.ecdsaconstantECDSA signing namespace.
crypto.ed25519constantEd25519 signing namespace.
crypto.hkdffunctionDerives cryptographic key material from a high-entropy input using HKDF-SHA256 (RFC 5869).
crypto.jwkconstantJWK-to-PEM conversion namespace.
crypto.random_bytesfunctionReturns n cryptographically secure random bytes from the operating system’s entropy source (OpenSSL…
crypto.rsaconstantRSA encryption and signing namespace.
crypto.x25519constantX25519 key exchange namespace.

Constants

aes_gcm

crypto.aes_gcm

AES-GCM authenticated encryption namespace.

See also: _AesGcm

aes_cbc

crypto.aes_cbc

AES-CBC encryption namespace.

See also: _AesCbc

chacha20

crypto.chacha20

ChaCha20-Poly1305 AEAD namespace.

See also: _ChaCha20

rsa

crypto.rsa

RSA encryption and signing namespace.

See also: _Rsa

ecdsa

crypto.ecdsa

ECDSA signing namespace.

See also: _Ecdsa

ed25519

crypto.ed25519

Ed25519 signing namespace.

See also: _Ed25519

x25519

crypto.x25519

X25519 key exchange namespace.

See also: _X25519

jwk

crypto.jwk

JWK-to-PEM conversion namespace.

See also: _Jwk

argon2

crypto.argon2

Argon2id password hashing namespace.

See also: _Argon2

Functions

random_bytes()

crypto.random_bytes(n) -> bytes

Returns n cryptographically secure random bytes from the operating system’s entropy source (OpenSSL RAND_bytes, which seeds from /dev/urandom or the OS equivalent).

Use this to generate keys, IVs, nonces, and salts. Never use math.random for these purposes.

var key = crypto.random_bytes(32)  # 256-bit AES key
var iv  = crypto.random_bytes(12)  # 96-bit GCM nonce

Parameters

  • n (number) — Number of bytes to generate. Must be between 1 and 65536.

Returns bytes

Raises CryptoError

hkdf()

crypto.hkdf(ikm, salt, info, length) -> bytes

Derives cryptographic key material from a high-entropy input using HKDF-SHA256 (RFC 5869).

HKDF is not a password hashing function: it does not strengthen low-entropy inputs. Use argon2 for passwords. HKDF is appropriate for:

  • Expanding X25519 shared secrets into symmetric keys. - Deriving multiple purpose-specific sub-keys from a single master key. - Stretching random key material to the required length.

The info parameter binds the derived key to a specific context and prevents key reuse across different purposes. It does not need to be secret.

Example

import crypto

# Derive an AES-256 encryption key and a separate HMAC key from a
# shared secret, binding each to its intended purpose.

var secret = crypto.x25519.exchange(my_priv, peer_pub)
var salt   = crypto.random_bytes(32)

var enc_key  = crypto.hkdf(secret, salt, bytes('encryption'), 32)
var hmac_key = crypto.hkdf(secret, salt, bytes('authentication'), 32)

Parameters

  • ikm (bytes) — Input key material (high entropy required).
  • salt (bytes) — Random salt. Use random_bytes(32) or a fixed well-known value if no random salt is available.
  • info (bytes) — Context string binding the key to its purpose.
  • length (number) — Number of output bytes. Maximum 8160.

Returns bytes

Raises CryptoError

Classes

CryptoError

class crypto.CryptoError < Error

Raised by crypto operations that fail due to invalid input, key material errors, or authentication failures. Always catch this class when decrypting untrusted data.

Constructor

crypto.CryptoError(message)

Parameters

  • message (string)

2024, Zuri Project

hash

import hash

This module provides a framework for cryptographic and non-cryptographic encryption.

Examples,

%> import hash
%>
%> hash.md5('Hello, World')
'82bb413746aee42f89dea2b59614f9ef'
%>
%> hash.sha256('Hello, World')
'03675ac53ff9cd1535ccc7dfcdfa2c458c5218371f418dc136f2d19ac1fbe8a5'
%>
%> hash.hmac_sha256('mykey', 'Hello, World')
'61035d3d2119ffdfd710913bf4161d5fba1c2d9431f7de7ef398d359eb1d2481'
%>
%> hash.hmac_sha256(bytes([10, 11, 12]), 'My secure text!')
'd782079145a3476fd4e018d44dd024034fa91f626f7f30f2009200c5ac757723'

The hash API

Every public name in hash, wherever it is declared. Each links to the page that documents it.

NameKindSummary
hash.blake2b512functionReturns the BLAKE2B-512 cryptographic hash of the given string or bytes.
hash.blake2s256functionReturns the BLAKE2S-256 cryptographic hash of the given string or bytes.
hash.fnv1functionReturns the 32 bit fnv1 hash of the given string or bytes.
hash.fnv1_64functionReturns the 64 bit fnv1 hash of the given string or bytes.
hash.fnv1afunctionReturns the 32 bit fnv1a hash of the given string or bytes.
hash.fnv1a_64functionReturns the 64 bit fnv1a hash of the given string or bytes.
hash.gostfunctionReturns the Gost cryptographic hash of the given string or bytes.
hash.hashfunctionReturns the hash digest for the given data using the given algorithm.
hash.hmacfunctionComputes an HMAC with the key and str using the given method.
hash.hmac_gostfunctionReturns the HMAC-GOST cryptographic hash of the given string or bytes.
hash.hmac_md4functionReturns the HMAC-MD4 cryptographic hash of the given string or bytes.
hash.hmac_md5functionReturns the HMAC-MD5 cryptographic hash of the given string or bytes.
hash.hmac_sha1functionReturns the HMAC-SHA1 cryptographic hash of the given string or bytes.
hash.hmac_sha224functionReturns the HMAC-SHA224 cryptographic hash of the given string or bytes.
hash.hmac_sha256functionReturns the HMAC-SHA256 cryptographic hash of the given string or bytes.
hash.hmac_sha384functionReturns the HMAC-SHA384 cryptographic hash of the given string or bytes.
hash.hmac_sha512functionReturns the HMAC-SHA512 cryptographic hash of the given string or bytes.
hash.hmac_whirlpoolfunctionReturns the HMAC-WHIRLPOOL cryptographic hash of the given string or bytes.
hash.idfunctionReturns the identification hash of a value as used in the underlying dictionary implementation.
hash.md4functionReturns the md4 hash of the given string or bytes.
hash.md5functionReturns the md5 hash of the given string or bytes.
hash.md5_filefunctionReturns the md5 hash of the given file.
hash.pbkdf2functionDerives a cryptographic key from a password using the PBKDF2 key derivation function defined in RFC 2898 §5.2…
hash.ripemd160functionReturns the RIPEMD-160 cryptographic hash of the given string or bytes.
hash.sha1functionReturns the sha1 hash of the given string or bytes.
hash.sha224functionReturns the sha224 hash of the given string or bytes.
hash.sha256functionReturns the sha256 hash of the given string or bytes.
hash.sha384functionReturns the sha384 hash of the given string or bytes.
hash.sha3_224functionReturns the SHA3-224 cryptographic hash of the given string or bytes.
hash.sha3_256functionReturns the SHA3-256 cryptographic hash of the given string or bytes.
hash.sha3_384functionReturns the SHA3-384 cryptographic hash of the given string or bytes.
hash.sha3_512functionReturns the SHA3-512 cryptographic hash of the given string or bytes.
hash.sha512functionReturns the sha512 hash of the given string or bytes.
hash.shake128functionReturns the SHAKE-128 cryptographic hash of the given string or bytes.
hash.shake256functionReturns the SHAKE-256 cryptographic hash of the given string or bytes.
hash.whirlpoolfunctionReturns the whirlpool hash of the given string or bytes.

Functions

id()

hash.id(value) -> number

Returns the identification hash of a value as used in the underlying dictionary implementation.

A class may override the result of this function by implementing the to_hash decorator.

Parameters

  • value (any)

Returns number

hash()

hash.hash(algorithm, data, as_bytes) -> string|bytes

Returns the hash digest for the given data using the given algorithm.

Supported algorithms includes:

  • FNV1 family: fnv1, fnv1a, fnv164, fnv1a64. - MD family: md2, md4, md5. - SHA family: sha, sha1, sha224, sha256, sha384, sha512, sha512-224, sha512-256, md5-sha1. - SHA3 family: sha3-224, sha3-256, sha3-384, sha3-512. - SHAKE family (XOF): shake128, shake256. - RIPEMD family: ripemd160. - WHIRLPOOL family: whirlpool. - Blake family: blake2s256, blake2b512. - SM family: sm3.

By default, this function returns the hexadecimal string representing the hash (since this is the most common application level usage). The function accepts a third boolean argument as_bytes which allows callers to specify if the result should be returned in the raw digest byte stream or not.

Parameters

  • algorithm (string)
  • data (string|bytes)
  • as_bytes (?bool)

Returns string|bytes

Note: Algorithm names are not case-sensitive.

md4()

hash.md4(str, as_bytes) -> string|bytes

Returns the md4 hash of the given string or bytes.

Parameters

  • str (string|bytes)
  • as_bytes (?bool)

Returns string|bytes

md5()

hash.md5(str, as_bytes) -> string|bytes

Returns the md5 hash of the given string or bytes.

Parameters

  • str (string|bytes)
  • as_bytes (?bool)

Returns string|bytes

md5_file()

hash.md5_file(f, as_bytes) -> string|bytes

Returns the md5 hash of the given file.

Parameters

  • file (file)
  • as_bytes (?bool)

Returns string|bytes

sha1()

hash.sha1(str, as_bytes) -> string|bytes

Returns the sha1 hash of the given string or bytes.

Parameters

  • str (string|bytes)
  • as_bytes (?bool)

Returns string|bytes

sha224()

hash.sha224(str, as_bytes) -> string|bytes

Returns the sha224 hash of the given string or bytes.

Parameters

  • str (string|bytes)
  • as_bytes (?bool)

Returns string|bytes

sha256()

hash.sha256(str, as_bytes) -> string|bytes

Returns the sha256 hash of the given string or bytes.

Parameters

  • str (string|bytes)
  • as_bytes (?bool)

Returns string|bytes

sha384()

hash.sha384(str, as_bytes) -> string|bytes

Returns the sha384 hash of the given string or bytes.

Parameters

  • str (string|bytes)
  • as_bytes (?bool)

Returns string|bytes

sha512()

hash.sha512(str, as_bytes) -> string|bytes

Returns the sha512 hash of the given string or bytes.

Parameters

  • str (string|bytes)
  • as_bytes (?bool)

Returns string|bytes

fnv1()

hash.fnv1(data, as_bytes) -> string|bytes

Returns the 32 bit fnv1 hash of the given string or bytes.

Parameters

  • data (string|bytes)
  • as_bytes (?bool)

Returns string|bytes

fnv1_64()

hash.fnv1_64(data, as_bytes) -> string|bytes

Returns the 64 bit fnv1 hash of the given string or bytes.

Parameters

  • data (string|bytes)
  • as_bytes (?bool)

Returns string|bytes

fnv1a()

hash.fnv1a(data, as_bytes) -> string|bytes

Returns the 32 bit fnv1a hash of the given string or bytes.

Parameters

  • data (string|bytes)
  • as_bytes (?bool)

Returns string|bytes

fnv1a_64()

hash.fnv1a_64(data, as_bytes) -> string|bytes

Returns the 64 bit fnv1a hash of the given string or bytes.

Parameters

  • data (string|bytes)
  • as_bytes (?bool)

Returns string|bytes

whirlpool()

hash.whirlpool(str, as_bytes) -> string|bytes

Returns the whirlpool hash of the given string or bytes.

Parameters

  • str (string|bytes)
  • as_bytes (?bool)

Returns string|bytes

gost()

hash.gost(data, as_bytes) -> string|bytes

Returns the Gost cryptographic hash of the given string or bytes.

Parameters

  • data (string|bytes)
  • as_bytes (?bool)

Returns string|bytes

sha3_224()

hash.sha3_224(data, as_bytes) -> string|bytes

Returns the SHA3-224 cryptographic hash of the given string or bytes.

Parameters

  • data (string|bytes)
  • as_bytes (?bool)

Returns string|bytes

sha3_256()

hash.sha3_256(data, as_bytes) -> string|bytes

Returns the SHA3-256 cryptographic hash of the given string or bytes.

Parameters

  • data (string|bytes)
  • as_bytes (?bool)

Returns string|bytes

sha3_384()

hash.sha3_384(data, as_bytes) -> string|bytes

Returns the SHA3-384 cryptographic hash of the given string or bytes.

Parameters

  • data (string|bytes)
  • as_bytes (?bool)

Returns string|bytes

sha3_512()

hash.sha3_512(data, as_bytes) -> string|bytes

Returns the SHA3-512 cryptographic hash of the given string or bytes.

Parameters

  • data (string|bytes)
  • as_bytes (?bool)

Returns string|bytes

shake128()

hash.shake128(data, as_bytes) -> string|bytes

Returns the SHAKE-128 cryptographic hash of the given string or bytes.

Parameters

  • data (string|bytes)
  • as_bytes (?bool)

Returns string|bytes

shake256()

hash.shake256(data, as_bytes) -> string|bytes

Returns the SHAKE-256 cryptographic hash of the given string or bytes.

Parameters

  • data (string|bytes)
  • as_bytes (?bool)

Returns string|bytes

blake2b512()

hash.blake2b512(data, as_bytes) -> string|bytes

Returns the BLAKE2B-512 cryptographic hash of the given string or bytes.

Parameters

  • data (string|bytes)
  • as_bytes (?bool)

Returns string|bytes

blake2s256()

hash.blake2s256(data, as_bytes) -> string|bytes

Returns the BLAKE2S-256 cryptographic hash of the given string or bytes.

Parameters

  • data (string|bytes)
  • as_bytes (?bool)

Returns string|bytes

ripemd160()

hash.ripemd160(data, as_bytes) -> string|bytes

Returns the RIPEMD-160 cryptographic hash of the given string or bytes.

Parameters

  • data (string|bytes)
  • as_bytes (?bool)

Returns string|bytes

hmac()

hash.hmac(method, key, str, as_bytes) -> string|bytes

Computes an HMAC with the key and str using the given method.

Parameters

  • method (function)
  • key (string|bytes)
  • str (string|bytes)
  • as_bytes (?bool)

Returns string|bytes

hmac_md4()

hash.hmac_md4(key, str, as_bytes) -> string|bytes

Returns the HMAC-MD4 cryptographic hash of the given string or bytes.

Parameters

  • key (string|bytes)
  • str (string|bytes)
  • as_bytes (?bool)

Returns string|bytes

hmac_md5()

hash.hmac_md5(key, str, as_bytes) -> string|bytes

Returns the HMAC-MD5 cryptographic hash of the given string or bytes.

Parameters

  • key (string|bytes)
  • str (string|bytes)
  • as_bytes (?bool)

Returns string|bytes

hmac_sha1()

hash.hmac_sha1(key, str, as_bytes) -> string|bytes

Returns the HMAC-SHA1 cryptographic hash of the given string or bytes.

Parameters

  • key (string|bytes)
  • str (string|bytes)
  • as_bytes (?bool)

Returns string|bytes

hmac_sha224()

hash.hmac_sha224(key, str, as_bytes) -> string|bytes

Returns the HMAC-SHA224 cryptographic hash of the given string or bytes.

Parameters

  • key (string|bytes)
  • str (string|bytes)
  • as_bytes (?bool)

Returns string|bytes

hmac_sha256()

hash.hmac_sha256(key, str, as_bytes) -> string|bytes

Returns the HMAC-SHA256 cryptographic hash of the given string or bytes.

Parameters

  • key (string|bytes)
  • str (string|bytes)
  • as_bytes (?bool)

Returns string|bytes

hmac_sha384()

hash.hmac_sha384(key, str, as_bytes) -> string|bytes

Returns the HMAC-SHA384 cryptographic hash of the given string or bytes.

Parameters

  • key (string|bytes)
  • str (string|bytes)
  • as_bytes (?bool)

Returns string|bytes

hmac_sha512()

hash.hmac_sha512(key, str, as_bytes) -> string|bytes

Returns the HMAC-SHA512 cryptographic hash of the given string or bytes.

Parameters

  • key (string|bytes)
  • str (string|bytes)
  • as_bytes (?bool)

Returns string|bytes

hmac_whirlpool()

hash.hmac_whirlpool(key, str, as_bytes) -> string|bytes

Returns the HMAC-WHIRLPOOL cryptographic hash of the given string or bytes.

Parameters

  • key (string|bytes)
  • str (string|bytes)
  • as_bytes (?bool)

Returns string|bytes

hmac_gost()

hash.hmac_gost(key, str, as_bytes) -> string|bytes

Returns the HMAC-GOST cryptographic hash of the given string or bytes.

Parameters

  • key (string|bytes)
  • str (string|bytes)
  • as_bytes (?bool)

Returns string|bytes

pbkdf2()

hash.pbkdf2(algorithm, password, salt, iterations, dk_len, as_bytes) -> string

Derives a cryptographic key from a password using the PBKDF2 key derivation function defined in RFC 2898 §5.2 (PKCS #5 v2.0), as updated by RFC 8018.

Examples

Password storage (derive then verify)
import hash

var salt = 'f3a8c2b104d7e569'   # 16 random bytes in production
var dk   = hash.pbkdf2('sha256', 'correct horse battery staple', salt, 600000)
# → 64 lowercase hex characters (32 bytes)

# Verification: re-derive and compare.
if hash.pbkdf2('sha256', candidate, salt, 600000) == dk {
  echo 'Password correct'
}
Raw-bytes key for symmetric encryption
import hash

# 32-byte key for AES-256, returned as a bytes object.
var key = hash.pbkdf2('sha256', passphrase, salt, 600000, 32, true)
Longer key with SHA-512
import hash

# 64 bytes; dk_len == hLen so only one T block is needed.
var dk = hash.pbkdf2('sha512', password, salt, 210000, 64)
echo dk.length()   # 128 hex characters = 64 bytes
Key that spans multiple PRF blocks
import hash

# 40 bytes with SHA-1 (hLen = 20) requires two T blocks.
var dk = hash.pbkdf2('sha1', 'secret', 'nacl', 4096, 40)
echo dk.length()   # 80 hex characters = 40 bytes

Security notes

  • Always use a unique, randomly generated salt for every password. Never derive the salt from the username, email, or any other predictable input. - Tune iterations so that derivation takes ~100 ms on your target hardware. Re-benchmark as server capacity increases over time. - For pure password storage where output size is not a concern, consider bcrypt (built into Zuri’s hash module). PBKDF2 is most appropriate when you need an arbitrarily long output : symmetric keys, key wrapping, or protocol key schedules. - When comparing derived keys, use a constant-time equality function to prevent timing side-channel attacks.

Parameters

  • algorithm (string) — HMAC variant used as the PRF. One of: 'sha1', 'sha224', 'sha256', 'sha384', 'sha512', 'md5'. Prefer 'sha256' or 'sha512' for new designs.
  • password (string|bytes) — The password (HMAC key). Any string or bytes value is accepted.
  • salt (string|bytes) — The cryptographic salt. Use at least 16 bytes of random data per password. Never reuse a salt across different passwords.
  • iterations (number) — Iteration count c (must be >= 1). OWASP 2023 minimums: ‘sha1’ → 1 300 000 ‘sha256’ → 600 000 ‘sha512’ → 210 000
  • dk_len (number) — Derived key length in bytes. Defaults to the PRF output length (hLen) when nil or omitted. Maximum: (2^32 - 1) * hLen.
  • as_bytes (bool) — true → return a bytes object. false → return a lowercase hex string (default).

Returns string — | bytes The derived key.

Raises Error


2021, Richard Ore and Zuri contributors

bcrypt

import bcrypt

Generating and verifying bcrypt password hashes, and reading information back out of an existing hash (its cost factor and salt).

hash() and compare() are the two functions almost every caller needs: hash a password before storing it, then compare a login attempt against the stored hash later. The salt is generated internally by a real cryptographically secure random source, embedded in the returned hash string, and never needs to be handled separately.

Example,

%> import bcrypt
%> var stored = bcrypt.hash('correct horse battery staple')
%> stored
'$2b$10$N9qo8uLOickgx2ZMRZoMy.MrqmVwj0dJmwB3vk...'
%> bcrypt.compare('correct horse battery staple', stored)
true
%> bcrypt.compare('wrong password', stored)
false

The bcrypt API

Every public name in bcrypt, wherever it is declared. Each links to the page that documents it.

NameKindSummary
bcrypt.DEFAULT_LOG2_ROUNDSconstantDefault cost factor used by hash() when rounds isn’t given.
bcrypt.comparefunctionChecks whether str is the password known_hash was generated from.
bcrypt.get_roundsfunctionReads the cost factor a hash was generated with back out of it.
bcrypt.get_saltfunctionReads the salt portion out of a hash: the first 29 characters, covering the $2x$rounds$salt prefix.
bcrypt.hashfunctionHashes str with bcrypt, generating a fresh random salt internally on every call: hashing the same string…
bcrypt.needs_rehashfunctionChecks whether a stored hash was generated with a lower cost factor than target_rounds, meaning it should…

Constants

DEFAULT_LOG2_ROUNDS

bcrypt.DEFAULT_LOG2_ROUNDS: number = 10

Default cost factor used by hash() when rounds isn’t given. Higher means slower to compute (and slower to brute-force): each increment roughly doubles the work. 10 is a reasonable default for an interactive login flow; raise it over time as hardware gets faster (see needs_rehash()).

Functions

hash()

bcrypt.hash(str: string, rounds: ?number) -> string

Hashes str with bcrypt, generating a fresh random salt internally on every call: hashing the same string twice always produces two different (but equally valid) hashes.

Parameters

  • str (string)
  • rounds (?number) — The cost factor, 4-31. Default DEFAULT_LOG2_ROUNDS.

Returns string

Raises Error if rounds is out of range.

compare()

bcrypt.compare(str: string, known_hash: string)

Checks whether str is the password known_hash was generated from.

Parameters

  • str (string)
  • known_hash (string)

Returns — bool: false for a wrong password, and also false (rather than raising) for a known_hash that isn’t a well-formed bcrypt hash at all.

get_rounds()

bcrypt.get_rounds(hash: string) -> number

Reads the cost factor a hash was generated with back out of it.

Parameters

  • hash (string)

Returns number

Raises Error if hash isn’t a well-formed bcrypt hash.

get_salt()

bcrypt.get_salt(hash: string) -> string

Reads the salt portion out of a hash: the first 29 characters, covering the $2x$rounds$salt prefix.

Parameters

  • hash (string)

Returns string

Raises Error if hash is shorter than a real bcrypt hash.

Note: this does not validate that hash is actually a well-formed bcrypt hash; call get_rounds() first if you need that checked.

needs_rehash()

bcrypt.needs_rehash(hash: string, target_rounds: number) -> bool

Checks whether a stored hash was generated with a lower cost factor than target_rounds, meaning it should be re-hashed (at the user’s next successful login, typically) to bring it up to the current standard.

Example,

%> if bcrypt.compare(password, user.password_hash) {
..   if bcrypt.needs_rehash(user.password_hash, 12) {
..     user.password_hash = bcrypt.hash(password, 12)
..   }
..   # ... proceed with login
.. }

Parameters

  • hash (string)
  • target_rounds (number)

Returns bool


2022, Richard Ore and The Zuri Contributors

jwt

import jwt

The jwt module provides a complete implementation of JSON Web Tokens (JWT) as defined in RFC 7519, with support for signing, verification, and inspection of tokens using HMAC, RSA-PSS, ECDSA, and Ed25519 algorithm families.

Supported algorithms:

  • HS256/HS384/HS512: HMAC with SHA-256/384/512
  • PS256/PS384/PS512: RSASSA-PSS with SHA-256/384/512. This module deliberately supports PS-family, not RS-family (RSASSA-PKCS1-v1_5) algorithms, because they’re older, deterministic standard that lacks a formal security proof and because they’re mostly deterministic while the RSASSA-PSS are probabilistic.

Also, while PKCS#1 v1.5 is simpler to set up, but its structural rigidity has led to historical implementation flaws in encryption (like Bleichenbacher attacks), making PSS cleaner and safer against misuse.

  • ES256: ECDSA P-256 with SHA-256
  • ES384: ECDSA P-384 with SHA-384
  • EdDSA: Ed25519 (RFC 8037)
  • none: Unsecured token (no signature, explicit opt-in required)

Signature verification uses a constant-time comparison for the HMAC algorithms (to prevent timing attacks against the shared secret) and the underlying public-key verification routine for every asymmetric algorithm.

Quick start

import jwt

# sign a token
var token = jwt.sign({ user_id: 42, role: 'admin' }, 'secret')

# verify and decode the token
var payload = jwt.verify(token, 'secret')
echo payload.user_id   # 42

For applications verifying many tokens with the same configuration, prefer a Verifier (see [[jwt.Verifier]]) over repeating options on every verify() call. Likewise, prefer a Signer (see [[jwt.Signer]]) for repeated signing.

When keys are resolved by kid from a JSON Web Key Set rather than fixed ahead of time, see [[jwt.Jwks]] and [[jwt.verify_with_jwks]].

The jwt API

Every public name in jwt, wherever it is declared. Each links to the page that documents it.

NameKindSummary
jwt.AlgorithmErrorclassRaised when an algorithm specified in a token header is not supported by this module, when the caller…
jwt.ClaimErrorclassRaised when a required claim is absent from the token payload, or when a registered claim (iss, aud, sub)…
jwt.EDDSAconstantEd25519 (EdDSA, RFC 8037) signing algorithm identifier.
jwt.ES256constantECDSA P-256 with SHA-256 signing algorithm identifier.
jwt.ES384constantECDSA P-384 with SHA-384 signing algorithm identifier.
jwt.HS256constantHMAC-SHA256 signing algorithm identifier.
jwt.HS384constantHMAC-SHA384 signing algorithm identifier.
jwt.HS512constantHMAC-SHA512 signing algorithm identifier.
jwt.JwksclassParses a JSON Web Key Set document (a dict shaped like { keys: [...] }, exactly what json.decode() on a…
jwt.JwtErrorclassBase error class for all errors raised by the jwt module.
jwt.MalformedTokenErrorclassRaised when a token cannot be decoded because its structure is not valid.
jwt.NONEconstantUnsecured algorithm identifier.
jwt.PS256constantRSA-PSS-SHA256 signing algorithm identifier.
jwt.PS384constantRSA-PSS-SHA384 signing algorithm identifier.
jwt.PS512constantRSA-PSS-SHA512 signing algorithm identifier.
jwt.SignatureErrorclassRaised when a token’s signature does not match its header and payload.
jwt.SignerclassA reusable token signer that holds a fixed secret and set of default signing options.
jwt.TokenclassRepresents a decoded JWT.
jwt.TokenExpiredErrorclassRaised when a token’s temporal claims fail validation:
jwt.VerifierclassA reusable token verifier that holds a fixed secret and set of options.
jwt.decodefunctionDecodes a JWT string into a Token instance without performing signature verification or claim validation.
jwt.encodefunctionEncodes a header dictionary and payload dictionary into a signed JWT string.
jwt.expires_infunctionReturns the number of seconds until the token expires, based on its exp claim and the current clock.
jwt.headerfunctionReturns the decoded header dictionary of a token without verifying its signature.
jwt.is_expiredfunctionReturns true when the token’s exp claim is in the past, without verifying the signature.
jwt.payloadfunctionReturns the decoded payload dictionary of a token without verifying its signature.
jwt.signfunctionCreates and signs a new JWT from the given payload.
jwt.verifyfunctionVerifies a JWT string and returns its payload (or the full Token when options.complete is true).
jwt.verify_with_jwksfunctionVerifies a token by resolving its signing key from a JWKS document rather than a single fixed secret.

Submodules

ModuleReached asSummary
jwt.codecjwt.*Algorithm identifiers and the low-level encoding helpers core.zu builds encode()/verify() out of:…
jwt.corejwt.*The core encode/decode/sign/verify functions the rest of the jwt module is built on.
jwt.errorsjwt.*The full error hierarchy raised by the jwt module.
jwt.jwksjwt.*Jwks, plus verify_with_jwks(): resolving a token’s signing key from a JSON Web Key Set (RFC 7517) by its…
jwt.signerjwt.*Signer, a reusable configured wrapper around core.sign().
jwt.tokenjwt.*The Token class returned by decode() and verify() (when options.complete is set).
jwt.verifierjwt.*Verifier, a reusable configured wrapper around core.verify().

2026, Richard Ore and The Zuri Contributors

jwt.codec

import jwt

Everything here is re-exported by jwt, so import jwt is enough and the names are called as jwt.*. Importing jwt.codec on its own works too and reaches the same definitions.

Algorithm identifiers and the low-level encoding helpers core.zu builds encode()/verify() out of: base64url, HMAC dispatch, a constant-time comparison, and the DER <-> raw-R‖S conversion ECDSA signatures need to become JOSE-shaped.

Constants

HS256

jwt.HS256 = 'HS256'

HMAC-SHA256 signing algorithm identifier.

HS384

jwt.HS384 = 'HS384'

HMAC-SHA384 signing algorithm identifier.

HS512

jwt.HS512 = 'HS512'

HMAC-SHA512 signing algorithm identifier.

PS256

jwt.PS256 = 'PS256'

RSA-PSS-SHA256 signing algorithm identifier.

This is deliberately PS256, not RS256: this module’s RSA backing (crypto.rsa) is RSASSA-PSS, not RSASSA-PKCS1-v1_5, and PS256/PS384/PS512 are the JOSE-registered names (RFC 7518 §3.5) for exactly that scheme.

PS384

jwt.PS384 = 'PS384'

RSA-PSS-SHA384 signing algorithm identifier.

PS512

jwt.PS512 = 'PS512'

RSA-PSS-SHA512 signing algorithm identifier.

ES256

jwt.ES256 = 'ES256'

ECDSA P-256 with SHA-256 signing algorithm identifier.

ES384

jwt.ES384 = 'ES384'

ECDSA P-384 with SHA-384 signing algorithm identifier.

EDDSA

jwt.EDDSA = 'EdDSA'

Ed25519 (EdDSA, RFC 8037) signing algorithm identifier.

NONE

jwt.NONE = 'none'

Unsecured algorithm identifier. Tokens signed with this algorithm carry no signature and must never be used to protect sensitive resources. Passing this value to sign() requires setting allow_none: true in options or an error will be raised.

jwt.core

import jwt

Everything here is re-exported by jwt, so import jwt is enough and the names are called as jwt.*. Importing jwt.core on its own works too and reaches the same definitions.

The core encode/decode/sign/verify functions the rest of the jwt module is built on. Verifier/Signer/Jwks are thin, stateful wrappers around verify()/sign() defined here.

Functions

encode()

jwt.encode(header, payload, secret, options) -> string

Encodes a header dictionary and payload dictionary into a signed JWT string.

This is the low-level encoding function. For most use cases, sign() is more convenient. Use encode() when you need full control over the header, such as when adding custom header parameters (kid, x5t, etc.).

The secret must be a string for HMAC algorithms, a PEM-encoded RSA private key for PS256/384/512, a PEM-encoded EC private key for ES256/384, or a PEM-encoded Ed25519 private key for EdDSA.

Parameters

  • header (dict) — must contain at minimum an ‘alg’ key
  • payload (dict)
  • secret (string)
  • options (?dict) — see sign() for supported option keys

Returns string

Raises AlgorithmError

Raises JwtError

decode()

jwt.decode(token) -> Token

Decodes a JWT string into a Token instance without performing signature verification or claim validation.

This function is useful for inspecting a token’s header or payload before deciding how to verify it (for example, to read the kid header parameter and look up the appropriate public key). It must not be used as the sole step when authenticating a request.

Parameters

  • token (string)

Returns Token

Raises MalformedTokenError

sign()

jwt.sign(payload, secret, options) -> string

Creates and signs a new JWT from the given payload.

The payload is merged with any registered claims derived from options before signing. Caller-supplied claims in payload always take precedence over option-derived claims.

Options

KeyTypeDefaultDescription
algorithmstringHS256Signing algorithm
expires_innumber:Token lifetime in seconds
not_beforenumber:Seconds until token becomes valid
issuerstring:Value for the iss claim
subjectstring:Value for the sub claim
audiencestring|list:Value for the aud claim
jwt_idstringautoValue for the jti claim
no_timestampboolfalseOmit the iat claim when true
headerdict:Extra header parameters
allow_noneboolfalsePermit the none algorithm

Example

import jwt

var token = jwt.sign(
  { user_id: 99, role: 'editor' },
  'my-secret-key',
  {
    algorithm: jwt.HS256,
    expires_in: 3600,
    issuer: 'api.example.com',
    subject: '99',
  }
)

Parameters

  • payload (dict)
  • secret (string)
  • options (?dict)

Returns string

Raises AlgorithmError

Raises JwtError

verify()

jwt.verify(token, secret, options) -> dict|Token

Verifies a JWT string and returns its payload (or the full Token when options.complete is true).

Verification performs the following steps in order:

  1. Structural decode: ensures the token is well-formed. 2. Algorithm check: rejects algorithms not in the options.algorithms list. 3. Signature verification: recomputes and compares the signature using a constant-time comparison for HMAC algorithms, or the underlying public-key verification for PS/ES/EdDSA algorithms. 4. Temporal claim validation: checks exp, nbf, and iat against the current clock, applying clock_tolerance leeway where configured. 5. Registered claim validation: checks iss, aud, and sub when the corresponding options are set.

An error is raised at the first failure; verification does not accumulate errors.

Options

KeyTypeDefaultDescription
algorithmslist[HS256,HS384,HS512]Accepted algorithm whitelist
issuerstring:Required iss claim value
subjectstring:Required sub claim value
audiencestring|list:Required aud claim value
clock_tolerancenumber0Leeway in seconds for exp, nbf, and iat
ignore_expirationboolfalseSkip exp validation
ignore_not_beforeboolfalseSkip nbf validation
completeboolfalseReturn full Token instead of payload dict
allow_noneboolfalsePermit the none algorithm

Example

import jwt

catch {
  var payload = jwt.verify(token, 'my-secret-key', {
    algorithms: [jwt.HS256],
    issuer: 'api.example.com',
    clock_tolerance: 5,
  })
  echo payload['user_id']
} as e

if e {
  # e is a JwtError
  echo 'token rejected: ' + e.message
}

Parameters

  • token (string)
  • secret (string)
  • options (dict)

Returns dict|Token

Raises MalformedTokenError

Raises AlgorithmError

Raises SignatureError

Raises TokenExpiredError

Raises ClaimError

header()

jwt.header(token) -> dict

Returns the decoded header dictionary of a token without verifying its signature.

This is useful for reading the kid or alg header parameters before selecting the appropriate key or algorithm for full verification.

Parameters

  • token (string)

Returns dict

Raises MalformedTokenError

payload()

jwt.payload(token) -> dict

Returns the decoded payload dictionary of a token without verifying its signature.

Do not use this to make authorization decisions. Use verify() instead.

Parameters

  • token (string)

Returns dict

Raises MalformedTokenError

is_expired()

jwt.is_expired(token, leeway) -> bool

Returns true when the token’s exp claim is in the past, without verifying the signature.

A token without an exp claim is never considered expired by this function.

Parameters

  • token (string)
  • leeway (number) — Optional seconds of tolerance. Default: 0.

Returns bool

Raises MalformedTokenError

expires_in()

jwt.expires_in(token) -> number|nil

Returns the number of seconds until the token expires, based on its exp claim and the current clock. Returns nil when the token has no exp claim. Returns a negative number when the token has already expired.

Parameters

  • token (string)

Returns number|nil

Raises MalformedTokenError

jwt.errors

import jwt

Everything here is re-exported by jwt, so import jwt is enough and the names are called as jwt.*. Importing jwt.errors on its own works too and reaches the same definitions.

The full error hierarchy raised by the jwt module. All of them inherit from JwtError, so a caller who just wants to catch “something went wrong with this token” only needs to catch that one class.

Classes

JwtError

class jwt.JwtError < Error

Base error class for all errors raised by the jwt module. Catching this class will intercept any jwt-specific error.

Constructor

jwt.JwtError(message)

Parameters

  • message (string)

MalformedTokenError

class jwt.MalformedTokenError < JwtError

Raised when a token cannot be decoded because its structure is not valid. This includes tokens that are not three dot-separated segments or that contain malformed base64url or JSON.

Constructor

jwt.MalformedTokenError(message)

SignatureError

class jwt.SignatureError < JwtError

Raised when a token’s signature does not match its header and payload. This indicates the token has been tampered with or was signed with a different key.

Constructor

jwt.SignatureError(message)

TokenExpiredError

class jwt.TokenExpiredError < JwtError

Raised when a token’s temporal claims fail validation:

  • exp (expiry): token has expired - nbf (not before): token is not yet valid - iat (issued at): token was issued in the future

Constructor

jwt.TokenExpiredError(message)

ClaimError

class jwt.ClaimError < JwtError

Raised when a required claim is absent from the token payload, or when a registered claim (iss, aud, sub) does not match the expected value.

Constructor

jwt.ClaimError(message)

AlgorithmError

class jwt.AlgorithmError < JwtError

Raised when an algorithm specified in a token header is not supported by this module, when the caller attempts to use the none algorithm without explicitly enabling it, or when a JWKS document has no key matching a token’s kid.

Constructor

jwt.AlgorithmError(message)

jwt.jwks

import jwt

Everything here is re-exported by jwt, so import jwt is enough and the names are called as jwt.*. Importing jwt.jwks on its own works too and reaches the same definitions.

Jwks, plus verify_with_jwks(): resolving a token’s signing key from a JSON Web Key Set (RFC 7517) by its kid header, rather than a single fixed secret/key.

Functions

verify_with_jwks()

jwt.verify_with_jwks(token, jwks: instance, options) -> dict|Token

Verifies a token by resolving its signing key from a JWKS document rather than a single fixed secret.

Reads the token’s kid header parameter, resolves the matching key in jwks, converts it to a PEM, and verifies the token with it exactly as verify() would with that PEM passed directly.

Parameters

  • token (string)
  • jwks (Jwks)
  • options (?dict) — See verify() for the full reference; note that algorithms should still be set explicitly here, the same as with any other verify() call: resolving a key by kid says nothing about which algorithm is safe to trust it with.

Returns dict|Token

Raises AlgorithmError When the token has no kid, or no key in jwks matches it.

Raises MalformedTokenError

Raises SignatureError

Raises TokenExpiredError

Raises ClaimError

Raises CryptoError

Classes

Jwks

class jwt.Jwks

Parses a JSON Web Key Set document (a dict shaped like { keys: [...] }, exactly what json.decode() on a fetched JWKS document produces) and resolves individual keys by their kid.

Example
import jwt
import json

var jwks = jwt.Jwks(json.decode(fetch_jwks_document()))
var payload = jwt.verify_with_jwks(token, jwks, { issuer: 'auth.example.com' })

Constructor

jwt.Jwks(jwks: dict)

Parameters

  • jwks (dict) — A decoded JWKS document: { keys: [...] }. Keys without a kid field are ignored, since there is no way to resolve them by kid.

Jwks.find()

jwt.Jwks.find(kid: string) -> dict

Returns the raw JWK dictionary for the given kid.

Parameters

  • kid (string)

Returns dict

Raises AlgorithmError When no key with this kid is present.

Jwks.to_pem()

jwt.Jwks.to_pem(kid: string) -> string

Resolves the given kid and converts it to a PEM public key via crypto.jwk.to_pem().

Parameters

  • kid (string)

Returns string — PEM-encoded public key.

Raises AlgorithmError

Raises CryptoError

jwt.signer

import jwt

Everything here is re-exported by jwt, so import jwt is enough and the names are called as jwt.*. Importing jwt.signer on its own works too and reaches the same definitions.

Signer, a reusable configured wrapper around core.sign().

Classes

Signer

class jwt.Signer

A reusable token signer that holds a fixed secret and set of default signing options.

Using a Signer instance is recommended when the same signing configuration is used throughout an application. Default options can be overridden on individual sign() calls.

Example
import jwt

var signer = jwt.Signer('my-secret', {
  algorithm: jwt.HS256,
  expires_in: 900,
  issuer: 'auth.example.com',
  audience: 'api.example.com',
})

var token = signer.sign({ user_id: 7, role: 'admin' })

Constructor

jwt.Signer(secret, options)

Parameters

  • secret (string) — The secret or PEM private key used for signing.
  • options (dict) — Default signing options (see sign() for reference).

Signer.sign()

jwt.Signer.sign(payload, options) -> string

Signs the given payload using the secret and default options bound to this Signer, returning a token string.

Options passed here are merged with the instance defaults, with per-call options taking precedence.

Parameters

  • payload (dict)
  • options (dict) — Optional per-call option overrides.

Returns string

Raises AlgorithmError

Raises JwtError

jwt.token

import jwt

Everything here is re-exported by jwt, so import jwt is enough and the names are called as jwt.*. Importing jwt.token on its own works too and reaches the same definitions.

The Token class returned by decode() and verify() (when options.complete is set).

Classes

Token

class jwt.Token

Represents a decoded JWT. Instances of this class are returned by decode() and verify(). The raw header and payload dictionaries are available as properties, and individual claims are accessible directly through the index operator.

Example
import jwt

var tok = jwt.decode('ey_j...')
echo tok.header['alg']     # HS256
echo tok.payload['sub']    # '1234567890'
echo tok['sub']            # '1234567890'  (shorthand)

Fields

FieldTypeDescription
headerThe decoded header dictionary.
payloadThe decoded payload dictionary.
signatureThe raw base64url-encoded signature segment as it appeared in the original token string.
rawThe original token string from which this instance was decoded.

Constructor

jwt.Token(header, payload, signature, raw)

Parameters

  • header (dict)
  • payload (dict)
  • signature (string)
  • raw (string)

Token.set_leeway()

jwt.Token.set_leeway(seconds: number)

Token.get()

jwt.Token.get(name: string) -> any

Returns the value of the named claim from the payload, or nil if the claim is not present.

Parameters

  • name (string)

Returns any

Token.is_expired()

jwt.Token.is_expired() -> bool

Returns true when the token carries an exp claim and the current time is past that value, honoring the leeway this Token was verified with when it came from verify().

A Token produced by a bare decode() (no verification) always has zero leeway.

Returns bool

jwt.verifier

import jwt

Everything here is re-exported by jwt, so import jwt is enough and the names are called as jwt.*. Importing jwt.verifier on its own works too and reaches the same definitions.

Verifier, a reusable configured wrapper around core.verify().

Classes

Verifier

class jwt.Verifier

A reusable token verifier that holds a fixed secret and set of options.

Using a Verifier instance is the recommended approach for applications that verify many tokens with the same configuration, as it avoids passing options on every call and makes the intent of the verification parameters explicit and auditable.

Example
import jwt
import http.middleware

var verifier = jwt.Verifier('my-secret', {
  algorithms: [jwt.HS256],
  issuer: 'auth.example.com',
  audience: 'api.example.com',
  clock_tolerance: 10,
})

# In a request handler:
var token = middleware.parse_bearer(req.headers['Authorization'])
var payload = verifier.verify(token)
echo payload['role']

Constructor

jwt.Verifier(secret, options)

Parameters

  • secret (string) — The secret or PEM public key used for verification.
  • options (dict) — Verification options (see verify() for reference).

Verifier.verify()

jwt.Verifier.verify(token, options) -> dict|Token

Verifies the given token string using the secret and options bound to this Verifier.

Additional options passed here are merged with the instance options, with per-call options taking precedence. This allows overriding individual parameters (such as audience) on a per-request basis without constructing a new Verifier.

Parameters

  • token (string)
  • options (dict) — Optional per-call option overrides.

Returns dict|Token

Raises MalformedTokenError

Raises AlgorithmError

Raises SignatureError

Raises TokenExpiredError

Raises ClaimError

Verifier.decode()

jwt.Verifier.decode(token) -> Token

Decodes the given token without verification. Delegates to decode().

Parameters

  • token (string)

Returns Token

Raises MalformedTokenError

uuid

import uuid

Provides RFC 9562 (and RFC 4122) compliant Universally Unique Identifier (UUID) generation, parsing, validation, and inspection.

A UUID is a 128-bit label represented as 32 lowercase hexadecimal digits, displayed in five groups separated by hyphens in the form:

xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx

where M encodes the version and N encodes the variant.

Supported versions

VersionAlgorithmRFC
v1Time-based (Gregorian time + MAC)RFC 4122
v3Name-based MD5RFC 4122
v4RandomRFC 4122
v5Name-based SHA-1RFC 4122
v6Time-ordered (reordered v1)RFC 9562
v7Unix-time ordered + randomRFC 9562
v8Custom / application-definedRFC 9562

The Nil UUID (00000000-0000-0000-0000-000000000000) and the Max UUID (ffffffff-ffff-ffff-ffff-ffffffffffff) are also defined as per RFC 9562.

Note: UUID v2 (DCE Security) is intentionally omitted. RFC 9562 declares v2 “out of scope”, and its specification lives in a separate DCE document rather than in the IETF UUID standard.

Quick start

import uuid

echo uuid.v4()           # e.g. '110e8400-e29b-41d4-a716-446655440000'
echo uuid.v7()           # time-ordered, database-friendly
echo uuid.is_valid('...')

var id = uuid.UUID('110e8400-e29b-41d4-a716-446655440000')
echo id.version()          # 4
echo id.variant()          # 'RFC 9562'
echo id.urn()            # 'urn:uuid:110e8400-...'

Namespace UUIDs (for v3 / v5)

RFC 9562 §Appendix C pre-defines four namespace UUIDs:

  • uuid.NAMESPACE_DNS: for fully-qualified domain names - uuid.NAMESPACE_URL: for URLs - uuid.NAMESPACE_OID: for ISO OIDs - uuid.NAMESPACE_X500: for X.500 distinguished names

The uuid API

Every public name in uuid, wherever it is declared. Each links to the page that documents it.

NameKindSummary
uuid.MAXconstantThe Max UUID: all 128 bits are one.
uuid.NAMESPACE_DNSconstantPre-defined namespace UUID for fully-qualified domain names (FQDN).
uuid.NAMESPACE_OIDconstantPre-defined namespace UUID for ISO Object Identifiers (OID).
uuid.NAMESPACE_URLconstantPre-defined namespace UUID for URLs.
uuid.NAMESPACE_X500constantPre-defined namespace UUID for X.500 Distinguished Names.
uuid.NILconstantThe Nil UUID: all 128 bits are zero.
uuid.UUIDclassRepresents a parsed, immutable UUID value.
uuid.from_bytesfunctionConverts a list of 16 byte integers (0–255) in big-endian order into a canonical UUID string.
uuid.from_intfunctionConverts an integer value (the numeric representation of a 128-bit UUID, as returned by UUID.int()) into a…
uuid.is_validfunctionReturns true if str is a syntactically valid UUID in canonical form…
uuid.max_uuidfunctionReturns the pre-defined Max UUID string.
uuid.nil_uuidfunctionReturns the pre-defined Nil UUID string.
uuid.parsefunctionParses a UUID string (in canonical, raw hex, URN, or brace-wrapped form) and returns a UUID object.
uuid.v1functionGenerates a UUID Version 1 (time-based) as defined in RFC 4122 §4.1 / RFC 9562 §5.1.
uuid.v3functionGenerates a UUID Version 3 (name-based, MD5) as defined in RFC 4122 §4.3 / RFC 9562 §5.3.
uuid.v4functionGenerates a UUID Version 4 (random) as defined in RFC 4122 §4.4 / RFC 9562 §5.4.
uuid.v5functionGenerates a UUID Version 5 (name-based, SHA-1) as defined in RFC 4122 §4.3 / RFC 9562 §5.5.
uuid.v6functionGenerates a UUID Version 6 (time-ordered) as defined in RFC 9562 §5.6.
uuid.v7functionGenerates a UUID Version 7 (Unix-time ordered) as defined in RFC 9562 §5.7.
uuid.v8functionGenerates a UUID Version 8 (custom / application-defined) as defined in RFC 9562 §5.8.
uuid.versionfunctionReturns the version number (1–8) of a UUID string, or nil if the UUID is the Nil or Max special form, or if…

Constants

NAMESPACE_DNS

uuid.NAMESPACE_DNS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'

Pre-defined namespace UUID for fully-qualified domain names (FQDN). Use with uuid.v3() or uuid.v5() when the name is a DNS hostname.

Value: 6ba7b810-9dad-11d1-80b4-00c04fd430c8

NAMESPACE_URL

uuid.NAMESPACE_URL = '6ba7b811-9dad-11d1-80b4-00c04fd430c8'

Pre-defined namespace UUID for URLs. Use with uuid.v3() or uuid.v5() when the name is a URL.

Value: 6ba7b811-9dad-11d1-80b4-00c04fd430c8

NAMESPACE_OID

uuid.NAMESPACE_OID = '6ba7b812-9dad-11d1-80b4-00c04fd430c8'

Pre-defined namespace UUID for ISO Object Identifiers (OID). Use with uuid.v3() or uuid.v5() when the name is an ISO OID.

Value: 6ba7b812-9dad-11d1-80b4-00c04fd430c8

NAMESPACE_X500

uuid.NAMESPACE_X500 = '6ba7b814-9dad-11d1-80b4-00c04fd430c8'

Pre-defined namespace UUID for X.500 Distinguished Names. Use with uuid.v3() or uuid.v5() when the name is an X.500 DN.

Value: 6ba7b814-9dad-11d1-80b4-00c04fd430c8

NIL

uuid.NIL = '00000000-0000-0000-0000-000000000000'

The Nil UUID: all 128 bits are zero. Defined in RFC 9562 §5.9 as a special UUID that signifies “no value”.

Value: 00000000-0000-0000-0000-000000000000

MAX

uuid.MAX = 'ffffffff-ffff-ffff-ffff-ffffffffffff'

The Max UUID: all 128 bits are one. Defined in RFC 9562 §5.10 as a special UUID often used as a sentinel upper-bound in range queries.

Value: ffffffff-ffff-ffff-ffff-ffffffffffff

Functions

is_valid()

uuid.is_valid(str) -> bool

Returns true if str is a syntactically valid UUID in canonical form (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx), case-insensitive.

This function checks only the structure and character set; it does not verify that the version and variant bits are set to any particular value, so the Nil and Max UUIDs are considered valid.

Example

import uuid

uuid.is_valid('f47ac10b-58cc-4372-a567-0e02b2c3d479') # true
uuid.is_valid('not-a-uuid')                           # false
uuid.is_valid(uuid.NIL)                               # true

Parameters

  • str (string) — The string to test.

Returns bool

version()

uuid.version(str: string) -> number

Returns the version number (1–8) of a UUID string, or nil if the UUID is the Nil or Max special form, or if str is not a valid UUID.

Parameters

  • str (string) — A canonical UUID string.

Returns number — | nil

v1()

uuid.v1(node: ?string, clock_seq: ?number) -> string

Generates a UUID Version 1 (time-based) as defined in RFC 4122 §4.1 / RFC 9562 §5.1.

A v1 UUID encodes a 60-bit timestamp counted in 100-nanosecond intervals since the Gregorian epoch (1582-10-15 00:00:00 UTC), a 14-bit clock sequence, and a 48-bit node identifier (MAC address or random).

Privacy note: v1 UUIDs embed timing and node information. For privacy-sensitive contexts prefer v4() or v7().

Example

import uuid
echo uuid.v1()  # e.g. '6ba7b810-9dad-11d1-80b4-00c04fd430c8'

Parameters

  • | (string) — nil node Optional 12-char hex string for the node field (48-bit MAC address). If nil, a random multicast node value is used (RFC 4122 §4.5).
  • | (number) — nil clock_seq Optional 14-bit clock sequence (0–16383). If nil, the internal monotonic sequence is used.

Returns string — Canonical UUID string.

v3()

uuid.v3(namespace: string, name: string) -> string

Generates a UUID Version 3 (name-based, MD5) as defined in RFC 4122 §4.3 / RFC 9562 §5.3.

Given the same namespace and name, v3() always returns the same UUID. The UUID is derived by computing the MD5 hash of the namespace UUID bytes concatenated with the UTF-8 encoded name bytes, then setting the version and variant bits.

Note: MD5 is cryptographically broken. For new designs, prefer v5() (SHA-1) or generate random UUIDs with v4().

Example

import uuid

echo uuid.v3(uuid.NAMESPACE_DNS, 'www.example.com')
# always: '5df41881-3aed-3515-88a7-2f4a814cf09e'

Parameters

  • namespace (string) — A UUID string to use as the namespace. Use the pre-defined NAMESPACE_* constants or any other valid UUID.
  • name (string) — The name within the namespace.

Returns string — Canonical UUID string.

Raises Error If namespace is not a valid UUID.

v4()

uuid.v4() -> string

Generates a UUID Version 4 (random) as defined in RFC 4122 §4.4 / RFC 9562 §5.4.

122 bits are filled with pseudo-random data; the remaining 6 bits encode the version (0100) and variant (10).

This is the most commonly used UUID version for general-purpose unique identifiers where time-ordering is not required.

Example

import uuid

echo uuid.v4()  # e.g. 'f47ac10b-58cc-4372-a567-0e02b2c3d479'

Returns string — Canonical UUID string.

v5()

uuid.v5(namespace: string, name: string) -> string

Generates a UUID Version 5 (name-based, SHA-1) as defined in RFC 4122 §4.3 / RFC 9562 §5.5.

Identical in structure to v3() but uses SHA-1 instead of MD5. SHA-1 is preferred over MD5 for new name-based UUIDs. Only the first 128 bits of the 160-bit SHA-1 digest are used.

Given the same namespace and name, v5() always returns the same UUID.

Example

import uuid

echo uuid.v5(uuid.NAMESPACE_URL, 'https://www.example.com')
# always: 'a0787afd-c170-5773-8d60-02739168de9f'

Parameters

  • namespace (string) — A UUID string to use as the namespace. Use the pre-defined NAMESPACE_* constants or any other valid UUID.
  • name (string) — The name within the namespace.

Returns string — Canonical UUID string.

Raises Error If namespace is not a valid UUID.

v6()

uuid.v6(node: ?string, clock_seq: ?number) -> string

Generates a UUID Version 6 (time-ordered) as defined in RFC 9562 §5.6.

v6 is a reordered variant of v1 that places the most significant timestamp bits first, making v6 UUIDs naturally sortable lexicographically by generation time. It retains the same 60-bit Gregorian timestamp, 14-bit clock sequence, and 48-bit node as v1.

v6 is the recommended replacement for v1 when time-ordered, monotonic IDs derived from a Gregorian clock are required. For most new designs, v7() (Unix-time based) is simpler and equally sortable.

Example

import uuid

echo uuid.v6()  # e.g. '1ef9e292-a7a4-6000-80b4-00c04fd430c8'

Parameters

  • | (string) — nil node Optional 12-char hex node (see v1()).
  • | (number) — nil clock_seq Optional 14-bit clock sequence (see v1()).

Returns string — Canonical UUID string.

v7()

uuid.v7() -> string

Generates a UUID Version 7 (Unix-time ordered) as defined in RFC 9562 §5.7.

v7 UUIDs embed a 48-bit Unix millisecond timestamp in the most significant bits, followed by a 12-bit sub-millisecond sequence counter (for monotonicity within the same millisecond) and 62 random bits for uniqueness.

v7 is the recommended version for new systems that need:

  • Time-ordered, lexicographically sortable identifiers. - Good database index locality (avoids B-tree fragmentation). - No MAC address leakage (unlike v1 / v6).

Example

import uuid

echo uuid.v7()  # e.g. '018f5e1a-2b3c-7d4e-9f0a-1b2c3d4e5f6a'

Layout (RFC 9562 §5.7):

0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           unix_ts_ms                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          unix_ts_ms           |  ver  |       rand_a          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var|                        rand_b                             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                            rand_b                             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Returns string — Canonical UUID string.

v8()

uuid.v8(a: number, b: number, c: number) -> string

Generates a UUID Version 8 (custom / application-defined) as defined in RFC 9562 §5.8.

v8 is intended for vendor-specific or experimental use cases where the caller needs to embed application-defined data into a UUID while retaining the standard format, version, and variant bits. RFC 9562 does not prescribe the meaning of any field beyond the version and variant.

The three parameters together provide 48 + 12 + 62 = 122 bits of application-controlled data.

Example

import uuid

# Embed a shard ID (a), a type tag (b), and a sequence number (c).
echo uuid.v8(0x0123456789ab, 0x0cd, 0x0123456789abcdef)

Parameters

  • a (number) — 48-bit application-defined field (bits 0–47). Values exceeding 48 bits are silently truncated.
  • b (number) — 12-bit application-defined field (bits 48–59). Values exceeding 12 bits are silently truncated.
  • c (number) — 62-bit application-defined field (bits 64–125). Values exceeding 62 bits are silently truncated.

Returns string — Canonical UUID string.

nil_uuid()

uuid.nil_uuid() -> string

Returns the pre-defined Nil UUID string.

Equivalent to the module-level constant uuid.NIL. Provided as a function for symmetry with the other generators.

Returns string — '00000000-0000-0000-0000-000000000000'

max_uuid()

uuid.max_uuid() -> string

Returns the pre-defined Max UUID string.

Equivalent to the module-level constant uuid.MAX. Provided as a function for symmetry with the other generators.

Returns string — 'ffffffff-ffff-ffff-ffff-ffffffffffff'

parse()

uuid.parse(str: string) -> UUID

Parses a UUID string (in canonical, raw hex, URN, or brace-wrapped form) and returns a UUID object.

This is equivalent to calling UUID(str) directly.

Example

import uuid

var id = uuid.parse('urn:uuid:f47ac10b-58cc-4372-a567-0e02b2c3d479')
echo id.version()   # 4

Parameters

  • str (string) — The UUID string to parse.

Returns UUID

Raises Error If str is not a recognisable UUID form.

from_bytes()

uuid.from_bytes(bytes_list: list|bytes) -> string

Converts a list of 16 byte integers (0–255) in big-endian order into a canonical UUID string.

Parameters

  • bytes_list (list) — A list of exactly 16 integers in [0, 255].

Returns string — Canonical UUID string.

Raises Error If bytes_list does not contain exactly 16 bytes.

from_int()

uuid.from_int(int_val) -> string

Converts an integer value (the numeric representation of a 128-bit UUID, as returned by UUID.int()) into a canonical UUID string.

Accepts either a bigint (the exact way to represent a full 128-bit value, and what UUID.int() itself returns) or a plain number for a small value that’s within safe integer precision (e.g. from_int(0)).

Parameters

  • int_val (number|bigint) — The integer value of the UUID.

Returns string — Canonical UUID string.

Classes

UUID

class uuid.UUID

Represents a parsed, immutable UUID value.

Instances expose the canonical string form plus convenience properties and methods for inspecting and converting the UUID.

Example
import uuid

var id = uuid.UUID('f47ac10b-58cc-4372-a567-0e02b2c3d479')
echo id.version()   # 4
echo id.variant()   # 'RFC 9562'
echo id.urn()     # 'urn:uuid:f47ac10b-58cc-4372-a567-0e02b2c3d479'
echo id.hex()     # 'f47ac10b58cc4372a5670e02b2c3d479'
echo id.int()     # integer value of the 128-bit UUID

Raises Error if the supplied string is not a valid UUID.

Fields

FieldTypeDescription
valuestringThe canonical (lowercase, hyphenated) string representation.

Constructor

uuid.UUID(uuid_str: string)

Creates a new UUID object from a canonical UUID string.

The constructor normalises the input to lowercase and accepts any of the following common forms:

  • xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (canonical, with hyphens) - xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (raw hex, no hyphens) - urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (URN form) - {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} (brace-wrapped GUID form)

Parameters

  • uuid_str (string) — The UUID to parse.

Raises Error If uuid_str is not a valid UUID in any accepted form.

UUID.version()

uuid.UUID.version()

Returns the UUID version number (1–8), or nil for the Nil and Max special-form UUIDs which carry no version.

The version is encoded in the high nibble of byte 6 (the M position in the canonical format xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx).

UUID.variant()

uuid.UUID.variant()

Returns a human-readable string describing the UUID variant field.

The variant occupies the high bits of byte 8 (the N position):

High bitsVariant string
0xx'NCS'
10x'RFC 9562'
110'Microsoft'
111'Future'

UUID.to_string()

uuid.UUID.to_string() -> string

Returns the string representation of the UUID in canonical lowercase hyphenated form: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.

Returns string

UUID.urn()

uuid.UUID.urn() -> string

Returns the UUID as a URN string per RFC 9562 §4: urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

Returns string

UUID.hex()

uuid.UUID.hex() -> string

Returns the raw 32-character lowercase hex representation of the UUID with all hyphens removed.

Returns string

UUID.int()

uuid.UUID.int() -> bigint

Returns the UUID as an integer (the numeric value of its 128 bits), as an exact bigint: a 128-bit value is always far beyond what a regular number can represent exactly, so this never uses one. Pass the result straight to from_int() to convert back.

Returns bigint

UUID.bytes()

uuid.UUID.bytes() -> list

Returns the UUID as a 16-element list of byte values (integers 0–255), in big-endian (network) byte order.

Returns list

UUID.is_nil()

uuid.UUID.is_nil() -> bool

Returns true if this UUID is the Nil UUID (00000000-0000-0000-0000-000000000000).

Returns bool

UUID.is_max()

uuid.UUID.is_max() -> bool

Returns true if this UUID is the Max UUID (ffffffff-ffff-ffff-ffff-ffffffffffff).

Returns bool

UUID.equals()

uuid.UUID.equals(other) -> bool

Compares this UUID with another UUID or canonical UUID string. Returns true if both UUIDs represent the same 128-bit value.

Parameters

  • | (UUID) — string other The UUID to compare against.

Returns bool

Raises TypeError if other is neither a UUID nor a string.

UUID.compare()

uuid.UUID.compare(other: instance) -> number

Compares this UUID with another UUID lexicographically by canonical string form. For time-ordered versions (v1, v6, v7) this is also a comparison by generation time.

Parameters

  • other (UUID)

Returns number — A negative number if this UUID sorts before other, a positive number if after, 0 if they’re equal.


2026, Richard Ore and The Zuri Contributors

validate

import validate

Schema-based input validation for Zuri applications.

The validate module provides a fluent, chainable API for defining field-level validation rules, composing them into schemas, and checking arbitrary data dictionaries against those schemas.


Quick start

import validate

var schema = validate.schema({
  name:     validate.required().string().max_length(100),
  email:    validate.required().string().email(),
  age:      validate.required().integer().between(18, 120),
  website:  validate.string().url().sometimes(),
})

var result = schema.check({
  name:  'Ada Lovelace',
  email: 'ada@example.com',
  age:   36,
})

if result.valid {
  echo 'All good.'
} else {
  for err in result.errors {
    echo '${err.field}: ${err.message}'
  }
}

Rule chaining

Every top-level function returns a Validator instance. Rules are evaluated in the order they are chained. By default, evaluation stops at the first failing rule for each field (bail mode). Pass .bail(false) to collect all errors for a field.

# Collects every error for "password" rather than stopping at the first.
var rules = validate.schema({
  password: validate.required().string()
    .min_length(8)
    .regex('/[A-Z]/')
    .regex('/[0-9]/')
    .bail(false),
})

Optional fields

var rules = validate.schema({
  # bio is optional; when present it must be at most 500 chars.
  bio: validate.string().max_length(500).sometimes(),

  # nickname is optional and may be nil.
  nickname: validate.nullable().string().max_length(30).sometimes(),
})

Cross-field validation

var schema = validate.schema({
  password:              validate.required().string().min_length(8),
  password_confirmation: validate.required().string().confirmed(),
  role:                  validate.required().string().is_in(['user', 'admin']),
  admin_code:            validate.required_if('role', 'admin').string(),
})

required_if() (like the other conditional-requirement functions below) must come before .string() in the chain: see “Sequential Validation” further down for why the order here matters.


Nested fields (dot-notation)

var schema = validate.schema({
  'address.street':  validate.required().string(),
  'address.city':    validate.required().string(),
  'address.country': validate.required().string().length(2).uppercase(),
})

List items (wildcard)

var schema = validate.schema({
  'tags.*': validate.required().string().max_length(32).alpha_dash(),
})

Custom rules

Inline function:

var schema = validate.schema({
  code: validate.required().string().custom(@(value) {
    return value.starts_with('ZURI-') and value.length() == 12
  }, 'Must be a valid Zuri code (ZURI-XXXXXXX).')
})

Subclassing Rule:

import validate { Rule, Validator }

class Slug < Rule {
  validate(value) {
    if !is_string(value) return false
    return !!value.match('/^[a-z0-9]+(?:-[a-z0-9]+)*$/')
  }
  error() { return 'The ${self.name} field must be a valid slug.' }
}

var schema = validate.schema({
  slug: Validator().required().use(Slug),
})

Raising on failure

import validate { ValidationError }

def create_user(req, res) {
  catch {
    schema.check_or_raise(req.body)
  } as e

  if e {
    return res.json({ errors: schema.group_errors(e.errors) }, 422)
  }

  return res.json({ ok: true })
}

HTTP handler pattern

import http
import validate
import validate { ValidationError }

var create_user_schema = validate.schema({
  name:     validate.required().string().max_length(100),
  email:    validate.required().string().email(),
  password: validate.required().string().min_length(8),
  role:     validate.required().string().is_in(['user', 'admin']).sometimes(),
})

server.handle('POST', '/users', @(req, res) {
  catch {
    create_user_schema.check_or_raise(req.body)
  } as e

  if e {
    return res.json({ errors: create_user_schema.group_errors(e.errors) }, 422)
  }
  # ... create user
  res.json({ status: 'created' }, 201)
})

Sequential Validation

The validation mode is sequential, meaning that each field is validated in turn. This is the behavior you expect from a form submission. It is important to keep this in mind when using this module.

Consider the example below:

var schema = validate.schema({
  name: validate.required_if('age', 30).string().max_length(100),
  age:  validate.required().integer().between(18, 120),
})

If the age field is empty, the validation of name will not raise any error. This is because the required_if rule is only applied if the age field is 30 or greater and the entire chain exists before the next rule string().

However, if the validation rules were modified like this:

var schema = validate.schema({
  name: validate.string().required_if('age', 30).max_length(100),
  age:  validate.required().integer().between(18, 120),
})

Even when the age field is empty, the validation of name will raise an error if it is empty. This is because the string rule comes before the required_if rule.

With this behavior in mind, it is advised that optional rules such as sometimes, nullable, *_if, *_unless rules are applied early in the chain unless you absolutely want them to fail irrespective.

The validate API

Every public name in validate, wherever it is declared. Each links to the page that documents it.

NameKindSummary
validate.RuleclassAbstract base class for all validation rules.
validate.SchemaclassBinds a ruleset to data and validates it.
validate.ValidationErrorclassRaised by Schema.check_or_raise() when validation fails.
validate.ValidatorclassFluent rule chain builder for a single field.
validate.acceptedfunctionThe value must be one of the accepted truthy representations: true, "true", "yes", "on", "1", 1.
validate.afterfunctionThe value must be a date string after ref.
validate.after_or_equalfunctionThe value must be a date string after or equal to ref.
validate.alphafunctionThe string must contain only ASCII alphabetic characters (a-z, A-Z).
validate.alpha_dashfunctionThe string must contain only ASCII alphanumeric characters, hyphens, and underscores.
validate.alpha_numfunctionThe string must contain only ASCII alphanumeric characters.
validate.beforefunctionThe value must be a date string before ref.
validate.before_or_equalfunctionThe value must be a date string before or equal to ref.
validate.betweenfunctionThe value’s size must be between min and max (inclusive).
validate.booleanfunctionThe value must be boolean-like: true, false, "true", "false", "1", "0", 1, or 0.
validate.confirmedfunctionThe value must match the ``field_name_confirmation sibling field.
validate.containsfunctionThe string must contain the given substring.
validate.customfunctionValidates with an inline anonymous function.
validate.custom_with_datafunctionValidates with an inline function that also receives the full data dictionary, enabling cross-field logic…
validate.datefunctionThe value must be a valid date string parseable by the date module.
validate.declinedfunctionThe value must be one of the declined falsy representations: false, "false", "no", "off", "0", 0.
validate.dictfunctionThe value must be a dictionary.
validate.differentfunctionThe value must differ from the value of other_field.
validate.distinctfunctionThe list must not contain duplicate values.
validate.doesnt_containfunctionThe string must not contain the given substring.
validate.doesnt_end_withfunctionThe string must not end with any of the given suffixes.
validate.doesnt_start_withfunctionThe string must not start with any of the given prefixes.
validate.eachfunctionEvery item in the list must pass the given Validator chain.
validate.emailfunctionThe value must be a syntactically valid email address.
validate.ends_withfunctionThe string must end with one of the given suffixes.
validate.equalsfunctionThe value must strictly equal expected.
validate.gtfunctionThe numeric value must be greater than n.
validate.gtefunctionThe numeric value must be greater than or equal to n.
validate.in_listfunctionEvery item in the list value must be contained in values.
validate.integerfunctionThe value must be an integer (no fractional part).
validate.ipfunctionThe value must be a valid IPv4 or IPv6 address.
validate.ipv4functionThe value must be a valid IPv4 address.
validate.ipv6functionThe value must be a valid IPv6 address.
validate.is_infunctionThe value must be one of the given allowed values (strict comparison).
validate.is_nilfunctionThe value must be nil.
validate.jsonfunctionThe value must be a valid JSON string.
validate.lengthfunctionThe string’s character count must equal exactly n.
validate.length_betweenfunctionThe string’s length must be between min and max characters (inclusive).
validate.listfunctionThe value must be a list.
validate.lowercasefunctionThe string must be entirely lowercase.
validate.ltfunctionThe numeric value must be less than n.
validate.ltefunctionThe numeric value must be less than or equal to n.
validate.maxfunctionThe value’s size must be at most max.
validate.max_itemsfunctionThe list must have at most max items.
validate.max_lengthfunctionThe string must be at most max characters long.
validate.minfunctionThe value’s size must be at least min.
validate.min_itemsfunctionThe list must have at least min items.
validate.min_lengthfunctionThe string must be at least min characters long.
validate.multiple_offunctionThe numeric value must be a multiple of n.
validate.negativefunctionThe numeric value must be strictly negative (less than zero).
validate.negative_or_zerofunctionThe numeric value must be zero or negative.
validate.not_blankfunctionThe string must not consist entirely of whitespace.
validate.not_equalsfunctionThe value must not equal forbidden.
validate.not_infunctionThe value must not be one of the given forbidden values.
validate.not_nilfunctionThe value must not be nil.
validate.not_regexfunctionThe string must not match the given regular expression pattern.
validate.nullablefunctionA nil value passes all subsequent rules without evaluation.
validate.numberfunctionThe value must be a number (integer or float).
validate.numericfunctionThe value must be numeric: either a number type or a string that converts cleanly to a number.
validate.positivefunctionThe numeric value must be strictly positive (greater than zero).
validate.positive_or_zerofunctionThe numeric value must be zero or positive.
validate.prohibitedfunctionThis field must be absent or nil: it is never permitted.
validate.prohibited_iffunctionThe field must be absent when other_field equals any of other_values.
validate.prohibited_unlessfunctionThe field must be absent unless other_field equals any of other_values.
validate.prohibitsfunctionWhen this field is present, none of the listed sibling fields may also be present.
validate.regexfunctionThe string must match the given regular expression pattern.
validate.requiredfunctionThe field must be present, non-nil, and non-empty.
validate.required_iffunctionThe field becomes required when other_field equals any value in other_values.
validate.required_unlessfunctionThe field becomes required unless other_field equals any value in other_values.
validate.required_withfunctionThe field becomes required when any of the listed sibling fields are present and non-blank.
validate.required_with_allfunctionThe field becomes required when all of the listed sibling fields are present and non-blank.
validate.required_withoutfunctionThe field becomes required when any of the listed sibling fields are absent or blank.
validate.required_without_allfunctionThe field becomes required when all of the listed sibling fields are absent or blank.
validate.rule.instantiate_rulefunctionBuilds one Rule instance from a [rule_class, ...args] entry: the shape Validator._rules stores each…
validate.rules.AcceptedclassPasses when the value equals one of the specified accepted values: true, "true", "yes", "on", "1",…
validate.rules.AfterclassPasses when the value is a date string after the given reference date.
validate.rules.AfterOrEqualclassPasses when the value is a date string after or equal to the given reference date.
validate.rules.AlphaclassPasses when the string contains only ASCII alphabetic characters (a-z, A-Z).
validate.rules.AlphaDashclassPasses when the string contains only ASCII alphanumeric characters, hyphens (-), and underscores (_).
validate.rules.AlphaNumclassPasses when the string contains only ASCII alphanumeric characters.
validate.rules.BeforeclassPasses when the value is a date string before the given reference date.
validate.rules.BeforeOrEqualclassPasses when the value is a date string before or equal to the given reference date.
validate.rules.BetweenclassPasses when the value’s size is between min and max (inclusive).
validate.rules.BooleanRuleclassPasses only when the value is a boolean (true or false).
validate.rules.ConfirmedclassPasses when the field’s value matches ``name_confirmation in the data.
validate.rules.ContainsclassPasses when the string contains the given substring.
validate.rules.CustomRuleclassCustom rule backed by a caller-supplied function.
validate.rules.CustomRuleWithDataclassCustom rule that also receives the full data dictionary.
validate.rules.DateRuleclassPasses when the value is a valid date string parseable by the date module (e.g. "2024-06-15",…
validate.rules.DeclinedclassPasses when the value equals one of the specified declined values: false, "false", "no", "off",…
validate.rules.DictRuleclassPasses only when the value is a dictionary.
validate.rules.DifferentclassPasses when the field’s value does not match the value of another field.
validate.rules.DistinctclassPasses when the list field contains no duplicate values.
validate.rules.DoesntContainclassPasses when the string does not contain the given substring.
validate.rules.DoesntEndWithclassPasses when the string does not end with any of the given suffixes.
validate.rules.DoesntStartWithclassPasses when the string does not start with any of the given prefixes.
validate.rules.EachclassPasses when every item in the list satisfies the given Validator chain.
validate.rules.EmailclassPasses when the value is a syntactically valid email address.
validate.rules.EndsWithclassPasses when the string ends with one of the given suffixes.
validate.rules.EqualsclassPasses when the field value is strictly equal to the given expected value.
validate.rules.GtclassPasses when the numeric value is greater than n.
validate.rules.GteclassPasses when the numeric value is greater than or equal to n.
validate.rules.InclassPasses when the value is contained in the given list of allowed values.
validate.rules.InListclassPasses when every item in the value list is contained in the allowed list.
validate.rules.IntegerRuleclassPasses only when the value is an integer (no fractional part).
validate.rules.IpclassPasses when the value is a valid IPv4 or IPv6 address.
validate.rules.Ipv4classPasses when the value is a valid IPv4 address.
validate.rules.Ipv6classPasses when the value is a valid IPv6 address.
validate.rules.JsonclassPasses when the value is a valid JSON string.
validate.rules.LengthclassPasses when a string’s length is exactly n characters.
validate.rules.LengthBetweenclassPasses when a string’s length is between min and max characters (inclusive).
validate.rules.ListRuleclassPasses only when the value is a list.
validate.rules.LowercaseclassPasses when the string value contains only lowercase characters.
validate.rules.LtclassPasses when the numeric value is less than n.
validate.rules.LteclassPasses when the numeric value is less than or equal to n.
validate.rules.MaxclassPasses when the value’s size is at most max.
validate.rules.MaxItemsclassPasses when the list field has at most max items.
validate.rules.MaxLengthclassPasses when a string’s length is at most max characters.
validate.rules.MinclassPasses when the value’s size is at least min.
validate.rules.MinItemsclassPasses when the list field has at least min items.
validate.rules.MinLengthclassPasses when a string’s length is at least min characters.
validate.rules.MultipleOfclassPasses when the numeric value is a multiple of n.
validate.rules.NegativeclassPasses when the numeric value is negative (strictly less than zero).
validate.rules.NegativeOrZeroclassPasses when the numeric value is negative or zero.
validate.rules.NilclassPasses when the value is nil.
validate.rules.NotBlankclassPasses when the value does not consist solely of whitespace.
validate.rules.NotEqualsclassPasses when the field value is not equal to the given forbidden value.
validate.rules.NotInclassPasses when the value is not contained in the given list of values.
validate.rules.NotNilclassPasses when the value is not nil.
validate.rules.NotRegexclassPasses when the string value does not match the given regular expression.
validate.rules.NullableclassPasses when the field is absent or nil.
validate.rules.NumberRuleclassPasses only when the value is a number (integer or float).
validate.rules.NumericRuleclassPasses only when the value is numeric (a number, or a string that can be losslessly converted to a number).
validate.rules.PositiveclassPasses when the numeric value is positive (strictly greater than zero).
validate.rules.PositiveOrZeroclassPasses when the numeric value is positive or zero.
validate.rules.ProhibitedclassPasses when this field is absent or nil.
validate.rules.ProhibitedIfclassPasses when this field is absent or nil if another field equals one of the given values.
validate.rules.ProhibitedUnlessclassPasses when this field is absent or nil unless another field equals one of the given values.
validate.rules.ProhibitsclassPasses when both this field and the listed sibling fields are either all present (non-nil) or all absent (nil…
validate.rules.RegexclassPasses when the string value matches the given regular expression.
validate.rules.RequiredclassFails when the field is absent, nil, an empty string, or an empty list.
validate.rules.RequiredIfclassPasses when this field is present and non-nil only if another field in the data satisfies a given condition.
validate.rules.RequiredUnlessclassPasses when this field is present and non-nil unless another field equals any of the given values.
validate.rules.RequiredWithclassPasses when this field is present and non-nil if any of the listed sibling fields are also present and…
validate.rules.RequiredWithAllclassPasses when this field is present and non-nil if all of the listed sibling fields are present and non-nil.
validate.rules.RequiredWithoutclassPasses when this field is present and non-nil if any of the listed sibling fields are absent or nil.
validate.rules.RequiredWithoutAllclassPasses when this field is present and non-nil if all of the listed sibling fields are absent or nil.
validate.rules.SameclassPasses when the field’s value matches the value of another field in the same data dictionary.
validate.rules.SizeclassPasses when the value’s size equals n.
validate.rules.StartsWithclassPasses when the string starts with one of the given prefixes.
validate.rules.StringRuleclassPasses only when the value is a string.
validate.rules.TimezoneclassPasses when the value is a real IANA timezone identifier (e.g. "UTC", "Africa/Lagos",…
validate.rules.UppercaseclassPasses when the string value contains only uppercase characters.
validate.rules.UrlclassPasses when the value is a syntactically valid HTTP or HTTPS URL.
validate.rules.UuidclassPasses when the value is a valid canonical UUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).
validate.samefunctionThe value must match the value of other_field in the same data dictionary.
validate.schemafunctionCreates a new Schema from the given ruleset dictionary.
validate.sizefunctionThe value’s size must equal exactly n.
validate.sometimesfunctionSkips the entire rule chain when the field is absent from the data dictionary or its value is nil / blank.
validate.starts_withfunctionThe string must start with one of the given prefixes.
validate.stringfunctionThe value must be a string.
validate.timezonefunctionThe value must be a valid timezone identifier recognised by the date module (e.g. "UTC",…
validate.uppercasefunctionThe string must be entirely uppercase.
validate.urlfunctionThe value must be a valid HTTP or HTTPS URL.
validate.usefunctionAttaches a pre-defined Rule subclass (the class itself, not an instance) directly.
validate.uuidfunctionThe value must be a valid canonical UUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).
validate.valuefunctionReturns a bare Validator with no rules pre-applied.

Submodules

ModuleReached asSummary
validate.rulevalidate.rule.*Defines the base Rule class that all built-in and custom validation rules extend.
validate.rulesimport validate.rulesAll built-in validation rule implementations.
validate.schemavalidate.schema.*
validate.validatorvalidate.validator.*
validate.validatorsvalidate.*Top-level convenience functions that create a fresh Validator instance with one rule pre-applied.

Functions

schema()

validate.schema(ruleset) -> Schema

Creates a new Schema from the given ruleset dictionary.

This is the primary entry point for defining a validation schema. Each key is a field name (supports dot-notation and .* wildcards) and each value must be a Validator instance.

import validate

var schema = validate.schema({
  username: validate.required().string().alpha_dash().max_length(30),
  email:    validate.required().string().email(),
})

Parameters

  • ruleset (dict)

Returns Schema

Raises ArgumentError

Raises ValueError


2026, Richard Ore and The Zuri Contributors

validate.rule

import validate.rule

validate lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled validate.rule.* needs import validate.rule.

Defines the base Rule class that all built-in and custom validation rules extend. To create a custom rule, subclass Rule and implement validate(value) and error().

Custom rule example

import validate { Rule, Validator }

class Palindrome < Rule {
  validate(value) {
    if !is_string(value) return false
    var s = value.lower()
    return s == s.reverse()
  }

  error() {
    return '${self.name} must be a palindrome'
  }
}

# Use directly on a Validator:
var v = Validator().use(Palindrome)

Functions

instantiate_rule()

validate.rule.instantiate_rule(entry, field_name) -> Rule

Builds one Rule instance from a [rule_class, ...args] entry: the shape Validator._rules stores each chained rule as: with field_name prepended as the rule’s first constructor argument.

Shared by Schema (running a field’s whole chain) and Each (running a nested chain once per list item), which is why this lives here rather than as a private helper duplicated in both: schema.zu and rules.zu both already depend on this file, and neither depends on the other, so this is the one shared place both can reach without a circular import. Not re-exported from index.zu: this is internal machinery for the module itself, not part of its public API.

Parameters

  • entry (list) — [rule_class, ...constructor_args].
  • field_name (string)

Returns Rule

Raises Error When entry has more than 3 extra constructor arguments.

Classes

Rule

class validate.Rule

Abstract base class for all validation rules.

Subclass this to create custom rules. The name property is injected by the Schema before validation runs, so error messages can reference the field name without the rule needing to know it at construction time.

Only validate and error need to be overridden. Rules that require constructor parameters should define their own constructor and call parent(name): or accept parameters before name and forward it.

The _data property is injected by the Schema before each field is validated when needs_data returns true. Rules that must compare against sibling fields (e.g. same, different, confirmed) should override needs_data() to return true and read self._data inside validate.

Fields

FieldTypeDescription
nameThe name of the field being validated.

Constructor

validate.Rule(name)

Parameters

  • name (string) — Field name: injected automatically by Schema.

Rule.set_data()

validate.Rule.set_data(data)

Sets the full data dictionary this rule can read from validate(). Called by Schema right after construction, only for rules whose needs_data() returns true: a real public setter so Schema never needs private-field access into a Rule it doesn’t own.

Parameters

  • data (dict)

Rule.validate()

validate.Rule.validate(value) -> bool

Returns true when the value satisfies this rule, false otherwise.

Parameters

  • value (any) — The value of the field being validated.

Returns bool

Rule.error()

validate.Rule.error() -> string

Returns the human-readable error message for this rule when validation fails. The message should reference self.name for the field label.

Returns string

Rule.needs_data()

validate.Rule.needs_data() -> bool

Override and return true when this rule needs access to the full data dictionary (e.g. to compare against another field). When true, the Schema injects the data dictionary into self._data before calling validate.

Returns bool

Rule.bails_all()

validate.Rule.bails_all() -> bool

Override and return true when this rule should bail out of all validation chains whether it fails or not, instead of continuing to the next rule. When true, the Schema will stop validating the field but will not raise a ValidationError.

Returns bool

validate.rules

import validate.rules

validate does not re-export this module, so it is reached only by importing it directly.

All built-in validation rule implementations. Rules are plain classes that extend Rule. They are not intended to be constructed directly; the Validator fluent builder and Schema handle construction and injection automatically.

Classes

Required

class validate.rules.Required < Rule

Fails when the field is absent, nil, an empty string, or an empty list.

Required.validate()

validate.rules.Required.validate(value)

Required.error()

validate.rules.Required.error()

Nullable

class validate.rules.Nullable < Rule

Passes when the field is absent or nil. Use this to mark a field as optional while still attaching other rules that run only when a value is present. Pair with sometimes on the Validator.

Nullable.validate()

validate.rules.Nullable.validate(value)

Nullable.error()

validate.rules.Nullable.error()

StringRule

class validate.rules.StringRule < Rule

Passes only when the value is a string.

StringRule.validate()

validate.rules.StringRule.validate(value)

StringRule.error()

validate.rules.StringRule.error()

NumberRule

class validate.rules.NumberRule < Rule

Passes only when the value is a number (integer or float).

NumberRule.validate()

validate.rules.NumberRule.validate(value)

NumberRule.error()

validate.rules.NumberRule.error()

IntegerRule

class validate.rules.IntegerRule < Rule

Passes only when the value is an integer (no fractional part).

IntegerRule.validate()

validate.rules.IntegerRule.validate(value)

IntegerRule.error()

validate.rules.IntegerRule.error()

BooleanRule

class validate.rules.BooleanRule < Rule

Passes only when the value is a boolean (true or false). Also accepts the strings "true", "false", "1", "0", and the integers 1 and 0.

BooleanRule.validate()

validate.rules.BooleanRule.validate(value)

BooleanRule.error()

validate.rules.BooleanRule.error()

ListRule

class validate.rules.ListRule < Rule

Passes only when the value is a list.

ListRule.validate()

validate.rules.ListRule.validate(value)

ListRule.error()

validate.rules.ListRule.error()

DictRule

class validate.rules.DictRule < Rule

Passes only when the value is a dictionary.

DictRule.validate()

validate.rules.DictRule.validate(value)

DictRule.error()

validate.rules.DictRule.error()

NumericRule

class validate.rules.NumericRule < Rule

Passes only when the value is numeric (a number, or a string that can be losslessly converted to a number).

NumericRule.validate()

validate.rules.NumericRule.validate(value)

NumericRule.error()

validate.rules.NumericRule.error()

Size

class validate.rules.Size < Rule

Passes when the value’s size equals n.

Size is measured as: character count for strings, item count for lists and dicts, and numeric value for numbers.

Constructor

validate.rules.Size(name, n)

Size.validate()

validate.rules.Size.validate(value)

Size.error()

validate.rules.Size.error()

Min

class validate.rules.Min < Rule

Passes when the value’s size is at least min.

Constructor

validate.rules.Min(name, min)

Min.validate()

validate.rules.Min.validate(value)

Min.error()

validate.rules.Min.error()

Max

class validate.rules.Max < Rule

Passes when the value’s size is at most max.

Constructor

validate.rules.Max(name, max)

Max.validate()

validate.rules.Max.validate(value)

Max.error()

validate.rules.Max.error()

Between

class validate.rules.Between < Rule

Passes when the value’s size is between min and max (inclusive).

Constructor

validate.rules.Between(name, min, max)

Between.validate()

validate.rules.Between.validate(value)

Between.error()

validate.rules.Between.error()

Length

class validate.rules.Length < Rule

Passes when a string’s length is exactly n characters.

Constructor

validate.rules.Length(name, n)

Length.validate()

validate.rules.Length.validate(value)

Length.error()

validate.rules.Length.error()

MinLength

class validate.rules.MinLength < Rule

Passes when a string’s length is at least min characters.

Constructor

validate.rules.MinLength(name, min)

MinLength.validate()

validate.rules.MinLength.validate(value)

MinLength.error()

validate.rules.MinLength.error()

MaxLength

class validate.rules.MaxLength < Rule

Passes when a string’s length is at most max characters.

Constructor

validate.rules.MaxLength(name, max)

MaxLength.validate()

validate.rules.MaxLength.validate(value)

MaxLength.error()

validate.rules.MaxLength.error()

LengthBetween

class validate.rules.LengthBetween < Rule

Passes when a string’s length is between min and max characters (inclusive).

Constructor

validate.rules.LengthBetween(name, min, max)

LengthBetween.validate()

validate.rules.LengthBetween.validate(value)

LengthBetween.error()

validate.rules.LengthBetween.error()

Gt

class validate.rules.Gt < Rule

Passes when the numeric value is greater than n.

Constructor

validate.rules.Gt(name, n)

Gt.validate()

validate.rules.Gt.validate(value)

Gt.error()

validate.rules.Gt.error()

Gte

class validate.rules.Gte < Rule

Passes when the numeric value is greater than or equal to n.

Constructor

validate.rules.Gte(name, n)

Gte.validate()

validate.rules.Gte.validate(value)

Gte.error()

validate.rules.Gte.error()

Lt

class validate.rules.Lt < Rule

Passes when the numeric value is less than n.

Constructor

validate.rules.Lt(name, n)

Lt.validate()

validate.rules.Lt.validate(value)

Lt.error()

validate.rules.Lt.error()

Lte

class validate.rules.Lte < Rule

Passes when the numeric value is less than or equal to n.

Constructor

validate.rules.Lte(name, n)

Lte.validate()

validate.rules.Lte.validate(value)

Lte.error()

validate.rules.Lte.error()

Positive

class validate.rules.Positive < Rule

Passes when the numeric value is positive (strictly greater than zero).

Positive.validate()

validate.rules.Positive.validate(value)

Positive.error()

validate.rules.Positive.error()

Negative

class validate.rules.Negative < Rule

Passes when the numeric value is negative (strictly less than zero).

Negative.validate()

validate.rules.Negative.validate(value)

Negative.error()

validate.rules.Negative.error()

PositiveOrZero

class validate.rules.PositiveOrZero < Rule

Passes when the numeric value is positive or zero.

PositiveOrZero.validate()

validate.rules.PositiveOrZero.validate(value)

PositiveOrZero.error()

validate.rules.PositiveOrZero.error()

NegativeOrZero

class validate.rules.NegativeOrZero < Rule

Passes when the numeric value is negative or zero.

NegativeOrZero.validate()

validate.rules.NegativeOrZero.validate(value)

NegativeOrZero.error()

validate.rules.NegativeOrZero.error()

MultipleOf

class validate.rules.MultipleOf < Rule

Passes when the numeric value is a multiple of n.

Constructor

validate.rules.MultipleOf(name, n)

MultipleOf.validate()

validate.rules.MultipleOf.validate(value)

MultipleOf.error()

validate.rules.MultipleOf.error()

Alpha

class validate.rules.Alpha < Rule

Passes when the string contains only ASCII alphabetic characters (a-z, A-Z).

Alpha.validate()

validate.rules.Alpha.validate(value)

Alpha.error()

validate.rules.Alpha.error()

AlphaNum

class validate.rules.AlphaNum < Rule

Passes when the string contains only ASCII alphanumeric characters.

AlphaNum.validate()

validate.rules.AlphaNum.validate(value)

AlphaNum.error()

validate.rules.AlphaNum.error()

AlphaDash

class validate.rules.AlphaDash < Rule

Passes when the string contains only ASCII alphanumeric characters, hyphens (-), and underscores (_).

AlphaDash.validate()

validate.rules.AlphaDash.validate(value)

AlphaDash.error()

validate.rules.AlphaDash.error()

StartsWith

class validate.rules.StartsWith < Rule

Passes when the string starts with one of the given prefixes.

Constructor

validate.rules.StartsWith(name, prefixes)

StartsWith.validate()

validate.rules.StartsWith.validate(value)

StartsWith.error()

validate.rules.StartsWith.error()

DoesntStartWith

class validate.rules.DoesntStartWith < Rule

Passes when the string does not start with any of the given prefixes.

Constructor

validate.rules.DoesntStartWith(name, prefixes)

DoesntStartWith.validate()

validate.rules.DoesntStartWith.validate(value)

DoesntStartWith.error()

validate.rules.DoesntStartWith.error()

EndsWith

class validate.rules.EndsWith < Rule

Passes when the string ends with one of the given suffixes.

Constructor

validate.rules.EndsWith(name, suffixes)

EndsWith.validate()

validate.rules.EndsWith.validate(value)

EndsWith.error()

validate.rules.EndsWith.error()

DoesntEndWith

class validate.rules.DoesntEndWith < Rule

Passes when the string does not end with any of the given suffixes.

Constructor

validate.rules.DoesntEndWith(name, suffixes)

DoesntEndWith.validate()

validate.rules.DoesntEndWith.validate(value)

DoesntEndWith.error()

validate.rules.DoesntEndWith.error()

Contains

class validate.rules.Contains < Rule

Passes when the string contains the given substring.

Constructor

validate.rules.Contains(name, needle)

Contains.validate()

validate.rules.Contains.validate(value)

Contains.error()

validate.rules.Contains.error()

DoesntContain

class validate.rules.DoesntContain < Rule

Passes when the string does not contain the given substring.

Constructor

validate.rules.DoesntContain(name, needle)

DoesntContain.validate()

validate.rules.DoesntContain.validate(value)

DoesntContain.error()

validate.rules.DoesntContain.error()

Regex

class validate.rules.Regex < Rule

Passes when the string value matches the given regular expression.

Constructor

validate.rules.Regex(name, pattern)

Regex.validate()

validate.rules.Regex.validate(value)

Regex.error()

validate.rules.Regex.error()

NotRegex

class validate.rules.NotRegex < Rule

Passes when the string value does not match the given regular expression.

Constructor

validate.rules.NotRegex(name, pattern)

NotRegex.validate()

validate.rules.NotRegex.validate(value)

NotRegex.error()

validate.rules.NotRegex.error()

Lowercase

class validate.rules.Lowercase < Rule

Passes when the string value contains only lowercase characters.

Lowercase.validate()

validate.rules.Lowercase.validate(value)

Lowercase.error()

validate.rules.Lowercase.error()

Uppercase

class validate.rules.Uppercase < Rule

Passes when the string value contains only uppercase characters.

Uppercase.validate()

validate.rules.Uppercase.validate(value)

Uppercase.error()

validate.rules.Uppercase.error()

NotBlank

class validate.rules.NotBlank < Rule

Passes when the value does not consist solely of whitespace. Use on string fields to prevent inputs like " ".

NotBlank.validate()

validate.rules.NotBlank.validate(value)

NotBlank.error()

validate.rules.NotBlank.error()

Email

class validate.rules.Email < Rule

Passes when the value is a syntactically valid email address.

Validates the structure local@domain.tld per a practical subset of RFC 5321. Does not perform DNS MX lookup.

Email.validate()

validate.rules.Email.validate(value)

Email.error()

validate.rules.Email.error()

Url

class validate.rules.Url < Rule

Passes when the value is a syntactically valid HTTP or HTTPS URL.

Url.validate()

validate.rules.Url.validate(value)

Url.error()

validate.rules.Url.error()

Ipv4

class validate.rules.Ipv4 < Rule

Passes when the value is a valid IPv4 address.

Ipv4.validate()

validate.rules.Ipv4.validate(value)

Ipv4.error()

validate.rules.Ipv4.error()

Ipv6

class validate.rules.Ipv6 < Rule

Passes when the value is a valid IPv6 address.

Ipv6.validate()

validate.rules.Ipv6.validate(value)

Ipv6.error()

validate.rules.Ipv6.error()

Ip

class validate.rules.Ip < Rule

Passes when the value is a valid IPv4 or IPv6 address.

Ip.validate()

validate.rules.Ip.validate(value)

Ip.error()

validate.rules.Ip.error()

Uuid

class validate.rules.Uuid < Rule

Passes when the value is a valid canonical UUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).

Uuid.validate()

validate.rules.Uuid.validate(value)

Uuid.error()

validate.rules.Uuid.error()

Json

class validate.rules.Json < Rule

Passes when the value is a valid JSON string.

Json.validate()

validate.rules.Json.validate(value)

Json.error()

validate.rules.Json.error()

DateRule

class validate.rules.DateRule < Rule

Passes when the value is a valid date string parseable by the date module (e.g. "2024-06-15", "2024-06-15T12:00:00Z").

DateRule.validate()

validate.rules.DateRule.validate(value)

DateRule.error()

validate.rules.DateRule.error()

After

class validate.rules.After < Rule

Passes when the value is a date string after the given reference date.

Both the value and the reference must be parseable by the date module.

Constructor

validate.rules.After(name, ref)

After.validate()

validate.rules.After.validate(value)

After.error()

validate.rules.After.error()

AfterOrEqual

class validate.rules.AfterOrEqual < Rule

Passes when the value is a date string after or equal to the given reference date.

Constructor

validate.rules.AfterOrEqual(name, ref)

AfterOrEqual.validate()

validate.rules.AfterOrEqual.validate(value)

AfterOrEqual.error()

validate.rules.AfterOrEqual.error()

Before

class validate.rules.Before < Rule

Passes when the value is a date string before the given reference date.

Constructor

validate.rules.Before(name, ref)

Before.validate()

validate.rules.Before.validate(value)

Before.error()

validate.rules.Before.error()

BeforeOrEqual

class validate.rules.BeforeOrEqual < Rule

Passes when the value is a date string before or equal to the given reference date.

Constructor

validate.rules.BeforeOrEqual(name, ref)

BeforeOrEqual.validate()

validate.rules.BeforeOrEqual.validate(value)

BeforeOrEqual.error()

validate.rules.BeforeOrEqual.error()

Timezone

class validate.rules.Timezone < Rule

Passes when the value is a real IANA timezone identifier (e.g. "UTC", "Africa/Lagos", "America/New_York"), checked against the full timezone database date.is_valid_timezone() uses.

Timezone.validate()

validate.rules.Timezone.validate(value)

Timezone.error()

validate.rules.Timezone.error()

In

class validate.rules.In < Rule

Passes when the value is contained in the given list of allowed values. Comparison is strict (type-sensitive).

Constructor

validate.rules.In(name, values)

In.validate()

validate.rules.In.validate(value)

In.error()

validate.rules.In.error()

NotIn

class validate.rules.NotIn < Rule

Passes when the value is not contained in the given list of values.

Constructor

validate.rules.NotIn(name, values)

NotIn.validate()

validate.rules.NotIn.validate(value)

NotIn.error()

validate.rules.NotIn.error()

InList

class validate.rules.InList < Rule

Passes when every item in the value list is contained in the allowed list. The field value must be a list.

Constructor

validate.rules.InList(name, values)

InList.validate()

validate.rules.InList.validate(value)

InList.error()

validate.rules.InList.error()

Accepted

class validate.rules.Accepted < Rule

Passes when the value equals one of the specified accepted values: true, "true", "yes", "on", "1", or 1.

Accepted.validate()

validate.rules.Accepted.validate(value)

Accepted.error()

validate.rules.Accepted.error()

Declined

class validate.rules.Declined < Rule

Passes when the value equals one of the specified declined values: false, "false", "no", "off", "0", or 0.

Declined.validate()

validate.rules.Declined.validate(value)

Declined.error()

validate.rules.Declined.error()

Equals

class validate.rules.Equals < Rule

Passes when the field value is strictly equal to the given expected value.

Constructor

validate.rules.Equals(name, expected)

Equals.validate()

validate.rules.Equals.validate(value)

Equals.error()

validate.rules.Equals.error()

NotEquals

class validate.rules.NotEquals < Rule

Passes when the field value is not equal to the given forbidden value.

Constructor

validate.rules.NotEquals(name, forbidden)

NotEquals.validate()

validate.rules.NotEquals.validate(value)

NotEquals.error()

validate.rules.NotEquals.error()

Same

class validate.rules.Same < Rule

Passes when the field’s value matches the value of another field in the same data dictionary. Commonly used for password confirmation.

Requires needs_data() == true so that the Schema injects _data.

Example
import validate

var schema = validate.schema({
  password:              validate.string().min_length(8),
  password_confirmation: validate.string().same('password'),
})

Constructor

validate.rules.Same(name, other_field)

Same.needs_data()

validate.rules.Same.needs_data()

Same.validate()

validate.rules.Same.validate(value)

Same.error()

validate.rules.Same.error()

Different

class validate.rules.Different < Rule

Passes when the field’s value does not match the value of another field.

Constructor

validate.rules.Different(name, other_field)

Different.needs_data()

validate.rules.Different.needs_data()

Different.validate()

validate.rules.Different.validate(value)

Different.error()

validate.rules.Different.error()

Confirmed

class validate.rules.Confirmed < Rule

Passes when the field’s value matches {name}_confirmation in the data.

This is the canonical Zuri spelling of Laravel’s confirmed rule.

Confirmed.needs_data()

validate.rules.Confirmed.needs_data()

Confirmed.validate()

validate.rules.Confirmed.validate(value)

Confirmed.error()

validate.rules.Confirmed.error()

RequiredIf

class validate.rules.RequiredIf < Rule

Passes when this field is present and non-nil only if another field in the data satisfies a given condition. If the other field equals any of the given values, this field becomes required.

Example
var rules = validate.schema({
  # "shipping_address" is required when "has_delivery" is true
  shipping_address: validate.string().required_if('has_delivery', true),
})

Constructor

validate.rules.RequiredIf(name, other_field, other_values)

RequiredIf.needs_data()

validate.rules.RequiredIf.needs_data()

RequiredIf.validate()

validate.rules.RequiredIf.validate(value)

RequiredIf.error()

validate.rules.RequiredIf.error()

RequiredUnless

class validate.rules.RequiredUnless < Rule

Passes when this field is present and non-nil unless another field equals any of the given values.

Constructor

validate.rules.RequiredUnless(name, other_field, other_values)

RequiredUnless.needs_data()

validate.rules.RequiredUnless.needs_data()

RequiredUnless.validate()

validate.rules.RequiredUnless.validate(value)

RequiredUnless.error()

validate.rules.RequiredUnless.error()

RequiredWith

class validate.rules.RequiredWith < Rule

Passes when this field is present and non-nil if any of the listed sibling fields are also present and non-nil.

Constructor

validate.rules.RequiredWith(name, fields)

RequiredWith.needs_data()

validate.rules.RequiredWith.needs_data()

RequiredWith.validate()

validate.rules.RequiredWith.validate(value)

RequiredWith.error()

validate.rules.RequiredWith.error()

RequiredWithAll

class validate.rules.RequiredWithAll < Rule

Passes when this field is present and non-nil if all of the listed sibling fields are present and non-nil.

Constructor

validate.rules.RequiredWithAll(name, fields)

RequiredWithAll.needs_data()

validate.rules.RequiredWithAll.needs_data()

RequiredWithAll.validate()

validate.rules.RequiredWithAll.validate(value)

RequiredWithAll.error()

validate.rules.RequiredWithAll.error()

RequiredWithout

class validate.rules.RequiredWithout < Rule

Passes when this field is present and non-nil if any of the listed sibling fields are absent or nil.

Constructor

validate.rules.RequiredWithout(name, fields)

RequiredWithout.needs_data()

validate.rules.RequiredWithout.needs_data()

RequiredWithout.validate()

validate.rules.RequiredWithout.validate(value)

RequiredWithout.error()

validate.rules.RequiredWithout.error()

RequiredWithoutAll

class validate.rules.RequiredWithoutAll < Rule

Passes when this field is present and non-nil if all of the listed sibling fields are absent or nil.

Constructor

validate.rules.RequiredWithoutAll(name, fields)

RequiredWithoutAll.needs_data()

validate.rules.RequiredWithoutAll.needs_data()

RequiredWithoutAll.validate()

validate.rules.RequiredWithoutAll.validate(value)

RequiredWithoutAll.error()

validate.rules.RequiredWithoutAll.error()

Prohibits

class validate.rules.Prohibits < Rule

Passes when both this field and the listed sibling fields are either all present (non-nil) or all absent (nil / blank).

Constructor

validate.rules.Prohibits(name, fields)

Prohibits.needs_data()

validate.rules.Prohibits.needs_data()

Prohibits.validate()

validate.rules.Prohibits.validate(value)

Prohibits.error()

validate.rules.Prohibits.error()

Prohibited

class validate.rules.Prohibited < Rule

Passes when this field is absent or nil. Useful to explicitly forbid a field from being submitted.

Prohibited.validate()

validate.rules.Prohibited.validate(value)

Prohibited.error()

validate.rules.Prohibited.error()

ProhibitedIf

class validate.rules.ProhibitedIf < Rule

Passes when this field is absent or nil if another field equals one of the given values.

Constructor

validate.rules.ProhibitedIf(name, other_field, other_values)

ProhibitedIf.needs_data()

validate.rules.ProhibitedIf.needs_data()

ProhibitedIf.validate()

validate.rules.ProhibitedIf.validate(value)

ProhibitedIf.error()

validate.rules.ProhibitedIf.error()

ProhibitedUnless

class validate.rules.ProhibitedUnless < Rule

Passes when this field is absent or nil unless another field equals one of the given values.

Constructor

validate.rules.ProhibitedUnless(name, other_field, other_values)

ProhibitedUnless.needs_data()

validate.rules.ProhibitedUnless.needs_data()

ProhibitedUnless.validate()

validate.rules.ProhibitedUnless.validate(value)

ProhibitedUnless.error()

validate.rules.ProhibitedUnless.error()

Distinct

class validate.rules.Distinct < Rule

Passes when the list field contains no duplicate values.

Distinct.validate()

validate.rules.Distinct.validate(value)

Distinct.error()

validate.rules.Distinct.error()

MinItems

class validate.rules.MinItems < Rule

Passes when the list field has at least min items.

Constructor

validate.rules.MinItems(name, min)

MinItems.validate()

validate.rules.MinItems.validate(value)

MinItems.error()

validate.rules.MinItems.error()

MaxItems

class validate.rules.MaxItems < Rule

Passes when the list field has at most max items.

Constructor

validate.rules.MaxItems(name, max)

MaxItems.validate()

validate.rules.MaxItems.validate(value)

MaxItems.error()

validate.rules.MaxItems.error()

Each

class validate.rules.Each < Rule

Passes when every item in the list satisfies the given Validator chain.

Example
import validate

var schema = validate.schema({
  tags: validate.list().each(validate.string().max_length(32)),
})

Constructor

validate.rules.Each(name, validator)

Each.validate()

validate.rules.Each.validate(value)

Each.error()

validate.rules.Each.error()

Nil

class validate.rules.Nil < Rule

Passes when the value is nil. Useful for asserting a field is absent.

Nil.validate()

validate.rules.Nil.validate(value)

Nil.error()

validate.rules.Nil.error()

NotNil

class validate.rules.NotNil < Rule

Passes when the value is not nil.

NotNil.validate()

validate.rules.NotNil.validate(value)

NotNil.error()

validate.rules.NotNil.error()

CustomRule

class validate.rules.CustomRule < Rule

Custom rule backed by a caller-supplied function.

Used internally by Validator.custom(). The function receives the field value and must return true to pass.

Constructor

validate.rules.CustomRule(name, fn, message)

CustomRule.validate()

validate.rules.CustomRule.validate(value)

CustomRule.error()

validate.rules.CustomRule.error()

CustomRuleWithData

class validate.rules.CustomRuleWithData < Rule

Custom rule that also receives the full data dictionary.

Used internally by Validator.custom_with_data(). The function receives (value, data) and must return true to pass.

Constructor

validate.rules.CustomRuleWithData(name, fn, message)

CustomRuleWithData.needs_data()

validate.rules.CustomRuleWithData.needs_data()

CustomRuleWithData.validate()

validate.rules.CustomRuleWithData.validate(value)

CustomRuleWithData.error()

validate.rules.CustomRuleWithData.error()

validate.schema

import validate.schema

validate lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled validate.schema.* needs import validate.schema.

validate.schema

Provides the Schema class which binds a ruleset dictionary to data and produces a structured validation result.

Features

  • Dot-notation keys: "address.city" resolves nested dictionaries.
  • Wildcard keys: "tags.*" validates every item in a list against a Validator chain.
  • Bail-on-first-error per field (default) or collect-all-errors mode.
  • Sometimes: skip rules when a field is absent.
  • Nullable: treat nil as passing for all subsequent rules.
  • Label override: substitute a human-readable name in error messages.
  • Required-field checking: fields with required() that are entirely absent from the data dict are caught and reported.
  • check(data): returns { valid: bool, errors: list }.
  • check_or_raise(data): raises ValidationError on failure.

Classes

ValidationError

class validate.ValidationError < Error

Raised by Schema.check_or_raise() when validation fails.

The errors property holds the same list of { field, message } dicts that Schema.check() returns under the errors key.

catch {
  schema.check_or_raise(req.body)
} as e

if e {
  echo e.message   # "Validation failed"
  echo e.errors    # [{ field: 'email', message: '...' }, ...]
}

Fields

FieldTypeDescription
errorsList of { field: string, message: string } dictionaries, one per failed rule.

Constructor

validate.ValidationError(errors)

Parameters

  • errors (list) — Validation error list from Schema.check().

Schema

class validate.Schema

Binds a ruleset to data and validates it.

Basic usage
import validate

var schema = validate.schema({
  name:  validate.required().string().max_length(100),
  email: validate.required().string().email(),
  age:   validate.required().integer().gte(18),
})

var result = schema.check({
  name:  'Ada Lovelace',
  email: 'ada@example.com',
  age:   36,
})

echo result.valid   # true
echo result.errors  # []
Nested field validation (dot-notation)
var schema = validate.schema({
  'address.city':    validate.required().string(),
  'address.country': validate.required().string().length(2).uppercase(),
})

schema.check({
  address: { city: 'Lagos', country: 'NG' }
})
Wildcard list validation
var schema = validate.schema({
  'tags.*': validate.required().string().max_length(32),
})

schema.check({ tags: ['zuri', 'backend', 'fast'] })
Raising on failure
catch {
  schema.check_or_raise(data)
} as e

if e {
  # e.errors is a list of { field, message } dicts
  for err in e.errors {
    echo '${err.field}: ${err.message}'
  }
}
schema.check_or_raise(data)
Grouped errors (errors keyed by field)
var result = schema.check(data)
var grouped = schema.group_errors(result.errors)
# { email: ['must be valid email'], age: ['must be >= 18'] }

Constructor

validate.Schema(ruleset)

Parameters

  • ruleset (dict) — Map of field keys to Validator instances.

Raises ArgumentError When ruleset is not a dictionary.

Raises ValueError When a key is not a string or a value is not a Validator.

Schema.check()

validate.Schema.check(data) -> dict

Validates data against the schema and returns a result dictionary.

The result always has the shape:

{
  valid:  bool,
  errors: list<{ field: string, message: string }>
}

Fields present in the data but absent from the schema are silently ignored. Fields present in the schema but absent from the data are validated against their rules (the required rule catches absent fields; all other rules skip nil values unless chained after required).

Parameters

  • data (dict)

Returns dict

Raises ArgumentError

Schema.check_or_raise()

validate.Schema.check_or_raise(data) -> dict

Validates data and raises a ValidationError if validation fails. Returns the result dictionary on success.

def create_user(req, res) {
  catch {
    schema.check_or_raise(req.body)
  } as e

  if e {
    return res.json({ errors: e.errors }, 422)
  }

  return res.json({ ok: true })
}

Parameters

  • data (dict)

Returns dict

Raises ValidationError

Raises ArgumentError

Schema.group_errors()

validate.Schema.group_errors(errors) -> dict

Converts a flat errors list (as returned by check) into a dictionary keyed by field name, where each value is a list of error message strings.

var result  = schema.check(data)
var grouped = schema.group_errors(result.errors)
# { 'email': ['must be a valid email address'], 'age': ['must be >= 18'] }

Parameters

  • errors (list) — The errors list from a check() result.

Returns dict

Schema.first_error()

validate.Schema.first_error(errors, field_key) -> string|nil

Returns the first error message for the given field, or nil when that field has no errors in the provided errors list.

var result = schema.check(data)
var msg    = schema.first_error(result.errors, 'email')

Parameters

  • errors (list) — The errors list from a check() result.
  • field_key (string) — The field to look up.

Returns string|nil

Schema.field_errors()

validate.Schema.field_errors(errors, field_key) -> list<string>

Returns all error messages for the given field as a list, or an empty list when that field has no errors.

Parameters

  • errors (list)
  • field_key (string)

Returns list<string>

Schema.has_error()

validate.Schema.has_error(errors, field_key) -> bool

Returns true when the given field has at least one error in the provided errors list.

Parameters

  • errors (list)
  • field_key (string)

Returns bool

Schema.ruleset()

validate.Schema.ruleset() -> dict

Returns a copy of this schema’s own ruleset dictionary (field key → Validator). A copy, not a live reference, so mutating the result can never affect this Schema itself.

Returns dict

Schema.extend()

validate.Schema.extend(other) -> Schema

Extends this schema with additional rules from another schema or a plain ruleset dictionary, returning a new Schema instance. Rules in other override rules for the same field key.

var base   = validate.schema({ name: validate.required().string() })
var extended = base.extend({
  email: validate.required().string().email(),
})

Parameters

  • other (dict|Schema)

Returns Schema

Schema.only()

validate.Schema.only(keys) -> Schema

Returns a new Schema containing only the rules for the given field keys.

var partial = full_schema.only(['name', 'email'])

Parameters

  • keys (list)

Returns Schema

Schema.except()

validate.Schema.except(keys) -> Schema

Returns a new Schema with the rules for the given field keys removed.

var without_admin = schema.except(['role', 'is_superuser'])

Parameters

  • keys (list)

Returns Schema

validate.validator

import validate.validator

validate lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled validate.validator.* needs import validate.validator.

validate.validator

Provides the Validator fluent builder. Each method appends one rule class (and its constructor arguments) to an internal chain. The chain is later resolved by Schema: it instantiates each rule with the field name injected, then calls validate(value) in order.

Rules stop executing for a field the moment one fails, unless bail(false) has been explicitly disabled. When sometimes() is set and the field is absent or nil, the entire rule chain is skipped.

Classes

Validator

class validate.Validator

Fluent rule chain builder for a single field.

Every method returns self so calls can be chained:

validate.string().email().max_length(254).required()

The order of chained rules is the order in which they are evaluated. Put required() first so that a missing value is caught before type-specific rules run against a nil.

Custom rules

Provide an inline anonymous function via custom():

validate.string().custom(@(value) {
  return value.starts_with('zuri-')
}, 'Must start with "zuri-".')

Or subclass Rule and add it via use():

import validate { Rule, Validator }

class Palindrome < Rule {
  validate(value) {
    if !is_string(value) return false
    var s = value.lower()
    return s == s.reverse()
  }
  error() { return '${self.name} must be a palindrome.' }
}

var v = Validator().string().use(Palindrome)

Validator.label()

validate.Validator.label(label) -> Validator

Sets a human-readable label for this field. When set, error messages substitute label for the raw field key.

validate.string().label('Email address').email()
# → "The Email address field must be a valid email address."

Parameters

  • label (string)

Returns Validator

Validator.sometimes()

validate.Validator.sometimes() -> Validator

Skips the entire rule chain when the field is absent from the data dictionary or its value is nil / blank. Use this to mark fields as optional while still validating them when they are present.

var rules = validate.schema({
  # "bio" is optional; when given it must be at most 500 chars.
  bio: validate.string().max_length(500).sometimes(),
})

Returns Validator

Validator.bail()

validate.Validator.bail(enabled) -> Validator

Controls whether validation stops at the first failing rule for this field. Defaults to true. Set to false to collect all errors.

Parameters

  • enabled (bool) — Pass false to disable early stopping.

Returns Validator

Validator.required()

validate.Validator.required() -> Validator

The field must be present, non-nil, and non-empty.

Returns Validator

Validator.nullable()

validate.Validator.nullable() -> Validator

Marks the field as nullable. A nil value will pass all subsequent rules without evaluation. Combine with sometimes() for a fully optional-nullable field.

Returns Validator

Validator.string()

validate.Validator.string() -> Validator

The field value must be a string.

Returns Validator

Validator.number()

validate.Validator.number() -> Validator

The field value must be a number (integer or float).

Returns Validator

Validator.integer()

validate.Validator.integer() -> Validator

The field value must be an integer (no fractional part). Also accepts string representations of integers.

Returns Validator

Validator.boolean()

validate.Validator.boolean() -> Validator

The field value must be boolean or a boolean-like string/number. Accepts: true, false, "true", "false", "1", "0", 1, 0.

Returns Validator

Validator.list()

validate.Validator.list() -> Validator

The field value must be a list.

Returns Validator

Validator.dict()

validate.Validator.dict() -> Validator

The field value must be a dictionary.

Returns Validator

Validator.numeric()

validate.Validator.numeric() -> Validator

The field value must be numeric: either a number type, or a string that converts cleanly to a number.

Returns Validator

Validator.size()

validate.Validator.size(n) -> Validator

The value’s size must equal n.

Size is: character count for strings, item count for lists/dicts, and numeric value for numbers.

Parameters

  • n (number)

Returns Validator

Validator.min()

validate.Validator.min(min) -> Validator

The value’s size must be at least min.

Parameters

  • min (number)

Returns Validator

Validator.max()

validate.Validator.max(max) -> Validator

The value’s size must be at most max.

Parameters

  • max (number)

Returns Validator

Validator.between()

validate.Validator.between(min, max) -> Validator

The value’s size must be between min and max (inclusive).

Parameters

  • min (number)
  • max (number)

Returns Validator

Validator.length()

validate.Validator.length(n) -> Validator

The string’s character count must equal exactly n.

Parameters

  • n (number)

Returns Validator

Validator.min_length()

validate.Validator.min_length(min) -> Validator

The string must be at least min characters long.

Parameters

  • min (number)

Returns Validator

Validator.max_length()

validate.Validator.max_length(max) -> Validator

The string must be at most max characters long.

Parameters

  • max (number)

Returns Validator

Validator.length_between()

validate.Validator.length_between(min, max) -> Validator

The string’s length must be between min and max characters (inclusive).

Parameters

  • min (number)
  • max (number)

Returns Validator

Validator.gt()

validate.Validator.gt(n) -> Validator

The numeric value must be greater than n.

Parameters

  • n (number)

Returns Validator

Validator.gte()

validate.Validator.gte(n) -> Validator

The numeric value must be greater than or equal to n.

Parameters

  • n (number)

Returns Validator

Validator.lt()

validate.Validator.lt(n) -> Validator

The numeric value must be less than n.

Parameters

  • n (number)

Returns Validator

Validator.lte()

validate.Validator.lte(n) -> Validator

The numeric value must be less than or equal to n.

Parameters

  • n (number)

Returns Validator

Validator.positive()

validate.Validator.positive() -> Validator

The numeric value must be strictly positive (greater than zero).

Returns Validator

Validator.negative()

validate.Validator.negative() -> Validator

The numeric value must be strictly negative (less than zero).

Returns Validator

Validator.positive_or_zero()

validate.Validator.positive_or_zero() -> Validator

The numeric value must be zero or positive.

Returns Validator

Validator.negative_or_zero()

validate.Validator.negative_or_zero() -> Validator

The numeric value must be zero or negative.

Returns Validator

Validator.multiple_of()

validate.Validator.multiple_of(n) -> Validator

The numeric value must be a multiple of n.

Parameters

  • n (number)

Returns Validator

Validator.alpha()

validate.Validator.alpha() -> Validator

The string must contain only ASCII alphabetic characters (a-z, A-Z).

Returns Validator

Validator.alpha_num()

validate.Validator.alpha_num() -> Validator

The string must contain only ASCII alphanumeric characters.

Returns Validator

Validator.alpha_dash()

validate.Validator.alpha_dash() -> Validator

The string must contain only ASCII alphanumeric characters, hyphens, and underscores.

Returns Validator

Validator.starts_with()

validate.Validator.starts_with(prefixes) -> Validator

The string must start with one of the given prefixes.

Parameters

  • prefixes (string|list)

Returns Validator

Validator.doesnt_start_with()

validate.Validator.doesnt_start_with(prefixes) -> Validator

The string must not start with any of the given prefixes.

Parameters

  • prefixes (string|list)

Returns Validator

Validator.ends_with()

validate.Validator.ends_with(suffixes) -> Validator

The string must end with one of the given suffixes.

Parameters

  • suffixes (string|list)

Returns Validator

Validator.doesnt_end_with()

validate.Validator.doesnt_end_with(suffixes) -> Validator

The string must not end with any of the given suffixes.

Parameters

  • suffixes (string|list)

Returns Validator

Validator.contains()

validate.Validator.contains(needle) -> Validator

The string must contain the given substring.

Parameters

  • needle (string)

Returns Validator

Validator.doesnt_contain()

validate.Validator.doesnt_contain(needle) -> Validator

The string must not contain the given substring.

Parameters

  • needle (string)

Returns Validator

Validator.regex()

validate.Validator.regex(pattern) -> Validator

The string must match the given regular expression pattern.

Parameters

  • pattern (string)

Returns Validator

Validator.not_regex()

validate.Validator.not_regex(pattern) -> Validator

The string must not match the given regular expression pattern.

Parameters

  • pattern (string)

Returns Validator

Validator.lowercase()

validate.Validator.lowercase() -> Validator

The string must be entirely lowercase.

Returns Validator

Validator.uppercase()

validate.Validator.uppercase() -> Validator

The string must be entirely uppercase.

Returns Validator

Validator.not_blank()

validate.Validator.not_blank() -> Validator

The string must not consist entirely of whitespace characters.

Returns Validator

Validator.email()

validate.Validator.email() -> Validator

The value must be a syntactically valid email address.

Returns Validator

Validator.url()

validate.Validator.url() -> Validator

The value must be a valid HTTP or HTTPS URL.

Returns Validator

Validator.ipv4()

validate.Validator.ipv4() -> Validator

The value must be a valid IPv4 address.

Returns Validator

Validator.ipv6()

validate.Validator.ipv6() -> Validator

The value must be a valid IPv6 address.

Returns Validator

Validator.ip()

validate.Validator.ip() -> Validator

The value must be a valid IPv4 or IPv6 address.

Returns Validator

Validator.uuid()

validate.Validator.uuid() -> Validator

The value must be a valid canonical UUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).

Returns Validator

Validator.json()

validate.Validator.json() -> Validator

The value must be a valid JSON string.

Returns Validator

Validator.date()

validate.Validator.date() -> Validator

The value must be a valid date string parseable by the date module.

Returns Validator

Validator.after()

validate.Validator.after(ref) -> Validator

The value must be a date after ref.

Parameters

  • ref (string) — A date string parseable by the date module.

Returns Validator

Validator.after_or_equal()

validate.Validator.after_or_equal(ref) -> Validator

The value must be a date after or equal to ref.

Parameters

  • ref (string)

Returns Validator

Validator.before()

validate.Validator.before(ref) -> Validator

The value must be a date before ref.

Parameters

  • ref (string)

Returns Validator

Validator.before_or_equal()

validate.Validator.before_or_equal(ref) -> Validator

The value must be a date before or equal to ref.

Parameters

  • ref (string)

Returns Validator

Validator.timezone()

validate.Validator.timezone() -> Validator

The value must be a valid timezone identifier recognised by the date module (e.g. "UTC", "Africa/Lagos").

Returns Validator

Validator.is_in()

validate.Validator.is_in(values) -> Validator

The value must be one of the given allowed values (strict comparison).

Parameters

  • values (list)

Returns Validator

Validator.not_in()

validate.Validator.not_in(values) -> Validator

The value must not be one of the given forbidden values.

Parameters

  • values (list)

Returns Validator

Validator.in_list()

validate.Validator.in_list(values) -> Validator

Every item in the list value must be contained in values.

Parameters

  • values (list)

Returns Validator

Validator.accepted()

validate.Validator.accepted() -> Validator

The value must be one of the accepted truthy representations: true, "true", "yes", "on", "1", 1.

Returns Validator

Validator.declined()

validate.Validator.declined() -> Validator

The value must be one of the declined falsy representations: false, "false", "no", "off", "0", 0.

Returns Validator

Validator.equals()

validate.Validator.equals(expected) -> Validator

The value must strictly equal expected.

Parameters

  • expected (any)

Returns Validator

Validator.not_equals()

validate.Validator.not_equals(forbidden) -> Validator

The value must not equal forbidden.

Parameters

  • forbidden (any)

Returns Validator

Validator.same()

validate.Validator.same(other_field) -> Validator

The value must match the value of other_field in the same data dictionary. Commonly used to confirm passwords.

Parameters

  • other_field (string)

Returns Validator

Validator.different()

validate.Validator.different(other_field) -> Validator

The value must differ from the value of other_field.

Parameters

  • other_field (string)

Returns Validator

Validator.confirmed()

validate.Validator.confirmed() -> Validator

The value must match the {field_name}_confirmation sibling field.

Returns Validator

Validator.required_if()

validate.Validator.required_if(other_field, other_values) -> Validator

The field becomes required when other_field equals any value in other_values.

Parameters

  • other_field (string)
  • other_values (any|list)

Returns Validator

Validator.required_unless()

validate.Validator.required_unless(other_field, other_values) -> Validator

The field becomes required unless other_field equals any value in other_values.

Parameters

  • other_field (string)
  • other_values (any|list)

Returns Validator

Validator.required_with()

validate.Validator.required_with(fields) -> Validator

The field becomes required when any of the listed sibling fields are present and non-blank.

Parameters

  • fields (string|list)

Returns Validator

Validator.required_with_all()

validate.Validator.required_with_all(fields) -> Validator

The field becomes required when all of the listed sibling fields are present and non-blank.

Parameters

  • fields (string|list)

Returns Validator

Validator.required_without()

validate.Validator.required_without(fields) -> Validator

The field becomes required when any of the listed sibling fields are absent or blank.

Parameters

  • fields (string|list)

Returns Validator

Validator.required_without_all()

validate.Validator.required_without_all(fields) -> Validator

The field becomes required when all of the listed sibling fields are absent or blank.

Parameters

  • fields (string|list)

Returns Validator

Validator.prohibits()

validate.Validator.prohibits(fields) -> Validator

When this field is present, none of the listed sibling fields may also be present.

Parameters

  • fields (string|list)

Returns Validator

Validator.prohibited()

validate.Validator.prohibited() -> Validator

This field must be absent or nil: it is never allowed.

Returns Validator

Validator.prohibited_if()

validate.Validator.prohibited_if(other_field, other_values) -> Validator

The field must be absent when other_field equals any of other_values.

Parameters

  • other_field (string)
  • other_values (any|list)

Returns Validator

Validator.prohibited_unless()

validate.Validator.prohibited_unless(other_field, other_values) -> Validator

The field must be absent unless other_field equals any of other_values.

Parameters

  • other_field (string)
  • other_values (any|list)

Returns Validator

Validator.distinct()

validate.Validator.distinct() -> Validator

The list must not contain duplicate values.

Returns Validator

Validator.min_items()

validate.Validator.min_items(min) -> Validator

The list must have at least min items.

Parameters

  • min (number)

Returns Validator

Validator.max_items()

validate.Validator.max_items(max) -> Validator

The list must have at most max items.

Parameters

  • max (number)

Returns Validator

Validator.each()

validate.Validator.each(validator) -> Validator

Every item in the list must pass the given Validator chain.

var rules = validate.schema({
  # A list of between 1 and 10 valid email strings.
  recipients: validate.list()
    .min_items(1).max_items(10)
    .each(validate.string().email()),
})

Parameters

  • validator (Validator)

Returns Validator

Validator.is_nil()

validate.Validator.is_nil() -> Validator

The value must be nil.

Returns Validator

Validator.not_nil()

validate.Validator.not_nil() -> Validator

The value must not be nil.

Returns Validator

Validator.custom()

validate.Validator.custom(fn, message) -> Validator

Adds a custom inline validation rule backed by an anonymous function.

The function receives the field value and must return true to pass. An optional message overrides the default error text.

var v = validate.string().custom(@(value) {
  return value.starts_with('zuri-')
}, 'Must be a valid Zuri identifier.')

Parameters

  • fn (function) — @(value) -> bool
  • message (string) — Optional custom error message.

Returns Validator

Validator.custom_with_data()

validate.Validator.custom_with_data(fn, message) -> Validator

Adds a custom inline rule whose function also receives the full data dictionary, allowing cross-field validation logic.

var v = validate.number().custom_with_data(@(value, data) {
  return value < data.get('max_price', 0)
}, 'Must be less than max_price.')

Parameters

  • fn (function) — @(value, data) -> bool
  • message (string) — Optional custom error message.

Returns Validator

Validator.use()

validate.Validator.use(rule_class) -> Validator

Adds a pre-constructed Rule subclass (the class itself, not an instance) to the chain. Use this to attach custom Rule subclasses that require no constructor arguments beyond the field name.

import validate { Rule, Validator }

class Palindrome < Rule {
  validate(value) {
    return value == ''.join(value.to_list().reverse())
  }

  error() {
    return '${self.name} must be a palindrome.'
  }
}

var v = Validator().string().use(Palindrome)

Parameters

  • rule_class (class) — A class that extends Rule.

Returns Validator

Validator.bail_enabled()

validate.Validator.bail_enabled() -> bool

Whether validation should stop at the first failing rule for this field (see bail()).

Returns bool

Validator.is_sometimes()

validate.Validator.is_sometimes() -> bool

Whether this field’s whole chain should be skipped when the field is absent or blank (see sometimes()).

Returns bool

Validator.get_label()

validate.Validator.get_label() -> ?string

This field’s human-readable label override, or nil if none was set (see label()).

Returns ?string

validate.validators

import validate

Everything here is re-exported by validate, so import validate is enough and the names are called as validate.*. Importing validate.validators on its own works too and reaches the same definitions.

Top-level convenience functions that create a fresh Validator instance with one rule pre-applied. Every rule exposed by the Validator class has a corresponding function here so callers never need to import Validator directly for common cases.

Every function returns a Validator, so additional rules can be chained immediately:

import validate

validate.required().string().email().max_length(254)
validate.integer().gte(0).lte(150)
validate.list().min_items(1).each(validate.string())
validate.string().starts_with('ZURI-').length(12)

Functions

value()

validate.value() -> Validator

Returns a bare Validator with no rules pre-applied. Useful when you want to compose rules manually or start with a custom rule.

Returns Validator

required()

validate.required() -> Validator

The field must be present, non-nil, and non-empty.

Returns Validator

nullable()

validate.nullable() -> Validator

A nil value passes all subsequent rules without evaluation. Combine with sometimes() for a fully optional-nullable field.

Returns Validator

sometimes()

validate.sometimes() -> Validator

Skips the entire rule chain when the field is absent from the data dictionary or its value is nil / blank. Use this to mark fields as optional while still validating them when they are present.

Returns Validator

string()

validate.string() -> Validator

The value must be a string.

Returns Validator

number()

validate.number() -> Validator

The value must be a number (integer or float).

Returns Validator

integer()

validate.integer() -> Validator

The value must be an integer (no fractional part). Also accepts string representations of integers.

Returns Validator

boolean()

validate.boolean() -> Validator

The value must be boolean-like: true, false, "true", "false", "1", "0", 1, or 0.

Returns Validator

list()

validate.list() -> Validator

The value must be a list.

Returns Validator

dict()

validate.dict() -> Validator

The value must be a dictionary.

Returns Validator

numeric()

validate.numeric() -> Validator

The value must be numeric: either a number type or a string that converts cleanly to a number.

Returns Validator

size()

validate.size(n) -> Validator

The value’s size must equal exactly n.

Size is: character count for strings, item count for lists/dicts, and numeric value for numbers.

Parameters

  • n (number)

Returns Validator

min()

validate.min(min) -> Validator

The value’s size must be at least min.

Parameters

  • min (number)

Returns Validator

max()

validate.max(max) -> Validator

The value’s size must be at most max.

Parameters

  • max (number)

Returns Validator

between()

validate.between(min, max) -> Validator

The value’s size must be between min and max (inclusive).

Parameters

  • min (number)
  • max (number)

Returns Validator

length()

validate.length(n) -> Validator

The string’s character count must equal exactly n.

Parameters

  • n (number)

Returns Validator

min_length()

validate.min_length(min) -> Validator

The string must be at least min characters long.

Parameters

  • min (number)

Returns Validator

max_length()

validate.max_length(max) -> Validator

The string must be at most max characters long.

Parameters

  • max (number)

Returns Validator

length_between()

validate.length_between(min, max) -> Validator

The string’s length must be between min and max characters (inclusive).

Parameters

  • min (number)
  • max (number)

Returns Validator

gt()

validate.gt(n) -> Validator

The numeric value must be greater than n.

Parameters

  • n (number)

Returns Validator

gte()

validate.gte(n) -> Validator

The numeric value must be greater than or equal to n.

Parameters

  • n (number)

Returns Validator

lt()

validate.lt(n) -> Validator

The numeric value must be less than n.

Parameters

  • n (number)

Returns Validator

lte()

validate.lte(n) -> Validator

The numeric value must be less than or equal to n.

Parameters

  • n (number)

Returns Validator

positive()

validate.positive() -> Validator

The numeric value must be strictly positive (greater than zero).

Returns Validator

negative()

validate.negative() -> Validator

The numeric value must be strictly negative (less than zero).

Returns Validator

positive_or_zero()

validate.positive_or_zero() -> Validator

The numeric value must be zero or positive.

Returns Validator

negative_or_zero()

validate.negative_or_zero() -> Validator

The numeric value must be zero or negative.

Returns Validator

multiple_of()

validate.multiple_of(n) -> Validator

The numeric value must be a multiple of n.

Parameters

  • n (number)

Returns Validator

alpha()

validate.alpha() -> Validator

The string must contain only ASCII alphabetic characters (a-z, A-Z).

Returns Validator

alpha_num()

validate.alpha_num() -> Validator

The string must contain only ASCII alphanumeric characters.

Returns Validator

alpha_dash()

validate.alpha_dash() -> Validator

The string must contain only ASCII alphanumeric characters, hyphens, and underscores.

Returns Validator

starts_with()

validate.starts_with(prefixes) -> Validator

The string must start with one of the given prefixes.

Parameters

  • prefixes (string|list)

Returns Validator

doesnt_start_with()

validate.doesnt_start_with(prefixes) -> Validator

The string must not start with any of the given prefixes.

Parameters

  • prefixes (string|list)

Returns Validator

ends_with()

validate.ends_with(suffixes) -> Validator

The string must end with one of the given suffixes.

Parameters

  • suffixes (string|list)

Returns Validator

doesnt_end_with()

validate.doesnt_end_with(suffixes) -> Validator

The string must not end with any of the given suffixes.

Parameters

  • suffixes (string|list)

Returns Validator

contains()

validate.contains(needle) -> Validator

The string must contain the given substring.

Parameters

  • needle (string)

Returns Validator

doesnt_contain()

validate.doesnt_contain(needle) -> Validator

The string must not contain the given substring.

Parameters

  • needle (string)

Returns Validator

regex()

validate.regex(pattern) -> Validator

The string must match the given regular expression pattern.

Parameters

  • pattern (string)

Returns Validator

not_regex()

validate.not_regex(pattern) -> Validator

The string must not match the given regular expression pattern.

Parameters

  • pattern (string)

Returns Validator

lowercase()

validate.lowercase() -> Validator

The string must be entirely lowercase.

Returns Validator

uppercase()

validate.uppercase() -> Validator

The string must be entirely uppercase.

Returns Validator

not_blank()

validate.not_blank() -> Validator

The string must not consist entirely of whitespace.

Returns Validator

email()

validate.email() -> Validator

The value must be a syntactically valid email address.

Returns Validator

url()

validate.url() -> Validator

The value must be a valid HTTP or HTTPS URL.

Returns Validator

ipv4()

validate.ipv4() -> Validator

The value must be a valid IPv4 address.

Returns Validator

ipv6()

validate.ipv6() -> Validator

The value must be a valid IPv6 address.

Returns Validator

ip()

validate.ip() -> Validator

The value must be a valid IPv4 or IPv6 address.

Returns Validator

uuid()

validate.uuid() -> Validator

The value must be a valid canonical UUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).

Returns Validator

json()

validate.json() -> Validator

The value must be a valid JSON string.

Returns Validator

date()

validate.date() -> Validator

The value must be a valid date string parseable by the date module.

Returns Validator

after()

validate.after(ref) -> Validator

The value must be a date string after ref.

Parameters

  • ref (string) — A date string parseable by the date module.

Returns Validator

after_or_equal()

validate.after_or_equal(ref) -> Validator

The value must be a date string after or equal to ref.

Parameters

  • ref (string)

Returns Validator

before()

validate.before(ref) -> Validator

The value must be a date string before ref.

Parameters

  • ref (string)

Returns Validator

before_or_equal()

validate.before_or_equal(ref) -> Validator

The value must be a date string before or equal to ref.

Parameters

  • ref (string)

Returns Validator

timezone()

validate.timezone() -> Validator

The value must be a valid timezone identifier recognised by the date module (e.g. "UTC", "Africa/Lagos", "America/New_York").

Returns Validator

is_in()

validate.is_in(values) -> Validator

The value must be one of the given allowed values (strict comparison).

Parameters

  • values (list)

Returns Validator

not_in()

validate.not_in(values) -> Validator

The value must not be one of the given forbidden values.

Parameters

  • values (list)

Returns Validator

in_list()

validate.in_list(values) -> Validator

Every item in the list value must be contained in values.

Parameters

  • values (list)

Returns Validator

accepted()

validate.accepted() -> Validator

The value must be one of the accepted truthy representations: true, "true", "yes", "on", "1", 1.

Returns Validator

declined()

validate.declined() -> Validator

The value must be one of the declined falsy representations: false, "false", "no", "off", "0", 0.

Returns Validator

equals()

validate.equals(expected) -> Validator

The value must strictly equal expected.

Parameters

  • expected (any)

Returns Validator

not_equals()

validate.not_equals(forbidden) -> Validator

The value must not equal forbidden.

Parameters

  • forbidden (any)

Returns Validator

same()

validate.same(other_field) -> Validator

The value must match the value of other_field in the same data dictionary. Commonly used to confirm passwords.

Parameters

  • other_field (string)

Returns Validator

different()

validate.different(other_field) -> Validator

The value must differ from the value of other_field.

Parameters

  • other_field (string)

Returns Validator

confirmed()

validate.confirmed() -> Validator

The value must match the {field_name}_confirmation sibling field.

Returns Validator

required_if()

validate.required_if(other_field, other_values) -> Validator

The field becomes required when other_field equals any value in other_values.

Parameters

  • other_field (string)
  • other_values (any|list)

Returns Validator

required_unless()

validate.required_unless(other_field, other_values) -> Validator

The field becomes required unless other_field equals any value in other_values.

Parameters

  • other_field (string)
  • other_values (any|list)

Returns Validator

required_with()

validate.required_with(fields) -> Validator

The field becomes required when any of the listed sibling fields are present and non-blank.

Parameters

  • fields (string|list)

Returns Validator

required_with_all()

validate.required_with_all(fields) -> Validator

The field becomes required when all of the listed sibling fields are present and non-blank.

Parameters

  • fields (string|list)

Returns Validator

required_without()

validate.required_without(fields) -> Validator

The field becomes required when any of the listed sibling fields are absent or blank.

Parameters

  • fields (string|list)

Returns Validator

required_without_all()

validate.required_without_all(fields) -> Validator

The field becomes required when all of the listed sibling fields are absent or blank.

Parameters

  • fields (string|list)

Returns Validator

prohibits()

validate.prohibits(fields) -> Validator

When this field is present, none of the listed sibling fields may also be present.

Parameters

  • fields (string|list)

Returns Validator

prohibited()

validate.prohibited() -> Validator

This field must be absent or nil: it is never permitted.

Returns Validator

prohibited_if()

validate.prohibited_if(other_field, other_values) -> Validator

The field must be absent when other_field equals any of other_values.

Parameters

  • other_field (string)
  • other_values (any|list)

Returns Validator

prohibited_unless()

validate.prohibited_unless(other_field, other_values) -> Validator

The field must be absent unless other_field equals any of other_values.

Parameters

  • other_field (string)
  • other_values (any|list)

Returns Validator

distinct()

validate.distinct() -> Validator

The list must not contain duplicate values.

Returns Validator

min_items()

validate.min_items(min) -> Validator

The list must have at least min items.

Parameters

  • min (number)

Returns Validator

max_items()

validate.max_items(max) -> Validator

The list must have at most max items.

Parameters

  • max (number)

Returns Validator

each()

validate.each(validator) -> Validator

Every item in the list must pass the given Validator chain.

validate.each(validate.string().email())
# Equivalent to: validate.list().each(validate.string().email())

Parameters

  • validator (Validator)

Returns Validator

is_nil()

validate.is_nil() -> Validator

The value must be nil.

Returns Validator

not_nil()

validate.not_nil() -> Validator

The value must not be nil.

Returns Validator

custom()

validate.custom(fn, message) -> Validator

Validates with an inline anonymous function. The function receives the field value and must return true to pass.

validate.custom(@(value) {
  return value.starts_with('ZURI-')
}, 'Must be a valid Zuri identifier.')

Parameters

  • fn (function) — @(value) -> bool
  • message (string) — Optional custom error message.

Returns Validator

custom_with_data()

validate.custom_with_data(fn, message) -> Validator

Validates with an inline function that also receives the full data dictionary, enabling cross-field logic without subclassing Rule.

validate.custom_with_data(@(value, data) {
  return value < data.get('max_price', 0)
}, 'Must be less than max_price.')

Parameters

  • fn (function) — @(value, data) -> bool
  • message (string) — Optional custom error message.

Returns Validator

use()

validate.use(rule_class) -> Validator

Attaches a pre-defined Rule subclass (the class itself, not an instance) directly. The class must extend Rule and accept only name as its constructor argument.

import validate { Rule }

class Palindrome < Rule {
  validate(value) {
    if !is_string(value) return false
    var s = value.lower()
    return s == s.reverse()
  }
  error() { return 'The ${self.name} field must be a palindrome.' }
}

validate.use(Palindrome).string()

Parameters

  • rule_class (class) — A class that extends Rule.

Returns Validator

compress

import compress

The compress library provides in-memory and streaming compression and decompression across several algorithms, plus archive-format support for TAR and ZIP.

Compression methods

MethodSubmoduleEncodeDecodeStreamingNotes
Deflatecompress.deflateyesyesyesRFC 1951 raw compress stream.
Zlibcompress.zlibyesyesyesRFC 1950 wrapper around Deflate; also re-exports Deflate’s GzipDecoder/GzipEncoder as DeflateDecoder/DeflateEncoder.
Gzipcompress.gzipyesyesyesRFC 1952 wrapper around Deflate. GzFile treats a .gz file like a regular file.
Zstandardcompress.zstdyesyesyesPure-Rust implementation (zrip).
LZ4compress.lz4yesyesyesFrame format, via lz4_flex.
Bzip2compress.bzip2yesyesyesPure-Rust implementation (libbz2-rs-sys). BzFile treats a .bz2 file like a regular file. Only decodes the first stream in a bzip2 “multistream” (see the submodule’s own docs).
Brotlicompress.brotliyesyesdecode onlyPure-Rust implementation. The streaming encoder buffers all input and compresses once in finish() rather than incrementally: see BrotliEncoder’s own docs.

Every streaming submodule follows the same shape: a compress()/ decompress() one-shot pair for when the whole buffer is already in memory, plus an Encoder/Decoder class pair (write()/flush()/finish()/reset() and read()/read_exact()/read_all()/read_as_string() respectively) for when the input arrives incrementally or the output needs to be consumed as it’s produced. total_in()/ total_out()/available()/finished() are available on both sides of every streaming class for introspection.

compress.checksum provides CRC-32 and Adler-32, independent of any particular compression method: used internally by zip to validate extracted entries, but also usable standalone.

Archive formats

FormatSubmoduleReadWriteCompression methods
TARcompress.taryesyesnone, gzip, bzip2 (COMPRESS_NONE/COMPRESS_GZIP/COMPRESS_BZIP)
ZIPcompress.zipyesyesstored, deflate, bzip2 (ZIP_STORED/ZIP_DEFLATE/ZIP_BZIP2); zip64 supported for archives/files exceeding the classic 4 GiB limit

Both tar and zip expose a low-level class (tar.Tar, zip.ZipArchive) for building an archive entry by entry or inspecting one in detail, and a pair of one-shot free functions (compress()/extract()) for the common case of “archive this whole file or directory” / “extract this whole archive”. zip.extract() refuses to write outside the destination directory even if the archive contains ../-style path-traversal entry names (a “Zip Slip” attack); a ZIP entry that fails to decode cleanly (an unsupported or encrypted entry, or one whose CRC-32 doesn’t match what the archive claims) is reported per-entry via ZipItem.error rather than silently skipped or allowed to abort the whole read.

TAR does not currently support the LZMA/xz compression method some .tar.xz archives use, nor Brotli (no established TAR convention exists for it). ZIP does not currently support LZMA (method 14), PPMd (method 98), or any of the ZIP encryption schemes (traditional PKWARE or AES): an encrypted entry is reported via ZipItem.error rather than attempted.

The compress API

Every public name in compress, wherever it is declared. Each links to the page that documents it.

NameKindSummary
compress.BEST_COMPRESSIONconstantBest compression level.
compress.BEST_SPEEDconstantBest speed compression.
compress.DEFAULT_COMPRESSIONconstantDefault compression level.
compress.DEFAULT_MEMORY_LEVELconstantDefault memory level
compress.DEFAULT_STRATEGYconstantDefault compression strategy.
compress.FILTEREDconstantFiltered compression strategy.
compress.FIXEDconstantFixed compression strategy.
compress.HUFFMAN_ONLYconstanthuffman only compression strategy
compress.MAX_WBITSconstantMaximum windows bit.
compress.NO_COMPRESSIONconstantNo compression level.
compress.RLEconstantRle compression strategy.
compress.ZlibDecoderclassStreaming zlib decompressor implemention.
compress.ZlibEncoderclassA streaming Deflate encoder.
compress.brotli.BrotliDecoderclassStreaming Brotli decompressor implementation.
compress.brotli.BrotliEncoderclassA streaming Brotli encoder.
compress.brotli.compressfunctionCompress data using Brotli.
compress.brotli.decompressfunctionDecompress Brotli-compressed data.
compress.bzip2.BzFileclassThe BzFile class implements a Bzip2 based I/O system that allows you to treat Bzip2 streams (bytes) as if…
compress.bzip2.Bzip2DecoderclassStreaming bzip2 decompressor implementation.
compress.bzip2.Bzip2EncoderclassA streaming Bzip2 encoder.
compress.bzip2.compressfunctionCompress data using the default options for Bzip2.
compress.bzip2.decompressfunctionDecompress Bzip2-compressed data.
compress.checksum.adler32functionUpdates a running Adler-32 checksum with the bytes buf[0,len-1] and return the updated checksum.
compress.checksum.crc32functionUpdate a running CRC-32 checksum with the bytes buf[0,len-1] and return the updated CRC-32 checksum.
compress.compressfunctionCompress compresses as much data as possible, and stops when the input buffer becomes empty or the output…
compress.decompressfunctionDecompress decompresses as much data as possible, and stops when the input buffer becomes empty or the output…
compress.deflate.DeflateDecoderclassStreaming deflate decompressor implemention.
compress.deflate.DeflateEncoderclassA streaming Deflate encoder.
compress.deflate.compressfunctionCompress data using the default options for Deflate.
compress.deflate.decompressfunctionDecompress a deflated data using default options.
compress.gzip.GzFileclassThe GzFile class implements a GZip based I/O system that allows you use treat Gzip streams (bytes) as if they…
compress.gzip.GzipDecoderclassStreaming gzip decompressor implemention.
compress.gzip.GzipEncoderclassA streaming GZip encoder.
compress.gzip.compressfunctionCompress data using the default options for GZip.
compress.gzip.decompressfunctionDecompress a GZipped data using default options.
compress.lz4.Lz4DecoderclassStreaming reader for decompressing the LZ4 frame format.
compress.lz4.Lz4EncoderclassStreaming lz4 compressor implemention.
compress.lz4.compressfunctionCompress data using the Lz4 block format.
compress.lz4.decompressfunctionDecompress a Lz4 compressed data.
compress.tar.COMPRESS_AUTOconstantAutomatically select and detect compression type (Default).
compress.tar.COMPRESS_BZIPconstantCreate and read archives with the BZip2 compression method.
compress.tar.COMPRESS_GZIPconstantCreate and read archives with the GZip compression method.
compress.tar.COMPRESS_NONEconstantCreate and read archives without any compression.
compress.tar.Tarclass
compress.tar.TarCorruptedErrorclassError thrown when a TAR archive is corrupted.
compress.tar.TarIOErrorclassError thrown when an I/O error occurs.
compress.tar.TarIllegalCompressionErrorclassError thrown when an illegal compression type is used.
compress.tar.compressfunctionCreate a new TAR ball from the file or directory in the given path and saves it to the destination path or…
compress.tar.extractfunctionExtracts a TAR file to the given destination or to the same directory as the source file with the same name…
compress.zip.ZIP_BZIP2constantCompression method that indicates Bzip2 compression
compress.zip.ZIP_DEFLATEconstantCompression method that indicates zlib Deflate compression
compress.zip.ZIP_EXTconstantThe default zip file extension
compress.zip.ZIP_FILE_COUNT_LIMITconstantThe maximum number of files in a zip archive when zip64 is not used
compress.zip.ZIP_FILE_MAXconstant
compress.zip.ZIP_MAXconstantThe maximum size of a zip archive when zip64 is not used
compress.zip.ZIP_STOREDconstantCompression method that indicates no compression
compress.zip.ZipArchiveclassZipArchive provides a class for zip archive creation, manipulation and extraction.
compress.zip.ZipFileclassZipFile represents an instance of zip file.
compress.zip.ZipItemclassZipItem represents a single file or directory in a zip archive.
compress.zip.compressfunctionCompresses the given path (file or directory) into the destination zip archive.
compress.zip.extractfunctionExtracts the zip archive at the file path to the given destination directory.
compress.zstd.ZstdDecoderclassStreaming zstd decompressor implemention.
compress.zstd.ZstdEncoderclassStreaming zstd compressor implemention.
compress.zstd.compressfunctionCompress data using the default options for Zstd.
compress.zstd.decompressfunctionDecompress a Zstd compressed data.

Submodules

ModuleReached asSummary
compress.brotlicompress.brotli.*The Brotli submodule for the compress module.
compress.bzip2compress.bzip2.*The Bzip2 submodule for the compress module.
compress.checksumcompress.checksum.*This is the checksum submodule for the compress module.
compress.deflatecompress.deflate.*This is the Deflate submodule for the compress module.
compress.gzipcompress.gzip.*The is the GZip submodule for the compress module.
compress.lz4compress.lz4.*This is the Lz4 submodule for the compress module.
compress.tarcompress.tar.*This module adds support for creating and extracting TAR archives.
compress.zipcompress.zip.*The zip module contains classes and functions to make working with zip archives easy.
compress.zlibcompress.*This is the Zlib submodule for the compress module.
compress.zstdcompress.zstd.*This is the Zstd submodule for the compress module.

1995-2017 Jean-loup Gailly and Mark Adler

compress.brotli

import compress

compress exposes this as compress.brotli, so import compress is enough and the names are called as compress.brotli.*. import compress.brotli reaches the same definitions directly.

The Brotli submodule for the compress module.

Functions

compress()

compress.brotli.compress(data, quality, window) -> bytes

Compress data using Brotli.

Parameters

  • data (bytes|string)
  • quality (?number) — Compression quality (0..=11, higher is slower but smaller): Default 11.
  • window (?number) — Base-2 logarithm of the sliding window size (10..=24): Default 22.

Returns bytes

decompress()

compress.brotli.decompress(data) -> bytes

Decompress Brotli-compressed data.

Parameters

  • data (bytes|string)

Returns bytes

Classes

BrotliEncoder

class compress.brotli.BrotliEncoder

A streaming Brotli encoder.

Example:

%> import compress.brotli
%> var encoder = brotli.BrotliEncoder()
%> encoder.write('hello world')
11
%> encoder.finish()
(1b 0a 00 f8 25 04 92 68 65 6c ...)

Note: Unlike the other streaming encoders in this module (Gzip, Bzip2, Zstd, Lz4), this one does not compress incrementally as write() is called: it buffers everything you give it and does the real work once, in finish(). available() therefore always reads 0 before finish() is called: there’s nothing waiting yet. This follows from the underlying encoder only exposing a “compress this all at once into a sink” interface, not a lower-level incremental one.

Constructor

compress.brotli.BrotliEncoder(quality, window)

Creates a new streaming Brotli encoder.

Parameters

  • quality (?number) — Compression quality (0..=11): Default 11.
  • window (?number) — Base-2 logarithm of the sliding window size (10..=24): Default 22.

BrotliEncoder.finish()

compress.brotli.BrotliEncoder.finish() -> bytes

Compresses everything written so far and returns the resulting byte stream.

Returns bytes

Note: Once finish is called, the encoder can no longer be reused.

BrotliEncoder.close()

compress.brotli.BrotliEncoder.close() -> bytes

Same as finish() for file API compartibility.

Returns bytes

Note: Once finish is called, the encoder can no longer be reused.

BrotliEncoder.reset()

compress.brotli.BrotliEncoder.reset() -> bytes

Finishes the current stream and installs a fresh one for the next frame.

Returns the previous byte stream containing the completed frame.

Returns bytes

BrotliEncoder.write()

compress.brotli.BrotliEncoder.write(data) -> number

Buffers data to be compressed once finish() or reset() is called, returning how many bytes were accepted.

Parameters

  • data (bytes|string)

Returns number

BrotliEncoder.available()

compress.brotli.BrotliEncoder.available() -> number

Returns the number of compressed bytes currently waiting in the encoder’s output buffer. Always 0 before finish(): see the class-level note on why this encoder isn’t incremental.

Returns number

BrotliEncoder.finished()

compress.brotli.BrotliEncoder.finished() -> bool

Returns whether finish() has completed the stream.

Returns bool

BrotliEncoder.total_in()

compress.brotli.BrotliEncoder.total_in() -> number

Number of uncompressed bytes consumed by the encoder so far (only updated once finish()/reset() actually compresses them: see the class-level note).

Returns number

BrotliEncoder.total_out()

compress.brotli.BrotliEncoder.total_out() -> number

Number of compressed bytes generated by the encoder so far.

Returns number

BrotliDecoder

class compress.brotli.BrotliDecoder

Streaming Brotli decompressor implementation.

Wraps a byte stream of compressed data and yields decompressed bytes.

Example:

%> import compress.brotli
%> var data = brotli.compress('hello world')
%> var decoder = brotli.BrotliDecoder(data)
%> decoder.read_as_string()
hello world

Constructor

compress.brotli.BrotliDecoder(source)

Creates a Brotli decoder

Parameters

  • source (bytes)

BrotliDecoder.reset()

compress.brotli.BrotliDecoder.reset(new_source)

Installs a new data source, discarding whatever remained of the previous one.

Parameters

  • new_source (bytes)

BrotliDecoder.close()

compress.brotli.BrotliDecoder.close()

Closes the decoder by resetting it into an empty stream.

BrotliDecoder.read()

compress.brotli.BrotliDecoder.read(length) -> bytes

Reads some bytes up to the amount of bytes specified by length from the current source. Returns an empty byte stream when there is no more data to read.

Parameters

  • length (number)

Returns bytes

BrotliDecoder.read_exact()

compress.brotli.BrotliDecoder.read_exact(length) -> bytes

Reads the exact number of bytes from the buffer. This method will raise an error if it encounters an unexpected EOF (end of file) or there are insufficient data to read to complete the required number of bytes.

Parameters

  • length (number)

Returns bytes

BrotliDecoder.read_all()

compress.brotli.BrotliDecoder.read_all() -> bytes

Reads all remaining bytes until EOF is encountered in the source.

Returns bytes

BrotliDecoder.read_as_string()

compress.brotli.BrotliDecoder.read_as_string() -> string

Reads all remaining bytes until EOF is encountered in the source and returns the data read as a string instead of a byte stream.

Returns string

BrotliDecoder.available()

compress.brotli.BrotliDecoder.available() -> number

Returns the number of compressed bytes currently waiting in the decoder’s output buffer. Always 0: this decoder decodes on demand rather than buffering ahead.

Returns number

BrotliDecoder.finished()

compress.brotli.BrotliDecoder.finished() -> bool

Returns whether the decoder has reached the end of the stream.

Returns bool

BrotliDecoder.total_in()

compress.brotli.BrotliDecoder.total_in() -> number

Number of compressed bytes in the decoder’s source.

Returns number

BrotliDecoder.total_out()

compress.brotli.BrotliDecoder.total_out() -> number

Number of uncompressed bytes generated by the decoder so far.

Returns number


2026, Richard Ore and Zuri contributors

compress.bzip2

import compress

compress exposes this as compress.bzip2, so import compress is enough and the names are called as compress.bzip2.*. import compress.bzip2 reaches the same definitions directly.

The Bzip2 submodule for the compress module.

Functions

compress()

compress.bzip2.compress(data, level) -> bytes

Compress data using the default options for Bzip2.

Parameters

  • data (bytes|string)
  • level (?number) — Compression level (1..=9): Default 9.

Returns bytes

decompress()

compress.bzip2.decompress(data) -> bytes

Decompress Bzip2-compressed data.

Parameters

  • data (bytes|string)

Returns bytes

Note: This only decodes the first bzip2 stream in data. Some tools (pbzip2, Wikipedia data dumps, …) concatenate several independent bzip2 streams back to back (“multistreams”); use Bzip2Decoder in a loop with reset() to decode all of them.

Classes

Bzip2Encoder

class compress.bzip2.Bzip2Encoder

A streaming Bzip2 encoder.

Example:

%> import compress.bzip2
%> var encoder = bzip2.Bzip2Encoder()
%> encoder.write('hello world')
11
%> encoder.finish()
(42 5a 68 39 31 41 59 26 53 59 ...)

Constructor

compress.bzip2.Bzip2Encoder(level, work_factor)

Creates a new streaming Bzip2 encoder.

Parameters

  • level (?number) — Compression level (1..=9): Default 9.
  • work_factor (?number) — Controls when the compressor falls back from its standard sorting algorithm to a slower but more robust one on highly repetitive input. 0..=250: 0 (the default) means “use bzip2’s own default of 30”.

Bzip2Encoder.finish()

compress.bzip2.Bzip2Encoder.finish() -> bytes

Flushes remaining data, writes the stream footer, and returns the inner byte stream.

Returns bytes

Note: Once finish is called, the encoder can no longer be reused.

Bzip2Encoder.close()

compress.bzip2.Bzip2Encoder.close() -> bytes

Same as finish() for file API compartibility.

Returns bytes

Note: Once finish is called, the encoder can no longer be reused.

Bzip2Encoder.reset()

compress.bzip2.Bzip2Encoder.reset() -> bytes

Finishes the current stream and installs a fresh one for the next frame.

Returns the previous byte stream containing the completed frame.

Returns bytes

Bzip2Encoder.write()

compress.bzip2.Bzip2Encoder.write(data) -> number

Writes data into this encoder’s byte stream, returning how many bytes were written.

Parameters

  • data (bytes|string)

Returns number

Bzip2Encoder.flush()

compress.bzip2.Bzip2Encoder.flush() -> bytes

Flushes this output stream and returns the compressed data produced so far, without terminating the bzip2 stream.

Returns bytes

Bzip2Encoder.available()

compress.bzip2.Bzip2Encoder.available() -> number

Returns the number of compressed bytes currently waiting in the encoder’s output buffer.

Returns number

Bzip2Encoder.finished()

compress.bzip2.Bzip2Encoder.finished() -> bool

Returns whether finish() has completed the stream.

Returns bool

Bzip2Encoder.total_in()

compress.bzip2.Bzip2Encoder.total_in() -> number

Number of uncompressed bytes consumed by the encoder.

Returns number

Bzip2Encoder.total_out()

compress.bzip2.Bzip2Encoder.total_out() -> number

Number of compressed bytes generated by the encoder.

Returns number

Bzip2Decoder

class compress.bzip2.Bzip2Decoder

Streaming bzip2 decompressor implementation.

Wraps a byte stream of compressed data and yields decompressed bytes.

Example:

%> import compress.bzip2
%> var data = bzip2.compress('hello world')
%> var decoder = bzip2.Bzip2Decoder(data)
%> decoder.read_as_string()
hello world

Note: only decodes a single bzip2 stream: see decompress()’s note about “multistreams”.

Constructor

compress.bzip2.Bzip2Decoder(source)

Creates a Bzip2 decoder

Parameters

  • source (bytes)

Bzip2Decoder.reset()

compress.bzip2.Bzip2Decoder.reset(new_source)

Installs a new data source, discarding whatever remained of the previous one.

Parameters

  • new_source (bytes)

Bzip2Decoder.close()

compress.bzip2.Bzip2Decoder.close()

Closes the decoder by resetting it into an empty stream.

Bzip2Decoder.read()

compress.bzip2.Bzip2Decoder.read(length) -> bytes

Reads some bytes up to the amount of bytes specified by length from the current source. Returns an empty byte stream when there is no more data to read.

Parameters

  • length (number)

Returns bytes

Bzip2Decoder.read_exact()

compress.bzip2.Bzip2Decoder.read_exact(length) -> bytes

Reads the exact number of bytes from the buffer. This method will raise an error if it encounters an unexpected EOF (end of file) or there are insufficient data to read to complete the required number of bytes.

Parameters

  • length (number)

Returns bytes

Bzip2Decoder.read_all()

compress.bzip2.Bzip2Decoder.read_all() -> bytes

Reads all remaining bytes until EOF is encountered in the source.

Returns bytes

Bzip2Decoder.read_as_string()

compress.bzip2.Bzip2Decoder.read_as_string() -> string

Reads all remaining bytes until EOF is encountered in the source and returns the data read as a string instead of a byte stream.

Returns string

Bzip2Decoder.available()

compress.bzip2.Bzip2Decoder.available() -> number

Returns the number of compressed bytes still unread in the decoder’s source.

Returns number

Bzip2Decoder.finished()

compress.bzip2.Bzip2Decoder.finished() -> bool

Returns whether the decoder has reached the end of the stream.

Returns bool

Bzip2Decoder.total_in()

compress.bzip2.Bzip2Decoder.total_in() -> number

Number of compressed bytes consumed by the decoder.

Returns number

Bzip2Decoder.total_out()

compress.bzip2.Bzip2Decoder.total_out() -> number

Number of uncompressed bytes generated by the decoder.

Returns number

BzFile

class compress.bzip2.BzFile

The BzFile class implements a Bzip2 based I/O system that allows you to treat Bzip2 streams (bytes) as if they were a file.

The class implements the essentials of a file except those that ties it to the operating system filesystem such as symbolic links, chmod and set time.

See the chapter on files in The Zuri Programming Language for more information.

Constructor

compress.bzip2.BzFile(path: string, mode: ?string, _inner)

Returns a new BzFile object bounded to a physical file at the given path and opened in the given mode. See [[file]] for a description of the supported file modes.

Parameters

  • path (string)
  • mode (?string)

Returns BzFile

BzFile.exists()

compress.bzip2.BzFile.exists() -> bool

Returns true if the underlying file bounded to BzFile actually exists or false otherwise.

Returns bool

BzFile.close()

compress.bzip2.BzFile.close()

Closes the stream to an opened BzFile. You’ll rarely ever need to call this method yourself in most use cases.

BzFile.flush()

compress.bzip2.BzFile.flush() -> bytes

Flushes the remaning data into the BzFile underlying file and returns the byte stream returned.

Returns bytes

BzFile.open()

compress.bzip2.BzFile.open() -> bool

Opens the stream to a BzFile for the operation originally specified on the BzFile object during creation.

You may need to call this method after a call to read() if the length isn’t specified or write() if you wish to read or write again as the BzFile will already be closed.

Returns bool

BzFile.read()

compress.bzip2.BzFile.read(length: ?number) -> bytes

Reads the content of an opened BzFile up to the specified length and returns it as string or bytes if the BzFile was opened in the binary mode. If the length is not specified, the BzFile will be read to the end.

This method requires that the BzFile be opened in the read mode (default mode) or a mode that supports reading. If you aren’t reading the full length of the BzFile, you’ll need to call the close() method to free the BzFile for further reading, otherwise, the close() method will be automatically called for you.

Parameters

  • length (number) — Default = -1

Returns bytes

Raises Error

BzFile.gets()

compress.bzip2.BzFile.gets(length: ?number) -> bytes

Same as read(), but doesn’t close the BzFile automatically.

Parameters

  • length (?number) — Default = -1, meaning read to the end.

Returns bytes

Raises Error

BzFile.write()

compress.bzip2.BzFile.write(data: bytes|string) -> number

Writes a string or bytes to an opened BzFile at the current insertion point. When the BzFile is opened with the a mode enabled, write will always start from the end of the BzFile.

If the seek() method has been previously called, write will begin from the seeked position, otherwise it will start at the beginning of the BzFile.

Parameters

  • `` (bytes|string)

Returns number

BzFile.puts()

compress.bzip2.BzFile.puts(data: bytes|string) -> number

Same as write(), but doesn’t open or close the BzFile automatically.

Parameters

  • `` (bytes|string)

Returns number

BzFile.number()

compress.bzip2.BzFile.number() -> number

Returns the integer file descriptor number that is used by the underlying implementation to request I/O operations from the operating system. This can be very useful for low-level interfaces that uses or act as BzFile descriptors.

Returns number

BzFile.is_tty()

compress.bzip2.BzFile.is_tty() -> bool

Returns true if the underlying file of BzFile is a TTY device or false otherwise.

Returns bool

BzFile.is_open()

compress.bzip2.BzFile.is_open() -> bool

Returns true if the BzFile is open for reading or writing and false otherwise.

Returns bool

BzFile.is_closed()

compress.bzip2.BzFile.is_closed()

Returns true if the BzFile is closed for reading or writing and false otherwise.

compress.bzip2.BzFile.symlink(path: string) -> bool

See [[file.symlink]]

Returns bool

BzFile.stats()

compress.bzip2.BzFile.stats() -> dict

Returns the statistics or details of the BzFile.

See the working with files documentation for more information about the stats() method.

Returns dict

BzFile.delete()

compress.bzip2.BzFile.delete() -> bool

Deletes the underlying file pointed to by BzFile

Any further attempt to perform most operations on the BzFile after calling delete() will raise an error.

Returns bool

BzFile.rename()

compress.bzip2.BzFile.rename(new_name) -> bool

Renames the underlying file pointed to by BzFile to the new name and returns true if it succeeds or false otherwise.

Returns bool

BzFile.copy()

compress.bzip2.BzFile.copy() -> [[compress.bzip2.BzFile]]

Returns a new BzFile reading (or writing) the same underlying path independently of this one, with its own decoder/encoder state and its own position.

Returns [[compress.bzip2.BzFile]]

BzFile.path()

compress.bzip2.BzFile.path() -> string

Returns the physical path of the file pointed to by BzFile.

Returns string

BzFile.abs_path()

compress.bzip2.BzFile.abs_path() -> string

Returns the absolute path of the physical file pointed to by BzFile.

Returns string

BzFile.truncate()

compress.bzip2.BzFile.truncate(length: ?int) -> bool

Truncates the entire BzFile if length is not given or truncates the BzFile such that only length number of bytes is left in it.

Returns bool

BzFile.chmod()

compress.bzip2.BzFile.chmod(number: int) -> bool

Updates the permission for the file bounded by BzFile.

Returns bool

BzFile.set_times()

compress.bzip2.BzFile.set_times(atime: int, mtime: int) -> bool

Sets the last access time and last modified time of the BzFile.

Returns bool

BzFile.seek()

compress.bzip2.BzFile.seek(position: int, seek_type: int) -> bool

Sets the position of a BzFile reader in the decompressed byte stream (not the raw, still-compressed bytes on disk, which have no useful correspondence to a decompressed offset). The seek_type argument must be one of [[io.SEEK_SET]], [[io.SEEK_CUR]] or [[io.SEEK_END]].

Seeking forward simply discards decompressed bytes until the target position, since a streaming decoder has no way to skip ahead without producing them. Seeking backward (or SEEK_END, which has to fully decode the stream once to learn its length) re-opens the underlying file and restarts decompression from the beginning, discarding up to the target position: an inherently expensive operation for a compressed stream, same as with any other streaming (de)compressor.

Not supported on a BzFile opened for writing.

Returns bool

BzFile.tell()

compress.bzip2.BzFile.tell() -> number

Returns the current position of the reader in the decompressed byte stream (bytes already consumed via read()/gets()/seek()), or of the writer in the uncompressed input already fed to write()/puts() for a BzFile opened for writing.

Returns number

BzFile.mode()

compress.bzip2.BzFile.mode() -> string

Returns the mode in which the current BzFile was opened.

Returns string

BzFile.name()

compress.bzip2.BzFile.name() -> string

Returns the name of the file pointed to by BzFile.

Returns string


2026, Richard Ore and Zuri contributors

compress.checksum

import compress

compress exposes this as compress.checksum, so import compress is enough and the names are called as compress.checksum.*. import compress.checksum reaches the same definitions directly.

This is the checksum submodule for the compress module.

Functions

adler32()

compress.checksum.adler32(data, initial) -> number

Updates a running Adler-32 checksum with the bytes buf[0,len-1] and return the updated checksum.

Parameters

  • data (bytes|string)
  • initial (?number) — Default 1: the algorithm’s own defined identity/seed value (RFC 1950). Pass the previously returned checksum here to continue an incremental computation across multiple calls.

Returns number

Note: An Adler-32 checksum is almost as reliable as a CRC-32 but can be computed much faster.

crc32()

compress.checksum.crc32(data, initial) -> number

Update a running CRC-32 checksum with the bytes buf[0,len-1] and return the updated CRC-32 checksum.

Parameters

  • data (bytes|string)
  • initial (?number) — Default 0: the algorithm’s own defined identity/seed value. Pass the previously returned checksum here to continue an incremental computation across multiple calls.

Returns number


2021, Richard Ore and Zuri contributors

compress.deflate

import compress

compress exposes this as compress.deflate, so import compress is enough and the names are called as compress.deflate.*. import compress.deflate reaches the same definitions directly.

This is the Deflate submodule for the compress module.

Functions

compress()

compress.deflate.compress(data) -> bytes

Compress data using the default options for Deflate.

Parameters

  • data (bytes|string)

Returns bytes

decompress()

compress.deflate.decompress(data) -> bytes

Decompress a deflated data using default options.

Parameters

  • data (bytes|string)

Returns bytes

Classes

DeflateEncoder

class compress.deflate.DeflateEncoder < GzipEncoder

A streaming Deflate encoder.

Example:

%> import compress.deflate
%> var encoder = deflate.DeflateEncoder()
%> encoder.write('hello world')
11
%> encoder.finish()
(cb 48 cd c9 c9 57 28 cf 2f ca 49 01 00)

Constructor

compress.deflate.DeflateEncoder(level)

Creates a new streaming Deflate encoder at the given level (0..=9).

Parameters

  • level (?number) — Default 1

DeflateDecoder

class compress.deflate.DeflateDecoder < GzipDecoder

Streaming deflate decompressor implemention. This decoder reads standard deflate blocks and frames produced at any deflate compression level.

Wraps a byte stream of compressed data and yields decompressed bytes.

Example:

%> import compress.deflate
%> var data = deflate.compress('hello world')
%> data
(cb 48 cd c9 c9 57 28 cf 2f ca 49 01 00)
%> 
%> var decoder = deflate.DeflateDecoder(data)
%> decoder.read_as_string()
hello world

This decoder is level-independent and supports standard deflate frames produced by all deflate compression levels.

Constructor

compress.deflate.DeflateDecoder(source)

Creates a Gzip decoder

Parameters

  • source (bytes)

2021, Richard Ore and Zuri contributors

compress.gzip

import compress

compress exposes this as compress.gzip, so import compress is enough and the names are called as compress.gzip.*. import compress.gzip reaches the same definitions directly.

The is the GZip submodule for the compress module.

Functions

compress()

compress.gzip.compress(data) -> bytes

Compress data using the default options for GZip.

Parameters

  • data (bytes|string)

Returns bytes

decompress()

compress.gzip.decompress(data) -> bytes

Decompress a GZipped data using default options.

Parameters

  • data (bytes|string)

Returns bytes

Classes

GzipEncoder

class compress.gzip.GzipEncoder

A streaming GZip encoder.

Example:

%> import compress.gzip
%> var encoder = gzip.GzipEncoder()
%> encoder.write('hello world')
11
%> encoder.finish()
(1f 8b 08 00 00 00 00 00 04 03 cb 48 cd c9 c9 57 28 cf 2f ca 49 01 00 85 11 4a 0d 0b 00 00 00)

Constructor

compress.gzip.GzipEncoder(level)

Creates a new streaming GZip encoder at the given level (0..=9).

Parameters

  • level (?number) — Default 1

GzipEncoder.finish()

compress.gzip.GzipEncoder.finish() -> bytes

Flushes remaining data, writes the content checksum, and returns the inner byte stream.

Returns bytes

Note: Once finish is called, the encoder can no longer be reused.

GzipEncoder.close()

compress.gzip.GzipEncoder.close() -> bytes

Same as finish() for file API compartibility.

Returns bytes

Note: Once finish is called, the encoder can no longer be reused.

GzipEncoder.reset()

compress.gzip.GzipEncoder.reset() -> bytes

Finishes the current frame and installs byte stream for the next one.

Returns the previous byte stream containing the completed frame. All internal buffers (hash tables, workspace, block scratch) stay allocated and are reused for the next frame.

Returns bytes

GzipEncoder.write()

compress.gzip.GzipEncoder.write(data) -> number

Writes data into this encoder’s byte stream, returning how many bytes were written.

Parameters

  • data (bytes|string)

Returns number

GzipEncoder.flush()

compress.gzip.GzipEncoder.flush() -> bytes

Flushes this output stream and returns the remaining compressed data in the stream, ensuring that all intermediately buffered contents reach their destination.

Returns bytes

GzipEncoder.available()

compress.gzip.GzipEncoder.available() -> number

Returns the number of compressed bytes currently waiting in the encoder’s output buffer.

Returns number

GzipEncoder.finished()

compress.gzip.GzipEncoder.finished() -> bool

Returns whether finish() has completed the stream.

Returns bool

GzipEncoder.total_in()

compress.gzip.GzipEncoder.total_in() -> number

Number of uncompressed bytes consumed by the encoder.

Returns number

GzipEncoder.total_out()

compress.gzip.GzipEncoder.total_out() -> number

Number of compressed bytes generated by the encoder.

Returns number

GzipDecoder

class compress.gzip.GzipDecoder

Streaming gzip decompressor implemention. This decoder reads standard gzip blocks and frames produced at any gzip compression level.

Wraps a byte stream of compressed data and yields decompressed bytes.

Example:

%> import compress.gzip
%> var data = gzip.compress('hello world')
%> data
(1f 8b 08 00 00 00 00 00 00 03 cb 48 cd c9 c9 57 28 cf 2f ca 49 01 00 85 11 4a 0d 0b 00 00 00)
%>
%> var decoder = gzip.GzipDecoder(data)
%> decoder.read_as_string()
hello world

This decoder is level-independent and supports standard gzip frames produced by all gzip compression levels.

Constructor

compress.gzip.GzipDecoder(source)

Creates a Gzip decoder

Parameters

  • source (bytes)

GzipDecoder.reset()

compress.gzip.GzipDecoder.reset(new_source)

Installs a new data source for the next frame, keeping all internal buffers allocated.

Parameters

  • new_source (bytes)

GzipDecoder.close()

compress.gzip.GzipDecoder.close()

Closes the decoder by resetting it into an empty stream.

GzipDecoder.read()

compress.gzip.GzipDecoder.read(length) -> bytes

Reads some bytes up to the amount of bytes specified by length from the current source. Returns an empty byte stream when there is no more data to read.

This function does not block or wait waiting for data, but reads as much data as is available to read when it runs.

Parameters

  • length (number)

Returns bytes

GzipDecoder.read_exact()

compress.gzip.GzipDecoder.read_exact(length) -> bytes

Reads the exact number of bytes from the buffer. This method will raise an error if it encounters an unexpected EOF (end of file) or there are insufficient data to read to complete the required number of bytes.

Parameters

  • length (number)

Returns bytes

GzipDecoder.read_all()

compress.gzip.GzipDecoder.read_all() -> bytes

Reads all remaining bytes until EOF is encountered in the source.

Returns bytes

GzipDecoder.read_as_string()

compress.gzip.GzipDecoder.read_as_string() -> string

Reads all remaining bytes until EOF is encountered in the source and returns the data read as a string instead of a byte stream.

Returns string

GzipDecoder.available()

compress.gzip.GzipDecoder.available() -> number

Returns the number of compressed bytes currently waiting in the decoder’s output buffer.

Returns number

GzipDecoder.finished()

compress.gzip.GzipDecoder.finished() -> bool

Returns whether finish() has completed the stream.

Returns bool

GzipDecoder.total_in()

compress.gzip.GzipDecoder.total_in() -> number

Number of compressed bytes consumed by the decoder.

Returns number

GzipDecoder.total_out()

compress.gzip.GzipDecoder.total_out() -> number

Number of uncompressed bytes generated by the decoder.

Returns number

GzFile

class compress.gzip.GzFile

The GzFile class implements a GZip based I/O system that allows you use treat Gzip streams (bytes) as if they were a file.

The class implements the essentials of a file except those that ties it to the operating system filesystem such as symbolic links, chmod and set time.

See the chapter on files in The Zuri Programming Language for more information.

Constructor

compress.gzip.GzFile(path: string, mode: ?string, _inner)

Returns a new GzFile object bounded to a physical file at the given path and opened in the given mode. See [[file]] for a description of the supported file modes.

Parameters

  • path (string)
  • mode (?string)

Returns GzFile

GzFile.exists()

compress.gzip.GzFile.exists() -> bool

Returns true if the underlying file bounded to GzFile actually exists or false otherwise.

Returns bool

GzFile.close()

compress.gzip.GzFile.close()

Closes the stream to an opened GzFile. You’ll rarely ever need to call this method yourself in most use cases.

GzFile.flush()

compress.gzip.GzFile.flush() -> bytes

Flushes the remaning data into the GzFile underlying file and returns the byte stream returned.

Returns bytes

GzFile.open()

compress.gzip.GzFile.open() -> bool

Opens the stream to a GzFile for the operation originally specified on the GzFile object during creation.

You may need to call this method after a call to read() if the length isn’t specified or write() if you wish to read or write again as the GzFile will already be closed.

Returns bool

GzFile.read()

compress.gzip.GzFile.read(length: ?number) -> bytes

Reads the content of an opened GzFile up to the specified length and returns it as string or bytes if the GzFile was opened in the binary mode. If the length is not specified, the GzFile will be read to the end.

This method requires that the GzFile be opened in the read mode (default mode) or a mode that supports reading. If you aren’t reading the full length of the GzFile, you’ll need to call the close() method to free the GzFile for further reading, otherwise, the close() method will be automatically called for you.

Parameters

  • length (number) — Default = -1

Returns bytes

Raises Error

GzFile.gets()

compress.gzip.GzFile.gets(length: ?number) -> bytes

Same as read(), but doesn’t close the GzFile automatically.

Parameters

  • length (?number) — Default = -1, meaning read to the end.

Returns bytes

Raises Error

GzFile.write()

compress.gzip.GzFile.write(data: bytes|string) -> number

Writes a string or bytes to an opened GzFile at the current insertion point. When the GzFile is opened with the a mode enabled, write will always start from the end of the GzFile.

If the seek() method has been previously called, write will begin from the seeked position, otherwise it will start at the beginning of the GzFile.

Parameters

  • `` (bytes|string)

Returns number

GzFile.puts()

compress.gzip.GzFile.puts(data: bytes|string) -> number

Same as write(), but doesn’t open or close the GzFile automatically.

Parameters

  • `` (bytes|string)

Returns number

GzFile.number()

compress.gzip.GzFile.number() -> number

Returns the integer file descriptor number that is used by the underlying implementation to request I/O operations from the operating system. This can be very useful for low-level interfaces that uses or act as GzFile descriptors.

Returns number

GzFile.is_tty()

compress.gzip.GzFile.is_tty() -> bool

Returns true if the underlying file of GzFile is a TTY device or false otherwise.

Returns bool

GzFile.is_open()

compress.gzip.GzFile.is_open() -> bool

Returns true if the GzFile is open for reading or writing and false otherwise.

Returns bool

GzFile.is_closed()

compress.gzip.GzFile.is_closed()

Returns true if the GzFile is closed for reading or writing and false otherwise.

compress.gzip.GzFile.symlink(path: string) -> bool

See [[file.symlink]]

Returns bool

GzFile.stats()

compress.gzip.GzFile.stats() -> dict

Returns the statistics or details of the GzFile.

See the working with files documentation for more information about the stats() method.

Returns dict

GzFile.delete()

compress.gzip.GzFile.delete() -> bool

Deletes the underlying file pointed to by GzFile

Any further attempt to perform most operations on the GzFile after calling delete() will raise an error.

Returns bool

GzFile.rename()

compress.gzip.GzFile.rename(new_name) -> bool

Renames the underlying file pointed to by GzFile to the new name and returns true if it succeeds or false otherwise.

Returns bool

GzFile.copy()

compress.gzip.GzFile.copy() -> [[io.GzFile]]

Returns a new GzFile reading (or writing) the same underlying path independently of this one, with its own decoder/encoder state and its own position.

Returns [[io.GzFile]]

GzFile.path()

compress.gzip.GzFile.path() -> string

Returns the physical path of the file pointed to by GzFile.

Returns string

GzFile.abs_path()

compress.gzip.GzFile.abs_path() -> string

Returns the absolute path of the physical file pointed to by GzFile.

Returns string

GzFile.truncate()

compress.gzip.GzFile.truncate(length: ?int) -> bool

Truncates the entire GzFile if length is not given or truncates the GzFile such that only length number of bytes is left in it.

Returns bool

GzFile.chmod()

compress.gzip.GzFile.chmod(number: int) -> bool

Updates the permission for the file bounded by GzFile.

Returns bool

GzFile.set_times()

compress.gzip.GzFile.set_times(atime: int, mtime: int) -> bool

Sets the last access time and last modified time of the GzFile.

Returns bool

GzFile.seek()

compress.gzip.GzFile.seek(position: int, seek_type: int) -> bool

Sets the position of a GzFile reader in the decompressed byte stream (not the raw, still-compressed bytes on disk, which have no useful correspondence to a decompressed offset). The seek_type argument must be one of [[io.SEEK_SET]], [[io.SEEK_CUR]] or [[io.SEEK_END]].

Seeking forward simply discards decompressed bytes until the target position, since a streaming decoder has no way to skip ahead without producing them. Seeking backward (or SEEK_END, which has to fully decode the stream once to learn its length) re-opens the underlying file and restarts decompression from the beginning, discarding up to the target position: an inherently expensive operation for a compressed stream, same as with any other streaming (de)compressor.

Not supported on a GzFile opened for writing.

Returns bool

GzFile.tell()

compress.gzip.GzFile.tell() -> number

Returns the current position of the reader in the decompressed byte stream (bytes already consumed via read()/gets()/seek()), or of the writer in the uncompressed input already fed to write()/puts() for a GzFile opened for writing.

Returns number

GzFile.mode()

compress.gzip.GzFile.mode() -> string

Returns the mode in which the current GzFile was opened.

Returns string

GzFile.name()

compress.gzip.GzFile.name() -> string

Returns the name of the file pointed to by GzFile.

Returns string


2021, Richard Ore and Zuri contributors

compress.lz4

import compress

compress exposes this as compress.lz4, so import compress is enough and the names are called as compress.lz4.*. import compress.lz4 reaches the same definitions directly.

This is the Lz4 submodule for the compress module.

This modules provides two ways to use lz4. The first way is through the Lz4Decoder and Lz4Encoder classes, which implement a streaming encoder and decoder with the lz4 frame format. Unless you have a specific reason to the contrary, you should only use the Lz4 classes which implement the lz4 frame format. Specifically, the lz4 frame format permits streaming compression or decompression.

The second way is through the compress and decompress functions. These functions provide access to the lz4 block format, and don’t support a streaming interface directly. You should only use these types if you know you specifically need the lz4 block format.

Functions

compress()

compress.lz4.compress(data) -> bytes

Compress data using the Lz4 block format. The uncompressed size will be prepended as a little endian unsigned integer 32.

Parameters

  • data (bytes|string)
  • level (?number)

Returns bytes

Note: This is the block format, not the frame format: Lz4Encoder/Lz4Decoder use a genuinely different, mutually incompatible binary layout. Data compressed here can only be decompressed with decompress(), never with Lz4Decoder, and vice versa.

decompress()

compress.lz4.decompress(data) -> bytes

Decompress a Lz4 compressed data. The first 4 bytes are expected to be the uncompressed size in little endian.

Parameters

  • data (bytes|string)

Returns bytes

Note: This expects the block format compress() produces, not the frame format Lz4Encoder produces: passing Lz4Encoder output here will fail (and vice versa for Lz4Decoder given compress() output). See the module docs.

Classes

Lz4Encoder

class compress.lz4.Lz4Encoder

Streaming lz4 compressor implemention. Data written to this encoder are compressed using the LZ4 frame format and are automatically buffered.

To ensure a well formed stream the encoder must be finalized by calling the finish() method.

Example:

%> import compress.lz4
%> var encoder = lz4.Lz4Encoder()
%> encoder.write('hello world')
11
%> encoder.finish()
(04 22 4d 18 60 40 82 0b 00 00 80 68 65 6c 6c 6f 20 77 6f 72 6c 64 00 00 00 00)

Note: This produces the LZ4 frame format: decode it with Lz4Decoder, not the one-shot decompress() (which expects a different, incompatible block format). See the module docs.

Constructor

compress.lz4.Lz4Encoder()

Creates a new streaming Lz4 encoder.

Lz4Encoder.finish()

compress.lz4.Lz4Encoder.finish() -> bytes

Flushes remaining data, writes the content checksum, and returns the inner byte stream.

Returns bytes

Note: Once finish is called, the encoder can no longer be reused.

Lz4Encoder.close()

compress.lz4.Lz4Encoder.close() -> bytes

Same as finish() for file API compartibility.

Returns bytes

Note: Once finish is called, the encoder can no longer be reused.

Lz4Encoder.write()

compress.lz4.Lz4Encoder.write(data) -> number

Writes data into this encoder’s byte stream, returning how many bytes were written.

Parameters

  • data (bytes|string)

Returns number

Lz4Encoder.write_all()

compress.lz4.Lz4Encoder.write_all(data)

Attempts to write an entire data into this encoder’s byte stream.

Parameters

  • data (bytes|string)

Lz4Encoder.flush()

compress.lz4.Lz4Encoder.flush()

Flushes this output stream, ensuring that all intermediately buffered contents reach their destination.

Lz4Decoder

class compress.lz4.Lz4Decoder

Streaming reader for decompressing the LZ4 frame format. Bytes read will be decompressed according to the LZ4 frame format.

Example:

%> import compress.lz4
%> var x = lz4.Lz4Encoder()
%> x.write('hello world')
11
%> var g = x.finish()
%> g
(04 22 4d 18 60 40 82 0b 00 00 80 68 65 6c 6c 6f 20 77 6f 72 6c 64 00 00 00 00)
%> 
%> var f = lz4.Lz4Decoder(g)
%> f.read_as_string()
hello world

Note: This reads the LZ4 frame format produced by Lz4Encoder: it cannot decode the one-shot compress()’s block format (use decompress() for that instead). See the module docs.

Constructor

compress.lz4.Lz4Decoder(source)

Creates a Lz4 decoder

Parameters

  • source (bytes)

Lz4Decoder.close()

compress.lz4.Lz4Decoder.close()

No op. Just for file API compartibility.

Lz4Decoder.read()

compress.lz4.Lz4Decoder.read(length) -> bytes

Reads some bytes up to the amount of bytes specified by length from the current source. Returns an empty byte stream when there is no more data to read.

This function does not block or wait waiting for data, but reads as much data as is available to read when it runs.

Parameters

  • length (number)

Returns bytes

Lz4Decoder.read_exact()

compress.lz4.Lz4Decoder.read_exact(length) -> bytes

Reads the exact number of bytes from the buffer. This method will raise an error if it encounters an unexpected EOF (end of file) or there are insufficient data to read to complete the required number of bytes.

Parameters

  • length (number)

Returns bytes

Lz4Decoder.read_all()

compress.lz4.Lz4Decoder.read_all() -> bytes

Reads all remaining bytes until EOF is encountered in the source.

Returns bytes

Lz4Decoder.read_as_string()

compress.lz4.Lz4Decoder.read_as_string() -> string

Reads all remaining bytes until EOF is encountered in the source and returns the data read as a string instead of a byte stream.

Returns string


2021, Richard Ore and Zuri contributors

compress.tar

import compress

compress exposes this as compress.tar, so import compress is enough and the names are called as compress.tar.*. import compress.tar reaches the same definitions directly.

This module adds support for creating and extracting TAR archives.

Important

The library exports two helper function compress() and extract() by that allows you to create and/or extract TAR archives.

This library supports the most popular extensions such as .tar.gz, .tar, .gz, .tgz, .tar.bz2, .bz2, and .tbz: the three archive shapes: uncompressed, gzip-compressed, and bzip2-compressed.

Extracting TAR archives


Use the helper function extract() for a quick way to extract TAR archives.

import tar

tar.extract('/path/to/archive.tar.gz', '/destination')

The destination can be omitted in which case the archive will be extracted into the same directory as the source with same name without the last extension. e.g. for /path/to/file.tar.gz will extract to /path/to/file.tar directory is the destination is not given.

See below for learn more about the extract() method

Creating a new TAR ball


To quickly create a new tarball, you can use the compress() helper function in the library like this,

import tar

tar.compress('/path/to/file/or/directory', '/destination.tar.gz')

The compress function can be used to compress a single file or an entire directory. Like the extract() function, you can choose to omit the destination parameter in which case compress will save the file to the current working directory with the same name as the file/directory with the extension .tar.gz.

See below for learn more about the compress() method

Constants

COMPRESS_AUTO

compress.tar.COMPRESS_AUTO

Automatically select and detect compression type (Default).

COMPRESS_NONE

compress.tar.COMPRESS_NONE = 0

Create and read archives without any compression.

COMPRESS_GZIP

compress.tar.COMPRESS_GZIP = 1

Create and read archives with the GZip compression method.

COMPRESS_BZIP

compress.tar.COMPRESS_BZIP = 2

Create and read archives with the BZip2 compression method.

Functions

compress()

compress.tar.compress(path: string, destination: string)

Create a new TAR ball from the file or directory in the given path and saves it to the destination path or ${NAME_OF_FILE}.tar.gz in the current directory if the destination is not given.

Parameters

  • path (string) — the file or directory that will be compressed.
  • destination (string) — the destination of the compressed TAR ball.

extract()

compress.tar.extract(file: string, destination: string) -> list

Extracts a TAR file to the given destination or to the same directory as the source file with the same name as the TAR file (without the last extension) if destination is not given.

Parameters

  • file (string) — the file to be extracted
  • destination (string) — the path to extract to (Optional).

Returns list — list of extracted items

Classes

TarCorruptedError

class compress.tar.TarCorruptedError < Error

Error thrown when a TAR archive is corrupted.

TarIOError

class compress.tar.TarIOError < Error

Error thrown when an I/O error occurs.

TarIllegalCompressionError

class compress.tar.TarIllegalCompressionError < Error

Error thrown when an illegal compression type is used.

Tar

class compress.tar.Tar

Tar.set_compression()

compress.tar.Tar.set_compression(level: ?int, type: ?int)

Set the compression level and type.

Parameters

  • level (int) — Compression level (0 to 9) - Default = 9
  • type (int) — Type of compression to use (use COMPRESS_* constants)
    • Default = COMPRESS_AUTO

Raises TarIllegalCompressionError

Tar.open()

compress.tar.Tar.open(path: string)

Open an existing Tar file for reading.

Parameters

  • file (string)

Raises TarIOError

Raises TarIllegalCompressionError

Tar.contents()

compress.tar.Tar.contents() -> list[dict]

Read the contents of a Tar archive

This function lists the files stored in the Tar, and returns an indexed array of FileInfo objects

The Tar is closed afer reading the contents, because rewinding is not possible in bzip2 streams. Reopen the file with open() again if you want to do additional operations

Returns list[dict]

Tar.extract()

compress.tar.Tar.extract(outdir: string, strip: ?int|string, exclude: ?string, include: ?string) -> list[string]

Extract an existing Tar archive

The strip parameter allows you to strip a certain number of path components from the filenames found in the Tar file, similar to the –strip-components feature of GNU tar. This is triggered when an integer is passed as strip. Alternatively a fixed string prefix may be passed in strip. If the filename matches this prefix, the prefix will be stripped. It is recommended to give prefixes with a trailing slash.

By default this will extract all files found in the Tar. You can restrict the output using the include and exclude parameter. Both expect a full regular expression (including delimiters and modifiers). If include is set, only files that match this expression will be extracted. Files that match the exclude expression will never be extracted. Both parameters can be used in combination. Expressions are matched against stripped filenames as described above.

The Tar is closed afterwards. Reopen the file with open() again if you want to do additional operations.

Parameters

  • outdir (string) — the target directory for extracting
  • strip (?int|string) — either the number of path components or a fixed prefix to strip
  • exclude (?string) — a regular expression of files to exclude
  • include (?string) — a regular expression of files to include

Returns list[string]

Raises TarIOError

Raises TarCorruptedError when an entry’s name would place it outside outdir, by being absolute or by climbing out through ... Nothing past that entry is extracted.

Tar.create()

compress.tar.Tar.create(path: ?string)

Create a new Tar file.

If file is empty, the Tar file will be created in memory.

Parameters

  • path (?string)

Tar.add_file()

compress.tar.Tar.add_file(path: string, header: ?string|dict)

Add a file to the current Tar using an existing file in the filesystem.

Parameters

  • path (string) — path to the original file
  • header (?string|dict) — either the name to use in Tar (string) or a dictionary oject with all meta data, empty to take from original.

Raises TarIOError

Tar.add_data()

compress.tar.Tar.add_data(data: bytes, header: ?string|dict)

Add a file to the current Tar using the given data as content.

If the header is set to nil or empty string, a file called Untitled-{CURRENT_TIMESTAMP} will be created.

Parameters

  • data (bytes) — binary content of the file to add
  • header (?string|dict) — either the name to use in Tar (string) or a dictionary oject with all meta data

Raises TarIOError

Tar.close()

compress.tar.Tar.close()

Add the closing footer to the archive if in write mode, close all file handles

After a call to this function no more data can be added to the archive, for read access no reading is allowed anymore

“Physically, an archive consists of a series of file entries terminated by an end-of-archive entry, which consists of two 512 blocks of zero bytes”

Raises TarIOError

Tar.get_archive()

compress.tar.Tar.get_archive() -> bytes

Returns the created in-memory Tar data.

This implicitly calls close() on the Tar.

Returns bytes

Raises TarIOError

Raises TarIllegalCompressionError

Tar.save()

compress.tar.Tar.save(path: string) -> bool

Save the created in-memory Tar data

Note: It is more memory effective to specify the filename in the create() function and let the library work on the new file directly.

Parameters

  • path (string)

Returns bool

Tar.add_directory()

compress.tar.Tar.add_directory(directory: string, file_blacklist: ?list, ext_blacklist: ?list)

Adds the specified directory recursively to the archive and set’s it path in the archive to dir.

Parameters

  • directory (string)
  • file_blacklist (list) — if not empty, this function will ignore every file with a matching path.
  • ext_blacklist (list) — if not empty, this function will ignore every file with a matching extension.

Raises TarIOError|Error

Tar.file_type()

compress.tar.Tar.file_type(f: string) -> int

Guesses the wanted compression from the given file.

Uses magic bytes for existing files, the file extension otherwise.

You don’t need to call this yourself. It’s used when you pass COMPRESS_AUTO somewhere.

Parameters

  • file (string)

Returns int — (one of COMPRESS_BZIP, COMPRESS_GZIP or COMPRESS_NONE)


2024, Richard Ore and The Zuri Contributors

compress.zip

import compress

compress exposes this as compress.zip, so import compress is enough and the names are called as compress.zip.*. import compress.zip reaches the same definitions directly.

The zip module contains classes and functions to make working with zip archives easy.

Constants

ZIP_FILE_MAX

compress.zip.ZIP_FILE_MAX = 4294967295

ZIP_FILE_COUNT_LIMIT

compress.zip.ZIP_FILE_COUNT_LIMIT: number = 65535

The maximum number of files in a zip archive when zip64 is not used

ZIP_MAX

compress.zip.ZIP_MAX: number

The maximum size of a zip archive when zip64 is not used

ZIP_EXT

compress.zip.ZIP_EXT: string = '.zip'

The default zip file extension

ZIP_STORED

compress.zip.ZIP_STORED: number = 0

Compression method that indicates no compression

ZIP_DEFLATE

compress.zip.ZIP_DEFLATE: number = 8

Compression method that indicates zlib Deflate compression

ZIP_BZIP2

compress.zip.ZIP_BZIP2: number = 12

Compression method that indicates Bzip2 compression

Functions

extract()

compress.zip.extract(file: string, destination: ?string, is_zip64: ?bool) -> bool

Extracts the zip archive at the file path to the given destination directory. If destination is not given, the file will be extracted into the current working directory.

This function returns true if the extraction was successful and false otherwise.

NOTE: Set is_zip64 to true if the size of the zip file exceeds ZIP_MAX.

Parameters

  • file (string)
  • destination (?string) — Default value is os.cwd().
  • is_zip64 (?bool) — Default value is false.

Returns bool

compress()

compress.zip.compress(path: string, destination: ?string, compression_method: ?number, use_zip64: ?bool) -> bool

Compresses the given path (file or directory) into the destination zip archive.

When an Error is thrown because max size was exceeded, some files could have already been compressed. In this case, the zip archive will should still be usable but not all desired files will be contained in it.

NOTE: Set use_zip64 to true when compressing files exceeding ZIP_FILE_MAX or ZIP_FILE_COUNT_LIMIT

Parameters

  • file (string)
  • destination (?string) — Default value is os.cwd().
  • compression_method (?number) — Default value is ZIP_DEFLATE
  • is_zip64 (?bool) — Default value is false.

Returns bool

Raises Error if file could not be written of zip max size exceeded.

Classes

ZipItem

class compress.zip.ZipItem

ZipItem represents a single file or directory in a zip archive.

Fields

FieldTypeDescription
namestringName of the file or directory
directorystringThe directory in which the file or subdirectory belongs
compression_methodstringThe compression method for this file
crcstringThe crc32 checksum for the file
last_modifiedDateThe last modified date for the file
compressed_sizenumberThe size of the file as compressed in the archive.
uncompressed_sizenumberThe size of the file when extracted from the archive
is_encryptedboolIf this file is encrypted or not.
permissionnumberThe file permission
errorstringError encountered when attempting to read/extract the file
databytesThe decompressed value of the zip item

ZipItem.from_dict()

compress.zip.ZipItem.from_dict(dict: dict) -> ZipItem

Creates a new ZipItem from a dictionary. The dictionary should contain the following keys: - name: string - dir: string — optional - compress_method: number - crc: number - filemtime: number - size_compressed: number - size_uncompressed: number - encrypted: boolean - error: string — optional - data: bytes - permission: number

Parameters

  • dict (dict)

Returns ZipItem

ZipItem.export()

compress.zip.ZipItem.export(base_dir: ?string) -> bool

Exports the ZipItem to file. If base_dir is given, the file will be exported into the base_dir and all ZipItem directories will be created inside of base_dir to reflect the ZipItem’s original structure.

This function returns true if the operation succeeds or false otherwise.

Parameters

  • base_dir (?string) — Default value is os.cwd().

Returns bool

ZipFile

class compress.zip.ZipFile

ZipFile represents an instance of zip file.

Fields

FieldTypeDescription
namestringThe name of the zip file
last_modifiedDateThe last modified date for the zip file
time_createdDateThe time when the zip file was created
sizenumberThe size of the zip file
handlefileThe file handle for this zip file
filesList<ZipItem>A list of the ZipItems in the zip file

ZipFile.export()

compress.zip.ZipFile.export(base_dir: ?string) -> bool

Exports the all files in the ZipFile to files on the machine. If base_dir is given, the files will be exported into the base_dir and all directories will be created inside of base_dir as is to reflect the ZipFile’s original structure.

This function returns true if the operation succeeds or false otherwise.

Parameters

  • base_dir (?string) — Default value is os.cwd().

Returns bool

ZipArchive

class compress.zip.ZipArchive

ZipArchive provides a class for zip archive creation, manipulation and extraction.

Fields

FieldTypeDescription
comment

Constructor

compress.zip.ZipArchive(path: string, compression_method: ?number, use_zip_64: ?bool)

Parameters

  • path (string)
  • compression_method (?number) — Default value is ZIP_DEFLATE
  • use_zip_64 (?bool) — Default value is false.

ZipArchive.create_dir()

compress.zip.ZipArchive.create_dir(name: string) -> bool

Adds a directory to the zip with the given name.

Parameters

  • name (string)

Returns bool

ZipArchive.create_file()

compress.zip.ZipArchive.create_file(path: string, data: bytes|string, stat: ?dict) -> bool

Adds a file to the path specified with the contents given data.

If the stat is given, it must be a valid dictionary derived from file.stat().

Parameters

  • path (string)
  • data (bytes|string)
  • stat (?dict)

Returns bool

ZipArchive.add_file()

compress.zip.ZipArchive.add_file(path: string, destination: ?string) -> bool

Adds an existing file to the archive. If destination is given, the file will be written to the destination path in the archive.

Parameters

  • path (string)
  • destination (?string)

Returns bool

ZipArchive.add_directory()

compress.zip.ZipArchive.add_directory(directory: string, file_blacklist: ?list, ext_blacklist: ?list) -> bool

Adds the specified directory recursively to the archive and set’s it path in the archive to dir.

  • If file_blacklist is not empty, this function will ignore every file with a matching path. - If ext_blacklist is not empty, this function will ignore every file with a matching.

Parameters

  • directory (string)
  • file_blacklist (?list) — Default value is []
  • ext_blacklist (?list) — Default value is []

Returns bool

ZipArchive.read()

compress.zip.ZipArchive.read() -> ZipFile

Reads the zip file in the specified path and returns a list of ZipFile describing it’s contents.

Parameters

  • path (string)

Returns ZipFile

ZipArchive.save()

compress.zip.ZipArchive.save() -> bool

Saves the current Zip archive to file.

Parameters

  • filename (string)

Returns bool


2022, Richard Ore and The Zuri Contributors

compress.zlib

import compress

Everything here is re-exported by compress, so import compress is enough and the names are called as compress.*. Importing compress.zlib on its own works too and reaches the same definitions.

This is the Zlib submodule for the compress module.

Constants

NO_COMPRESSION

compress.NO_COMPRESSION: number = 0

No compression level.

BEST_SPEED

compress.BEST_SPEED: number = 1

Best speed compression.

BEST_COMPRESSION

compress.BEST_COMPRESSION: number = 9

Best compression level.

DEFAULT_COMPRESSION

compress.DEFAULT_COMPRESSION: number

Default compression level.

FILTERED

compress.FILTERED: number = 1

Filtered compression strategy.

HUFFMAN_ONLY

compress.HUFFMAN_ONLY = 2

huffman only compression strategy

RLE

compress.RLE: number = 3

Rle compression strategy.

FIXED

compress.FIXED: number = 4

Fixed compression strategy.

DEFAULT_STRATEGY

compress.DEFAULT_STRATEGY: number = 0

Default compression strategy.

DEFAULT_MEMORY_LEVEL

compress.DEFAULT_MEMORY_LEVEL: number = 8

Default memory level

MAX_WBITS

compress.MAX_WBITS: number = 15

Maximum windows bit.

Functions

compress()

compress.compress(data, level, strategy, wbits, memory_level) -> bytes

Compress compresses as much data as possible, and stops when the input buffer becomes empty or the output buffer becomes full.

  • The compression level must be DEFAULT_COMPRESSION, or between 0 and 9: 1 gives best speed, 9 gives best compression, 0 gives no compression at all (the input data is simply copied a block at a time). DEFAULT_COMPRESSION requests a default compromise between speed and compression (currently equivalent to level 6)

  • The wbits parameter is the base two logarithm of the window size (the size of the history buffer). It should be in the range 8..15 for this version of the library. Larger values of this parameter result in better compression at the expense of memory usage. The default value is 15.

For the current implementation of compress(), a wbits value of 8 (a window size of 256 bytes) is not supported. As a result, a request for 8 will result in 9 (a 512-byte window).

wbits can also be -8..-15 for raw compress. In this case, -wbits determines the window size. compress() will then generate raw compress data with no zlib header or trailer, and will not compute a check value.

wbits can also be greater than 15 for optional gzip encoding. Add 16 to wbits to write a simple gzip header and trailer around the compressed data instead of a zlib wrapper. The gzip header will have no file name, no extra data, no comment, no modification time (set to zero), no header crc, and the operating system will be set to the appropriate value, if the operating system can be determined by the runtime.

For raw compress or gzip encoding, a request for a 256-byte window is rejected as invalid, since only the zlib header provides a means of transmitting the window size to the uncompressor.

  • The strategy parameter is used to tune the compression algorithm. Use the value DEFAULT_STRATEGY for normal data, FILTERED for data produced by a filter (or predictor), HUFFMAN_ONLY to force Huffman encoding only (no string match), or RLE to limit match distances to one (run-length encoding). Filtered data consists mostly of small values with a somewhat random distribution. In this case, the compression algorithm is tuned to compress them better. The effect of FILTERED is to force more Huffman coding and less string matching; it is somewhat intermediate between DEFAULT_STRATEGY and HUFFMAN_ONLY. RLE is designed to be almost as fast as HUFFMAN_ONLY, but give better compression for PNG image data. The strategy parameter only affects the compression ratio but not the correctness of the compressed output even if it is not set appropriately. FIXED prevents the use of dynamic Huffman codes, allowing for a simpler decoder for special applications.

  • The memory_level parameter specifies how much memory should be allocated for the internal compression state. memory_level 1 uses minimum memory but is slow and reduces compression ratio; memory_level 9 uses maximum memory for optimal speed. The default value is 8.

{.list}

Parameters

  • data (bytes|string)
  • level (?int) — Default value is DEFAULT_COMPRESSION.
  • strategy (?int) — Default value is DEFAULT_STRATEGY.
  • wbits (?int) — Default value is MAX_WBITS.
  • memory_level (?int) — Default value is DEFAULT_MEMORY_LEVEL.

Returns bytes

decompress()

compress.decompress(data, wbits) -> bytes

Decompress decompresses as much data as possible, and stops when the input buffer becomes empty or the output buffer becomes full.

  • In this implementation, decompress() always flushes as much output as possible to the output buffer, and always uses the faster approach on the first call.

  • The wbits parameter is the base two logarithm of the maximum window size (the size of the history buffer). It should be in the range 8..15 for this version of the library. The default value is

  1. wbits must be greater than or equal to the wbits value provided to compress() while compressing, or it must be equal to 15 if compress() is used with the default values. If a compressed stream with a larger window size is given as input, decompress() will return with the error code data error instead of trying to allocate a larger window.

wbits can also be zero to request that decompress use the window size in the zlib header of the compressed stream.

wbits can also be -8..-15 for raw decompress. In this case, -wbits determines the window size. decompress() will then process raw compress data, not looking for a zlib or gzip header, not generating a check value, and not looking for any check values for comparison at the end of the stream. This is for use with other formats that use the compress compressed data format such as zip. Those formats provide their own check values. If a custom format is developed using the raw compress format for compressed data, it is recommended that a check value such as an Adler-32 or a CRC-32 be applied to the uncompressed data as is done in the zlib, gzip, and zip formats. For most applications, the zlib format should be used as is. Note that comments on the use in compress() applies to the magnitude of wbits.

wbits can also be greater than 15 for optional gzip decoding. Add 32 to wbits to enable zlib and gzip decoding with automatic header detection, or add 16 to decode only the gzip format (the zlib format will return a data error). decompress() will not automatically decode concatenated gzip streams.

  • decompress() can decompress either zlib-wrapped or gzip-wrapped compress data. If the compression uses gzip-wrapper, the correct wbits may need to be set.

Parameters

  • data (bytes|string)
  • wbits (?int) — Default value is MAX_WBITS.

Returns bytes

Classes

ZlibEncoder

class compress.ZlibEncoder < GzipEncoder

A streaming Deflate encoder.

Example:

%> import compress.zlib
%> var encoder = zlib.ZlibEncoder()
%> encoder.write('hello world')
11
%> encoder.finish()
(78 01 cb 48 cd c9 c9 57 28 cf 2f ca 49 01 00 1a 0b 04 5d)

Constructor

compress.ZlibEncoder(level)

Creates a new streaming Deflate encoder at the given level (0..=9).

Parameters

  • level (?number) — Default 1

ZlibDecoder

class compress.ZlibDecoder < GzipDecoder

Streaming zlib decompressor implemention. This decoder reads standard zlib blocks and frames produced at any zlib compression level.

Wraps a byte stream of compressed data and yields decompressed bytes.

Example:

%> import compress.zlib
%> var data = zlib.compress('hello world')
%> data
(78 9c cb 48 cd c9 c9 57 28 cf 2f ca 49 01 00 1a 0b 04 5d)
%>
%> var decoder = zlib.ZlibDecoder(data)
%> decoder.read_as_string()
hello world

This decoder is level-independent and supports standard zlib frames produced by all zlib compression levels.

Constructor

compress.ZlibDecoder(source)

Creates a Zlib decoder

Parameters

  • source (bytes)

2021, Richard Ore and Zuri contributors

compress.zstd

import compress

compress exposes this as compress.zstd, so import compress is enough and the names are called as compress.zstd.*. import compress.zstd reaches the same definitions directly.

This is the Zstd submodule for the compress module. It implements zstd compression levels -8 through 4 (Fast and DFast strategies), targeting high-speed compression for data transfers. It produces standard zstd frames decompressible by any compliant decoder.

Functions

compress()

compress.zstd.compress(data, level) -> bytes

Compress data using the default options for Zstd.

Parameters

  • data (bytes|string)
  • level (?number)

Returns bytes

decompress()

compress.zstd.decompress(data) -> bytes

Decompress a Zstd compressed data.

Parameters

  • data (bytes|string)

Returns bytes

Classes

ZstdEncoder

class compress.zstd.ZstdEncoder

Streaming zstd compressor implemention. This encoder supports levels -8 through 4 for fast transfer pipelines.

Buffers input until a full block (128 KiB) is ready, then compresses and writes it to the underlying writer. Call finish() to flush the final block, write the content checksum, and recover the writer.

Internal buffers (hash tables, sequence scratch, block encoder workspace) are allocated once and reused across blocks. To reuse them across multiple frames, call reset() instead of finish.

Example:

%> import compress.zstd
%> var encoder = zstd.ZstdEncoder()
%> encoder.write('hello world')
11
%> encoder.finish()
(28 b5 2f fd 04 00 59 00 00 68 65 6c 6c 6f 20 77 6f 72 6c 64 68 69 1e b2)

The normal match finder operates within a sliding window (512 KiB at L1). LDM (Long Distance Matching) finds matches at distances up to 1 << window_log bytes by sampling positions into a separate hash table. Useful for data with long-range repeats: log files, database dumps, source archives.

window_log controls the maximum match distance (24 = 16 MiB, 27 = 128 MiB). The main cost is memory: the encoder allocates an 8 MiB LDM hash table plus a window buffer of 1 << window_log bytes, and the decoder allocates a window buffer of the same size (declared in the frame header). At window_log 27 that is ~136 MiB on each side. On data without long-range repeats, LDM adds overhead with no ratio benefit.

ZstdEncoder own persistent hash tables and workspace buffers. Call reset() to start a new frame while reusing all allocations.

Constructor

compress.zstd.ZstdEncoder(level, window_log, ldm)

Creates a new streaming Zstd encoder at the given level (-8..=4).

Negative levels (-8 through -1) unlocks zstd’s fastest compression tiers. They can be useful when throughput matters more than ratio.

Level 0 maps to the module’s default which is currently level 1.

Positive levels spend more match-finding work for better ratios while staying in the Fast/DFast range.

Parameters

  • level (?number) — Default 1
  • window_log (?number) — Default 10
  • ldm (?bool) — Default false

ZstdEncoder.finish()

compress.zstd.ZstdEncoder.finish() -> bytes

Flushes remaining data, writes the content checksum, and returns the inner byte stream.

Returns bytes

Note: Once finish is called, the encoder can no longer be reused.

ZstdEncoder.close()

compress.zstd.ZstdEncoder.close() -> bytes

Same as finish() for file API compartibility.

Returns bytes

Note: Once finish is called, the encoder can no longer be reused.

ZstdEncoder.reset()

compress.zstd.ZstdEncoder.reset() -> bytes

Finishes the current frame and installs byte stream for the next one.

Returns the previous byte stream containing the completed frame. All internal buffers (hash tables, workspace, block scratch) stay allocated and are reused for the next frame.

Returns bytes

ZstdEncoder.write()

compress.zstd.ZstdEncoder.write(data) -> number

Writes data into this encoder’s byte stream, returning how many bytes were written.

Parameters

  • data (bytes|string)

Returns number

ZstdEncoder.write_all()

compress.zstd.ZstdEncoder.write_all(data)

Attempts to write an entire data into this encoder’s byte stream.

Parameters

  • data (bytes|string)

ZstdEncoder.flush()

compress.zstd.ZstdEncoder.flush()

Flushes this output stream, ensuring that all intermediately buffered contents reach their destination.

ZstdDecoder

class compress.zstd.ZstdDecoder

Streaming zstd decompressor implemention. This decoder reads standard zstd blocks and frames produced at any zstd compression level.

Wraps a byte stream of compressed data and yields decompressed bytes. Supports multi-frame streams and skippable frames.

Example:

%> import compress.zstd
%> var data = zstd.compress('hello world')
%> data
(28 b5 2f fd 24 0b 59 00 00 68 65 6c 6c 6f 20 77 6f 72 6c 64 68 69 1e b2)
%> 
%> var decoder = zstd.ZstdDecoder(data)
%> decoder.read_as_string()
hello world

ZstdDecoder own persistent hash tables and workspace buffers. Call reset() to start a new frame while reusing all allocations.

This decoder is level-independent and supports standard zstd frames produced by all zstd compression levels.

Constructor

compress.zstd.ZstdDecoder(source)

Creates a Zstd decoder

Parameters

  • source (bytes)

ZstdDecoder.reset()

compress.zstd.ZstdDecoder.reset(new_source)

Installs a new data source for the next frame, keeping all internal buffers allocated.

Parameters

  • new_source (bytes)

ZstdDecoder.close()

compress.zstd.ZstdDecoder.close()

Closes the decoder by resetting it into an empty stream.

ZstdDecoder.read()

compress.zstd.ZstdDecoder.read(length) -> bytes

Reads some bytes up to the amount of bytes specified by length from the current source. Returns an empty byte stream when there is no more data to read.

This function does not block or wait waiting for data, but reads as much data as is available to read when it runs.

Parameters

  • length (number)

Returns bytes

ZstdDecoder.read_exact()

compress.zstd.ZstdDecoder.read_exact(length) -> bytes

Reads the exact number of bytes from the buffer. This method will raise an error if it encounters an unexpected EOF (end of file) or there are insufficient data to read to complete the required number of bytes.

Parameters

  • length (number)

Returns bytes

ZstdDecoder.read_all()

compress.zstd.ZstdDecoder.read_all() -> bytes

Reads all remaining bytes until EOF is encountered in the source.

Returns bytes

ZstdDecoder.read_as_string()

compress.zstd.ZstdDecoder.read_as_string() -> string

Reads all remaining bytes until EOF is encountered in the source and returns the data read as a string instead of a byte stream.

Returns string


2021, Richard Ore and Zuri contributors

isolate

import isolate

High-throughput parallel concurrency backed by a pool of operating-system isolate threads.

Architectural Model: Shared-Nothing Isolates

Modern multi-core computing demands concurrency models that scale linearly with CPU core count while safeguarding applications against data races and memory corruption. Zuri addresses this by adopting a Shared-Nothing Isolate architecture (similar to the Actor model and Dart/Erlang isolates) rather than shared-memory green threads.

In Zuri, every isolate thread executes within its own self-contained Virtual Machine (VM) instance. Each isolate possesses an independent generational garbage-collected heap, nursery allocator, call frame stack, register array, and global module namespace. Isolates run concurrently across physical CPU cores without sharing heap pointers or mutable structures with other isolates or the main thread.

Inter-isolate communication is accomplished strictly across defined boundary barriers. When values, closures, or data structures are transmitted via spawn(), join(), or Channel queues, Zuri traverses the object graph and constructs an isomorphic copy on the destination isolate’s heap. This transfer barrier faithfully preserves cyclic graphs, shared object identity within the payload, and class prototypes without sharing mutable memory across thread boundaries.

Memory Footprint & Task Density

High-concurrency systems frequently face a dilemma between heavy operating system threads and complex coroutine runtimes. Zuri reconciles high task density with OS-level multi-core execution through a two-tier scheduling design:

Instead of allocating a dedicated OS thread or a dedicated runtime stack for every dispatched task, calling isolate.spawn() creates a lightweight Task descriptor (the callee and its arguments, already captured into a heap-independent snapshot) and pushes it onto a shared, mutex-guarded queue.

  • Isolate Pooling & Reuse: A fixed pool of isolate OS threads, configured via configure() and defaulting to the system’s available CPU core count is pre-warmed at startup. These isolate threads continuously drain the task queue, executing tasks sequentially within their long-lived isolate contexts. - Small, Fixed-Size Task Descriptors: Because a queued task is a captured value snapshot rather than a suspended OS thread, applications can enqueue large task bursts (e.g. 100,000+ jobs) without paying for a dedicated stack or heap per task. The fixed part of a task (its queue slot plus its join handle) is a few hundred bytes; a task’s actual total footprint on top of that scales with whatever the callee’s own arguments and captured state need to carry across the boundary, same as any other value transfer described above.

Garbage Collection & Scalability Under Load

A frequent performance bottleneck in multi-threaded runtimes is garbage collection cross-talk. When multiple threads share a single global heap, an allocation spike in one thread can trigger global Stop-The-World (STW) pauses, lock contention on shared allocation arenas, or heavy write-barrier synchronization that degrades throughput across all CPU cores.

Zuri eliminates GC interference through complete heap segregation:

  • Local Generational Collections: Every isolate isolate manages its own nursery and old-generation heaps. Young-generation minor collections and major mark-sweep cycles execute entirely on the local isolate thread. - Zero Stop-The-World Pauses Across Threads: A garbage collection cycle on Isolate A never halts, pauses, or synchronizes with Isolate B or the main thread. Compute-heavy, allocation-intensive workloads (such as parallel tree building, matrix algebra, and image rendering) achieve near-linear multi-core speedup without GC contention.

Data Safety & Race Conditions

Concurrency bugs in shared-memory architectures such as data races, torn reads, and lock inversion deadlocks often arise from uncontrolled concurrent access to mutable heap memory.

  • Data-Race Free by Construction: Because memory is physically partitioned into isolated heaps, two isolates can never concurrently mutate the same object in memory. Data races on heap objects are fundamentally impossible, eliminating the need for mutex locks or synchronized wrappers around user data structures. - Snapshot Closure Semantics: When a closure is passed to an isolate, its captured local variables and referenced global definitions are snapshot at dispatch time. The isolate receives an isolated clone of the captured state; subsequent mutations performed by the caller never affect the isolate, and isolate mutations never leak back to the caller.

Comparison with Go Goroutines

Both Go and Zuri provide lightweight concurrency abstractions, but they optimize for different runtime trade-offs:

Go utilizes Communicating Sequential Processes (CSP) built on M:N green threads multiplexed across OS threads with a single shared global heap. Zuri utilizes Thread-Pooled Shared-Nothing Isolates with deep-copy transfer barriers.

FeatureGo GoroutinesZuri Isolates
Concurrency ParadigmM:N green threads on a single shared heapThread-pooled shared-nothing isolates / actors
Memory SafetyShared memory; data races possible without mutexesData-race free by design via physical isolation
GC ImpactShared global GC cycles and allocator write barriersIndependent per-isolate GC; zero STW cross-talk
Task Density~2 KB per goroutine stacka few hundred bytes of fixed queue/handle overhead per task; pooled thread reuse
CommunicationShared-memory pointer channels or locksDeep-copied value graphs or moved native resources
Primary StrengthsUltra-high-concurrency non-blocking I/O multiplexingCompute-heavy parallel processing & task pipelines

Values That Can Cross Isolate Boundaries

Zuri’s value transfer engine supports virtually all language data types across isolate boundaries, including:

  • Primitives: nil, bool, number, bigint, string, bytes, and range. - Collections: list and dict (including nested, multidimensional, and cyclic structures). - Object-Oriented Structures: Class instances, class declarations (preserving static fields, superclasses, and method tables), and bound methods. - Executable Code: Functions, closures (capturing upvalues), anonymous lambdas, and top-level scripts.
  • Modules: imported modules and module bindings, including one a closure captured.
  • Native Handles: Ptr handles (moved linearly), Channel handles, and Isolate handles.

A module is reloaded on the receiving isolate rather than copied, so the recipient gets that isolate’s own instance of it, with its own top-level state.

Non-Transferable Types

Attempting to transmit an active operating system file handle (file(...)) across a isolate boundary raises an IsolateError.

Native Resources & Ownership Transfer

Operating system resources encapsulated within native pointers (Ptr)—such as database connections, network sockets, or compression streams—cannot be safely duplicated across threads.

Zuri enforces linear ownership transfer (move semantics) for Ptr values: - When a Ptr is sent through spawn(), join(), or a Channel, ownership is transferred to the recipient, and the sender’s handle is permanently invalidated.

  • Subsequent attempts to read or use an invalidated Ptr will raise an error. - In contrast, synchronization primitives like Channel and Isolate handles are thread-safe and may be freely shared among multiple isolates.

Failure Isolation & Fault Tolerance

A software defect or unhandled exception within one isolate is strictly contained and will never terminate peer isolates, corrupt the pool, or crash the host process.

  • Structured Error Propagation: Raised exceptions in a isolate task are captured and re-raised as IsolateError (complete with original source file and line stack traces) when join() or try_join() is invoked. - Automatic Isolate Recycling: In the rare event of an unrecoverable isolate panic, the affected isolate thread is safely retired and replaced with a fresh isolate, maintaining full pool capacity. - Un-Joined Failure Diagnostics: If an isolate fails and its handle is dropped without being joined, a diagnostic warning is emitted to ensure errors are never silently swallowed.

Cancellation Semantics

Applications can cancel running or queued isolates via Isolate.cancel().

  • Preemptive Blocking Interruption: If an isolate is blocked on a channel operation (Channel.send(), Channel.recv()), an isolate join (Isolate.join()), or a multiplexer (select(), wait_any(), wait_all()), cancel() interrupts the blocking call promptly with a IsolateCancelledError. - Cooperative Compute Polling: During active computational loops, code can cooperatively poll isolate.is_cancelled() to gracefully terminate execution.

Structured Concurrency (isolate.scope())

Unbounded background concurrency can lead to orphan tasks, leaked resources, and missed errors if spawned isolates outlive their intended context. Zuri provides Structured Concurrency through isolate.scope(body) and the Scope handle.

  • Lifetime Bounded to Lexical Scope: Any isolate spawned via s.spawn() within a scope() block is guaranteed to complete before scope() returns. - Fail-Fast Automatic Cancellation: If the scope’s body raises an exception or if any child isolate fails, all other sibling isolates in that scope are automatically sent a cancellation signal (cancel()), preventing wasted computation. - Clean Teardown & Error Prioritization: scope() waits for all cancelled siblings to finish shutting down before propagating the root failure to the caller.

Examples

Basic Task Spawning

import isolate

def calculate_primes(limit) {
  var count = 0
  iter var n = 2; n <= limit; n++ {
    var is_prime = true
    iter var d = 2; d * d <= n; d++ {
      if n % d == 0 {
        is_prime = false
        break
      }
    }
    if is_prime count++
  }
  return count
}

var task = isolate.spawn(calculate_primes, 100_000)
echo "Primes found: ${task.join()}"

Parallel Batch Mapping

import isolate

def square(n) {
  return n * n
}

var numbers = [1, 2, 3, 4, 5, 6, 7, 8]
var results = isolate.map(square, numbers)
echo "Squares: ${results}"

Structured Concurrency (scope)

import isolate

var total = isolate.scope(def(s) {
  var w1 = s.spawn(def() { return 10 * 2 })
  var w2 = s.spawn(def() { return 20 * 2 })
  return w1.join() + w2.join()
})
echo "Total: ${total}"

Publish-Subscribe (Broadcast)

import isolate

var hub = isolate.broadcast()
var sub1 = hub.subscribe()
var sub2 = hub.subscribe()

isolate.spawn(def(c) { echo "Isolate 1: ${c.recv()}" }, sub1)
isolate.spawn(def(c) { echo "Isolate 2: ${c.recv()}" }, sub2)

hub.send("Hello Subscribers!")
hub.close()

The isolate API

Every public name in isolate, wherever it is declared. Each links to the page that documents it.

NameKindSummary
isolate.BroadcastclassA one-to-many publish-subscribe message distribution hub.
isolate.ChannelclassA thread-safe, multi-producer multi-consumer (MPMC) message queue.
isolate.IsolateclassA handle to an isolate, returned by spawn().
isolate.IsolateCancelledErrorclassRaised from Isolate.join(), Channel.send(), Channel.recv(), isolate.wait_any(), isolate.wait_all(),…
isolate.IsolateErrorclassRaised when an isolate’s function raises an uncaught error, a Channel operation fails, or a value cannot…
isolate.IsolateTimeoutErrorclassRaised when Isolate.join(), Channel.send(), or Channel.recv() is given a timeout and it elapses…
isolate.ScopeclassA structured concurrency supervisor and task nursery.
isolate.active_countfunction
isolate.broadcastfunctionCreates a new one-to-many Broadcast distribution hub.
isolate.channelfunctionCreates a new thread-safe Channel.
isolate.configurefunctionSets how many isolate threads the pool may run at once.
isolate.cpu_countfunction
isolate.is_cancelledfunctionChecks whether the currently running isolate has been asked to stop via its handle’s cancel().
isolate.is_shutdownfunction
isolate.mapfunctionRuns fn once per item in items, in parallel across the isolate pool, and returns the results in the same…
isolate.pool_sizefunctionHow many isolate threads the pool may run at once.
isolate.queued_countfunction
isolate.scopefunctionExecutes body(s) within a structured concurrency scope.
isolate.selectfunctionMultiplexes across multiple channels, blocking until at least one channel is ready to deliver a value or is…
isolate.shutdownfunctionStops the pool from accepting any further spawn() calls, then blocks until every isolate already spawned;…
isolate.spawnfunction
isolate.spawn_namedfunctionSame as spawn(), but gives the isolate a name; purely a debugging label, read back via Isolate.name()…
isolate.started_countfunctionHow many isolate threads the pool has started so far.
isolate.wait_allfunctionBlocks until every one of isolates has finished, then returns their results in the same order as isolates.
isolate.wait_anyfunctionBlocks until at least one of isolates finishes, and returns that one.

Submodules

ModuleReached asSummary
isolate.broadcastisolate.broadcast.*One-to-many (fan-out) publish-subscribe message distribution for isolate isolates.
isolate.channelisolate.channel.*Thread-safe, multi-producer multi-consumer (MPMC) communication queues for passing values and messages across…
isolate.errorisolate.error.*IsolateError, kept in its own leaf module (no imports of its own) so both index and channel can depend…

Functions

spawn()

isolate.spawn(fn: function, ...args: list)

spawn_named()

isolate.spawn_named(name: string, fn: function, ...args: list) -> Isolate

Same as spawn(), but gives the isolate a name; purely a debugging label, read back via Isolate.name() and included in the “unobserved failure” warning (see the module docs’ “Failure isolation” section) if this one fails and is never join()ed or try_join()ed.

Parameters

  • name (string)
  • fn (function) — a closure, bound method, or class method.
  • args (...any)

Returns Isolate

Raises TypeError if fn is not callable

Raises IsolateError if fn is a native function or class, fn/args cannot cross an isolate boundary, or the pool has been shutdown()

scope()

isolate.scope(body: function)

Executes body(s) within a structured concurrency scope.

isolate.scope() establishes a structured task boundary, guaranteeing that all isolates spawned via s.spawn() or s.spawn_named() finish before scope() returns.

Lifecycle & Error Semantics

  1. Completion Guarantee: scope() blocks until body(s) returns and all child isolates have terminated. 2. Automatic Cancellation: If body raises an exception or if any child isolate fails, all other still-running children in the scope are automatically sent a cancellation signal (cancel()). 3. Graceful Teardown: scope() waits for all cancelled isolates to cleanly finish their teardown before returning or re-raising. 4. Error Priority: If body raised an exception, body’s exception is re-raised. Otherwise, the earliest child failure (excluding secondary IsolateCancelledError cascades) is re-raised.
import isolate

var result = isolate.scope(def(s) {
  var task1 = s.spawn(def() { return 100 })
  var task2 = s.spawn(def() { return 200 })
  return task1.join() + task2.join()
})
echo "Total: ${result}" # 300

Parameters

  • body (function) — A callback function def(s) that receives a fresh Scope.

Returns — any: The return value of body(s).

Raises IsolateError if a child isolate failed and body did not raise.

Raises any: Any exception raised by body(s).

configure()

isolate.configure(threads: int)

Sets how many isolate threads the pool may run at once. Has no effect if the pool has already started; its size is fixed once the first isolate runs.

The size is a ceiling. A thread is started only when an isolate is waiting and every thread already started is busy, so a pool sized for peak load costs only the threads the load actually needed.

Parameters

  • threads (int)

Returns — bool: true if applied, false if the pool had already started.

Raises ValueError if threads is less than 1

pool_size()

isolate.pool_size() -> int

How many isolate threads the pool may run at once. Starts the pool, with whatever size configure() set or the machine’s CPU count otherwise, if it has not started yet.

Returns int

started_count()

isolate.started_count() -> int

How many isolate threads the pool has started so far. Threads start as isolates need them, so this grows with the most isolates ever waiting at once and never passes pool_size().

Returns int

cpu_count()

isolate.cpu_count()

Returns — int: the number of logical CPUs this machine reports.

shutdown()

isolate.shutdown(timeout: ?number)

Stops the pool from accepting any further spawn() calls, then blocks until every isolate already spawned; queued or already running; has finished. Nothing in flight is abandoned.

Permanent: once called, every spawn() for the rest of the process raises IsolateError, even if this call itself later times out.

Parameters

  • timeout (number) — seconds to wait before giving up. Waits indefinitely if omitted.

Raises IsolateTimeoutError if timeout elapses with isolates still in flight

active_count()

isolate.active_count()

Returns — int: isolates an isolate is actively running right now. Does not include ones still queued_count().

queued_count()

isolate.queued_count()

Returns — int: isolates spawned but not yet picked up by a isolate.

is_shutdown()

isolate.is_shutdown()

Returns — bool: true if shutdown() has been called.

wait_any()

isolate.wait_any(isolates: list, timeout: ?number)

Blocks until at least one of isolates finishes, and returns that one. Useful for reacting to whichever of several tasks completes first, rather than joining them one at a time in a fixed order.

Parameters

  • isolates ([Isolate]) — must not be empty.
  • timeout (number) — seconds to wait before giving up. Waits indefinitely if omitted.

Returns — Isolate: whichever one finished first.

Raises ValueError if isolates is empty

Raises IsolateTimeoutError if timeout elapses before any of them finish

Raises IsolateCancelledError if the CALLING isolate is cancelled while blocked here

wait_all()

isolate.wait_all(isolates: list, timeout: ?number) -> list

Blocks until every one of isolates has finished, then returns their results in the same order as isolates.

Parameters

  • isolates ([Isolate]) — may be empty (returns []).
  • timeout (number) — seconds to wait for all of them combined, not per isolate. Waits indefinitely if omitted.

Returns list

Raises IsolateError from the first one (in isolates order) whose function raised, once every one of them has finished

Raises IsolateTimeoutError if timeout elapses before all of them finish

Raises IsolateCancelledError if the CALLING isolate is cancelled while blocked here

map()

isolate.map(fn: function, items: list, timeout: ?number) -> list

Runs fn once per item in items, in parallel across the isolate pool, and returns the results in the same order as items. Equivalent to spawning an isolate per item and passing the handles to wait_all().

Parameters

  • fn (function) — a closure, bound method, or class method.
  • items (list)
  • timeout (number) — seconds to wait for the whole batch combined. Waits indefinitely if omitted.

Returns list

Raises IsolateError from the first item (in items order) whose call raised, once every call has finished

Raises IsolateTimeoutError if timeout elapses before every call finishes

Raises IsolateCancelledError if the CALLING isolate is cancelled while blocked here

is_cancelled()

isolate.is_cancelled() -> bool

Checks whether the currently running isolate has been asked to stop via its handle’s cancel(). Meant to be polled periodically by long-running or looping isolate code so it can return early.

Calling this outside an isolate (from the main script, or from another thread) always returns false.

Returns bool

Classes

Isolate

class isolate.Isolate

A handle to an isolate, returned by spawn().

Constructor

isolate.Isolate(ptr)

Isolate.join()

isolate.Isolate.join(timeout: ?number) -> any

Blocks until the isolate finishes and returns its result. May be called more than once; later calls return immediately with the same result.

Parameters

  • timeout (number) — seconds to wait before giving up. Waits indefinitely if omitted.

Returns any

Raises IsolateError if the isolate’s function raised an uncaught error

Raises IsolateTimeoutError if timeout elapses first

Raises IsolateCancelledError if the CALLING isolate (not this one) is cancelled while blocked here

Isolate.cancel()

isolate.Isolate.cancel()

Requests that the isolate stop. Has no effect on an isolate that has already finished.

If the isolate is currently blocked in Channel.send()/ recv(), Isolate.join(), wait_any(), wait_all(), or select(), that call is interrupted within roughly 50ms and raises IsolateCancelledError.

Otherwise; plain, non-blocking code; cancellation is cooperative: it does not interrupt code that’s already running. The isolate’s own function must check isolate.is_cancelled() and return on its own.

Isolate.is_cancelled()

isolate.Isolate.is_cancelled()

Returns — bool: true if cancel() has been called on this isolate.

Isolate.name()

isolate.Isolate.name()

Returns — ?string: the name given via spawn_named(), or nil if this isolate was started with plain spawn().

Isolate.try_join()

isolate.Isolate.try_join() -> any

Returns the isolate’s result, or nil if it has not finished yet. Does not block.

Returns any

Raises IsolateError if the isolate’s function raised an uncaught error

Note: an isolate that returns nil is indistinguishable from one still running. Use is_done() to tell them apart.

Isolate.is_done()

isolate.Isolate.is_done()

Returns — bool: true if the isolate has finished.

Isolate.status()

isolate.Isolate.status()

Unlike try_join(), never marks a failure as “observed”; safe to poll purely to check progress without silencing the warning a failure that’s never actually join()ed/try_join()ed would otherwise get (see the module docs’ “Failure isolation” section).

Returns — string: 'pending' while still running, 'done' once finished successfully, or 'error' once finished with an uncaught error.

Isolate.handle()

isolate.Isolate.handle()

This isolate’s underlying native handle, needed by wait_any() to operate on a plain list of Isolates from outside the class. Not meant for direct use.

Scope

class isolate.Scope

A structured concurrency supervisor and task nursery.

Scope manages the lifecycle of a set of concurrent child isolates spawned within a isolate.scope() block. It guarantees that:

  1. Lifetime Bounding: All isolates spawned via s.spawn() or s.spawn_named() are tracked by the scope and cannot outlive the enclosing scope(body) block. 2. Fail-Fast Error Handling: If any child isolate raises an unhandled error (or if the scope body itself fails), all remaining sibling isolates are automatically cancelled (cancel()), preventing wasted computation. 3. Deterministic Teardown: scope() waits for all active and cancelled children to complete execution before re-raising the root failure or returning the body’s result.

Scope instances cannot be constructed directly; they are created and passed to the callback function by isolate.scope(body).

Examples
Parallel Task Aggregation
import isolate

var summary = isolate.scope(@(s) {
  var task_a = s.spawn(@() { return "Task A complete" })
  var task_b = s.spawn(@() { return "Task B complete" })
  return "${task_a.join()} & ${task_b.join()}"
})
echo summary # "Task A complete & Task B complete"
Fail-Fast Child Cancellation
import isolate

catch {
  isolate.scope(def(s) {
    # Spawn an isolate that fails quickly
    s.spawn(def() {
      raise "critical error in child"
    })
    # Sibling isolate is automatically cancelled as soon as child 1 fails
    s.spawn(def() {
      iter var i = 0; i < 1_000_000; i++ {
        if isolate.is_cancelled() break
      }
    })
  })
} as err {
  echo "Scope caught child failure: ${err.message}"
}

Constructor

isolate.Scope()

Scope.spawn()

isolate.Scope.spawn(fn: function, ...args: list)

Spawns a concurrent isolate task tracked by this scope.

The spawned isolate is bound to this scope’s lifecycle: isolate.scope() will not return until this isolate has completed. If this isolate fails, all sibling isolates in the scope are automatically cancelled.

isolate.scope(def(s) {
  var w = s.spawn(def(x, y) { return x + y }, 10, 20)
  echo "Result: ${w.join()}" # 30
})

Parameters

  • fn (function) — A callable function, closure, or method to execute.
  • args (...any) — Arguments to pass to fn.

Returns — Isolate: A handle to the spawned isolate.

Raises TypeError if fn is not callable.

Raises IsolateError if fn is a native function or class, if fn/args cannot cross isolate boundaries, or if the isolate pool has shut down.

Scope.spawn_named()

isolate.Scope.spawn_named(name: string, fn: function, ...args: list)

Spawns a named concurrent isolate task tracked by this scope.

Operates identically to Scope.spawn(), but assigns an identifier name used in diagnostic error messages and debugging traces.

isolate.scope(def(s) {
  var w = s.spawn_named("fetch-user", fetch_user, user_id)
  return w.join()
})

Parameters

  • name (string) — A human-readable identifier for the isolate.
  • fn (function) — A callable function, closure, or method to execute.
  • args (...any) — Arguments to pass to fn.

Returns — Isolate: A handle to the spawned isolate.

Raises TypeError if fn is not callable.

Raises IsolateError if fn is a native function or class, if fn/args cannot cross isolate boundaries, or if the isolate pool has shut down.

Scope.children()

isolate.Scope.children()

Returns all Isolate handles spawned within this scope.

isolate.scope(def(s) {
  s.spawn(task1)
  s.spawn(task2)

  for child in s.children() {
    echo "Child isolate active"
  }
})

Returns — list: A list of Isolate instances in the order they were spawned.


2026, Richard Ore and Zuri contributors

isolate.broadcast

import isolate.broadcast

isolate lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled isolate.broadcast.* needs import isolate.broadcast.

One-to-many (fan-out) publish-subscribe message distribution for isolate isolates.

Overview

While a standard Channel provides one-to-one or many-to-one message delivery (where each sent message is consumed by exactly one receiver), a Broadcast hub provides one-to-many publish-subscribe semantics. Every value published via Broadcast.send() is cloned and delivered to all currently active subscriber channels.

Broadcast is built on top of Zuri’s thread-safe Channel primitives, inheriting the same deep-copy memory isolation guarantees: published values cross isolate barriers cleanly without shared mutable state.

Re-exported by the isolate module, so import isolate is sufficient to use isolate.Broadcast and isolate.broadcast().

Subscription & Message Delivery

  • Dynamic Subscription: Calling broadcast.subscribe() registers a new subscriber and returns a dedicated Channel for that subscriber. - No Historical Replay: Subscribers only receive messages published after they subscribe. Past messages published prior to subscription are not replayed. - Independent Consumer Queues: Each subscriber possesses its own queue buffer. One slow consumer reading slowly from its channel does not prevent fast consumers from receiving their copies at full speed.

Backpressure & Drop-on-Full Semantics

  • Unbounded Subscriber Channels (capacity: 0 or nil): Each subscriber channel grows dynamically. Published messages are always delivered to all subscribers without dropping. - Bounded Subscriber Channels (capacity: n > 0): When a subscriber’s individual channel buffer reaches capacity n, the broadcaster uses a non-blocking send (timeout: 0). If the buffer is full, the message is dropped specifically for that slow subscriber rather than blocking the publisher or stalling other subscribers. This prevents a single lagging isolate from causing system-wide backpressure or deadlocks.

Lifecycle & Graceful Teardown

  • Unsubscribing: A subscriber can be removed by calling broadcast.unsubscribe(ch). The channel stops receiving new broadcasts, but any previously queued messages remain readable. - Closing: Calling broadcast.close() closes the broadcast hub and all active subscriber channels. Subscribed isolates will drain their remaining buffered messages, after which subsequent recv() calls return nil. Attempting to call send() or subscribe() on a closed broadcast raises a IsolateError. - Automatic Pruning: If a subscriber closes its channel independently, the broadcast hub automatically detects the closed channel during the next send() and prunes it from the subscriber registry.

Examples

1. Publish-Subscribe Event Fan-Out

import isolate

var hub = isolate.broadcast()

# Isolate 1: Logging service
var sub1 = hub.subscribe()
var logger = isolate.spawn(def(ch) {
  while true {
    var msg = ch.recv()
    if msg == nil break
    echo "[Logger] ${msg}"
  }
}, sub1)

# Isolate 2: Metrics service
var sub2 = hub.subscribe()
var metrics = isolate.spawn(def(ch) {
  var count = 0
  while true {
    var msg = ch.recv()
    if msg == nil break
    count++
  }
  return count
}, sub2)

# Broadcast events to all subscribers
hub.send("event: user_login")
hub.send("event: page_view")
hub.close()

logger.join()
echo "Total metrics events: ${metrics.join()}"

2. Dynamic Unsubscription

import isolate

var hub = isolate.broadcast()
var sub = hub.subscribe()

hub.send("first message")
echo sub.recv() # "first message"

hub.unsubscribe(sub)
hub.send("second message") # Not delivered to sub

echo sub.try_recv() # nil
hub.close()

Functions

broadcast()

isolate.broadcast(capacity: ?int) -> Broadcast

Creates a new one-to-many Broadcast distribution hub.

import isolate
var hub = isolate.broadcast()

Parameters

  • capacity (?int) — Per-subscriber buffer capacity (nil or 0 means unbounded).

Returns Broadcast

Classes

Broadcast

class isolate.Broadcast

A one-to-many publish-subscribe message distribution hub.

Constructor

isolate.Broadcast(capacity: ?int)

Constructs a new Broadcast hub.

import isolate
var hub = isolate.broadcast()      # Unbounded subscriber buffers
var bounded_hub = isolate.broadcast(50) # Drop on full after 50 items

Parameters

  • capacity (?int) — Buffer capacity for each subscriber channel created via subscribe(). nil or 0 (the default) creates unbounded channels. When bounded, slow subscribers whose buffers are full will drop missed broadcasts rather than blocking the publisher.

Broadcast.subscribe()

isolate.Broadcast.subscribe()

Registers a new subscriber to this broadcast hub and returns a dedicated Channel for receiving published messages.

Subscribers only receive messages sent after the moment of subscription.

var sub_ch = hub.subscribe()
isolate.spawn(isolate_func, sub_ch)

Returns — Channel: A channel delivering published broadcasts.

Raises IsolateError if the broadcast hub has already been closed.

Broadcast.unsubscribe()

isolate.Broadcast.unsubscribe(channel)

Unregisters a subscriber channel from this broadcast hub.

The channel immediately stops receiving new broadcasts. Any messages already buffered in the channel remain readable via recv().

If channel was not subscribed or was already removed, this call is a no-op.

hub.unsubscribe(sub_ch)

Parameters

  • channel (Channel) — The subscriber channel returned by subscribe().

Broadcast.send()

isolate.Broadcast.send(value)

Delivers value to all currently registered subscribers.

If a subscriber’s channel has a bounded capacity and is currently full, the message is dropped for that specific subscriber without blocking the sender or delaying other subscribers.

Any subscriber channels discovered to be closed are automatically pruned.

hub.send("alert: system update")

Parameters

  • value (any) — The value or object graph to broadcast.

Raises IsolateError if the broadcast hub has been closed.

Broadcast.close()

isolate.Broadcast.close()

Closes the broadcast hub and all active subscriber channels.

Subscribed channels can continue draining any remaining buffered messages. Once drained, subsequent calls to recv() on subscriber channels will return nil. Calling send() or subscribe() after closing raises IsolateError.

hub.close()

Broadcast.is_closed()

isolate.Broadcast.is_closed()

Checks whether the broadcast hub has been closed.

if !hub.is_closed() {
  hub.send("active")
}

Returns — bool: true if closed, false otherwise.

Broadcast.subscriber_count()

isolate.Broadcast.subscriber_count()

Returns the current number of active subscribers registered to this hub.

echo "Subscribers listening: ${hub.subscriber_count()}"

Returns — int: Count of registered subscriber channels.


2026, Richard Ore and Zuri contributors

isolate.channel

import isolate.channel

isolate lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled isolate.channel.* needs import isolate.channel.

Thread-safe, multi-producer multi-consumer (MPMC) communication queues for passing values and messages across isolate isolates.

Overview

Channels provide synchronized, data-race-free message passing between concurrent isolate isolates. Because Zuri isolates execute within isolated heaps, sending a value over a Channel deep-copies the object graph across the boundary barrier, preserving internal references and graph topologies without sharing mutable memory.

Channel handles are re-exported by the isolate module, making them accessible directly via isolate.Channel or isolate.channel().

Channel Types & Backpressure

  • Unbounded Channels (capacity: 0 or nil): Messages are enqueued without blocking send(). The queue dynamically grows as needed. - Bounded Channels (capacity: n > 0): The channel holds at most n unread messages. Calling send() when the queue is full blocks the sending isolate until a consumer calls recv(), providing automatic backpressure to prevent producer tasks from exhausting system memory.

Channel Lifecycle

  • Sending: Channel.send(val, timeout) pushes a value into the channel. If the channel is bounded and full, the call blocks until capacity is available or timeout elapses. - Receiving: Channel.recv(timeout) blocks until a value arrives. If the channel is closed and all queued items have been drained, recv() returns nil. - Non-blocking Peek: Channel.try_recv() retrieves the next value immediately without blocking, returning nil if the channel is currently empty. - Closing: Channel.close() shuts down the channel for subsequent sends. Messages already in the queue remain readable until drained. Subsequent calls to send() on a closed channel raise a IsolateError.

Multiplexing with select()

The select() function allows an isolate to wait on multiple channels simultaneously. It blocks until at least one channel is ready to deliver a value (or is closed), returning a pair [ready_channel, value].

Examples

1. Producer-Consumer Pipeline

import isolate

var ch = isolate.channel(10)  # bounded channel with capacity 10

def producer(out_ch) {
  iter var i = 1; i <= 5; i++ {
    out_ch.send("item-${i}")
  }
  out_ch.close()
}

var producer_task = isolate.spawn(producer, ch)

while true {
  var msg = ch.recv()
  if msg == nil {
    break  # the channel is closed and empty
  }

  echo "Received: ${msg}"
}

producer_task.join()

2. Fan-Out / Fan-In Isolate Pool

import isolate

var results = isolate.channel()

def isolate_job(isolate_id, count, out_ch) {
  iter var i = 1; i <= count; i++ {
    out_ch.send("isolate-${isolate_id}-result-${i}")
  }
}

var w1 = isolate.spawn(isolate_job, 1, 3, results)
var w2 = isolate.spawn(isolate_job, 2, 3, results)

iter var i = 0; i < 6; i++ {
  echo results.recv()
}

w1.join()
w2.join()

3. Multiplexing Channels with select()

import isolate

var ch1 = isolate.channel()
var ch2 = isolate.channel()

isolate.spawn(@(c) { c.send("from ch1") }, ch1)
isolate.spawn(@(c) { c.send("from ch2") }, ch2)

iter var i = 0; i < 2; i++ {
  var result = isolate.select([ch1, ch2], 1.0)
  var ready_ch = result[0]
  var value = result[1]
  echo "Got: ${value}"
}

Functions

channel()

isolate.channel(capacity: ?int) -> Channel

Creates a new thread-safe Channel.

import isolate
var unbounded = isolate.channel()
var bounded = isolate.channel(50)

Parameters

  • capacity (?int) — Maximum buffered messages. nil or 0 means unbounded.

Returns Channel

select()

isolate.select(channels: list, timeout: ?number)

Multiplexes across multiple channels, blocking until at least one channel is ready to deliver a value or is closed.

The ready value is consumed from the selected channel exactly as if recv() were called on it directly.

If multiple channels are ready simultaneously, ties are resolved in favor of the channel with the lower index in channels.

import isolate
var ch1 = isolate.channel()
var ch2 = isolate.channel()

var res = isolate.select([ch1, ch2], 1.5)
var active_ch = res[0]
var msg = res[1]
echo "Received ${msg} from channel"

Parameters

  • channels (list) — A non-empty list of Channel instances.
  • timeout (?number) — Maximum seconds to wait before timing out. Waits indefinitely if omitted or nil.

Returns — list: A two-element list [ready_channel, value], where value is the message received (nil if the channel was closed and drained).

Raises ValueError if channels is empty.

Raises IsolateTimeoutError if timeout seconds elapse before any channel is ready.

Raises IsolateCancelledError if the calling isolate is cancelled while blocked.

Classes

Channel

class isolate.Channel

A thread-safe, multi-producer multi-consumer (MPMC) message queue.

Constructor

isolate.Channel(capacity: ?int)

Constructs a new Channel with the specified capacity limit.

import isolate
var unbounded = isolate.channel()
var bounded = isolate.channel(100)

Parameters

  • capacity (?int) — Maximum number of buffered items before send() blocks for backpressure. nil or 0 creates an unbounded channel.

Raises ValueError if capacity is negative.

Channel.send()

isolate.Channel.send(value, timeout: ?number)

Sends value into the channel.

If the channel is bounded and currently full, this call blocks the calling isolate until space becomes available, the timeout elapses, or the caller is cancelled.

ch.send("data payload")
ch.send(42, 0.5)  # wait at most 500ms

Parameters

  • value (any) — The value or object graph to send.
  • timeout (?number) — Maximum seconds to wait before timing out. Waits indefinitely if omitted or nil.

Raises IsolateError if the channel has been closed.

Raises IsolateTimeoutError if timeout seconds elapse before space is available.

Raises IsolateCancelledError if the calling isolate is cancelled while blocked.

Channel.recv()

isolate.Channel.recv(timeout: ?number)

Blocks until a message is available and retrieves it.

If the channel is closed and all buffered messages have been received, recv() returns nil.

var item = ch.recv()
var timed_item = ch.recv(2.0)

Parameters

  • timeout (?number) — Maximum seconds to wait for a message. Waits indefinitely if omitted or nil.

Returns — any: The received value, or nil if the channel is closed and empty.

Raises IsolateTimeoutError if timeout seconds elapse before a value is received.

Raises IsolateCancelledError if the calling isolate is cancelled while blocked.

Channel.try_recv()

isolate.Channel.try_recv()

Attempts to receive a value without blocking.

var item = ch.try_recv()
if item != nil {
  echo "Processed: ${item}"
}

Returns — any: The next available message, or nil if the channel is currently empty or closed.

Channel.close()

isolate.Channel.close()

Closes the channel.

Buffered messages remain available for consumers to read via recv() or try_recv(). Calling send() after closing raises IsolateError.

ch.close()

Channel.is_closed()

isolate.Channel.is_closed()

Checks whether the channel has been closed.

if !ch.is_closed() {
  ch.send("active")
}

Returns — bool: true if the channel is closed, false otherwise.

Channel.length()

isolate.Channel.length()

Returns the current number of buffered, unread messages in the channel.

var pending = ch.length()

Returns — int: Number of queued items.

Channel.handle()

isolate.Channel.handle()

Returns the underlying native channel pointer.

Used internally by select(). Not intended for direct application usage.


2026, Richard Ore and Zuri contributors

isolate.error

import isolate.error

isolate lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled isolate.error.* needs import isolate.error.

IsolateError, kept in its own leaf module (no imports of its own) so both index and channel can depend on it without a circular import between them. Re-exported by isolate.

Classes

IsolateError

class isolate.IsolateError < Error

Raised when an isolate’s function raises an uncaught error, a Channel operation fails, or a value cannot cross an isolate boundary.

Constructor

isolate.IsolateError(message)

IsolateTimeoutError

class isolate.IsolateTimeoutError < IsolateError

Raised when Isolate.join(), Channel.send(), or Channel.recv() is given a timeout and it elapses before the operation completes.

Constructor

isolate.IsolateTimeoutError(message)

IsolateCancelledError

class isolate.IsolateCancelledError < IsolateError

Raised from Isolate.join(), Channel.send(), Channel.recv(), isolate.wait_any(), isolate.wait_all(), or isolate.select() when the CALLING isolate; the one blocked in that call, not whatever it was waiting on; is cancel()ed while still waiting.

Constructor

isolate.IsolateCancelledError(message)

imagine

import imagine

Reading, writing, drawing and transforming raster images.

imagine covers the whole path an image takes through a program: decoding it from a file or an upload, resizing and cropping it, adjusting its colour, drawing shapes and text onto it, compositing several together, and writing the result back out in whatever format suits.

import imagine { Image, Font, LANCZOS }

Image.open('photo.jpg')
  .thumbnail(600, 600, LANCZOS)
  .sharpen(0.6)
  .save('thumb.webp', { quality: 82 })

The shape of the API

Almost everything is a method on [[imagine.Image]], and almost everything returns an image, so operations chain. There is one rule worth learning up front:

  • Operations that change the image’s size return a new image. resize(), crop(), rotate(), flip() and pad() leave the original alone.
  • Everything else changes the image in place. Drawing, filters and compositing modify the image you called them on and hand it back for chaining.

Call clone() when you want to keep an image before filtering it.

Colours

Anywhere a colour is expected you can write a [[imagine.Color]], a hexadecimal string, a CSS colour name, a packed 0xRRGGBBAA number, or a list of channels. These are all the same red:

image.fill(Color(255, 0, 0))
image.fill('#ff0000')
image.fill('red')
image.fill(0xFF0000FF)
image.fill([255, 0, 0])

Alpha runs 0 (transparent) to 255 (opaque), as in CSS and PNG. The colour space conversions come from [[colors]], so hue is in degrees and saturation and lightness are in percentage points.

Pixels

An image is 8-bit RGBA with straight (not premultiplied) alpha, laid out row by row with no padding, so pixel (x, y) begins at byte (y * width + x) * 4. [[imagine.Canvas.pixels]] hands back that buffer live, which is the fastest way to write an operation this module does not provide:

var buffer = image.pixels()
var total = buffer.length()

iter var at = 0; at < total; at += 4 {
  buffer[at] = 255 - buffer[at]
}

Formats

Read: PNG, JPEG, GIF, BMP, TIFF, TGA, WebP, QOI, ICO, PNM, WBMP. Write: all of those plus AVIF.

AVIF is write-only, and TGA carries no signature of its own so it can only be identified by its file extension or by naming the format outright. [[imagine.capabilities]] reports what this build can actually do, so a program can check rather than guess.

Animated GIF and animated WebP are read through [[imagine.Animation]], and animated GIF can be written.

Drawing

Every filled shape becomes a polygon and goes through one anti-aliased scanline rasterizer, and every outline becomes the polygon around its stroke and goes through the same one. Shapes this module does not name can be drawn by handing [[imagine.Canvas.fill_polygon]] the points.

import imagine { Image, Color }

Image(300, 200, 'white')
  .fill_rounded_rect(20, 20, 260, 160, 16, '#4f46e5')
  .circle(150, 100, 50, 'white', { thickness: 4 })
  .save('card.png')

Text

Text needs a font, loaded from a file with [[imagine.Font.load]] or found on the machine with [[imagine.Font.system]]. Glyphs are placed by advance width with kerning; complex scripts that need shaping are not supported, and the Font documentation says exactly what that rules out.

Memory

Four bytes per pixel, so a 6000x4000 photograph occupies 96 MB however small its file was. For images whose size you do not control, [[imagine.probe]] reads the dimensions out of the header without decoding anything.

var header = imagine.probe(upload)

if header == nil or header.width * header.height > 40000000 {
  raise Exception('image is missing or too large')
}

The imagine API

Every public name in imagine, wherever it is declared. Each links to the page that documents it.

NameKindSummary
imagine.ALIGN_CENTERconstantLines of a multi-line string are centred against each other.
imagine.ALIGN_LEFTconstantLines of a multi-line string start at the same left edge.
imagine.ALIGN_RIGHTconstantLines of a multi-line string end at the same right edge.
imagine.AVIFconstantAVIF.
imagine.AnimationclassA sequence of images with a delay between each, read from or written to an animated format.
imagine.BICUBICconstantBicubic (Catmull-Rom).
imagine.BILINEARconstantBilinear.
imagine.BLACKconstantOpaque black.
imagine.BLEND_ADDconstantAdds the channels, clamped at white.
imagine.BLEND_COLOR_BURNconstantDarkens the backdrop toward the source.
imagine.BLEND_COLOR_DODGEconstantBrightens the backdrop toward the source.
imagine.BLEND_DARKENconstantKeeps whichever channel is darker.
imagine.BLEND_DIFFERENCEconstantThe absolute difference between the two colours.
imagine.BLEND_EXCLUSIONconstantLike difference, but lower contrast in the midtones.
imagine.BLEND_HARD_LIGHTconstantOverlay with the roles of source and backdrop swapped.
imagine.BLEND_LIGHTENconstantKeeps whichever channel is lighter.
imagine.BLEND_MULTIPLYconstantMultiplies the two colours.
imagine.BLEND_NORMALconstantOrdinary alpha compositing: the source is painted over the destination according to its alpha.
imagine.BLEND_OVERLAYconstantMultiply where the backdrop is dark, screen where it is light.
imagine.BLEND_SCREENconstantThe inverse of multiply.
imagine.BLEND_SOFT_LIGHTconstantA gentler hard light, as if the source were a diffuse spotlight.
imagine.BLEND_SUBTRACTconstantSubtracts the source from the backdrop, clamped at black.
imagine.BLUEconstantOpaque pure blue.
imagine.BMPconstantBMP.
imagine.BOTTOMconstant
imagine.BOTTOM_LEFTconstant
imagine.BOTTOM_RIGHTconstant
imagine.BoundsErrorclassRaised when a rectangle, crop or resize falls outside the image, or when a dimension is zero or negative.
imagine.CAP_BUTTconstantThe stroke stops dead at its endpoint.
imagine.CAP_ROUNDconstantThe stroke ends in a half-disc, so it reaches half its width past the endpoint.
imagine.CAP_SQUAREconstantThe stroke ends in a square, so it reaches half its width past the endpoint, the same distance a round cap…
imagine.CENTERconstant
imagine.CYANconstantOpaque cyan.
imagine.CanvasclassA mutable RGBA pixel surface and everything that draws onto one.
imagine.ColorclassAn 8-bit RGBA colour.
imagine.DecodeErrorclassRaised when image data cannot be read: the bytes are not an image at all, the format is one this build cannot…
imagine.EDGE_CLAMPconstantPixels off the edge take the value of the nearest edge pixel.
imagine.EDGE_TRANSPARENTconstantPixels off the edge are treated as transparent black.
imagine.EDGE_WRAPconstantPixels off the edge wrap to the opposite side.
imagine.EncodeErrorclassRaised when an image cannot be written in the requested format, either because this build has no encoder for…
imagine.FLIP_BOTHconstantMirror both ways at once, which is the same as rotating 180 degrees.
imagine.FLIP_HORIZONTALconstantMirror left to right.
imagine.FLIP_VERTICALconstantMirror top to bottom.
imagine.FontclassA typeface at a particular size, ready to draw with.
imagine.FontErrorclassRaised when a font cannot be parsed, cannot be found on the system, or does not carry the horizontal metrics…
imagine.FormatErrorclassRaised when a format name is not one imagine knows, or when a file extension cannot be mapped to a format.
imagine.GAUSSIANconstantGaussian.
imagine.GIFconstantGIF.
imagine.GRAYconstantOpaque mid grey.
imagine.GREENconstantOpaque pure green.
imagine.ICOconstantWindows ICO.
imagine.ImageclassA raster image: a rectangle of 8-bit RGBA pixels, everything that draws onto one, and everything that…
imagine.ImageErrorclassBase class for every error the imagine module raises.
imagine.JPEGconstantJPEG.
imagine.LANCZOSconstantLanczos with a 3-lobe window.
imagine.LEFTconstant
imagine.MAGENTAconstantOpaque magenta.
imagine.NEARESTconstantNearest-neighbour.
imagine.PNGconstantPNG.
imagine.PNMconstantNetpbm (PBM, PGM, PPM).
imagine.QOIconstantQOI, the Quite OK Image format.
imagine.REDconstantOpaque pure red.
imagine.RIGHTconstant
imagine.StrokeFontclassThe built-in font: a sans-serif drawn from centre-line strokes rather than loaded from a file.
imagine.TGAconstantTruevision TGA.
imagine.TIFFconstantTIFF.
imagine.TOPconstant
imagine.TOP_LEFTconstantWhere a smaller image or a piece of text sits inside a larger box, used by Image.cover(), Image.contain()…
imagine.TOP_RIGHTconstant
imagine.TRANSPARENTconstant
imagine.WBMPconstantWireless Bitmap.
imagine.WEBPconstantWebP.
imagine.WHITEconstantOpaque white.
imagine.YELLOWconstantOpaque yellow.
imagine.capabilitiesfunctionWhat this build can do, as a dictionary holding decode, encode and animated, each a list of format…
imagine.createfunctionCreates a new image.
imagine.decodefunctionDecodes image data held in memory.
imagine.detectfunctionIdentifies image data by its contents, returning a format name or nil.
imagine.filters.LUMAconstantRec.
imagine.filters.MATRIX_LUMAconstantThe luma weights the colour-matrix filters use, from the SVG and CSS filter specifications.
imagine.filters.box_blur_kernelfunctionA 3x3 box blur kernel; every neighbour counts equally.
imagine.filters.brightness_lutfunctionA lookup table that adds a fixed amount to every value.
imagine.filters.build_lutfunctionBuilds a 256-entry lookup table from a function.
imagine.filters.combine_matricesfunctionMultiplies two colour matrices, giving one matrix with the effect of applying first and then second.
imagine.filters.contrast_lutfunctionA lookup table that pushes values away from or toward mid-grey.
imagine.filters.duotone_matrixfunctionA colour matrix that replaces every pixel’s colour with a blend between two colours chosen by its brightness,…
imagine.filters.edge_kernelfunctionA 3x3 Laplacian kernel that leaves only the edges.
imagine.filters.emboss_kernelfunctionA 3x3 kernel that lifts edges into a grey relief.
imagine.filters.gamma_lutfunctionA lookup table applying a gamma curve.
imagine.filters.gaussian_kernelfunctionA square Gaussian kernel with the given standard deviation.
imagine.filters.grayscale_matrixfunctionA colour matrix that collapses every colour to its grey of equal perceived brightness.
imagine.filters.hue_rotate_matrixfunctionA colour matrix that rotates every hue around the colour wheel by an angle in degrees, leaving brightness and…
imagine.filters.identity_lutfunctionA lookup table that leaves every value alone.
imagine.filters.identity_matrixfunctionThe identity colour matrix: applying it changes nothing.
imagine.filters.invert_lutfunctionA lookup table that inverts every value.
imagine.filters.levels_lutfunctionA lookup table implementing a levels adjustment.
imagine.filters.mean_removal_kernelfunctionA 3x3 kernel that removes local mean, exaggerating detail.
imagine.filters.posterize_lutfunctionA lookup table that reduces each channel to a fixed number of evenly spaced steps.
imagine.filters.saturation_matrixfunctionA colour matrix that scales saturation.
imagine.filters.scale_lutfunctionA lookup table that scales every value by a factor.
imagine.filters.sepia_matrixfunctionA colour matrix approximating the warm brown cast of a sepia photograph.
imagine.filters.sharpen_kernelfunctionA 3x3 sharpening kernel.
imagine.filters.smooth_kernelfunctionA 3x3 smoothing kernel weighted toward the centre pixel.
imagine.filters.threshold_lutfunctionA lookup table that forces every value to black or white.
imagine.formats.animatablefunctionEvery format this build can read as an animation.
imagine.formats.decode_wbmpfunctionDecodes a WBMP into straight RGBA pixels.
imagine.formats.detectfunctionIdentifies image data by its content rather than its name, or returns nil when the bytes are not a…
imagine.formats.encode_wbmpfunctionEncodes straight RGBA pixels as a WBMP.
imagine.formats.extension_forfunctionThe file extension a format is normally written with, without a leading dot.
imagine.formats.from_extensionfunctionThe format name a file extension implies, or nil when the extension is not one this module knows.
imagine.formats.mime_forfunctionThe IANA media type for a format, suitable for a Content-Type header.
imagine.formats.normalizefunctionNormalises a format name, accepting the common aliases.
imagine.formats.probefunctionReads an image’s format and dimensions without decoding its pixels.
imagine.formats.readablefunctionEvery format this build can read.
imagine.formats.writablefunctionEvery format this build can write.
imagine.openfunctionOpens an image file.
imagine.probefunctionReads an image’s format and dimensions from its header, without decoding any pixels.
imagine.strokefont.ASCENDERconstantHow far the tallest glyphs rise above the baseline.
imagine.strokefont.CAP_HEIGHTconstantThe height of a capital letter.
imagine.strokefont.DESCENDERconstantHow far descenders fall below the baseline, as a negative number.
imagine.strokefont.EMconstantThe em square’s height in design units.
imagine.strokefont.GLYPHSconstantEvery glyph, keyed by character.
imagine.strokefont.NOTDEFconstantWhat an unmapped character draws: an empty box, the same convention a font uses for a glyph it does not have.
imagine.strokefont.WEIGHTconstantThe default stroke width in design units, a little under a tenth of the em, which is the usual weight for a…
imagine.strokefont.X_HEIGHTconstantThe height of a lowercase letter with no ascender.

Submodules

ModuleReached asSummary
imagine.animationimagine.animation.*Animation: an ordered set of frames with per-frame delays, which is what an animated GIF or WebP decodes to…
imagine.canvasimagine.canvas.*Canvas: the drawing surface.
imagine.colorimagine.*Color: one RGBA colour, and the conversions between the ways of naming one.
imagine.constantsimagine.*Every named constant the module takes as an argument: resampling filters, blend modes, line caps, edge…
imagine.errorsimagine.*Every error the module raises, under ImageError as their root.
imagine.filtersimagine.filters.*The maths behind the filters: colour matrices, lookup tables and convolution kernels.
imagine.fontimagine.font.*Font: a loaded TrueType or OpenType face, ready to measure and draw text with.
imagine.formatsimagine.formats.*What the build can actually read and write, and how to tell one format from another.
imagine.imageimagine.image.*Image: a raster image as an RGBA pixel buffer, and everything that transforms one.
imagine.strokefontimagine.strokefont.*StrokeFont: the font built into the module, defined as stroke geometry rather than glyph outlines.

Functions

open()

imagine.open(source, options) -> Image

Opens an image file.

A shorthand for [[imagine.Image.open]].

Parameters

  • source (string|file)
  • options (?dict)

Returns Image

decode()

imagine.decode(data, options) -> Image

Decodes image data held in memory.

A shorthand for [[imagine.Image.decode]].

Parameters

  • data (bytes)
  • options (?dict)

Returns Image

create()

imagine.create(width, height, fill) -> Image

Creates a new image.

A shorthand for the [[imagine.Image]] constructor.

Parameters

  • width (number)
  • height (number)
  • fill (?Color|string|number)

Returns Image

probe()

imagine.probe(data) -> ?dict

Reads an image’s format and dimensions from its header, without decoding any pixels.

Returns {format, width, height}, or nil when the data is not a recognisable image. Reading a large JPEG’s header costs microseconds where decoding it costs tens of milliseconds, which makes this the right way to size up an upload before committing to it.

Parameters

  • data (bytes)

Returns ?dict

detect()

imagine.detect(data) -> ?string

Identifies image data by its contents, returning a format name or nil.

Parameters

  • data (bytes)

Returns ?string

capabilities()

imagine.capabilities() -> dict

What this build can do, as a dictionary holding decode, encode and animated, each a list of format names.

Worth checking rather than assuming: AVIF appears under encode but not decode, and which formats are compiled in can differ between builds.

Returns dict


2021, Richard Ore and Zuri contributors

imagine.animation

import imagine.animation

imagine lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled imagine.animation.* needs import imagine.animation.

Animation: an ordered set of frames with per-frame delays, which is what an animated GIF or WebP decodes to and encodes from.

Frames are full images rather than deltas, so each can be edited on its own and the encoder works out what actually changed.

Classes

Animation

class imagine.Animation

A sequence of images with a delay between each, read from or written to an animated format.

Frames are whole images, already composited against whatever came before them, so frame 5 can be pulled out and used on its own without replaying the first four. That is the useful shape almost all of the time, and it is why the disposal and transparency rules an animated GIF is built from never surface here.

import imagine { Animation }

var animation = Animation.open('loading.gif')

echo '${animation.length()} frames, ${animation.duration()}ms total'

animation
  .map(@(frame) {
    return frame.grayscale()
  })
  .save('loading-grey.gif')
What can be read and written

Animated GIF and animated WebP can both be read. Only GIF can be written; an animation saved as WebP raises EncodeError rather than quietly writing one frame. [[imagine.capabilities]] lists what is available under animated.

Memory

Every frame is a full image, so a 100-frame 500x500 animation is 100 MB decoded. Animations are worth streaming frame by frame when they are large, which frames() allows by handing back the list itself.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

imagine.Animation(frames: list, delay, repeat, format)

Creates an animation from a list of images.

Every frame must be the same size, since animated formats have one canvas that each frame paints into.

Parameters

  • frames (list) — A list of [[imagine.Image]].
  • delay (?number|list) — Milliseconds per frame, either one number for all of them or one per frame - Default: 100.
  • repeat (?number) — How many times to loop, 0 for forever - Default: 0.
  • format (?string) — The format these frames were decoded from, recorded as metadata - Default: nil.

Raises ImageError When the frames are not all images of one size.

Animation.open()

imagine.Animation.open(source) -> Animation

Opens an animated image file.

A still image opens as a one-frame animation rather than an error, so code that handles both does not need to branch.

Parameters

  • source (string|file)

Returns Animation

Raises DecodeError When the file is not a readable image.

Animation.decode()

imagine.Animation.decode(data: bytes) -> Animation

Decodes animated image data held in memory.

Parameters

  • data (bytes)

Returns Animation

Raises DecodeError When the data is not a readable image.

Animation.length()

imagine.Animation.length() -> number

How many frames the animation has.

Returns number

Animation.frames()

imagine.Animation.frames() -> list

The frames, as a list of [[imagine.Image]].

This is the live list, not a copy: changing an image in it changes the animation, which is what makes frame-by-frame editing possible without duplicating everything. Use map() when you want the original left alone.

Returns list

Animation.frame()

imagine.Animation.frame(index: number) -> Image

One frame by index, counting from zero.

Parameters

  • index (number)

Returns Image

Raises ImageError When there is no such frame.

Animation.delays()

imagine.Animation.delays() -> list

The per-frame delays in milliseconds.

Returns list

Animation.duration()

imagine.Animation.duration() -> number

How long one pass through the animation takes, in milliseconds.

Returns number

Animation.width()

imagine.Animation.width() -> number

The frames’ shared width in pixels.

Returns number

Animation.height()

imagine.Animation.height() -> number

The frames’ shared height in pixels.

Returns number

Animation.format()

imagine.Animation.format() -> ?string

The format this animation was decoded from, or nil.

Returns ?string

Animation.repeat()

imagine.Animation.repeat(times: number)

Sets how many times the animation loops when written out.

Parameters

  • times (number) — 0 loops forever.

Returns — self

Animation.delay()

imagine.Animation.delay(delay)

Sets the delay between frames.

Parameters

  • delay (number|list) — One value for every frame, or one per frame.

Returns — self

Animation.map()

imagine.Animation.map(fn: function) -> Animation

Applies a function to every frame, returning a new animation.

Each frame is copied before the function sees it, so a filter chain works directly as the body and this animation is left alone. That copy is the reason map() costs as much memory again as the animation itself; to filter in place instead, walk frames() and change each image directly.

var grey = animation.map(@(frame) {
  return frame.grayscale().blur(1)
})

Parameters

  • fn (function(1))

Returns Animation

Raises ImageError When the function returns anything but an image.

Animation.reverse()

imagine.Animation.reverse() -> Animation

Returns a new animation with the frames in reverse order.

The frames themselves are shared with this animation rather than copied, since reordering them changes nothing about them. Editing one afterwards therefore shows up in both; map() is the one that copies.

Returns Animation

Animation.to_image()

imagine.Animation.to_image() -> Image

Returns the animation as a single image: its first frame.

Returns Image

Animation.encode()

imagine.Animation.encode(format, options) -> bytes

Encodes the animation and returns the bytes.

Parameters

  • format (?string) — Default: GIF.
  • options (?dict)

Returns bytes

Raises EncodeError When the format cannot hold an animation.

Animation.save()

imagine.Animation.save(path: string, options)

Writes the animation to a file.

Parameters

  • path (string)
  • options (?dict) — format, plus everything encode() takes.

Returns — self

Animation.to_string()

imagine.Animation.to_string()

2026, Richard Ore and Zuri contributors

imagine.canvas

import imagine.canvas

imagine lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled imagine.canvas.* needs import imagine.canvas.

Canvas: the drawing surface. Every shape the module can draw is a method on it.

A canvas wraps an Image’s pixel buffer rather than copying it, so drawing goes straight into the image being built. The shape methods share three primitives underneath (span fill, blend, and stroke), which is why a new shape rarely needs new native code.

Classes

Canvas

class imagine.Canvas

A mutable RGBA pixel surface and everything that draws onto one.

Canvas is the lower half of Image: it owns the pixel buffer and knows how to put colour into it, but nothing about files, formats or resizing. It is separate mostly so the drawing code can be read on its own, and it is rarely worth using directly; Image inherits all of this.

How drawing works here

Every filled shape becomes a polygon and goes through one scanline rasterizer, and every stroked shape becomes the polygon outlining its stroke and goes through the same one. That means anti-aliasing, clipping and the fill rule are implemented once rather than once per primitive, and a shape this module does not offer can be drawn by handing fill_polygon() the points for it.

Coordinates

The origin is the top-left corner. Integer coordinates fall on pixel corners, not centres, so a rectangle from (0, 0) to (10, 10) covers exactly the first ten pixels in each direction with no half-covered edge. Fractional coordinates are meaningful and are what anti-aliasing acts on.

Drawing outside the surface is not an error. Anything that falls outside is clipped away, which is what makes it safe to draw a shape that only partly overlaps the image.

Constructor

imagine.Canvas(pixels: bytes, width: number, height: number)

Wraps an existing pixel buffer.

The buffer is taken as-is and not copied, so the caller must not keep writing to it independently. Image is the supported way to get a Canvas; this constructor exists for code that already has pixels from somewhere else.

Parameters

  • pixels (bytes) — width * height * 4 bytes of RGBA.
  • width (number)
  • height (number)

Canvas.width()

imagine.Canvas.width() -> number

The width in pixels.

Returns number

Canvas.height()

imagine.Canvas.height() -> number

The height in pixels.

Returns number

Canvas.pixels()

imagine.Canvas.pixels() -> bytes

The raw RGBA pixel buffer.

This is the live buffer, not a copy: writing to it changes the image. Pixel (x, y) starts at byte (y * width + x) * 4 and runs red, green, blue, alpha, with alpha straight rather than premultiplied.

Handing this out is deliberate. Reading and writing bytes directly is far faster than a method call per pixel, so an operation this module does not provide can still be written efficiently in Zuri.

Returns bytes

Canvas.set_pixels()

imagine.Canvas.set_pixels(pixels: bytes)

Replaces the surface’s pixels with another buffer.

The buffer must hold exactly width * height * 4 bytes, and is adopted rather than copied, so whatever produced it must not keep writing to it afterwards.

The pair to pixels(): that one hands the buffer out so an operation this module does not provide can be written directly, and this one takes a buffer back. Saving a copy before a destructive filter and restoring it afterwards is the usual reason to want it.

Parameters

  • pixels (bytes)

Returns — self

Raises ImageError When the buffer is the wrong size.

Canvas.clip()

imagine.Canvas.clip(x, y, width, height)

Restricts every subsequent drawing operation to a rectangle.

The rectangle is intersected with the surface, so a clip larger than the image is the same as no clip. Passing no arguments, or calling clear_clip(), removes it.

Parameters

  • x (number)
  • y (number)
  • width (number)
  • height (number)

Returns — self

Canvas.clear_clip()

imagine.Canvas.clear_clip()

Removes any clipping rectangle.

Returns — self

Canvas.get_clip()

imagine.Canvas.get_clip() -> ?dict

The current clipping rectangle as {x, y, width, height}, or nil when there isn’t one.

Returns ?dict

Canvas.antialias()

imagine.Canvas.antialias(enabled)

Turns anti-aliasing on or off for subsequent drawing.

On by default. Turning it off makes every edge hard, which is faster and is what you want for pixel-exact output such as barcodes, or when drawing into a mask that will be thresholded anyway.

Parameters

  • enabled (bool)

Returns — self

Canvas.thickness()

imagine.Canvas.thickness(thickness: number)

Sets the stroke width in pixels for lines and outlines.

Strokes are centred on the path, so a thickness of 4 puts 2 pixels on each side. The default is 1.

Parameters

  • thickness (number)

Returns — self

Canvas.cap()

imagine.Canvas.cap(style)

Sets how the two ends of an open stroke are finished.

CAP_ROUND (the default) ends in a half-disc, CAP_SQUARE in a square, and both reach half the stroke’s width past the endpoint. CAP_BUTT stops exactly at it.

Reach for CAP_BUTT when the coordinates have to mean exactly what they say: segments meeting end to end, a scale bar of a known length, the pieces of a dashed line. The joins between a path’s segments are always round and are not affected by this.

chart.cap(CAP_BUTT).thickness(6)
chart.line(40, 200, 40, 40, '#334155')

Parameters

  • style (string) — CAP_BUTT, CAP_ROUND or CAP_SQUARE.

Returns — self

Canvas.get_cap()

imagine.Canvas.get_cap() -> string

The line cap in force.

Returns string

Canvas.get_pixel()

imagine.Canvas.get_pixel(x, y) -> ?Color

Returns the colour at a pixel, or nil when the coordinates fall outside the image.

Clipping does not apply; a clip restricts what is written, not what can be read.

Parameters

  • x (number)
  • y (number)

Returns ?Color

Canvas.set_pixel()

imagine.Canvas.set_pixel(x, y, color)

Replaces the colour at a pixel, ignoring whatever was there.

This overwrites rather than blends, so drawing a half-transparent colour leaves a half-transparent pixel instead of compositing it over the old one. Use blend_pixel() for the compositing version, which is what every other drawing method uses.

Coordinates outside the image or outside the clip are ignored.

Parameters

  • x (number)
  • y (number)
  • color (Color|string|number)

Returns — self

Canvas.pixel()

imagine.Canvas.pixel(x, y, color)

Draws one pixel, compositing it over whatever is already there.

The single-pixel member of the drawing family, alongside line() and rect(). It is the same operation as blend_pixel(), under the name that reads correctly in a chain of drawing calls.

Coordinates outside the image or outside the clip are ignored.

Parameters

  • x (number)
  • y (number)
  • color (Color|string|number)

Returns — self

Canvas.blend_pixel()

imagine.Canvas.blend_pixel(x, y, color)

Composites a colour over the pixel already there, the way every drawing operation does.

Coordinates outside the image or outside the clip are ignored.

Parameters

  • x (number)
  • y (number)
  • color (Color|string|number)

Returns — self

Canvas.fill_polygon()

imagine.Canvas.fill_polygon(points, color, options)

Fills a polygon.

The points are a flat list of alternating x and y values, or a list of [x, y] pairs; both are accepted because both read naturally depending on where the points came from. The outline is closed automatically, so the last point does not need to repeat the first.

Self-intersecting outlines are filled by the non-zero winding rule by default, which is what fills a five-pointed star solid. Pass { even_odd: true } for the other convention, which leaves the middle of that star empty.

Parameters

  • points (list)
  • color (Color|string|number)
  • options (?dict) — even_odd (bool).

Returns — self

Canvas.polygon()

imagine.Canvas.polygon(points, color, options)

Draws a polygon’s outline, closing it back to the first point.

Parameters

  • points (list)
  • color (Color|string|number)
  • options (?dict) — thickness.

Returns — self

Canvas.polyline()

imagine.Canvas.polyline(points, color, options)

Draws a connected run of line segments without closing it.

Parameters

  • points (list)
  • color (Color|string|number)
  • options (?dict) — thickness.

Returns — self

Canvas.line()

imagine.Canvas.line(x1, y1, x2, y2, color, options)

Draws a straight line between two points.

The line is thickness() pixels wide, centred on the path, and anti-aliased unless that has been turned off.

Parameters

  • x1 (number)
  • y1 (number)
  • x2 (number)
  • y2 (number)
  • color (Color|string|number)
  • options (?dict) — thickness.

Returns — self

Canvas.rect()

imagine.Canvas.rect(x, y, width, height, color, options)

Draws the outline of a rectangle.

The stroke is centred on the rectangle’s edge, so half of a thick outline falls inside it and half outside.

Parameters

  • x (number)
  • y (number)
  • width (number)
  • height (number)
  • color (Color|string|number)
  • options (?dict) — thickness.

Returns — self

Canvas.fill_rect()

imagine.Canvas.fill_rect(x, y, width, height, color)

Fills a rectangle.

Parameters

  • x (number)
  • y (number)
  • width (number)
  • height (number)
  • color (Color|string|number)

Returns — self

Canvas.rounded_rect()

imagine.Canvas.rounded_rect(x, y, width, height, radius, color, options)

Draws the outline of a rectangle with rounded corners.

A radius larger than half the shorter side is reduced to fit, so a very large radius gives a stadium shape rather than a broken one.

Parameters

  • x (number)
  • y (number)
  • width (number)
  • height (number)
  • radius (number)
  • color (Color|string|number)
  • options (?dict) — thickness.

Returns — self

Canvas.fill_rounded_rect()

imagine.Canvas.fill_rounded_rect(x, y, width, height, radius, color)

Fills a rectangle with rounded corners.

Parameters

  • x (number)
  • y (number)
  • width (number)
  • height (number)
  • radius (number)
  • color (Color|string|number)

Returns — self

Canvas.circle()

imagine.Canvas.circle(cx, cy, radius, color, options)

Draws the outline of a circle.

Parameters

  • cx (number) — The centre’s x coordinate.
  • cy (number) — The centre’s y coordinate.
  • radius (number)
  • color (Color|string|number)
  • options (?dict) — thickness.

Returns — self

Canvas.fill_circle()

imagine.Canvas.fill_circle(cx, cy, radius, color)

Fills a circle.

Parameters

  • cx (number)
  • cy (number)
  • radius (number)
  • color (Color|string|number)

Returns — self

Canvas.ellipse()

imagine.Canvas.ellipse(cx, cy, rx, ry, color, options)

Draws the outline of an ellipse.

Parameters

  • cx (number) — The centre’s x coordinate.
  • cy (number) — The centre’s y coordinate.
  • rx (number) — The horizontal radius.
  • ry (number) — The vertical radius.
  • color (Color|string|number)
  • options (?dict) — thickness.

Returns — self

Canvas.fill_ellipse()

imagine.Canvas.fill_ellipse(cx, cy, rx, ry, color)

Fills an ellipse.

Parameters

  • cx (number)
  • cy (number)
  • rx (number)
  • ry (number)
  • color (Color|string|number)

Returns — self

Canvas.arc()

imagine.Canvas.arc(cx, cy, rx, ry, start, end, color, options)

Draws an elliptical arc.

Angles are in degrees, measured clockwise from three o’clock, matching the direction the y axis runs. An end angle below the start angle sweeps the long way round.

Parameters

  • cx (number)
  • cy (number)
  • rx (number)
  • ry (number)
  • start (number) — The starting angle in degrees.
  • end (number) — The ending angle in degrees.
  • color (Color|string|number)
  • options (?dict) — thickness.

Returns — self

Canvas.pie()

imagine.Canvas.pie(cx, cy, rx, ry, start, end, color)

Fills a pie slice: an arc closed back through the centre.

Parameters

  • cx (number)
  • cy (number)
  • rx (number)
  • ry (number)
  • start (number) — The starting angle in degrees.
  • end (number) — The ending angle in degrees.
  • color (Color|string|number)

Returns — self

Canvas.chord()

imagine.Canvas.chord(cx, cy, rx, ry, start, end, color)

Fills the region between an arc and its chord.

Parameters

  • cx (number)
  • cy (number)
  • rx (number)
  • ry (number)
  • start (number)
  • end (number)
  • color (Color|string|number)

Returns — self

Canvas.bezier()

imagine.Canvas.bezier(x1, y1, cx1, cy1, cx2, cy2, x2, y2, color, options)

Draws a cubic Bezier curve through four control points.

The curve starts at the first point, ends at the fourth, and is pulled toward the two in between without passing through them.

Parameters

  • x1 (number)
  • y1 (number)
  • cx1 (number) — The first control point’s x coordinate.
  • cy1 (number)
  • cx2 (number) — The second control point’s x coordinate.
  • cy2 (number)
  • x2 (number)
  • y2 (number)
  • color (Color|string|number)
  • options (?dict) — thickness.

Returns — self

Canvas.fill()

imagine.Canvas.fill(color)

Paints every pixel of the surface, ignoring the clip and whatever was there before.

This is a replace, not a blend: filling with a transparent colour empties the image rather than leaving it unchanged.

Parameters

  • color (Color|string|number)

Returns — self

Canvas.clear()

imagine.Canvas.clear()

Clears the surface to fully transparent.

Returns — self

Canvas.flood_fill()

imagine.Canvas.flood_fill(x, y, color, tolerance)

Flood fills the connected region of similar colour containing (x, y).

Similarity is measured against the colour that was at the starting point, as the largest difference across the four channels. tolerance is that difference in 0-255 units, so 0 spreads only through exactly equal pixels and 255 fills everything.

Filling is four-connected: it spreads up, down, left and right, but not diagonally, so a one-pixel diagonal line holds it back. The fill is never anti-aliased.

Parameters

  • x (number)
  • y (number)
  • color (Color|string|number)
  • tolerance (?number) — Default: 0.

Returns — self

Canvas.linear_gradient()

imagine.Canvas.linear_gradient(x1, y1, x2, y2, stops, options)

Fills a rectangle with a linear gradient running between two points.

The gradient’s direction and length are the vector from (x1, y1) to (x2, y2). Everything before the first point takes the first stop’s colour and everything past the second takes the last stop’s, so a short vector across a large area gives a hard transition with flat bands on either side.

# top to bottom, dark to light
image.linear_gradient(0, 0, 0, image.height(), ['#0f172a', '#334155'])

Parameters

  • x1 (number)
  • y1 (number)
  • x2 (number)
  • y2 (number)
  • stops (list) — Colours, or [offset, colour] pairs with the offset from 0 to 1.
  • options (?dict) — rect (a {x, y, width, height} to confine the fill to, default the whole surface), blend (bool, default false: composite over what is there rather than replacing it).

Returns — self

Canvas.radial_gradient()

imagine.Canvas.radial_gradient(cx, cy, radius, stops, options)

Fills a rectangle with a radial gradient spreading from a centre.

# a soft vignette
image.radial_gradient(cx, cy, radius, [
  [0, Color(0, 0, 0, 0)],
  [1, Color(0, 0, 0, 180)],
], { blend: true })

Parameters

  • cx (number) — The centre’s x coordinate.
  • cy (number) — The centre’s y coordinate.
  • radius (number) — Where the last stop lands.
  • stops (list) — Colours, or [offset, colour] pairs.
  • options (?dict) — rect, blend. The same as linear_gradient().

Returns — self

Canvas.draw_mask()

imagine.Canvas.draw_mask(mask: bytes, mask_width, mask_height, x, y, color)

Paints a colour through an 8-bit coverage mask.

The mask is one byte per pixel, 0 for nothing and 255 for full coverage, laid out row-major with no padding. Coverage scales the colour’s alpha, so this is how anti-aliased text and any externally rasterized shape reach the surface.

Parameters

  • mask (bytes)
  • mask_width (number)
  • mask_height (number)
  • x (number) — Where the mask’s top-left corner lands.
  • y (number)
  • color (Color|string|number)

Returns — self


2026, Richard Ore and Zuri contributors

imagine.color

import imagine

Everything here is re-exported by imagine, so import imagine is enough and the names are called as imagine.*. Importing imagine.color on its own works too and reaches the same definitions.

Color: one RGBA colour, and the conversions between the ways of naming one.

It reads and writes hex, RGB, HSL and HSV, blends against another colour, and reports its own luminance. The named constants are the handful worth not spelling out every time.

Constants

TRANSPARENT

imagine.TRANSPARENT

BLACK

imagine.BLACK: Color

Opaque black.

WHITE

imagine.WHITE: Color

Opaque white.

GRAY

imagine.GRAY: Color

Opaque mid grey.

RED

imagine.RED: Color

Opaque pure red.

GREEN

imagine.GREEN: Color

Opaque pure green.

This is the additive primary, not CSS’s green, which is half as bright. Color.named('green') gives that one.

BLUE

imagine.BLUE: Color

Opaque pure blue.

YELLOW

imagine.YELLOW: Color

Opaque yellow.

CYAN

imagine.CYAN: Color

Opaque cyan.

MAGENTA

imagine.MAGENTA: Color

Opaque magenta.

Classes

Color

class imagine.Color

An 8-bit RGBA colour.

Alpha runs 0 (fully transparent) to 255 (fully opaque), the same convention as CSS, PNG and every modern image format. Colours are immutable: every method that would change one returns a new Color, so a colour held in a variable is safe to pass around and reuse.

import imagine { Color }

var red = Color(255, 0, 0)
var glass = red.with_alpha(128)
var brand = Color.hex('#4f46e5')
Relationship to the colors module

The colour space conversions here are the standard library’s, from [[colors]]; Color is the value type that carries a result around and puts it into a pixel buffer. Anything colors can do to a hexadecimal string can be done to a Color by way of to_hex() and Color.hex().

Three conventions follow from that and are worth knowing:

  • Hue is in degrees and saturation, lightness and value are in percentage points (0-100), exactly as in colors and in CSS. They are not 0-1 ratios.

  • to_hex() returns a leading #, because that is the form you paste into a stylesheet. colors returns the bare form. Both are accepted everywhere either is.

  • A malformed hexadecimal colour or an unknown colour name raises ValueError, which is what colors reports for them, rather than this module’s own ImageError. An argument of the wrong type raises TypeError from the declaration it failed.

  • printable — has a @to_string(), so echo and print() show something useful

  • serializable — has a @to_json(), so it can be handed straight to json.encode()

Fields

FieldTypeDescription
rnumberThe red channel, 0-255.
gnumberThe green channel, 0-255.
bnumberThe blue channel, 0-255.
anumberThe alpha channel, 0 (transparent) to 255 (opaque).

Constructor

imagine.Color(r, g, b, a)

Creates a colour from its channels.

Values outside 0-255 are clamped rather than rejected, and fractional values are rounded.

Parameters

  • r (number) — The red channel.
  • g (number) — The green channel.
  • b (number) — The blue channel.
  • a (?number) — The alpha channel - Default: 255 (opaque).

Color.rgb()

imagine.Color.rgb(r, g, b) -> Color

Creates an opaque colour from red, green and blue channels.

The same as calling the constructor with three arguments; it exists so a call site that also uses Color.hsl() or Color.lab() can name the space it is working in.

Parameters

  • r (number)
  • g (number)
  • b (number)

Returns Color

Color.rgba()

imagine.Color.rgba(r, g, b, a) -> Color

Creates a colour from red, green, blue and alpha channels.

Parameters

  • r (number)
  • g (number)
  • b (number)
  • a (number)

Returns Color

Color.gray()

imagine.Color.gray(value, a) -> Color

Creates a shade of grey.

Parameters

  • value (number) — The brightness, 0 (black) to 255 (white).
  • a (?number) — The alpha channel - Default: 255.

Returns Color

Color.packed()

imagine.Color.packed(value: number) -> Color

Creates a colour from a packed 0xRRGGBBAA integer.

Alpha is in the least significant byte, which is the layout to_packed() produces and the one this module passes around internally.

%> Color.packed(0xFF0000FF).to_hex()
'#ff0000'

Parameters

  • value (number)

Returns Color

Color.hex()

imagine.Color.hex(spec) -> Color

Creates a colour from a CSS-style hexadecimal string.

All four CSS lengths are accepted, with or without the leading #, in either case: #rgb, #rgba, #rrggbb and #rrggbbaa. The short forms double each digit, so #f0a is #ff00aa.

Parameters

  • spec (string)

Returns Color

Raises ValueError When spec is not a hexadecimal colour.

Color.named()

imagine.Color.named(name) -> Color

Looks up one of the CSS named colours.

Matching ignores case, spaces, hyphens and underscores, so 'Dark Sea Green' and 'darkseagreen' are the same colour. The full CSS Color Level 4 list is available, including both spellings of the greys and transparent.

Parameters

  • name (string)

Returns Color

Raises ValueError When name is not a CSS colour name.

Color.hsl()

imagine.Color.hsl(h, s, l, a) -> Color

Creates a colour from hue, saturation and lightness.

Parameters

  • h (number) — The hue in degrees. Wraps, so 400 is the same as 40.
  • s (number) — The saturation in percentage points, 0-100.
  • l (number) — The lightness in percentage points, 0 (black) to 100 (white).
  • a (?number) — The alpha channel, 0-255 - Default: 255.

Returns Color

Color.hsv()

imagine.Color.hsv(h, s, v, a) -> Color

Creates a colour from hue, saturation and value (also called HSB).

Parameters

  • h (number) — The hue in degrees.
  • s (number) — The saturation in percentage points, 0-100.
  • v (number) — The value in percentage points, 0-100.
  • a (?number) — The alpha channel, 0-255 - Default: 255.

Returns Color

Color.hwb()

imagine.Color.hwb(h, w, b, a) -> Color

Creates a colour from hue, whiteness and blackness.

Parameters

  • h (number) — The hue in degrees.
  • w (number) — The whiteness in percentage points, 0-100.
  • b (number) — The blackness in percentage points, 0-100.
  • a (?number) — The alpha channel, 0-255 - Default: 255.

Returns Color

Color.cmyk()

imagine.Color.cmyk(c, m, y, k, a) -> Color

Creates a colour from cyan, magenta, yellow and key components.

This is the naive conversion, not a colour-managed one: it takes no account of an output profile, so it is right for generating colours and wrong for predicting what a printing press will do.

Parameters

  • c (number) — 0-100.
  • m (number) — 0-100.
  • y (number) — 0-100.
  • k (number) — 0-100.
  • a (?number) — The alpha channel, 0-255 - Default: 255.

Returns Color

Color.xyz()

imagine.Color.xyz(x, y, z, a) -> Color

Creates a colour from CIE XYZ tristimulus values, against the D65 white point.

Parameters

  • x (number)
  • y (number)
  • z (number)
  • a (?number) — The alpha channel, 0-255 - Default: 255.

Returns Color

Color.lab()

imagine.Color.lab(l, a_star, b_star, a) -> Color

Creates a colour from CIE Lab*, against the D65 white point.

Lab is perceptually uniform, which makes it the right space for interpolating between two colours when the intermediate steps need to look evenly spaced.

Parameters

  • l (number) — Lightness, 0-100.
  • a_star (number) — The green-red axis.
  • b_star (number) — The blue-yellow axis.
  • a (?number) — The alpha channel, 0-255 - Default: 255.

Returns Color

Color.parse()

imagine.Color.parse(value) -> Color

Turns whatever a caller passed into a Color.

Every drawing method calls this on its colour argument, so a colour can be written the shortest way that reads clearly at the call site. Accepted forms:

  • a Color, returned unchanged
  • a hexadecimal string, '#4f46e5' or '4f46e5'
  • a CSS colour name, 'rebeccapurple'
  • a packed 0xRRGGBBAA number
  • a list of 3 or 4 channels, [255, 0, 0]

Parameters

  • value (Color|string|number|list)

Returns Color

Raises ImageError When value is not a colour in any of those forms.

Color.to_packed()

imagine.Color.to_packed() -> number

Returns the colour as a packed 0xRRGGBBAA integer.

Returns number

Color.to_hex()

imagine.Color.to_hex(always_alpha) -> string

Returns the colour as a hexadecimal string with a leading #.

The alpha digits are included only when the colour is not fully opaque, unless always_alpha asks for them, so the common case reads as the familiar six digits.

%> Color(255, 0, 0).to_hex()
'#ff0000'
%> Color(255, 0, 0, 128).to_hex()
'#ff000080'

Parameters

  • always_alpha (?bool) — Always emit the alpha digits - Default: false.

Returns string

Color.to_hsl()

imagine.Color.to_hsl() -> dict

Returns the colour in HSL, as a dictionary holding h (degrees), s and l (percentage points) and a (0-255).

Returns dict

Color.to_hsv()

imagine.Color.to_hsv() -> dict

Returns the colour in HSV, as a dictionary holding h (degrees), s and v (percentage points) and a (0-255).

Returns dict

Color.to_hwb()

imagine.Color.to_hwb() -> dict

Returns the colour in HWB, as a dictionary holding h (degrees), w and b (percentage points) and a (0-255).

Returns dict

Color.to_cmyk()

imagine.Color.to_cmyk() -> dict

Returns the colour in CMYK, as a dictionary holding c, m, y and k (percentage points) and a (0-255).

Returns dict

Color.to_xyz()

imagine.Color.to_xyz() -> dict

Returns the colour in CIE XYZ, as a dictionary holding x, y, z and a (0-255).

Returns dict

Color.to_lab()

imagine.Color.to_lab() -> dict

Returns the colour in CIE Lab*, as a dictionary holding l, a_star, b_star and a (0-255).

The axes are named a_star and b_star rather than a and b so neither collides with the alpha channel.

Returns dict

Color.with_alpha()

imagine.Color.with_alpha(a) -> Color

Returns a copy of the colour with a different alpha channel.

Parameters

  • a (number) — The new alpha, 0-255.

Returns Color

Color.fade()

imagine.Color.fade(factor) -> Color

Returns a copy of the colour with its alpha scaled by a factor.

A factor of 0.5 halves whatever opacity the colour already had, rather than setting it to half opaque, so fading an already translucent colour behaves the way stacking two layers would.

Parameters

  • factor (number) — 0 (transparent) to 1 (unchanged).

Returns Color

Color.lighten()

imagine.Color.lighten(amount) -> Color

Returns a lighter version of the colour, keeping its hue.

Parameters

  • amount (number) — Percentage points of HSL lightness to add, 0-100.

Returns Color

Color.darken()

imagine.Color.darken(amount) -> Color

Returns a darker version of the colour, keeping its hue.

Parameters

  • amount (number) — Percentage points of HSL lightness to remove, 0-100.

Returns Color

Color.saturate()

imagine.Color.saturate(amount) -> Color

Returns a more saturated version of the colour.

Parameters

  • amount (number) — Percentage points of HSL saturation to add, 0-100.

Returns Color

Color.desaturate()

imagine.Color.desaturate(amount) -> Color

Returns a less saturated version of the colour.

An amount of 100 removes all colour, which is not the same as to_grayscale(): this keeps HSL lightness, that one weights the channels for perceived brightness.

Parameters

  • amount (number) — Percentage points of HSL saturation to remove, 0-100.

Returns Color

Color.rotate_hue()

imagine.Color.rotate_hue(degrees) -> Color

Returns the colour rotated around the hue wheel.

Parameters

  • degrees (number) — How far to rotate. Negative rotates backwards.

Returns Color

Color.mix()

imagine.Color.mix(other, t) -> Color

Returns a linear blend of this colour and another.

All four channels are interpolated, alpha included, so mixing a transparent colour in also makes the result more transparent.

Parameters

  • other (Color|string|number|list) — The colour to blend toward.
  • t (number) — 0 returns this colour, 1 returns other.

Returns Color

Color.over()

imagine.Color.over(backdrop) -> Color

Composites this colour over an opaque backdrop and returns the flattened result.

This is what happens when a translucent colour is drawn onto a solid background, and it is how a colour has to be resolved before writing it to a format with no alpha channel, such as JPEG.

Parameters

  • backdrop (Color|string|number|list)

Returns Color

Color.luminance()

imagine.Color.luminance() -> number

Returns the relative luminance of the colour, 0 (black) to 1 (white), as defined by WCAG 2.

The alpha channel is ignored: luminance is a property of the colour itself, and a translucent colour’s apparent brightness depends on whatever is behind it. Composite with over() first if that matters.

Returns number

Color.contrast_ratio()

imagine.Color.contrast_ratio(other) -> number

Returns the WCAG 2 contrast ratio between this colour and another, from 1 (identical) to 21 (black against white).

Text is generally held to need a ratio of at least 4.5 against its background, or 3 at large sizes.

Parameters

  • other (Color|string|number|list)

Returns number

Color.is_dark()

imagine.Color.is_dark() -> bool

Returns true when the colour is dark enough that light text reads better on it than dark text.

The threshold is the luminance at which contrast against white overtakes contrast against black.

Returns bool

Color.is_light()

imagine.Color.is_light() -> bool

The opposite of is_dark().

Returns bool

Color.best_contrast()

imagine.Color.best_contrast(light, dark) -> Color

Returns whichever of two colours reads better on top of this one.

Parameters

  • light (?Color|string) — The colour to use on a dark background - Default: white.
  • dark (?Color|string) — The colour to use on a light background - Default: black.

Returns Color

Color.to_grayscale()

imagine.Color.to_grayscale() -> Color

Returns the grey with the same perceived brightness as this colour, keeping its alpha.

This uses the Rec. 601 luma weights, which is what image editors mean by “greyscale” and what Image.grayscale() applies. It is deliberately not [[colors.grayscale]], which sets HSL saturation to zero and so keeps a bright yellow as bright as a dark blue.

Returns Color

Color.invert()

imagine.Color.invert() -> Color

Returns the colour with its red, green and blue channels inverted.

Alpha is left alone, since inverting it would turn a visible colour invisible rather than changing how it looks.

Returns Color

Color.equals()

imagine.Color.equals(other) -> bool

Returns true when both colours have identical channels.

Parameters

  • other (any)

Returns bool

Color.to_list()

imagine.Color.to_list() -> list

Returns the colour as a list of its four channels.

Returns list

Color.to_dict()

imagine.Color.to_dict() -> dict

Returns the colour as a dictionary holding r, g, b and a.

Returns dict

Color.to_string()

imagine.Color.to_string()

2026, Richard Ore and Zuri contributors

imagine.constants

import imagine

Everything here is re-exported by imagine, so import imagine is enough and the names are called as imagine.*. Importing imagine.constants on its own works too and reaches the same definitions.

Every named constant the module takes as an argument: resampling filters, blend modes, line caps, edge handling, file formats, anchor points, text alignment and flip axes.

They are gathered here so that a call reads as imagine.BILINEAR rather than a bare number whose meaning has to be looked up.

Constants

NEAREST

imagine.NEAREST: string = 'nearest'

Nearest-neighbour. Picks the single closest source pixel and copies it, so it is the fastest filter and the only one that keeps hard edges perfectly hard.

Right for pixel art, QR codes, and any image where a blurred edge would be wrong. Wrong for photographs, where it produces visible stair-stepping.

BILINEAR

imagine.BILINEAR: string = 'bilinear'

Bilinear. Averages the four surrounding pixels.

A good default when speed matters more than sharpness. Enlarging with it looks soft; shrinking by more than about half loses detail, because it samples too few source pixels to represent what it discards.

BICUBIC

imagine.BICUBIC: string = 'bicubic'

Bicubic (Catmull-Rom). Fits a curve through sixteen surrounding pixels.

Noticeably sharper than bilinear when enlarging, at a few times the cost. A reasonable default for photographs.

GAUSSIAN

imagine.GAUSSIAN: string = 'gaussian'

Gaussian. Weights the neighbourhood by a bell curve.

Softer than bicubic on purpose. Useful when the result will be sharpened afterwards, or when resampling noisy input where a sharper filter would emphasise the noise.

LANCZOS

imagine.LANCZOS: string = 'lanczos'

Lanczos with a 3-lobe window. The highest quality filter here and the slowest.

The usual choice for thumbnails, where the image is being shrunk a long way and every remaining pixel matters. It can produce faint ringing next to very high-contrast edges, which is the price of its sharpness.

BLEND_NORMAL

imagine.BLEND_NORMAL: string = 'normal'

Ordinary alpha compositing: the source is painted over the destination according to its alpha. This is the default for every drawing and compositing operation.

BLEND_MULTIPLY

imagine.BLEND_MULTIPLY: string = 'multiply'

Multiplies the two colours. The result is never lighter than either input, so white leaves the backdrop alone and black forces black. The usual way to paint a shadow.

BLEND_SCREEN

imagine.BLEND_SCREEN: string = 'screen'

The inverse of multiply. The result is never darker than either input, so black leaves the backdrop alone and white forces white. The usual way to paint a glow.

BLEND_OVERLAY

imagine.BLEND_OVERLAY: string = 'overlay'

Multiply where the backdrop is dark, screen where it is light. Increases contrast while keeping highlights and shadows.

BLEND_DARKEN

imagine.BLEND_DARKEN: string = 'darken'

Keeps whichever channel is darker.

BLEND_LIGHTEN

imagine.BLEND_LIGHTEN: string = 'lighten'

Keeps whichever channel is lighter.

BLEND_COLOR_DODGE

imagine.BLEND_COLOR_DODGE: string = 'color_dodge'

Brightens the backdrop toward the source. Strong effect; saturates quickly.

BLEND_COLOR_BURN

imagine.BLEND_COLOR_BURN: string = 'color_burn'

Darkens the backdrop toward the source. The counterpart of dodge.

BLEND_HARD_LIGHT

imagine.BLEND_HARD_LIGHT: string = 'hard_light'

Overlay with the roles of source and backdrop swapped.

BLEND_SOFT_LIGHT

imagine.BLEND_SOFT_LIGHT: string = 'soft_light'

A gentler hard light, as if the source were a diffuse spotlight.

BLEND_DIFFERENCE

imagine.BLEND_DIFFERENCE: string = 'difference'

The absolute difference between the two colours. Identical images blended this way come out black, which makes it a quick visual diff.

BLEND_EXCLUSION

imagine.BLEND_EXCLUSION: string = 'exclusion'

Like difference, but lower contrast in the midtones.

BLEND_ADD

imagine.BLEND_ADD: string = 'add'

Adds the channels, clamped at white. Also called linear dodge.

BLEND_SUBTRACT

imagine.BLEND_SUBTRACT: string = 'subtract'

Subtracts the source from the backdrop, clamped at black.

CAP_BUTT

imagine.CAP_BUTT: string = 'butt'

The stroke stops dead at its endpoint. Nothing is drawn past the coordinates given.

The right choice when segments have to meet exactly, when a stroke’s length must be precisely what was asked for, or for the pieces of a dashed line.

CAP_ROUND

imagine.CAP_ROUND: string = 'round'

The stroke ends in a half-disc, so it reaches half its width past the endpoint. The default.

CAP_SQUARE

imagine.CAP_SQUARE: string = 'square'

The stroke ends in a square, so it reaches half its width past the endpoint, the same distance a round cap does but with corners.

EDGE_CLAMP

imagine.EDGE_CLAMP: string = 'clamp'

Pixels off the edge take the value of the nearest edge pixel. The default, and what keeps a blur from darkening the border.

EDGE_TRANSPARENT

imagine.EDGE_TRANSPARENT: string = 'transparent'

Pixels off the edge are treated as transparent black. Correct when the image really does end there and you want the blur to fade out.

EDGE_WRAP

imagine.EDGE_WRAP: string = 'wrap'

Pixels off the edge wrap to the opposite side. For images meant to tile seamlessly.

PNG

imagine.PNG: string = 'png'

PNG. Lossless, alpha, universally supported. The right default for anything with sharp edges, text or transparency.

JPEG

imagine.JPEG: string = 'jpeg'

JPEG. Lossy, no alpha. The right choice for photographs, and the wrong one for anything with hard edges. Transparent pixels are flattened against the background option on export, white unless you say otherwise.

GIF

imagine.GIF: string = 'gif'

GIF. At most 256 colours per frame, one-bit transparency, and the only animated format this module can write.

BMP

imagine.BMP: string = 'bmp'

BMP. Uncompressed and enormous, but readable by anything.

TIFF

imagine.TIFF: string = 'tiff'

TIFF. Lossless, alpha, common in printing and scanning workflows.

TGA

imagine.TGA: string = 'tga'

Truevision TGA. Lossless with alpha, still common in game asset pipelines.

WEBP

imagine.WEBP: string = 'webp'

WebP. Lossless with alpha, and smaller than PNG for the same pixels.

Reading handles lossy and lossless WebP, still and animated. Writing produces a lossless still, so the quality option does not apply and a photograph written here will be larger than one a lossy WebP encoder would produce. Reach for JPEG or AVIF when size matters more than exactness.

AVIF

imagine.AVIF: string = 'avif'

AVIF. The smallest files of anything here at a given quality.

Write only. Opening an AVIF raises DecodeError, and [[imagine.capabilities]] lists AVIF under encode but not decode, so a program can check rather than discover it at run time.

QOI

imagine.QOI: string = 'qoi'

QOI, the Quite OK Image format. Lossless with alpha, encodes and decodes several times faster than PNG at a modest size penalty. Good for caches and intermediate files.

ICO

imagine.ICO: string = 'ico'

Windows ICO. An icon container; a single image is written as one entry.

PNM

imagine.PNM: string = 'pnm'

Netpbm (PBM, PGM, PPM). Trivially simple and trivially large.

WBMP

imagine.WBMP: string = 'wbmp'

Wireless Bitmap. One bit per pixel, no compression, no alpha.

Writing thresholds each pixel on its perceived brightness, so a colour image comes out as a black and white silhouette. Reading gives opaque black and white pixels.

TOP_LEFT

imagine.TOP_LEFT: string = 'top_left'

Where a smaller image or a piece of text sits inside a larger box, used by Image.cover(), Image.contain() and Image.place().

TOP

imagine.TOP: string = 'top'

TOP_RIGHT

imagine.TOP_RIGHT: string = 'top_right'

LEFT

imagine.LEFT: string = 'left'

CENTER

imagine.CENTER: string = 'center'
imagine.RIGHT: string = 'right'

BOTTOM_LEFT

imagine.BOTTOM_LEFT: string = 'bottom_left'

BOTTOM

imagine.BOTTOM: string = 'bottom'

BOTTOM_RIGHT

imagine.BOTTOM_RIGHT: string = 'bottom_right'

ALIGN_LEFT

imagine.ALIGN_LEFT: string = 'left'

Lines of a multi-line string start at the same left edge. The default.

ALIGN_CENTER

imagine.ALIGN_CENTER: string = 'center'

Lines of a multi-line string are centred against each other.

ALIGN_RIGHT

imagine.ALIGN_RIGHT: string = 'right'

Lines of a multi-line string end at the same right edge.

FLIP_HORIZONTAL

imagine.FLIP_HORIZONTAL: number = 1

Mirror left to right.

FLIP_VERTICAL

imagine.FLIP_VERTICAL: number = 2

Mirror top to bottom.

FLIP_BOTH

imagine.FLIP_BOTH: number = 3

Mirror both ways at once, which is the same as rotating 180 degrees.


2026, Richard Ore and Zuri contributors

imagine.errors

import imagine

Everything here is re-exported by imagine, so import imagine is enough and the names are called as imagine.*. Importing imagine.errors on its own works too and reaches the same definitions.

Every error the module raises, under ImageError as their root.

The subclasses separate the cases a caller treats differently: input that is not an image it can read, output it cannot write, a format it does not know, a coordinate outside the surface, and a font that will not load.

Classes

ImageError

class imagine.ImageError < Error

Base class for every error the imagine module raises.

Catching ImageError catches everything this module throws on its own. The subclasses below exist so a caller can tell an unreadable upload apart from a coordinate mistake in their own code without matching on message text.

  • printable — has a @to_string(), so echo and print() show something useful

ImageError.to_string()

imagine.ImageError.to_string()

DecodeError

class imagine.DecodeError < ImageError

Raised when image data cannot be read: the bytes are not an image at all, the format is one this build cannot decode, or the file is truncated or corrupt.

Anything arriving from outside the program (an upload, a download, a file a user picked) can raise this, so it is the one error a server handling images must always be ready for.

  • printable — has a @to_string(), so echo and print() show something useful

DecodeError.to_string()

imagine.DecodeError.to_string()

EncodeError

class imagine.EncodeError < ImageError

Raised when an image cannot be written in the requested format, either because this build has no encoder for it or because the encoder rejected the image or the options given.

  • printable — has a @to_string(), so echo and print() show something useful

EncodeError.to_string()

imagine.EncodeError.to_string()

FormatError

class imagine.FormatError < ImageError

Raised when a format name is not one imagine knows, or when a file extension cannot be mapped to a format.

  • printable — has a @to_string(), so echo and print() show something useful

FormatError.to_string()

imagine.FormatError.to_string()

BoundsError

class imagine.BoundsError < ImageError

Raised when a rectangle, crop or resize falls outside the image, or when a dimension is zero or negative.

Drawing operations do not raise this. A line running off the edge of the canvas is clipped, which is what every drawing API does and what callers expect; it is only the operations that must return an image of an exact size that have no sensible way to continue.

  • printable — has a @to_string(), so echo and print() show something useful

BoundsError.to_string()

imagine.BoundsError.to_string()

FontError

class imagine.FontError < ImageError

Raised when a font cannot be parsed, cannot be found on the system, or does not carry the horizontal metrics text layout needs.

  • printable — has a @to_string(), so echo and print() show something useful

FontError.to_string()

imagine.FontError.to_string()

2026, Richard Ore and Zuri contributors

imagine.filters

import imagine

imagine exposes this as imagine.filters, so import imagine is enough and the names are called as imagine.filters.*. import imagine.filters reaches the same definitions directly.

The maths behind the filters: colour matrices, lookup tables and convolution kernels.

Image exposes these as named methods, and this is where the numbers those methods pass come from. Building a matrix, a LUT or a kernel here and applying it yourself is how to get a filter the module does not already name.

Constants

LUMA

imagine.filters.LUMA: list = [...]

Rec. 601 luma weights.

These are what “grayscale” means to an image editor, and what [[imagine.Color.to_grayscale]] uses, so a grey produced by Image.grayscale() and one produced by Color.to_grayscale() agree. They are deliberately not the WCAG linear-light weights that [[colors.relative_luminance]] uses; those answer a different question, about contrast rather than appearance.

MATRIX_LUMA

imagine.filters.MATRIX_LUMA: list = [...]

The luma weights the colour-matrix filters use, from the SVG and CSS filter specifications.

Saturation and hue rotation are defined against these, so a saturate() here matches what a browser does to the same image.

Functions

identity_matrix()

imagine.filters.identity_matrix() -> list

The identity colour matrix: applying it changes nothing.

A useful starting point for building one by hand.

Returns list

grayscale_matrix()

imagine.filters.grayscale_matrix() -> list

A colour matrix that collapses every colour to its grey of equal perceived brightness.

Returns list

sepia_matrix()

imagine.filters.sepia_matrix() -> list

A colour matrix approximating the warm brown cast of a sepia photograph.

The coefficients are the widely used Microsoft ones, which are what most software means by “sepia”.

Returns list

saturation_matrix()

imagine.filters.saturation_matrix(amount: number) -> list

A colour matrix that scales saturation.

An amount of 0 removes all colour, 1 leaves the image alone, and values above 1 push saturation past its original level. Amounts below 0 pass through the grey point and out the other side, which inverts hues; that is well defined but rarely what anyone wants.

Parameters

  • amount (number)

Returns list

hue_rotate_matrix()

imagine.filters.hue_rotate_matrix(degrees: number) -> list

A colour matrix that rotates every hue around the colour wheel by an angle in degrees, leaving brightness and saturation alone.

This is the rotation from the SVG filter specification, which approximates a true rotation in a luma-preserving space closely enough that the difference is not visible.

Parameters

  • degrees (number) — Positive rotates toward red, negative away from it.

Returns list

duotone_matrix()

imagine.filters.duotone_matrix(shadow, highlight) -> list

A colour matrix that replaces every pixel’s colour with a blend between two colours chosen by its brightness, a duotone.

Dark pixels take shadow, light ones take highlight, and everything between is interpolated. Alpha is untouched.

Parameters

  • shadow (Color|string|number) — The colour black maps to.
  • highlight (Color|string|number) — The colour white maps to.

Returns list

combine_matrices()

imagine.filters.combine_matrices(first: list, second: list) -> list

Multiplies two colour matrices, giving one matrix with the effect of applying first and then second.

Combining matrices this way and applying the result once is both faster and more accurate than applying each in turn, because the intermediate result is never rounded back to 8 bits.

Parameters

  • first (list)
  • second (list)

Returns list

build_lut()

imagine.filters.build_lut(fn: function) -> bytes

Builds a 256-entry lookup table from a function.

The function is called once per possible input value, 0 through 255, and whatever it returns is clamped to a byte. This is the general way to write a tone curve: build the table once, and the image is then rewritten at memory speed no matter how large it is.

import imagine { filters }

# A steep S-curve, for contrast that keeps its highlights.
var curve = filters.build_lut(@(value) {
  var t = value / 255
  return 255 * t * t * (3 - 2 * t)
})

Parameters

  • fn (function(1)) — Called with an input value, returns the output.

Returns bytes

identity_lut()

imagine.filters.identity_lut() -> bytes

A lookup table that leaves every value alone.

Returns bytes

brightness_lut()

imagine.filters.brightness_lut(delta: number) -> bytes

A lookup table that adds a fixed amount to every value.

Parameters

  • delta (number) — -255 to 255. Positive brightens.

Returns bytes

contrast_lut()

imagine.filters.contrast_lut(amount: number) -> bytes

A lookup table that pushes values away from or toward mid-grey.

The amount runs -100 (everything collapses to grey) through 0 (no change) to 100 (everything is forced to black or white). This is the contrast curve most image editors use.

Parameters

  • amount (number) — -100 to 100.

Returns bytes

gamma_lut()

imagine.filters.gamma_lut(gamma: number) -> bytes

A lookup table applying a gamma curve.

Values below 1 darken the midtones and values above 1 lighten them, while black and white stay put. Correcting an image encoded at one gamma for display at another is the usual reason to reach for this.

Parameters

  • gamma (number) — Greater than zero.

Returns bytes

invert_lut()

imagine.filters.invert_lut() -> bytes

A lookup table that inverts every value.

Returns bytes

threshold_lut()

imagine.filters.threshold_lut(level: number) -> bytes

A lookup table that forces every value to black or white.

Parameters

  • level (number) — Values below this become 0, the rest become 255.

Returns bytes

posterize_lut()

imagine.filters.posterize_lut(levels: number) -> bytes

A lookup table that reduces each channel to a fixed number of evenly spaced steps.

Parameters

  • levels (number) — 2 or more. Two gives a hard black and white split.

Returns bytes

scale_lut()

imagine.filters.scale_lut(factor: number) -> bytes

A lookup table that scales every value by a factor.

Applied to the alpha channel, this is what Image.opacity() does.

Parameters

  • factor (number) — 0 to 1, though larger values are allowed and clamp.

Returns bytes

levels_lut()

imagine.filters.levels_lut(black: number, white: number, gamma: ?number) -> bytes

A lookup table implementing a levels adjustment.

Everything at or below black becomes 0, everything at or above white becomes 255, and the range between is stretched to fill the whole scale with gamma applied along the way. This is the single most useful correction for a flat or washed-out photograph.

Parameters

  • black (number) — The input value that becomes black, 0-255.
  • white (number) — The input value that becomes white, 0-255.
  • gamma (?number) — The midtone curve - Default: 1 (linear).

Returns bytes

sharpen_kernel()

imagine.filters.sharpen_kernel(strength) -> list

A 3x3 sharpening kernel.

The strength is how much the centre pixel is emphasised over its neighbours; 1 is a moderate sharpen and values much above 3 start producing halos around edges.

Parameters

  • strength (?number) — Default: 1.

Returns list

box_blur_kernel()

imagine.filters.box_blur_kernel() -> list

A 3x3 box blur kernel; every neighbour counts equally.

Cheaper than a Gaussian and visibly boxier. Image.blur() uses a real Gaussian instead; this is here for the cases that want the harder look, or a single very cheap pass.

Returns list

emboss_kernel()

imagine.filters.emboss_kernel() -> list

A 3x3 kernel that lifts edges into a grey relief.

Used with an offset of 128, so flat areas come out mid-grey rather than black; Image.emboss() supplies that offset.

Returns list

edge_kernel()

imagine.filters.edge_kernel() -> list

A 3x3 Laplacian kernel that leaves only the edges.

Its weights sum to zero, so it needs an explicit divisor of 1 rather than the usual “divide by the sum”.

Returns list

mean_removal_kernel()

imagine.filters.mean_removal_kernel() -> list

A 3x3 kernel that removes local mean, exaggerating detail.

Returns list

smooth_kernel()

imagine.filters.smooth_kernel(weight) -> list

A 3x3 smoothing kernel weighted toward the centre pixel.

A larger weight keeps more of the original and smooths less.

Parameters

  • weight (?number) — The centre weight - Default: 8.

Returns list

gaussian_kernel()

imagine.filters.gaussian_kernel(sigma: number) -> list

A square Gaussian kernel with the given standard deviation.

Image.blur() uses a separable Gaussian instead, which is far faster for anything but the smallest radius; this exists for code that wants the kernel itself, to combine with another.

Parameters

  • sigma (number) — Greater than zero.

Returns list


2026, Richard Ore and Zuri contributors

imagine.font

import imagine.font

imagine lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled imagine.font.* needs import imagine.font.

Font: a loaded TrueType or OpenType face, ready to measure and draw text with.

Glyph rasterisation and metrics come from the font file itself, so kerning and line height are whatever the designer specified rather than an approximation.

Classes

Font

class imagine.Font

A typeface at a particular size, ready to draw with.

A Font is immutable and cheap to copy: size() hands back another Font sharing the same parsed face, so keeping one font around and asking it for several sizes costs nothing extra.

import imagine { Image, Font, Color }

var title = Font.load('assets/Inter.ttf').size(32)
var body = title.size(14)

Image(400, 120, Color.WHITE)
  .text(20, 20, 'Quarterly report', title, '#111111')
  .text(20, 64, 'Revenue is up 12% year on year.', body, '#555555')
  .save('report.png')
What text layout does and does not do

Glyphs are positioned by advance width with kerning applied, and \n starts a new line. That covers Latin, Greek, Cyrillic and anything else written left to right without contextual shaping.

It does not do complex shaping: Arabic letters will not join, Indic clusters will not reorder, and ligatures are not substituted. Those need a shaping engine, and a standard library that pretended to do them would be worse than one that says plainly it does not.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

imagine.Font(handle, size, source)

Wraps an already-parsed face.

Not the way to get a Font; use load(), from_bytes(), system() or default(), all of which end up here.

Parameters

  • ptr — handle
  • size (number)
  • source (string)

Font.load()

imagine.Font.load(path: string, size) -> Font

Loads a TrueType or OpenType font from a file.

The parsed face is cached by absolute path, so loading the same file twice in one process parses it once.

Parameters

  • path (string) — A path to a .ttf, .otf or .ttc file.
  • size (?number) — The size in pixels - Default: 16.

Returns Font

Font.from_bytes()

imagine.Font.from_bytes(data: bytes, size) -> Font

Parses a font already held in memory.

Useful for a font embedded in the program, downloaded, or read out of an archive. Nothing is cached, since there is no path to key a cache on; call this once and keep the result.

Parameters

  • data (bytes)
  • size (?number) — The size in pixels - Default: 16.

Returns Font

Font.system()

imagine.Font.system(family, size) -> Font

Finds a font installed on this machine by family name.

The standard font directories for the platform are searched, and the match ignores case, spaces and hyphens, so 'DejaVu Sans', 'dejavusans' and 'DejaVuSans' all find the same file. A family with several weights matches whichever file the search reaches first, which is why load() is the right call when a specific weight matters.

Raises FontError when nothing matches.

Parameters

  • family (string)
  • size (?number) — The size in pixels - Default: 16.

Returns Font

Font.sans()

imagine.Font.sans(size) -> Font

Finds a reasonable sans-serif font installed on this machine.

A short list of the families most likely to be installed is tried in turn. This is a convenience for scripts and tests, not something to rely on for output that has to look the same everywhere: which font it lands on depends entirely on the machine, and a container image with no fonts installed has none to find.

Raises FontError when the machine has no usable font, with a message saying so rather than a parse failure.

Parameters

  • size (?number) — The size in pixels - Default: 16.

Returns Font

Font.builtin()

imagine.Font.builtin(size) -> StrokeFont

The built-in font, which is always available.

imagine ships no font file, so system() and sans() both depend on what happens to be installed and can fail. This one cannot: it is drawn from stroke geometry rather than loaded, and scales to any size.

The result is a [[imagine.StrokeFont]], not a Font. The two have the same interface, so anywhere a font is accepted either works. Read that class’s own documentation for what the built-in font is and is not suitable for.

var font = Font.builtin(20)

chart.text(12, 8, 'requests / second', font, '#334155')

Parameters

  • size (?number) — The size in pixels - Default: 16.

Returns StrokeFont

Font.size()

imagine.Font.size(pixels: number) -> Font

Returns the same typeface at a different size.

The parsed face is shared, so this is cheap enough to call in a loop.

Parameters

  • pixels (number) — The size in pixels.

Returns Font

Font.pixel_size()

imagine.Font.pixel_size() -> number

The size in pixels this font draws at.

Returns number

Font.name()

imagine.Font.name() -> string

The typeface’s own name, as recorded in the font file.

Returns string

Font.source()

imagine.Font.source() -> string

Where this font came from: an absolute path, or '<memory>' for one parsed from bytes.

Returns string

Font.metrics()

imagine.Font.metrics() -> dict

The font’s vertical metrics at its current size, in pixels.

The dictionary holds:

  • name: the typeface’s name.
  • ascent: how far the tallest glyphs rise above the baseline.
  • descent: how far descenders fall below it, as a negative number.
  • line_gap: the designer’s recommended extra space between lines.
  • line_height: ascent minus descent plus line gap, which is the distance between consecutive baselines at single spacing.

Returns dict

Font.measure()

imagine.Font.measure(text, options) -> dict

Measures a string without drawing it.

Returns a dictionary holding width and height in pixels, baseline (the first line’s baseline, measured down from the top of the box) and lines (how many lines the string has).

The box covers the text’s advance widths. A glyph can paint a fraction of a pixel outside it, which is why Image.text() allows for a little slack when it draws.

Parameters

  • text (string)
  • options (?dict) — tracking, line_height, align.

Returns dict

Font.wrap()

imagine.Font.wrap(text, width: number, options) -> string

Breaks a string into lines that fit within a width.

Returns the same text with newlines inserted, ready to hand to Image.text(). Newlines already in the text are kept as paragraph breaks, and runs of spaces are collapsed to one.

Words are kept whole where they can be. A single word too long for the width is broken between characters rather than allowed to overflow, since overflowing text in an image cannot be scrolled to.

var body = Font.load('Inter.ttf', 16)
var text = body.wrap(article, 520)

page.text(40, 120, text, body, '#334155')

Parameters

  • text (string)
  • width (number) — The maximum line width in pixels.
  • options (?dict) — tracking, as measure() takes.

Returns string

Font.render()

imagine.Font.render(text, options) -> dict

Rasterizes a string into an 8-bit coverage mask.

Returns a dictionary holding coverage (the mask, one byte per pixel), width, height and baseline. Drawing the mask through Canvas.draw_mask() is what Image.text() does; this is here for code that wants the mask itself, to use as a stencil or an alpha channel.

Parameters

  • text (string)
  • options (?dict) — tracking, line_height, align.

Returns dict

Font.to_string()

imagine.Font.to_string()

2026, Richard Ore and Zuri contributors

imagine.formats

import imagine

imagine exposes this as imagine.formats, so import imagine is enough and the names are called as imagine.formats.*. import imagine.formats reaches the same definitions directly.

What the build can actually read and write, and how to tell one format from another.

detect() and probe() identify an image from its leading bytes rather than its file name, which is the only reliable way to do it. readable(), writable() and animatable() report what this particular build supports, since that depends on which codecs were compiled in.

Functions

from_extension()

imagine.formats.from_extension(path: string) -> ?string

The format name a file extension implies, or nil when the extension is not one this module knows.

The argument may be a bare extension or a whole path, with or without a leading dot, in any case.

%> imagine.formats.from_extension('photo.JPG')
'jpeg'
%> imagine.formats.from_extension('.webp')
'webp'

Parameters

  • path (string)

Returns ?string

extension_for()

imagine.formats.extension_for(format: string) -> string

The file extension a format is normally written with, without a leading dot.

Parameters

  • format (string)

Returns string

Raises FormatError When format is not a known format.

mime_for()

imagine.formats.mime_for(format: string) -> string

The IANA media type for a format, suitable for a Content-Type header.

Parameters

  • format (string)

Returns string

Raises FormatError When format is not a known format.

normalize()

imagine.formats.normalize(format: string) -> string

Normalises a format name, accepting the common aliases.

'JPG', 'jpeg' and '.jpg' all come back as 'jpeg'.

Parameters

  • format (string)

Returns string

Raises FormatError When format is not a known format.

detect()

imagine.formats.detect(data: bytes) -> ?string

Identifies image data by its content rather than its name, or returns nil when the bytes are not a recognisable image.

This reads only as much of the header as it needs, so it is cheap enough to run over an upload before deciding whether to decode it.

var kind = imagine.formats.detect(upload)

if kind == nil {
  raise Exception('that is not an image')
}

Parameters

  • data (bytes)

Returns ?string

probe()

imagine.formats.probe(data: bytes) -> ?dict

Reads an image’s format and dimensions without decoding its pixels.

Returns a dictionary holding format, width and height, or nil when the data is not a recognisable image. Reading a multi-megapixel JPEG’s header costs a few microseconds where decoding it costs tens of milliseconds, so this is the right way to reject an oversized upload before committing to it.

Parameters

  • data (bytes)

Returns ?dict

readable()

imagine.formats.readable() -> list

Every format this build can read.

Returns list

writable()

imagine.formats.writable() -> list

Every format this build can write.

Returns list

animatable()

imagine.formats.animatable() -> list

Every format this build can read as an animation.

Returns list

decode_wbmp()

imagine.formats.decode_wbmp(data: bytes)

Decodes a WBMP into straight RGBA pixels.

Set bits become opaque white and clear bits opaque black; the format has no colour and no transparency.

Parameters

  • data (bytes)

Returns — dict: pixels, width, height.

Raises DecodeError When the data is not a well-formed WBMP.

encode_wbmp()

imagine.formats.encode_wbmp(pixels: bytes, width: number, height: number, threshold: ?number) -> bytes

Encodes straight RGBA pixels as a WBMP.

Each pixel is reduced to one bit by comparing its perceived brightness against threshold. Transparent pixels are treated as white, since the format has no way to say “nothing here” and white is what a viewer will show as background.

Parameters

  • pixels (bytes)
  • width (number)
  • height (number)
  • threshold (?number) — The brightness cut, 0-255 - Default: 128.

Returns bytes


2026, Richard Ore and Zuri contributors

imagine.image

import imagine.image

imagine lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled imagine.image.* needs import imagine.image.

Image: a raster image as an RGBA pixel buffer, and everything that transforms one.

Resizing, cropping, rotation, compositing, colour adjustment and the named filters are all methods here. Every one produces a new image rather than altering the receiver, except where a method says otherwise.

Classes

Image

class imagine.Image < Canvas

A raster image: a rectangle of 8-bit RGBA pixels, everything that draws onto one, and everything that reshapes, filters or writes one out.

import imagine { Image, Color, LANCZOS }

Image.open('photo.jpg')
  .thumbnail(400, 400, LANCZOS)
  .save('thumb.webp')
Which operations copy and which do not

There is one rule, and it is worth learning because it is the only thing here that is not obvious:

  • Anything that changes the image’s size returns a new Image. resize(), crop(), rotate(), flip(), pad() and the rest leave the original untouched.
  • Everything else changes the image in place and returns it. Drawing, filters and compositing all work on the image you called them on, and return it so calls can be chained.

That means a chain mixing the two kinds still reads correctly, and clone() is there for the times you want to keep the original before a filter.

var original = Image.open('photo.jpg')
var small = original.thumbnail(200, 200)   # original untouched

small.grayscale()                          # small is now grey
Memory

An image costs four bytes per pixel, so a 6000x4000 photograph is 96 MB in memory however small its file was. Reach for [[imagine.formats.probe]] before opening anything whose size you do not control.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

imagine.Image(width: number, height: number, fill)

Creates a new image.

The image starts fully transparent unless a fill colour is given.

Parameters

  • width (number) — At least 1.
  • height (number) — At least 1.
  • fill (?Color|string|number) — A colour to fill with - Default: transparent.

Raises BoundsError When either dimension is below 1.

Image.open()

imagine.Image.open(source, options) -> Image

Opens an image file.

The format is detected from the file’s contents, not its name, so a mislabelled file still opens correctly.

JPEGs carrying an EXIF orientation tag are rotated upright, since that is what the photograph was meant to look like and almost never what the raw pixels are. Pass { orient: false } to get the pixels exactly as stored.

Parameters

  • source (string|file) — A path, or an already-open file.
  • options (?dict) — orient (bool, default true), format (string).

Returns Image

Raises DecodeError When the file is not a readable image.

Image.decode()

imagine.Image.decode(data: bytes, options) -> Image

Decodes image data held in memory.

The format is detected from the data unless format says otherwise, in which case a file that is not really that format fails loudly rather than being decoded as whatever it actually is.

Parameters

  • data (bytes)
  • options (?dict) — orient (bool, default true), format (string).

Returns Image

Raises DecodeError When the data is not a readable image.

Image.from_pixels()

imagine.Image.from_pixels(width, height, pixels, format) -> Image

Wraps an existing RGBA pixel buffer as an image.

The buffer is adopted, not copied, so whatever produced it must not keep writing to it. Pixels run red, green, blue, alpha with straight (not premultiplied) alpha and no row padding.

Parameters

  • width (number)
  • height (number)
  • pixels (bytes) — Exactly width * height * 4 bytes.
  • format (?string) — The format these pixels were decoded from, recorded as metadata - Default: nil.

Returns Image

Image.size()

imagine.Image.size() -> dict

The image’s dimensions as {width, height}.

Returns dict

Image.format()

imagine.Image.format() -> ?string

The format this image was decoded from, or nil for one created in memory.

This records where the image came from and never constrains where it can go; a PNG can always be saved as a JPEG.

Returns ?string

Image.set_format()

imagine.Image.set_format(format)

Records which format this image should be considered to have come from.

The only thing this changes is what encode() and save() write when no format is given. It is set for you when an image is decoded; setting it by hand is for pixels that arrived some other way and whose origin you happen to know.

Parameters

  • format (?string) — A format name, or nil to clear it.

Returns — self

Image.bounds()

imagine.Image.bounds() -> dict

The rectangle covering the whole image, as {x, y, width, height}.

Returns dict

Image.is_opaque()

imagine.Image.is_opaque() -> bool

true when every pixel is fully opaque.

This walks the alpha channel, so it costs a pass over the image. Worth knowing before choosing an output format: an opaque image loses nothing as a JPEG.

Returns bool

Image.info()

imagine.Image.info() -> dict

A summary of the image as a dictionary holding width, height, format, pixels (the count, not the buffer) and opaque.

Returns dict

Image.clone()

imagine.Image.clone() -> Image

An independent copy of the image, sharing nothing with it.

Returns Image

Image.layer()

imagine.Image.layer(fill) -> Image

A new image of the same size, fully transparent.

The usual way to build up a composite: draw onto the layer, then bring it back with draw_image() under whichever blend mode and opacity the effect calls for.

Parameters

  • fill (?Color|string|number) — A colour to fill with - Default: transparent.

Returns Image

Image.resize()

imagine.Image.resize(width, height, filter) -> Image

Returns the image resampled to an exact size.

The aspect ratio is not preserved; the result is exactly the size asked for. Use thumbnail() or cover() when the proportions matter.

Parameters

  • width (number) — At least 1.
  • height (number) — At least 1.
  • filter (?string) — A resampling filter - Default: LANCZOS.

Returns Image

Raises BoundsError When either dimension is below 1.

Image.scale()

imagine.Image.scale(factor: number, filter) -> Image

Returns the image resampled by a factor, keeping its proportions.

Parameters

  • factor (number) — Greater than zero. 0.5 halves each side.
  • filter (?string) — A resampling filter - Default: LANCZOS.

Returns Image

Image.thumbnail()

imagine.Image.thumbnail(max_width, max_height, filter) -> Image

Returns the image shrunk to fit inside a box, keeping its proportions.

The result is no larger than the box in either direction, and usually smaller in one. An image that already fits is returned at its original size rather than enlarged, which is what makes this the right call for thumbnails: enlarging a small image to fill a thumbnail box only makes it blurry.

Parameters

  • max_width (number)
  • max_height (number)
  • filter (?string) — A resampling filter - Default: LANCZOS.

Returns Image

Image.fit()

imagine.Image.fit(width, height, filter) -> Image

Returns the image scaled to fit inside a box, keeping its proportions, enlarging it if it is smaller than the box.

The difference from thumbnail() is exactly that: this one will scale up.

Parameters

  • width (number)
  • height (number)
  • filter (?string) — A resampling filter - Default: LANCZOS.

Returns Image

Image.cover()

imagine.Image.cover(width, height, options) -> Image

Returns the image filling a box exactly, keeping its proportions by cropping whatever overflows.

This is what a CSS background-size: cover does, and what almost every avatar or card thumbnail wants: the box is filled edge to edge with no distortion and no letterboxing, at the cost of losing some of the image.

Parameters

  • width (number)
  • height (number)
  • options (?dict) — anchor (where the kept part comes from, default CENTER), filter.

Returns Image

Image.contain()

imagine.Image.contain(width, height, options) -> Image

Returns the image fitted inside a box exactly, keeping its proportions by padding whatever is left over.

The counterpart to cover(): nothing is lost, but the result has bars on two sides.

Parameters

  • width (number)
  • height (number)
  • options (?dict) — background (the padding colour, default transparent), anchor (default CENTER), filter.

Returns Image

Image.crop()

imagine.Image.crop(x, y, width, height) -> Image

Returns a rectangular region of the image.

The rectangle must lie entirely inside the image. Clamping a crop that runs off the edge would hand back different dimensions than were asked for, which is a worse surprise than an error; use pad() first if the region really is meant to extend past the edge.

Parameters

  • x (number)
  • y (number)
  • width (number)
  • height (number)

Returns Image

Raises BoundsError When the rectangle does not fit inside the image.

Image.trim()

imagine.Image.trim(options) -> Image

Returns the image with a uniform border trimmed away.

The border colour is taken from the top-left pixel unless background names one. tolerance is how far a pixel may differ from it and still count as border, measured as the largest difference across the four channels.

An image that is entirely border is returned as a 1x1 image rather than an empty one, since an image with no pixels is not a thing this module can represent.

Parameters

  • options (?dict) — background, tolerance (default 0).

Returns Image

Image.pad()

imagine.Image.pad(top, right, bottom, left, background) -> Image

Returns the image with a border added on each side.

Negative amounts are not accepted; use crop() to remove edges.

Parameters

  • top (number)
  • right (?number) — Default: the same as top.
  • bottom (?number) — Default: the same as top.
  • left (?number) — Default: the same as right.
  • background (?Color|string|number) — Default: transparent.

Returns Image

Image.rotate()

imagine.Image.rotate(degrees: number, background) -> Image

Returns the image rotated by an angle in degrees, clockwise.

Quarter turns are exact and lossless. Any other angle resamples the pixels and grows the canvas to hold the rotated corners, filling the gaps with background.

Parameters

  • degrees (number)
  • background (?Color|string|number) — The colour behind the corners
    • Default: transparent.

Returns Image

Image.rotate_90()

imagine.Image.rotate_90() -> Image

Returns the image turned a quarter turn clockwise. Lossless.

Returns Image

Image.rotate_180()

imagine.Image.rotate_180() -> Image

Returns the image turned upside down. Lossless.

Returns Image

Image.rotate_270()

imagine.Image.rotate_270() -> Image

Returns the image turned a quarter turn anticlockwise. Lossless.

Returns Image

Image.flip()

imagine.Image.flip(mode) -> Image

Returns the image mirrored.

Parameters

  • mode (?number) — FLIP_HORIZONTAL, FLIP_VERTICAL or FLIP_BOTH
    • Default: FLIP_HORIZONTAL.

Returns Image

Image.flip_horizontal()

imagine.Image.flip_horizontal() -> Image

Returns the image mirrored left to right.

Returns Image

Image.flip_vertical()

imagine.Image.flip_vertical() -> Image

Returns the image mirrored top to bottom.

Returns Image

Image.transpose()

imagine.Image.transpose() -> Image

Returns the image reflected across its main diagonal, so a w x h image becomes h x w.

Returns Image

Image.apply_lut()

imagine.Image.apply_lut(red, green, blue, alpha)

Rewrites every pixel through up to four 256-entry lookup tables, one per channel.

This is the primitive behind most of the adjustments below, and it is the fastest way to apply any per-channel tone curve: the table is built once whatever the image’s size, and applying it is a single indexed load per channel.

A nil table leaves that channel alone. Tables are built with the helpers in [[imagine.filters]], or with [[imagine.filters.build_lut]] for a curve of your own.

Parameters

  • red (?bytes) — A 256-entry table, or nil.
  • green (?bytes)
  • blue (?bytes)
  • alpha (?bytes)

Returns — self

Image.apply_matrix()

imagine.Image.apply_matrix(matrix)

Rewrites every pixel through a 4x5 colour matrix.

The matrix is a flat list of 20 numbers, laid out row by row, and is the same shape SVG and CSS filters use. The last entry of each row is a constant added afterwards, in 0-255 units.

r' = m0*r  + m1*g  + m2*b  + m3*a  + m4
g' = m5*r  + m6*g  + m7*b  + m8*a  + m9
b' = m10*r + m11*g + m12*b + m13*a + m14
a' = m15*r + m16*g + m17*b + m18*a + m19

Applying several matrices in a row is both slower and less accurate than combining them with [[imagine.filters.combine_matrices]] and applying the result once.

Parameters

  • matrix (list) — 20 numbers.

Returns — self

Image.brightness()

imagine.Image.brightness(delta)

Adds a fixed amount to every colour channel.

Parameters

  • delta (number) — -255 to 255. Positive brightens.

Returns — self

Image.contrast()

imagine.Image.contrast(amount)

Pushes colours away from or toward mid-grey.

Parameters

  • amount (number) — -100 (flat grey) through 0 (unchanged) to 100 (hard black and white).

Returns — self

Image.gamma()

imagine.Image.gamma(gamma)

Applies a gamma curve, moving the midtones without touching black or white.

Parameters

  • gamma (number) — Greater than zero. Below 1 darkens, above 1 lightens.

Returns — self

Image.levels()

imagine.Image.levels(black, white, gamma)

Stretches a range of input values across the full scale.

The single most useful correction for a flat or washed-out photograph: everything at or below black becomes black, everything at or above white becomes white, and the rest is spread between them.

Parameters

  • black (number) — The input value that becomes black, 0-255.
  • white (number) — The input value that becomes white, 0-255.
  • gamma (?number) — A midtone curve to apply along the way - Default: 1.

Returns — self

Image.invert()

imagine.Image.invert()

Inverts the colour channels, producing a photographic negative.

Alpha is untouched, so a transparent image stays transparent rather than becoming opaque.

Returns — self

Image.threshold()

imagine.Image.threshold(level)

Forces every pixel to black or white by comparing its brightness against a threshold.

The image is converted to grey first, so the split is made on perceived brightness rather than on each channel separately.

Parameters

  • level (?number) — The cut, 0-255 - Default: 128.

Returns — self

Image.posterize()

imagine.Image.posterize(levels)

Reduces each channel to a fixed number of evenly spaced steps.

This flattens gradients into visible bands. For a reduction that picks the colours to keep rather than spacing them evenly, use quantize().

Parameters

  • levels (number) — 2 or more.

Returns — self

Image.opacity()

imagine.Image.opacity(factor)

Scales the alpha channel, making the whole image more transparent.

The scaling is relative: applying 0.5 twice leaves a quarter of the original opacity, not half.

Parameters

  • factor (number) — 0 (invisible) to 1 (unchanged).

Returns — self

Image.grayscale()

imagine.Image.grayscale()

Converts the image to shades of grey.

Channels are weighted for perceived brightness, so a bright yellow comes out light and a deep blue comes out dark, which is what the eye expects.

Returns — self

Image.sepia()

imagine.Image.sepia()

Gives the image the warm brown cast of an old photograph.

Returns — self

Image.saturate()

imagine.Image.saturate(amount)

Scales the image’s saturation.

Parameters

  • amount (number) — 0 removes all colour, 1 leaves it alone, above 1 intensifies.

Returns — self

Image.hue_rotate()

imagine.Image.hue_rotate(degrees)

Rotates every hue around the colour wheel, leaving brightness and saturation alone.

Parameters

  • degrees (number)

Returns — self

Image.duotone()

imagine.Image.duotone(shadow, highlight)

Maps the image’s brightness onto a two-colour ramp.

Dark pixels take shadow, light ones take highlight, and everything between is interpolated.

Parameters

  • shadow (Color|string|number)
  • highlight (Color|string|number)

Returns — self

Image.tint()

imagine.Image.tint(color, amount)

Blends a flat colour into the image.

At an amount of 1 the image becomes that colour entirely; at 0.2 it takes on a wash of it. Alpha is untouched, so transparent areas stay transparent.

Parameters

  • color (Color|string|number)
  • amount (?number) — 0 to 1 - Default: 0.5.

Returns — self

Image.colorize()

imagine.Image.colorize(color)

Converts to grey and then tints, producing a monochrome image in a single colour.

Parameters

  • color (Color|string|number)

Returns — self

Image.quantize()

imagine.Image.quantize(colors, dither)

Reduces the image to at most a given number of colours.

Unlike posterize(), the colours kept are chosen to suit the image rather than spaced evenly, so a photograph survives far fewer of them. Dithering trades visible banding for a fine stipple, which almost always looks better at low colour counts.

This is also a preview of what GIF export will do, since GIF cannot hold more than 256 colours per frame.

Parameters

  • colors (?number) — 2 to 256 - Default: 256.
  • dither (?bool) — Default: false.

Returns — self

Image.premultiply()

imagine.Image.premultiply()

Multiplies the colour channels by alpha.

imagine keeps straight alpha everywhere, so this is for handing pixels to something that expects them premultiplied. It loses precision in near-transparent pixels and cannot be perfectly undone.

Returns — self

Image.unpremultiply()

imagine.Image.unpremultiply()

Undoes premultiply(), as far as 8 bits allow.

Returns — self

Image.convolve()

imagine.Image.convolve(kernel, options)

Applies a convolution kernel to the image.

The kernel is a flat list of size * size numbers with an odd size. The default divisor of 0 means “divide by the kernel’s own sum”, which is what nearly every published kernel expects; pass an explicit divisor for the ones whose weights sum to zero.

Alpha goes through the kernel along with the colour channels unless keep_alpha says otherwise, so blurring a shape softens its edge rather than leaving a hard cutout. Turn that off for any kernel whose weights do not sum to one: a Laplacian over a uniformly opaque image sums to zero, and would make the whole result invisible.

Parameters

  • kernel (list)
  • options (?dict) — divisor (default 0), offset (default 0), edge (default EDGE_CLAMP), keep_alpha (default false).

Returns — self

Image.blur()

imagine.Image.blur(radius)

Blurs the image with a true Gaussian.

The radius is the blur’s standard deviation in pixels: about two thirds of each pixel’s contribution falls within that distance, and the visible spread is roughly three times it. Cost grows with the radius but only linearly, because the blur is separable.

Alpha is premultiplied for the duration, so blurring a shape on a transparent background does not drag a dark halo into its edge.

Parameters

  • radius (?number) — Greater than zero - Default: 2.

Returns — self

Image.sharpen()

imagine.Image.sharpen(strength)

Sharpens the image.

Parameters

  • strength (?number) — 1 is moderate; much above 3 starts producing halos - Default: 1.

Returns — self

Image.smooth()

imagine.Image.smooth(weight)

Softens the image with a weighted 3x3 average.

Cheaper and harsher than blur(), and fixed at one pixel of reach.

Parameters

  • weight (?number) — How much of the original to keep - Default: 8.

Returns — self

Image.emboss()

imagine.Image.emboss()

Turns the image into a grey relief, as if stamped into metal.

Returns — self

Image.edges()

imagine.Image.edges()

Leaves only the image’s edges, everything flat going black.

Returns — self

Image.mean_removal()

imagine.Image.mean_removal()

Exaggerates local detail by removing the local mean.

Returns — self

Image.pixelate()

imagine.Image.pixelate(size)

Replaces each block of pixels with its average, producing the familiar censoring mosaic.

Parameters

  • size (?number) — The block’s side in pixels - Default: 8.

Returns — self

Image.draw_image()

imagine.Image.draw_image(source: instance, x, y, options)

Draws another image onto this one.

The source is clipped to this image’s edges, and its position may be negative, so a sprite hanging off the top-left corner draws correctly.

var badge = Image.open('badge.png')

photo.draw_image(badge, 12, 12, { opacity: 0.8 })

Parameters

  • source (Image)
  • x (?number) — Default: 0.
  • y (?number) — Default: 0.
  • options (?dict) — opacity (0-1, default 1), blend (default BLEND_NORMAL).

Returns — self

Image.place()

imagine.Image.place(source: instance, anchor, options)

Draws another image onto this one, positioned by anchor rather than by coordinates.

photo.place(watermark, BOTTOM_RIGHT, { margin: 16, opacity: 0.5 })

Parameters

  • source (Image)
  • anchor (?string) — Default: CENTER.
  • options (?dict) — margin (inset from the edges, default 0), plus everything draw_image() takes.

Returns — self

Image.mask()

imagine.Image.mask(mask: instance)

Uses another image as an alpha mask.

Each of this image’s pixels keeps its colour and takes its transparency from the mask: where the mask is opaque white this image shows through fully, where the mask is black or transparent this image disappears, and greys give partial transparency.

The mask’s own alpha counts too, so a shape drawn on a transparent background works as a mask without being filled in first.

The mask must be the same size as this image.

Parameters

  • mask (Image)

Returns — self

Raises BoundsError When the mask is a different size.

Image.flatten()

imagine.Image.flatten(background)

Composites the image over a solid colour, leaving it fully opaque.

This is what happens on export to a format without an alpha channel; doing it explicitly first lets you choose the colour and see the result.

Parameters

  • background (?Color|string|number) — Default: white.

Returns — self

Image.histogram()

imagine.Image.histogram() -> dict

Counts how many pixels hold each value, per channel.

Returns a dictionary of four 256-entry lists: red, green, blue and luma, where luma is perceived brightness. Fully transparent pixels are skipped, since their colour is not visible and would otherwise pile up at whatever the buffer happens to hold there.

A histogram is what tells you an image is underexposed (everything bunched at the low end), flat (bunched in the middle), or clipped (a spike at 0 or 255).

Returns dict

Image.auto_levels()

imagine.Image.auto_levels(options)

Stretches the image’s tones to fill the full range.

The black and white points are found from the brightness histogram, ignoring a small fraction at each end so that a handful of stray pixels cannot decide the result. This is the automatic version of levels(), and it is the first thing to try on a flat or hazy photograph.

An image that already spans the full range, or one with no visible pixels at all, is left alone.

Parameters

  • options (?dict) — clip (the fraction to ignore at each end, 0 to 0.2 - Default: 0.005), gamma (a midtone curve to apply as well - Default: 1).

Returns — self

Image.average_color()

imagine.Image.average_color() -> Color

The average colour of the image.

Pixels are weighted by their alpha, so a mostly transparent image reports the colour of the part you can actually see rather than having it washed out by whatever is behind the transparency. The returned colour’s own alpha is the mean alpha.

An image with nothing visible in it returns transparent black.

Returns Color

Image.dominant_colors()

imagine.Image.dominant_colors(count, options) -> list

The colours that occupy the most of the image, most common first.

The image is shrunk and reduced to a small palette first, so this costs about the same whatever the original size. Nearly transparent pixels are ignored.

The usual use is picking a background or accent colour to show while a photograph is still loading.

var accent = photo.dominant_colors(1)[0]

Parameters

  • count (?number) — How many to return - Default: 5.
  • options (?dict) — sample (the size the image is reduced to before counting - Default: 96), palette (how many colours to quantize to - Default: 32).

Returns list

Image.difference()

imagine.Image.difference(other: instance) -> number

How different two images are, from 0 (identical) to 1.

The score is the mean difference across the colour channels, weighted by how opaque each pixel is in either image, so transparent regions do not count as agreement. Images of different sizes are never comparable and return 1.

if rendered.difference(expected) > 0.01 {
  raise Exception('the rendering changed')
}

Parameters

  • other (Image)

Returns number

Image.text()

imagine.Image.text(x, y, text, font, color, options)

Draws a string.

x and y are the top-left corner of the text’s box, not its baseline, because that is what you know when placing text in a layout. Pass { baseline: true } to treat y as the first line’s baseline instead.

A \n starts a new line. Multi-line text is aligned by the align option, which positions the lines against each other, not against the image.

var font = Font.load('Inter.ttf').size(28)

card.text(24, 24, 'Hello\nthere', font, '#111111', {
  align: ALIGN_CENTER,
  line_height: 1.4,
})

Parameters

  • x (number)
  • y (number)
  • text (string)
  • font (Font)
  • color (Color|string|number)
  • options (?dict) — align, line_height (multiplier, default 1), tracking (extra pixels between characters, default 0), baseline (bool, default false), width (wrap to this many pixels).

Returns — self

Image.text_size()

imagine.Image.text_size(text, font, options) -> dict

Measures a string as text() would draw it, without drawing it.

Returns a dictionary holding width, height, baseline and lines.

Parameters

  • text (string)
  • font (Font)
  • options (?dict) — The same options text() takes, width included, so a measurement of wrapped text matches what would actually be drawn.

Returns dict

Image.place_text()

imagine.Image.place_text(text, font, color, anchor, options)

Draws a string positioned by anchor rather than by coordinates.

Parameters

  • text (string)
  • font (Font)
  • color (Color|string|number)
  • anchor (?string) — Default: CENTER.
  • options (?dict) — margin, plus everything text() takes.

Returns — self

Image.encode()

imagine.Image.encode(format, options) -> bytes

Encodes the image and returns the bytes.

Options understood, by format:

  • quality (1-100) for JPEG and AVIF. Defaults to 85 for JPEG and 80 for AVIF. It does not apply to WebP, which is written losslessly.
  • compression for PNG: 'fast', 'default' or 'best'. All three are lossless; they trade encoding time for file size.
  • speed for AVIF (1-10, lower is slower and smaller) and for GIF (1-30, lower is slower and picks better colours; 15 by default).
  • background for JPEG, the colour transparent pixels are flattened against. Defaults to white.
  • threshold (0-255) for WBMP, the brightness cut.

Parameters

  • format (?string) — Default: the format this image was decoded from, or PNG.
  • options (?dict)

Returns bytes

Raises EncodeError When the format cannot be written.

Image.save()

imagine.Image.save(path: string, options)

Writes the image to a file.

The format is taken from the path’s extension unless format overrides it, so save('out.webp') writes a WebP.

Parameters

  • path (string)
  • options (?dict) — format, plus everything encode() takes.

Returns — self

Raises FormatError When the extension names no format this module knows and no format option is given.

Image.to_data_url()

imagine.Image.to_data_url(format, options) -> string

Encodes the image as a data: URL, ready to drop into an img tag or a stylesheet.

Base64 costs a third more bytes than the raw image, so this suits icons and small graphics rather than photographs.

Parameters

  • format (?string) — Default: PNG.
  • options (?dict) — Everything encode() takes.

Returns string

Image.to_png()

imagine.Image.to_png(options) -> bytes

Encodes the image as a PNG. Lossless, with alpha.

Parameters

  • options (?dict) — compression.

Returns bytes

Image.to_jpeg()

imagine.Image.to_jpeg(quality, options) -> bytes

Encodes the image as a JPEG. Lossy, no alpha.

Parameters

  • quality (?number) — 1-100 - Default: 85.
  • options (?dict) — background.

Returns bytes

Image.to_webp()

imagine.Image.to_webp(options) -> bytes

Encodes the image as a WebP.

Parameters

  • options (?dict)

Returns bytes

Image.to_avif()

imagine.Image.to_avif(quality, options) -> bytes

Encodes the image as an AVIF.

Small files, slow encoding. Lower speed values are slower and produce smaller files.

Parameters

  • quality (?number) — 1-100 - Default: 80.
  • options (?dict) — speed (1-10, default 6).

Returns bytes

Image.to_gif()

imagine.Image.to_gif(options) -> bytes

Encodes the image as a single-frame GIF, quantized to 256 colours.

Parameters

  • options (?dict)

Returns bytes

Image.to_bmp()

imagine.Image.to_bmp(options) -> bytes

Encodes the image as a BMP.

Parameters

  • options (?dict)

Returns bytes

Image.to_tiff()

imagine.Image.to_tiff(options) -> bytes

Encodes the image as a TIFF.

Parameters

  • options (?dict)

Returns bytes

Image.to_qoi()

imagine.Image.to_qoi(options) -> bytes

Encodes the image as a QOI. Lossless with alpha, and several times faster than PNG to read and write.

Parameters

  • options (?dict)

Returns bytes

Image.to_tga()

imagine.Image.to_tga(options) -> bytes

Encodes the image as a Targa.

Parameters

  • options (?dict)

Returns bytes

Image.to_ico()

imagine.Image.to_ico(options) -> bytes

Encodes the image as a Windows icon.

Parameters

  • options (?dict)

Returns bytes

Image.to_wbmp()

imagine.Image.to_wbmp(threshold) -> bytes

Encodes the image as a one-bit Wireless Bitmap.

Parameters

  • threshold (?number) — The brightness cut, 0-255 - Default: 128.

Returns bytes

Image.to_string()

imagine.Image.to_string()

2026, Richard Ore and Zuri contributors

imagine.strokefont

import imagine.strokefont

imagine lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled imagine.strokefont.* needs import imagine.strokefont.

StrokeFont: the font built into the module, defined as stroke geometry rather than glyph outlines.

It exists so that text can be drawn with no font file present at all. Because a glyph is a path rather than a filled shape, it scales to any size and takes a weight without needing a separate face per weight.

Constants

EM

imagine.strokefont.EM: number = 20

The em square’s height in design units. A font asked for 20 pixels draws its em at 20 pixels, so scale is simply size / EM.

CAP_HEIGHT

imagine.strokefont.CAP_HEIGHT: number = 14

The height of a capital letter.

X_HEIGHT

imagine.strokefont.X_HEIGHT: number = 10

The height of a lowercase letter with no ascender.

ASCENDER

imagine.strokefont.ASCENDER: number = 15

How far the tallest glyphs rise above the baseline.

DESCENDER

imagine.strokefont.DESCENDER: number

How far descenders fall below the baseline, as a negative number.

WEIGHT

imagine.strokefont.WEIGHT: number

The default stroke width in design units, a little under a tenth of the em, which is the usual weight for a regular sans-serif.

GLYPHS

imagine.strokefont.GLYPHS = {...}

Every glyph, keyed by character.

advance is how far the pen moves after drawing, in design units. strokes is a list of polylines.

NOTDEF

imagine.strokefont.NOTDEF = {...}

What an unmapped character draws: an empty box, the same convention a font uses for a glyph it does not have.

Classes

StrokeFont

class imagine.StrokeFont

The built-in font: a sans-serif drawn from centre-line strokes rather than loaded from a file.

imagine deliberately ships no font file, so [[imagine.Font.system]] and [[imagine.Font.sans]] both depend on what happens to be installed. This one is always there. It is defined as geometry in [[imagine.strokefont]], drawn through the same anti-aliased rasterizer everything else uses, and therefore scales cleanly to any size and takes a weight as a parameter.

import imagine { Image, StrokeFont }

Image(300, 80, 'white')
  .text(16, 22, 'Always available', StrokeFont(28), '#0f172a')
  .save('label.png')

It has the same interface as [[imagine.Font]], so anywhere a font is accepted this can be used instead.

What it is for

Labels, chart axes, watermarks, diagrams, placeholder text, and any output that has to work on a machine with no fonts installed. The look is geometric and single-weight, closer to a technical drawing than to a typeface, because that is what centre-line strokes give you honestly.

It is not a substitute for real typography. For body text, a headline, or anything where the shapes matter, load an actual font with [[imagine.Font.load]].

Coverage

Printable ASCII, from space through ~. Anything else draws the empty box a font uses for a glyph it does not have, so text in another script comes out visibly missing rather than silently blank.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

imagine.StrokeFont(size: ?number, weight: ?number)

Creates the built-in font at a size.

Parameters

  • size (?number) — The size in pixels - Default: 16.
  • weight (?number) — The stroke width, as a fraction of the size - Default: 0.085, a regular weight. Around 0.13 reads as bold.

StrokeFont.size()

imagine.StrokeFont.size(pixels) -> StrokeFont

Returns the same font at a different size.

Parameters

  • pixels (number)

Returns StrokeFont

StrokeFont.weight()

imagine.StrokeFont.weight(weight) -> StrokeFont

Returns the same font at a different stroke weight.

Parameters

  • weight (number) — A fraction of the size. 0.085 is regular, 0.13 reads as bold, 0.05 as light.

Returns StrokeFont

StrokeFont.pixel_size()

imagine.StrokeFont.pixel_size() -> number

The size in pixels this font draws at.

Returns number

StrokeFont.name()

imagine.StrokeFont.name() -> string

The font’s name.

Returns string

StrokeFont.source()

imagine.StrokeFont.source() -> string

Where this font came from.

Returns string

StrokeFont.has_glyph()

imagine.StrokeFont.has_glyph(character) -> bool

true when this font has a glyph for a character, which for the built-in font means printable ASCII.

Parameters

  • character (string)

Returns bool

StrokeFont.metrics()

imagine.StrokeFont.metrics() -> dict

The font’s vertical metrics at its current size, in pixels.

The dictionary holds name, ascent, descent (negative), line_gap and line_height, the same shape [[imagine.Font]] reports. The em is the size, so single-spaced lines sit exactly size pixels apart.

Returns dict

StrokeFont.wrap()

imagine.StrokeFont.wrap(text, width: number, options) -> string

Breaks a string into lines that fit within a width, returning the text with newlines inserted.

Parameters

  • text (string)
  • width (number) — The maximum line width in pixels.
  • options (?dict) — tracking.

Returns string

StrokeFont.measure()

imagine.StrokeFont.measure(text, options) -> dict

Measures a string without drawing it.

Returns width, height, baseline (the first line’s baseline, measured down from the top of the box) and lines.

Parameters

  • text (string)
  • options (?dict) — tracking, line_height, align.

Returns dict

StrokeFont.render()

imagine.StrokeFont.render(text, options) -> dict

Rasterizes a string into an 8-bit coverage mask.

Returns coverage, width, height and baseline, the same shape [[imagine.Font]] returns, which is what lets both be handed to Image.text() interchangeably.

Parameters

  • text (string)
  • options (?dict) — tracking, line_height, align.

Returns dict

StrokeFont.to_string()

imagine.StrokeFont.to_string()

2026, Richard Ore and Zuri contributors

sql

import sql

One way to talk to a relational database, whichever one it is.

sql is the entry point for every database Zuri supports. It defines what a database adapter has to provide, supplies everything that is the same whichever engine answers, and picks the adapter from the connection string. Changing database means changing that string, and whatever SQL the engines genuinely spell differently.

import sql

var db = sql.open(':memory:')

db.exec('create table posts (id integer primary key, title text)')

var id = db.insert('posts', { title: 'Hello' })

for post in db.query('select * from posts where id = ?', [id]) {
  echo post.title
}

db.close()

The only line above that names an engine is the first. Point it at postgres://localhost/app or mysql://localhost/app and the rest runs unchanged: the ? becomes $1 on PostgreSQL and stays ? on MySQL, the insert gets a RETURNING clause where the engine has no last insert id, and a unique key violation still arrives as UniqueViolation.

What is here

open()opens a connection from a connection string
pool()opens a pool of them
register()adds an adapter of your own
Connectionwhat a program holds and runs statements on
Transactionan open transaction, and the savepoints inside it
Statementa statement compiled once and run many times
Cursora result read a row at a time
ResultSetthe rows a query returned
Decimalan exact decimal, for a column a float must not hold
Timea signed span of time, as a TIME column holds one
Schemawhat tables exist and what is in them
SqlErrorthe root of every error a database raises

Parameters

Values are bound, never pasted into the statement. Write ? for positional parameters or :name for named ones and sql translates them into whatever the adapter wants:

db.query('select * from posts where author = ? and year = ?', [name, 2026])
db.query('select * from posts where author = :who', { who: name })

Errors

Every error is a SqlError. The subclasses are the same on every engine, so the code that handles a duplicate key does not change when the engine does:

catch {
  db.insert('users', { email: address })
} as error {
  if instance_of(error, sql.UniqueViolation) {
    echo 'that address is already registered'
  } else {
    raise error
  }
}

Isolates

A connection belongs to the isolate that opened it and cannot be shared with another. An isolate that needs a database opens its own connection or its own pool.

The sql API

Every public name in sql, wherever it is declared. Each links to the page that documents it.

NameKindSummary
sql.AuthenticationErrorclassThe server rejected the credentials.
sql.CheckViolationclassA CHECK constraint rejected the row.
sql.ClosedErrorclassThe connection, statement, cursor, transaction or pool was already closed.
sql.ConnectionclassAn open connection to a database.
sql.ConnectionErrorclassThe database could not be reached, or the connection dropped.
sql.CursorclassA result being read a row at a time.
sql.DeadlockErrorclassTwo transactions were each waiting on the other, and the engine broke the tie by aborting this one.
sql.DecimalclassAn exact decimal number.
sql.DriverclassOpens connections to one kind of database.
sql.DriverConnectionclassOne open connection to a database, as the layer above it sees one.
sql.DriverCursorclassA result being read a batch at a time rather than all at once.
sql.DriverStatementclassA statement compiled once and run many times.
sql.ExecResultclassWhat a statement that changed something reports.
sql.ForeignKeyViolationclassA foreign key has no matching row, or a referenced row is still in use by another table.
sql.INDEXEDconstant?1, ?2 and so on.
sql.IntegrityErrorclassA constraint rejected the data.
sql.NUMBEREDconstant$1, $2 and so on, numbered from one in binding order.
sql.NotNullViolationclassA column declared NOT NULL was given nothing.
sql.NotSupportedErrorclassThe engine cannot do what was asked, and no approximation would be honest.
sql.PoolclassA pool of connections to one database.
sql.PoolErrorclassSomething went wrong with a connection pool rather than with a database.
sql.PoolExhaustedErrorclassEvery connection was busy and none came free within the acquire timeout.
sql.ProtocolErrorclassThe server sent something the adapter could not make sense of.
sql.QUESTIONconstantA bare ? for every parameter, in order.
sql.QueryErrorclassThe engine refused the statement.
sql.READ_COMMITTEDconstantEach transaction sees rows committed before its own statement started, and nothing a concurrent transaction…
sql.READ_UNCOMMITTEDconstantA transaction can see rows another has written and not committed.
sql.REPEATABLE_READconstantA transaction reads the same rows throughout, whatever anyone else commits while it runs.
sql.RawclassMarks a fragment of SQL to be used as written rather than bound.
sql.ResultSetclassThe rows a query returned, with the shape of the result beside them.
sql.SERIALIZABLEconstantConcurrent transactions produce a result some serial order of them could also have produced.
sql.SchemaclassIntrospection for one connection.
sql.SchemaAdapterclassThe introspection queries for one engine.
sql.SerializationErrorclassTwo transactions could not both be treated as though they ran alone.
sql.SqlErrorclassBase class for every error this module and its adapters raise.
sql.StatementclassA prepared statement.
sql.TimeclassA signed span of time, as a SQL TIME column holds one.
sql.TimeoutErrorclassAn operation ran out of time.
sql.TransactionclassAn open transaction.
sql.TransactionErrorclassA transaction could not be carried out as asked.
sql.UniqueViolationclassA unique index or primary key already holds this value.
sql.column_shapefunctionThe description of one column, as every adapter returns it.
sql.crud.deletefunctionBuilds a DELETE.
sql.crud.insertfunctionBuilds an INSERT.
sql.crud.insert_manyfunctionBuilds an INSERT carrying several rows in one statement.
sql.crud.quote_tablefunctionRenders a table name, which may carry a schema.
sql.crud.selectfunctionBuilds a SELECT.
sql.crud.updatefunctionBuilds an UPDATE.
sql.crud.wherefunctionBuilds the WHERE clause for a dictionary of conditions.
sql.decimalfunctionBuilds a decimal, the same as Decimal() but as a function.
sql.default_capabilitiesfunctionThe capability flags a driver reports, with the value each takes when a driver does not say otherwise.
sql.driverfunctionThe adapter registered under name.
sql.driver_forfunctionThe adapter a connection string names.
sql.driversfunctionThe names of every registered adapter.
sql.mysql.CACHING_SHA2constantThe default since MySQL 8.0.
sql.mysql.CAPABILITIESconstantWhat the client asks for and what the server offers.
sql.mysql.CLEARconstantSends the password as it is.
sql.mysql.COMMANDSconstantThe commands this adapter sends.
sql.mysql.DEFAULT_PORTconstantThe port a MySQL server listens on unless told otherwise.
sql.mysql.DRIVERconstantThe driver instance sql registers for MySQL.
sql.mysql.ED25519constantMariaDB’s signature-based plugin.
sql.mysql.FLAGSconstantWhat the server says about a column beyond its type.
sql.mysql.MARIADB_DRIVERconstantThe driver instance sql registers for MariaDB.
sql.mysql.MAX_PARAMETERSconstantMost parameters one statement can bind.
sql.mysql.MariaDbDriverclassOpens MariaDB connections.
sql.mysql.MySqlConnectionclassA connection to MySQL or MariaDB.
sql.mysql.MySqlCursorclassA result being read in batches.
sql.mysql.MySqlDriverclassOpens MySQL connections.
sql.mysql.MySqlStatementclassA prepared statement.
sql.mysql.NATIVEconstantMySQL’s original plugin, and still MariaDB’s default.
sql.mysql.SHA256constantMySQL 5.7’s stronger option, superseded by the caching one.
sql.mysql.SSL_MODESconstantWhat sslmode can be.
sql.mysql.TYPESconstantThe type byte the server puts on a column, and that a parameter carries back.
sql.mysql.auth.NONEconstantNamed in a handshake by a server that wants no password at all.
sql.mysql.auth.SUPPORTEDconstantThe plugins this adapter can answer.
sql.mysql.auth.caching_sha2_passwordfunctionThe fast caching_sha2_password response.
sql.mysql.auth.clear_passwordfunctionThe password as the server reads it when it is sent outright: the bytes and the zero that ends them.
sql.mysql.auth.ed25519_passwordfunctionThe client_ed25519 response, which MariaDB uses.
sql.mysql.auth.encrypt_passwordfunctionThe password encrypted to the server’s public key, for the plugins that need the password itself over a…
sql.mysql.auth.native_passwordfunctionThe mysql_native_password response.
sql.mysql.auth.obfuscatefunctionThe password masked with the scramble, which is the form the two RSA plugins encrypt.
sql.mysql.auth.response_forfunctionThe first response to send for plugin, before any back and forth.
sql.mysql.auth.supportsfunctionWhether plugin is one this adapter knows how to answer.
sql.mysql.class_forfunctionThe class that best describes an error the server reported.
sql.mysql.connectfunctionOpens a MySQL connection directly, without going through sql.
sql.mysql.connection.CACHE_LIMITconstantHow many compiled statements a connection keeps before releasing the one it has gone longest without using.
sql.mysql.driver.DEFAULT_TIMEOUTconstantHow long to wait for the socket and for each read, in milliseconds.
sql.mysql.ed25519.DconstantThe curve constant, -121665/121666.
sql.mysql.ed25519.D2constantTwice the curve constant, which is what the addition uses.
sql.mysql.ed25519.LconstantThe order of the base point’s subgroup.
sql.mysql.ed25519.PconstantThe field prime, 2^255 - 19.
sql.mysql.ed25519.password_keyfunctionExpands a password into the 64 bytes MariaDB signs with.
sql.mysql.ed25519.public_keyfunctionThe public key matching an expanded key.
sql.mysql.ed25519.signfunctionSigns message with an expanded key.
sql.mysql.errors.CLASSESconstantSQLSTATE classes, for numbers the table above does not name.
sql.mysql.errors.SPECIFICconstantError numbers specific enough to name a class of their own.
sql.mysql.packets.COMPRESS_THRESHOLDconstantBelow this, a compressed packet is sent uncompressed instead.
sql.mysql.packets.CursorclassReads values out of one packet payload, keeping its own position.
sql.mysql.packets.EXACT_INTEGER_LIMITconstantLargest integer a Zuri number holds exactly.
sql.mysql.packets.LENENC_NULLconstantThe first byte of a length-encoded integer that means the value is NULL.
sql.mysql.packets.MAX_PAYLOADconstantThe largest payload a single packet can carry.
sql.mysql.packets.WriterclassBuilds one packet payload.
sql.mysql.protocol.CURSOR_NONEconstantNo cursor: the whole result comes back at once.
sql.mysql.protocol.CURSOR_READ_ONLYconstantAsks the server to keep the result open rather than send it all.
sql.mysql.protocol.STATUSconstantWhat the server says about the session after each command.
sql.mysql.raise_forfunctionRaises the right class for a server error report.
sql.mysql.results.exec_resultfunctionWhat a statement run for its effect reports.
sql.mysql.results.select_resultfunctionWhat a statement run for its rows reports.
sql.mysql.schema.MySqlSchemaclassAnswers introspection questions about a MySQL database.
sql.mysql.type_name_offunctionThe name of a column’s type, as MySQL itself would write it.
sql.mysql.types.BINARY_CHARSETconstantThe collation id that means a column holds bytes rather than text.
sql.mysql.types.UNSIGNED_FLAGconstantThe flag byte that goes with a parameter’s type to mark it unsigned.
sql.mysql.types.decode_binaryfunctionReads one value out of a binary protocol row.
sql.mysql.types.decode_textfunctionReads one value out of a text protocol row.
sql.mysql.types.describe_columnsfunctionReduces the server’s column descriptors to the two fields the shared contract asks for.
sql.mysql.types.is_binaryfunctionWhether a column holds bytes rather than text.
sql.mysql.types.is_unsignedfunctionWhether a column’s values are unsigned.
sql.mysql.types.parameter_typefunctionThe type byte and flag a value is sent as.
sql.mysql.types.write_parameterfunctionAppends a value in the form its type byte promises.
sql.openfunctionOpens a connection.
sql.params.STYLESconstantEvery style a driver may declare.
sql.params.tokenizefunctionSplits sql into the pieces that matter, in order.
sql.parse_timefunctionReads a SQL TIME literal.
sql.placeholderfunctionThe placeholder text for the parameter at position, counting from one.
sql.poolfunctionOpens a pool of connections.
sql.pool.DEFAULT_IDLE_TIMEOUTconstantHow long an idle connection is kept before being closed, in milliseconds.
sql.pool.DEFAULT_MAXconstantHow many connections a pool opens at most, when it is not told.
sql.pool.DEFAULT_MAX_LIFETIMEconstantHow long any connection is kept before being replaced, in milliseconds.
sql.postgres.DEFAULT_PORTconstantThe port a PostgreSQL server listens on unless told otherwise.
sql.postgres.DEFAULT_REGISTRYconstantThe registry every connection uses unless given one of its own.
sql.postgres.DRIVERconstantThe driver instance sql registers for this engine.
sql.postgres.ListenerclassSubscribes a connection to channels and collects what arrives.
sql.postgres.MAX_PARAMETERSconstantMost parameters one statement can bind.
sql.postgres.OIDSconstantPostgreSQL’s built-in type OIDs, by name.
sql.postgres.PostgresConnectionclassAn open PostgreSQL connection.
sql.postgres.PostgresCursorclassA portal being read.
sql.postgres.PostgresDriverclassOpens PostgreSQL connections.
sql.postgres.PostgresStatementclassA compiled PostgreSQL statement.
sql.postgres.SSL_MODESconstantWhat sslmode can be.
sql.postgres.TypeRegistryclassWhich decoder and encoder each OID uses.
sql.postgres.affected_rowsfunctionThe number of rows a command tag reports.
sql.postgres.auth.SCRAM_SHA_256constantThe mechanism this adapter implements.
sql.postgres.auth.client_firstfunctionThe client’s opening SCRAM message.
sql.postgres.auth.client_prooffunctionWorks out the client’s proof from the server’s challenge.
sql.postgres.auth.md5_responsefunctionBuilds the response to an MD5 password request.
sql.postgres.auth.noncefunctionA fresh SCRAM nonce.
sql.postgres.auth.offers_scramfunctionWhether the server offered SCRAM-SHA-256 among its mechanisms.
sql.postgres.auth.parse_scramfunctionSplits a SCRAM message into its key=value parts.
sql.postgres.auth.verify_serverfunctionChecks the server’s closing message really is from a server that knows the password.
sql.postgres.class_forfunctionThe class that best describes sqlstate.
sql.postgres.connectfunctionOpens a PostgreSQL connection directly, without going through sql.
sql.postgres.connection.CACHE_LIMITconstantHow many compiled statements a connection keeps.
sql.postgres.describe_columnsfunctionReduces the server’s column descriptors to the two fields the shared contract asks for.
sql.postgres.driver.DEFAULT_TIMEOUTconstantHow long to wait for the socket and for each read, in milliseconds.
sql.postgres.errors.CLASSESconstantSQLSTATE classes, for codes the table above does not name.
sql.postgres.errors.SPECIFICconstantCodes specific enough to name a class of their own.
sql.postgres.messages.AUTHENTICATIONconstantMessages the server sends, by tag.
sql.postgres.messages.AUTH_CLEARTEXTconstant
sql.postgres.messages.AUTH_MD5constant
sql.postgres.messages.AUTH_OKconstantAuthentication requests, by the number that follows the tag.
sql.postgres.messages.AUTH_SASLconstant
sql.postgres.messages.AUTH_SASL_CONTINUEconstant
sql.postgres.messages.AUTH_SASL_FINALconstant
sql.postgres.messages.BACKEND_KEY_DATAconstant
sql.postgres.messages.BIND_COMPLETEconstant
sql.postgres.messages.CLOSE_COMPLETEconstant
sql.postgres.messages.COMMAND_COMPLETEconstant
sql.postgres.messages.COPY_DATAconstant
sql.postgres.messages.COPY_DONEconstant
sql.postgres.messages.COPY_IN_RESPONSEconstant
sql.postgres.messages.COPY_OUT_RESPONSEconstant
sql.postgres.messages.DATA_ROWconstant
sql.postgres.messages.EMPTY_QUERY_RESPONSEconstant
sql.postgres.messages.ERROR_RESPONSEconstant
sql.postgres.messages.NOTICE_RESPONSEconstant
sql.postgres.messages.NOTIFICATION_RESPONSEconstant
sql.postgres.messages.NO_DATAconstant
sql.postgres.messages.PARAMETER_DESCRIPTIONconstant
sql.postgres.messages.PARAMETER_STATUSconstant
sql.postgres.messages.PARSE_COMPLETEconstant
sql.postgres.messages.PORTAL_SUSPENDEDconstant
sql.postgres.messages.PROTOCOL_VERSIONconstantThe protocol version this adapter speaks: 3.0, as a single number with the major version in the high half.
sql.postgres.messages.READY_FOR_QUERYconstant
sql.postgres.messages.ROW_DESCRIPTIONconstant
sql.postgres.messages.ReaderclassReads framed messages from a connected stream.
sql.postgres.messages.SSL_REQUESTconstantThe number the server recognises as a request to start TLS.
sql.postgres.messages.WriterclassBuilds one outgoing message.
sql.postgres.messages.read_cstringfunctionSplits a run of zero-terminated strings.
sql.postgres.raise_forfunctionRaises the right class for a server error report.
sql.postgres.schema.PostgresSchemaclassAnswers introspection questions about a PostgreSQL database.
sql.postgres.type_name_offunctionThe name of the type an OID stands for, or the OID as text for one this adapter has no name for, which is…
sql.postgres.types.ARRAY_ELEMENTSconstantThe element type each array OID holds.
sql.postgres.types.EPOCH_OFFSETconstantSeconds between the Unix epoch and PostgreSQL’s, which is 2000-01-01 rather than 1970-01-01.
sql.postgres.types.date_from_microsfunctionTurns a count of microseconds since the PostgreSQL epoch into a date.Date in UTC.
sql.postgres.types.decode_arrayfunctionReads an array back, however many dimensions it has.
sql.postgres.types.decode_boolfunctionA decoder reads one column value from its binary form.
sql.postgres.types.decode_boxfunction
sql.postgres.types.decode_byteafunction
sql.postgres.types.decode_circlefunction
sql.postgres.types.decode_datefunctiondate is a count of days from 2000-01-01, with no time part.
sql.postgres.types.decode_float4function
sql.postgres.types.decode_float8function
sql.postgres.types.decode_inetfunctioninet and cidr share a form: address family, prefix bits, a flag saying which of the two it is, then the…
sql.postgres.types.decode_int2function
sql.postgres.types.decode_int4function
sql.postgres.types.decode_int8function
sql.postgres.types.decode_intervalfunctioninterval is a span rather than a point, so it comes back as its parts.
sql.postgres.types.decode_jsonfunction
sql.postgres.types.decode_jsonbfunctionjsonb carries a version byte in front of the text, which every server so far sets to 1.
sql.postgres.types.decode_lsegfunction
sql.postgres.types.decode_macaddrfunction
sql.postgres.types.decode_numericfunctionDecodes PostgreSQL’s numeric into an exact Decimal.
sql.postgres.types.decode_numeric_valuefunction
sql.postgres.types.decode_oidfunction
sql.postgres.types.decode_pointfunction
sql.postgres.types.decode_textfunction
sql.postgres.types.decode_timefunctiontime is microseconds since midnight, with no date part.
sql.postgres.types.decode_timestampfunctionBoth timestamp and timestamptz are microseconds from the PostgreSQL epoch.
sql.postgres.types.decode_timetzfunctiontimetz is a time followed by its offset in seconds west of UTC.
sql.postgres.types.decode_uuidfunction
sql.postgres.types.encode_arrayfunctionBuilds the binary form of an array.
sql.postgres.types.encode_boolfunctionAn encoder turns a Zuri value into the bytes of its binary form.
sql.postgres.types.encode_byteafunction
sql.postgres.types.encode_datefunction
sql.postgres.types.encode_float4function
sql.postgres.types.encode_float8function
sql.postgres.types.encode_int2function
sql.postgres.types.encode_int4function
sql.postgres.types.encode_int8function
sql.postgres.types.encode_jsonfunction
sql.postgres.types.encode_jsonbfunctionjsonb takes the same text behind the version byte the server expects.
sql.postgres.types.encode_numericfunctionEncodes a Decimal, a number or a bigint as numeric.
sql.postgres.types.encode_textfunction
sql.postgres.types.encode_timestampfunction
sql.postgres.types.encode_uuidfunction
sql.postgres.types.micros_from_datefunctionThe microseconds since the PostgreSQL epoch a date.Date stands for.
sql.postgres.types.read_int16functionReads a signed 16 bit big-endian integer.
sql.postgres.types.read_int32functionReads a signed 32 bit big-endian integer.
sql.postgres.types.read_int64functionReads a signed 64 bit big-endian integer.
sql.postgres.types.read_uint16functionReads an unsigned 16 bit big-endian integer.
sql.postgres.types.read_uint32functionReads an unsigned 32 bit big-endian integer.
sql.postgres.types.text_offunctionA bytes slice as a UTF-8 string.
sql.postgres.types.write_int16functionWrites a signed 16 bit big-endian integer.
sql.postgres.types.write_int32functionWrites a signed 32 bit big-endian integer.
sql.postgres.types.write_int64functionWrites a signed 64 bit big-endian integer.
sql.rawfunctionBuilds a Raw.
sql.registerfunctionRegisters an adapter, so open() recognises its connection strings.
sql.scanfunctionWhat placeholders sql uses, without needing any values.
sql.split_statementsfunctionSplits a script into its statements.
sql.sqlite.BackupclassA copy in progress between two connections.
sql.sqlite.BlobclassAn open blob handle.
sql.sqlite.DEFAULT_BUSY_TIMEOUTconstantHow long a statement waits for another writer by default.
sql.sqlite.DRIVERconstantThe driver instance sql registers for this engine.
sql.sqlite.MAX_PARAMETERSconstantMost parameters one statement can bind, which is SQLite’s own limit.
sql.sqlite.MEMORYconstantThe path that means a private database held in memory, which is discarded when the connection closes.
sql.sqlite.SqliteConnectionclassAn open SQLite database.
sql.sqlite.SqliteCursorclassA result being stepped.
sql.sqlite.SqliteDriverclassOpens SQLite databases.
sql.sqlite.SqliteStatementclassA prepared SQLite statement.
sql.sqlite.backup.DEFAULT_PAGESconstantHow many pages a step copies when no size is given.
sql.sqlite.base_typefunctionThe bare type name from a declaration, upper cased and without any size or precision.
sql.sqlite.class_forfunctionThe class that best describes code.
sql.sqlite.connectfunctionOpens a SQLite database directly, without going through sql.
sql.sqlite.connection.CACHE_LIMITconstantHow many compiled statements a connection keeps for reuse.
sql.sqlite.conversion_forfunctionWhich conversion a column’s declaration calls for: 'bool', 'date', 'json', or nil for a column that…
sql.sqlite.driver.OPEN_CREATEconstant
sql.sqlite.driver.OPEN_FULLMUTEXconstant
sql.sqlite.driver.OPEN_MEMORYconstant
sql.sqlite.driver.OPEN_NOMUTEXconstant
sql.sqlite.driver.OPEN_PRIVATECACHEconstant
sql.sqlite.driver.OPEN_READONLYconstant
sql.sqlite.driver.OPEN_READWRITEconstant
sql.sqlite.driver.OPEN_SHAREDCACHEconstant
sql.sqlite.driver.OPEN_URIconstant
sql.sqlite.errors.primaryfunctionAn extended code’s primary code, which is its low byte.
sql.sqlite.reraisefunctionRe-raises an error from the native layer as the right class.
sql.sqlite.schema.SqliteSchemaclassAnswers introspection questions about a SQLite database.
sql.sqlite.split_messagefunctionSplits the [code] message form the native layer raises.
sql.sqlite.statement.bind_allfunctionBinds values to a reset statement, in order.
sql.sqlite.statement.describefunctionDescribes the columns a compiled statement returns.
sql.sqlite.types.BOOLEAN_TYPESconstantDeclared types that mean a boolean.
sql.sqlite.types.JSON_TYPESconstantDeclared types that mean JSON held in a text column.
sql.sqlite.types.TEMPORAL_TYPESconstantDeclared types that mean a point in time.
sql.sqlite.types.conversions_forfunctionThe conversions for a whole result, one per column, worked out once so each row does not have to look at the…
sql.sqlite.types.decodefunctionApplies one column’s conversion to one stored value.
sql.sqlite.types.decode_rowfunctionApplies a result’s conversions to one row of stored values.
sql.timefunctionBuilds a Time.
sql.time_from_secondsfunctionBuilds a Time of total seconds, splitting it into parts.
sql.translatefunctionRewrites sql into style and puts the values in binding order.
sql.types.ISO_DATE_FORMATconstantThe same, without the time, for a column that holds only a date.
sql.types.ISO_FORMATconstantThe format this module writes timestamps in: ISO 8601 with microsecond precision and an explicit UTC offset.
sql.types.flattenfunctionReduces value to something an engine with no richer type can store: a timestamp becomes ISO text, a list or…
sql.types.from_isofunctionReads ISO 8601 text back into a date.Date.
sql.types.from_jsonfunctionDecodes JSON text, returning nil for anything that will not parse.
sql.types.integer_from_textfunctionReads an integer, however long, as a number where one holds it exactly and a bigint where it does not.
sql.types.is_intervalfunctionWhether value is a Time.
sql.types.is_structuredfunctionWhether value is something this module stores as JSON.
sql.types.is_temporalfunctionWhether value is a date.Date.
sql.types.to_isofunctionFormats a date.Date as ISO 8601 text.
sql.types.to_jsonfunctionEncodes value as JSON text.

Submodules

ModuleReached asSummary
sql.connectionsql.connection.*The connection a program holds.
sql.crudsql.crud.*Building the four statements that are the same everywhere.
sql.cursorsql.cursor.*Reading a result without holding all of it.
sql.decimalsql.decimal.*Exact decimal numbers, for the money column that must not be a float.
sql.driversql.driver.*The contract every database adapter implements, and the capability flags that let one adapter differ from…
sql.errorssql.*Every error a database raises, under one root, in one shape, whatever engine it came from.
sql.mysqlimport sql.mysqlThe MySQL and MariaDB adapter.
sql.paramssql.params.*Rewriting a statement’s placeholders into whatever the active adapter expects.
sql.poolsql.pool.*Keeping connections open and lending them out.
sql.postgresimport sql.postgresThe PostgreSQL adapter.
sql.resultsql.result.*What a statement hands back: the rows of a query, or the count of a statement that changed something.
sql.schemasql.schema.*Asking a database what is in it.
sql.sqliteimport sql.sqliteThe SQLite adapter.
sql.statementsql.statement.*A statement compiled once and run many times.
sql.transactionsql.transaction.*Transactions, and the nesting that savepoints make possible.
sql.typessql.types.*How Zuri values and database values correspond, for the parts that are the same whichever engine is…

Functions

register()

sql.register(driver) -> Driver

Registers an adapter, so open() recognises its connection strings.

An adapter is anything implementing Driver. Registering one under a name already taken replaces it, which is how a program swaps in an adapter of its own for an engine sql already knows.

sql.register(MyOracleDriver())

var db = sql.open('oracle://localhost/app')

Parameters

  • driver (Driver)

Returns Driver — The driver, so a registration can be an expression.

Raises SqlError if driver is not one.

drivers()

sql.drivers() -> list[string]

The names of every registered adapter.

Returns list[string]

driver()

sql.driver(name: string) -> Driver

The adapter registered under name.

Parameters

  • name (string)

Returns Driver

Raises SqlError if nothing is registered under that name.

driver_for()

sql.driver_for(dsn: string) -> Driver

The adapter a connection string names.

The scheme decides: sqlite:// and postgres:// reach the adapters of those names, and a string with no scheme at all is taken as a SQLite path, which is the only form that could not be anything else.

Parameters

  • dsn (string)

Returns Driver

Raises SqlError if no adapter claims the scheme.

open()

sql.open(dsn, options) -> Connection

Opens a connection.

sql.open('sqlite://./app.db')
sql.open('postgres://alice:secret@localhost/app')
sql.open('./app.db')

An options dictionary works too, and needs driver to say which adapter it is for:

sql.open({ driver: 'sqlite', path: './app.db', journal_mode: 'wal' })

Parameters

  • dsn (string|dict) — A connection string, or options.
  • options (dict|nil) — Extra options, merged over whatever the connection string carried. Anything an adapter accepts in its own options goes here.

Returns Connection

Raises ConnectionError if the database cannot be reached.

pool()

sql.pool(dsn, options) -> Pool

Opens a pool of connections.

Parameters

  • dsn (string|dict) — As open() takes.
  • options (dict|nil) — The pool’s own settings, and any the adapter accepts. See Pool for what the pool reads.

Returns Pool


2026, Richard Ore and Zuri contributors

sql.connection

import sql.connection

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.connection.* needs import sql.connection.

The connection a program holds.

Everything that is the same whichever database is underneath lives here: translating placeholders, shaping results, running transactions and their savepoints, generating the four statements that are always the same, and getting an id back from an insert on an engine that has no last insert id.

What is left over is the adapter’s, reachable through native() for the cases where an engine’s own feature is the point.

Classes

Connection

class sql.Connection

An open connection to a database.

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
schemaSchemaIntrospection: what tables exist, what columns they have, and how they are indexed.

Constructor

sql.Connection(connection, driver)

Parameters

  • connection (DriverConnection) — The adapter’s own connection.
  • driver (Driver) — The driver that opened it.

Connection.driver_connection()

sql.Connection.driver_connection() -> DriverConnection

The adapter’s own connection, for the engine-specific things the shared contract does not cover.

Returns DriverConnection

Connection.native()

sql.Connection.native() -> DriverConnection

The same thing, under the name that reads better at a call site.

db.native().create_function('slug', 1, @(text) => slugify(text))

Returns DriverConnection

Connection.driver()

sql.Connection.driver() -> Driver

The driver behind this connection.

Returns Driver

Connection.driver_name()

sql.Connection.driver_name() -> string

The adapter’s name, such as 'sqlite'.

Returns string

Connection.capabilities()

sql.Connection.capabilities() -> dict

What this engine can do.

Returns dict

Connection.supports()

sql.Connection.supports(flag: string) -> bool

Whether this engine supports flag.

if db.supports('returning') {
  ...
}

Parameters

  • flag (string)

Returns bool

Connection.server_version()

sql.Connection.server_version() -> string

The engine’s version, as it reports it.

Returns string

Connection.query()

sql.Connection.query(sql: string, params) -> ResultSet

Runs a query and reads its whole result.

Parameters

  • sql (string) — Written with ? or :name placeholders.
  • params (list|dict|nil)

Returns ResultSet

Connection.exec()

sql.Connection.exec(sql: string, params) -> ExecResult

Runs a statement for its effect.

Parameters

  • sql (string)
  • params (list|dict|nil)

Returns ExecResult

Connection.fetch_one()

sql.Connection.fetch_one(sql: string, params) -> dict|nil

The first row of a query, or nil when it returns none.

Parameters

  • sql (string)
  • params (list|dict|nil)

Returns dict|nil

Connection.fetch_all()

sql.Connection.fetch_all(sql: string, params) -> list[dict]

Every row of a query.

Parameters

  • sql (string)
  • params (list|dict|nil)

Returns list[dict]

Connection.fetch_value()

sql.Connection.fetch_value(sql: string, params, fallback) -> any

The first column of the first row, for a query written to return one value.

Parameters

  • sql (string)
  • params (list|dict|nil)
  • fallback (any) — What to return for an empty result.

Returns any

Connection.fetch_column()

sql.Connection.fetch_column(sql: string, params, column) -> list

One column’s values, as a list.

Parameters

  • sql (string)
  • params (list|dict|nil)
  • column (string|number|nil) — Which column, the first by default.

Returns list

Connection.stream()

sql.Connection.stream(sql: string, params, options) -> Cursor

Runs a query and returns a cursor over its result rather than reading all of it.

Parameters

  • sql (string)
  • params (list|dict|nil)
  • options (dict|nil) — { batch } on engines that fetch in batches.

Returns Cursor

Connection.prepare()

sql.Connection.prepare(sql: string) -> Statement

Compiles a statement for repeated use.

Parameters

  • sql (string)

Returns Statement

Connection.exec_script()

sql.Connection.exec_script(sql: string)

Runs one or more statements for their effect, as a script.

This is the path for a schema file or a batch of pragmas. It binds no parameters and returns no rows, and an engine that cannot run several statements at once runs them one at a time.

Parameters

  • sql (string)

Connection.insert()

sql.Connection.insert(table: string, values: dict, options) -> any

Inserts a row and returns its id.

How the id comes back depends on the engine, which is the point of having this rather than writing the insert by hand. An engine with a last insert id reports one; an engine without gets a RETURNING clause added. An engine that can do neither raises rather than returning something that is not an id.

var id = db.insert('posts', { title: 'Hello', body: 'World' })

Parameters

  • table (string)
  • values (dict) — Column to value.
  • options (dict|nil) — { returning } names the column to read the id from, which matters on an engine using RETURNING when the key is not called id.

Returns any — The new row’s id, or nil where the engine reports none and no returning column was named.

Connection.insert_many()

sql.Connection.insert_many(table: string, rows: list) -> number

Inserts several rows in one statement.

Every row has to name the same columns. Large batches are split so that no one statement binds more parameters than the engine allows.

Parameters

  • table (string)
  • rows (list) — Dictionaries of column to value.

Returns number — How many rows were inserted.

Connection.update()

sql.Connection.update(table: string, values: dict, where) -> number

Updates the rows matching where.

Passing nil for where updates every row, which has to be asked for rather than happening because a dictionary came out empty.

Parameters

  • table (string)
  • values (dict) — Column to new value.
  • where (dict|nil) — Column to value; a list matches any of them and nil matches null.

Returns number — How many rows changed.

Connection.delete()

sql.Connection.delete(table: string, where) -> number

Deletes the rows matching where.

Parameters

  • table (string)
  • where (dict|nil)

Returns number — How many rows went.

Connection.find()

sql.Connection.find(table: string, where, options) -> ResultSet

Selects the rows matching where.

db.find('posts', { published: true }, {
  columns: ['id', 'title'],
  order: ['created_at desc'],
  limit: 10,
})

Parameters

  • table (string)
  • where (dict|nil)
  • options (dict|nil) — columns, order, limit and offset.

Returns ResultSet

Connection.find_one()

sql.Connection.find_one(table: string, where, options) -> dict|nil

The first row matching where, or nil.

Parameters

  • table (string)
  • where (dict|nil)
  • options (dict|nil)

Returns dict|nil

Connection.count()

sql.Connection.count(table: string, where) -> number

How many rows match where.

Parameters

  • table (string)
  • where (dict|nil)

Returns number

Connection.transaction()

sql.Connection.transaction(body: function, isolation) -> any

Runs body inside a transaction, committing when it returns and rolling back when it raises.

db.transaction(@(tx) {
  tx.exec('insert into audit (what) values (?)', ['transfer'])
  tx.update('accounts', { balance: 0 }, { id: 1 })
})

A transaction() called inside another takes a savepoint, so only the outermost commits and a failure inside undoes just that inner piece of work.

Parameters

  • body (function) — Called with a Transaction.
  • isolation (string|nil) — One of the level constants, for the outermost transaction only.

Returns any — Whatever body returned.

Connection.begin()

sql.Connection.begin(isolation) -> Transaction

Opens a transaction to be committed or rolled back by hand.

The closure form is safer and should be preferred. This exists for a transaction whose lifetime is not a block, such as one held open across an entire request.

Parameters

  • isolation (string|nil)

Returns Transaction

Connection.in_transaction()

sql.Connection.in_transaction() -> bool

Whether a transaction is open on this connection.

Returns bool

Connection.ping()

sql.Connection.ping() -> bool

Checks the connection is still usable.

Returns bool

Connection.close()

sql.Connection.close()

Closes the connection, or returns it to its pool when it came from one.

Safe to call more than once.

Connection.is_closed()

sql.Connection.is_closed() -> bool

Whether this connection has been closed.

Returns bool

Connection.to_string()

sql.Connection.to_string()

2026, Richard Ore and Zuri contributors

sql.crud

import sql.crud

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.crud.* needs import sql.crud.

Building the four statements that are the same everywhere.

Inserting a row, updating rows that match, deleting rows that match and selecting rows that match are written the same way against every engine, apart from how identifiers are quoted and how placeholders are spelled. Both of those come from the driver, so these builders produce correct SQL for whichever adapter is active.

This is not a query builder and is not trying to become one. There is no join here, no subquery, no expression tree. Anything past a flat list of equality where is written as SQL, which is a better language for it than any chain of method calls.

Every value becomes a bound parameter. Nothing built here interpolates a value into the statement text, which is what makes these safe to hand user input.

Functions

raw()

sql.raw(sql: string) -> Raw

Builds a Raw.

Parameters

  • sql (string)

Returns Raw

quote_table()

sql.crud.quote_table(driver, table: string) -> string

Renders a table name, which may carry a schema.

'public.users' becomes "public"."users", and a name with a dot that is genuinely part of it can be quoted by passing it already quoted.

Parameters

  • driver (Driver)
  • table (string)

Returns string

where()

sql.crud.where(driver, filter, start: number) -> dict

Builds the WHERE clause for a dictionary of conditions.

A value of nil becomes IS NULL rather than = NULL, which no row ever satisfies. A list becomes IN (...), and an empty list becomes a condition no row satisfies, which is what “in nothing” means. A Raw is used as written.

Parameters

  • driver (Driver)
  • filter (dict|nil)
  • start (number) — The binding position the first parameter takes.

Returns dict — { clause, values }, the clause empty when there are no conditions.

insert()

sql.crud.insert(driver, table: string, values: dict, returning) -> dict

Builds an INSERT.

Parameters

  • driver (Driver)
  • table (string)
  • values (dict)
  • returning (string|nil) — A column to return, for engines that report an inserted id that way rather than through a last insert id. Ignored where the driver does not support RETURNING.

Returns dict — { sql, values }

Raises QueryError if values is empty.

insert_many()

sql.crud.insert_many(driver, table: string, rows: list) -> dict

Builds an INSERT carrying several rows in one statement.

Every row has to name the same columns, in any order; a row that names a different set raises rather than being padded with nulls, because a missing column and a null column mean different things.

Parameters

  • driver (Driver)
  • table (string)
  • rows (list) — Dictionaries of column to value.

Returns dict — { sql, values }

Raises QueryError if rows is empty, a row has no columns, or the rows disagree about which columns they have.

update()

sql.crud.update(driver, table: string, values: dict, filter) -> dict

Builds an UPDATE.

An update with no where changes every row, which is occasionally what someone means and usually not, so it has to be asked for by passing nil rather than happening by default when a condition dictionary turns out empty.

Parameters

  • driver (Driver)
  • table (string)
  • values (dict)
  • filter (dict|nil)

Returns dict — { sql, values }

Raises QueryError if values is empty.

delete()

sql.crud.delete(driver, table: string, filter) -> dict

Builds a DELETE.

Parameters

  • driver (Driver)
  • table (string)
  • filter (dict|nil)

Returns dict — { sql, values }

select()

sql.crud.select(driver, table: string, filter, options) -> dict

Builds a SELECT.

Parameters

  • driver (Driver)
  • table (string)
  • filter (dict|nil)
  • options (dict|nil) — columns (a list of names, all of them by default), order (a list of names, each optionally followed by desc), limit and offset.

Returns dict — { sql, values }

Classes

Raw

class sql.Raw

Marks a fragment of SQL to be used as written rather than bound.

The escape hatch for the cases where a value is not a value:

db.update('posts', { views: sql.raw('views + 1') }, { id: 7 })

It is exactly as dangerous as it sounds. A Raw built from anything a user supplied is a SQL injection, so build them from literals and keep values in the parameters where they belong.

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
sqlstringThe fragment, as it will appear in the statement.

Constructor

sql.Raw(sql)

Parameters

  • sql (string)

Raw.to_string()

sql.Raw.to_string()

2026, Richard Ore and Zuri contributors

sql.cursor

import sql.cursor

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.cursor.* needs import sql.cursor.

Reading a result without holding all of it.

query() builds the whole result in memory, which is the right thing for the hundreds of rows most queries return and the wrong thing for the millions some do. A cursor reads the same result a row at a time, so the memory a loop needs is the size of one row rather than of the table.

var cursor = db.stream('select * from events')

for row in cursor {
  handle(row)
}

Running to the end closes the cursor. A loop that stops early does not, so anything that might break should close it, which using makes hard to forget.

Classes

Cursor

class sql.Cursor

A result being read a row at a time.

  • printable — has a @to_string(), so echo and print() show something useful
  • iterable — can be walked with for and iter

Constructor

sql.Cursor(cursor)

Parameters

  • cursor (DriverCursor) — The adapter’s own cursor.

Cursor.columns()

sql.Cursor.columns() -> list[dict]

The result’s columns, each a dictionary with name and type.

Returns list[dict]

Cursor.column_names()

sql.Cursor.column_names() -> list[string]

The names of the result’s columns, in order.

Returns list[string]

Cursor.position()

sql.Cursor.position() -> number

How many rows have been read so far.

Returns number

Cursor.next()

sql.Cursor.next() -> dict|nil

The next row as a dictionary keyed by column name, or nil once the result is finished.

Returns dict|nil

Raises ClosedError if the cursor was closed.

Cursor.take()

sql.Cursor.take(count: number) -> list[dict]

The next count rows, or fewer at the end of the result.

For work that batches naturally: inserting a thousand rows at a time into somewhere else, say.

Parameters

  • count (number)

Returns list[dict]

Cursor.rest()

sql.Cursor.rest() -> list[dict]

Reads whatever is left into a list.

This gives up the point of a cursor, so it is for the case where a result turned out to be small after all.

Returns list[dict]

Cursor.is_closed()

sql.Cursor.is_closed() -> bool

Whether the cursor has been closed or read to the end.

Returns bool

Cursor.close()

sql.Cursor.close()

Releases the cursor. Safe to call more than once.

Cursor.to_string()

sql.Cursor.to_string()

2026, Richard Ore and Zuri contributors

sql.decimal

import sql.decimal

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.decimal.* needs import sql.decimal.

Exact decimal numbers, for the money column that must not be a float.

PostgreSQL’s numeric holds a decimal value exactly. A number in Zuri is a double, which holds 0.1 only approximately, so reading a balance of 0.10 into one and writing it back is not guaranteed to store what was there before. Decimal keeps the value as an integer and a scale, so 10.05 is exactly ten and five hundredths and stays that way through every read and write.

var price = Decimal('19.99')
var tax = price.multiply(Decimal('0.20'))

echo tax.to_string()
# 3.9980

Addition, subtraction and multiplication are exact. Division is not, in general, so it takes the number of decimal places to produce and rounds half away from zero, the rule money is normally counted by.

Functions

decimal()

sql.decimal(value, scale) -> Decimal

Builds a decimal, the same as Decimal() but as a function.

Parameters

  • value (string|number|bigint)
  • scale (number|nil)

Returns Decimal

Classes

Decimal

class sql.Decimal

An exact decimal number.

Held as an integer and a scale: unscaled is the digits with the point removed, scale is how many of them are after the point. So 1.05 is 105 at scale 2, and 1.050 is 1050 at scale 3. The two are equal in value and differ in how they print, which is what lets a column declared numeric(10, 3) round trip without gaining or losing a trailing zero.

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
unscaledbigintThe digits, with the decimal point removed, as a bigint.
scalenumberHow many digits are after the decimal point.

Constructor

sql.Decimal(value, scale)

Builds a decimal.

From text, which is the exact form and the one to prefer:

Decimal('19.99')
Decimal('-0.0001')
Decimal('1.2e3')

From a number, which is only as exact as the double was:

Decimal(19.99)

Or from the parts, which is what the PostgreSQL adapter uses:

Decimal(1999, 2)

Parameters

  • value (string|number|bigint) — The value, or its unscaled digits when scale is given.
  • scale (number|nil) — The number of decimal places, when value is the unscaled integer.

Raises QueryError if the text is not a number.

Decimal.is_negative()

sql.Decimal.is_negative() -> bool

Whether this is negative.

Returns bool

Decimal.is_zero()

sql.Decimal.is_zero() -> bool

Whether this is exactly zero, whatever its scale.

Returns bool

Decimal.rescale()

sql.Decimal.rescale(places: number) -> Decimal

The same value at a different number of decimal places.

Adding places is exact. Removing them rounds half away from zero, so 2.5 to no places is 3 and -2.5 is -3.

Parameters

  • places (number)

Returns Decimal

Decimal.add()

sql.Decimal.add(other) -> Decimal

The sum of this and other, exactly.

The result carries the larger of the two scales, so no digit is lost.

Parameters

  • other (Decimal)

Returns Decimal

Decimal.subtract()

sql.Decimal.subtract(other) -> Decimal

The difference, exactly.

Parameters

  • other (Decimal)

Returns Decimal

Decimal.multiply()

sql.Decimal.multiply(other) -> Decimal

The product, exactly.

The scales add, as they do when multiplying by hand: two places times two places is four places.

Parameters

  • other (Decimal)

Returns Decimal

Decimal.divide()

sql.Decimal.divide(other, places: number) -> Decimal

The quotient to places decimal places, rounded half away from zero.

Division is the one operation with no exact answer in general, so the number of places is required rather than guessed.

Parameters

  • other (Decimal)
  • places (number)

Returns Decimal

Raises QueryError if other is zero.

Decimal.to_number()

sql.Decimal.to_number() -> number

The value as a number, which is approximate for anything a double cannot hold exactly.

Returns number

Decimal.compare()

sql.Decimal.compare(other) -> number

Compares this with other: negative, zero or positive, the same shape a sort comparator takes.

Parameters

  • other (Decimal)

Returns number

Decimal.equals()

sql.Decimal.equals(other) -> bool

Whether this and other are the same value, whatever their scales. 1.5 and 1.50 are equal.

Parameters

  • other (Decimal)

Returns bool

Decimal.to_string()

sql.Decimal.to_string() -> string

The value written out, with exactly scale digits after the point.

Returns string


2026, Richard Ore and Zuri contributors

sql.driver

import sql.driver

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.driver.* needs import sql.driver.

The contract every database adapter implements, and the capability flags that let one adapter differ from another without the code above them having to know which is in use.

An adapter supplies four things: a Driver that knows how to read a connection string and open a connection, a DriverConnection that runs statements, a DriverStatement for a prepared one, and a DriverCursor for reading a result a batch at a time. Everything a program touches directly is built on top of these, so an adapter that implements them gets pooling, transactions, the CRUD helpers, placeholder translation and the error hierarchy without writing any of it.

The methods here raise NotImplementedError. An adapter that forgets one fails where the gap is rather than somewhere further down, and an adapter for an engine that genuinely cannot do something raises NotSupportedError from its own override instead, which reads differently on purpose: the first is an unfinished adapter, the second is an honest limit of the engine.

Constants

READ_COMMITTED

sql.READ_COMMITTED = 'read committed'

Each transaction sees rows committed before its own statement started, and nothing a concurrent transaction commits afterwards within that statement.

REPEATABLE_READ

sql.REPEATABLE_READ = 'repeatable read'

A transaction reads the same rows throughout, whatever anyone else commits while it runs.

SERIALIZABLE

sql.SERIALIZABLE = 'serializable'

Concurrent transactions produce a result some serial order of them could also have produced. The engine may abort one to keep that promise, which surfaces as SerializationError.

READ_UNCOMMITTED

sql.READ_UNCOMMITTED = 'read uncommitted'

A transaction can see rows another has written and not committed. PostgreSQL accepts the name and gives read committed anyway, which its own documentation is explicit about.

Functions

default_capabilities()

sql.default_capabilities() -> dict

The capability flags a driver reports, with the value each takes when a driver does not say otherwise.

A driver’s capabilities() starts from this and overrides what differs, so a flag added here later does not break an adapter written before it existed.

FlagMeaning
placeholder_styleHow the engine spells a parameter.
named_parametersWhether the engine binds parameters by name.
last_insert_idWhether the engine reports the id of the row just inserted.
returningWhether INSERT ... RETURNING works.
transactionsWhether BEGIN, COMMIT and ROLLBACK work.
savepointsWhether SAVEPOINT works, which is what nested transactions are built on.
transactional_ddlWhether CREATE, ALTER and DROP stay inside
a transaction, rather than committing it.
isolation_levelsThe levels the engine will accept.
server_side_cursorsWhether a result can be read without the whole of it arriving first.
prepared_statementsWhether a statement can be compiled once and run repeatedly.
multiple_statementsWhether one call may carry several
statements separated by semicolons.
arraysWhether the engine has an array type.
jsonWhether the engine has a JSON type, as opposed to storing JSON in text.
blobsWhether the engine stores binary values.
decimalsWhether the engine has an exact decimal type.
booleansWhether the engine has a real boolean type.
upsertWhether an insert can update the row it collided
with, however the engine spells it.
schemasWhether tables live in named schemas.
concurrent_writersWhether two connections can write at once.
max_parametersMost parameters one statement may bind, or nil for no practical limit.
identifier_quoteThe character an identifier is quoted with.
default_portThe port used when a connection string omits
one, or nil for an engine with no port.

Returns dict

Classes

Driver

class sql.Driver

Opens connections to one kind of database.

A driver holds no connection of its own and no state worth sharing, so one instance per engine is registered with sql.register() and reused for every connection it opens.

Driver.name()

sql.Driver.name() -> string

The name this driver is registered under, such as 'sqlite'.

Appears on every error the adapter raises, so it wants to be the name a reader would recognise.

Returns string

Driver.schemes()

sql.Driver.schemes() -> list[string]

The URL schemes this driver claims, without the ://.

sql.open() picks a driver by matching a connection string’s scheme against these, which is what lets the same call open any of the engines. Several are allowed: PostgreSQL answers to both postgres and postgresql.

Returns list[string]

Driver.capabilities()

sql.Driver.capabilities() -> dict

What this engine can and cannot do.

Built from default_capabilities() with the differences applied.

Returns dict

Driver.parse_dsn()

sql.Driver.parse_dsn(dsn) -> dict

Turns a connection string or an options dictionary into the normalised options connect() takes.

Each adapter reads the forms its own engine’s tooling uses, so a connection string copied from somewhere else works unchanged.

Parameters

  • dsn (string|dict)

Returns dict

Driver.connect()

sql.Driver.connect(options: dict) -> DriverConnection

Opens a connection with options from parse_dsn().

Parameters

  • options (dict)

Returns DriverConnection

Driver.quote_identifier()

sql.Driver.quote_identifier(name: string) -> string

Quotes name so the engine reads it as an identifier whatever it contains or collides with.

The default doubles the engine’s own quote character, which is what the SQL standard says and what the built-in adapters do, MySQL included once its quote character is taken into account. An engine whose quoting differs overrides this.

Parameters

  • name (string)

Returns string

Driver.supports()

sql.Driver.supports(flag: string) -> bool

Whether this driver supports flag.

Parameters

  • flag (string) — A key of default_capabilities().

Returns bool

DriverConnection

class sql.DriverConnection

One open connection to a database, as the layer above it sees one.

Adapters implement this; programs use sql.Connection, which wraps it and adds everything that is the same across engines.

Fields

FieldTypeDescription
driverDriverThe Driver that opened this connection.

DriverConnection.execute()

sql.DriverConnection.execute(sql: string, values: list) -> dict

Runs a statement for its effect and reports what it did.

Parameters

  • sql (string) — Already translated into this engine’s own placeholder style.
  • values (list) — Bound in order.

Returns dict — { rows_affected, last_insert_id }, the second nil on an engine that does not report one.

DriverConnection.select()

sql.DriverConnection.select(sql: string, values: list) -> dict

Runs a statement and reads its whole result.

Parameters

  • sql (string)
  • values (list)

Returns dict — { columns, rows }, where columns is a list of { name, type } dictionaries and rows a list of value lists in the same order.

DriverConnection.open_cursor()

sql.DriverConnection.open_cursor(sql: string, values: list, options: dict) -> DriverCursor

Runs a statement and returns a cursor over its result rather than reading all of it.

On an engine without server side cursors this still avoids holding every row at once only as far as the engine allows; the capability flag says which.

Parameters

  • sql (string)
  • values (list)
  • options (dict) — { batch }, the rows to fetch at a time.

Returns DriverCursor

DriverConnection.prepare()

sql.DriverConnection.prepare(sql: string) -> DriverStatement

Compiles a statement for repeated use.

Parameters

  • sql (string)

Returns DriverStatement

DriverConnection.begin()

sql.DriverConnection.begin(isolation)

Opens a transaction.

Parameters

  • isolation (string|nil) — One of the level constants, or nil for the engine’s default.

DriverConnection.commit()

sql.DriverConnection.commit()

Commits the open transaction.

DriverConnection.rollback()

sql.DriverConnection.rollback()

Rolls the open transaction back.

DriverConnection.savepoint()

sql.DriverConnection.savepoint(name: string)

Marks a point inside the open transaction that can be returned to.

Parameters

  • name (string)

DriverConnection.release_savepoint()

sql.DriverConnection.release_savepoint(name: string)

Discards a savepoint, keeping everything done since it was taken.

Parameters

  • name (string)

DriverConnection.rollback_to()

sql.DriverConnection.rollback_to(name: string)

Undoes everything done since name was taken, leaving the transaction open and the savepoint still in place.

Parameters

  • name (string)

DriverConnection.in_transaction()

sql.DriverConnection.in_transaction() -> bool

Whether a transaction is currently open on this connection.

Returns bool

DriverConnection.last_insert_id()

sql.DriverConnection.last_insert_id() -> number|bigint|nil

The id of the row most recently inserted on this connection.

Only meaningful where last_insert_id is supported; elsewhere it raises NotSupportedError, and Connection.insert() uses RETURNING instead.

Returns number|bigint|nil

DriverConnection.ping()

sql.DriverConnection.ping() -> bool

Checks the connection is still usable, cheaply.

A pool calls this before handing a connection out, so it wants to be the smallest round trip the engine offers.

Returns bool

DriverConnection.server_version()

sql.DriverConnection.server_version() -> string

The engine’s version, as the engine reports it.

Returns string

DriverConnection.schema_adapter()

sql.DriverConnection.schema_adapter() -> SchemaAdapter

This engine’s introspection, which Connection.schema goes through.

The default refuses, so an adapter that has not written the queries says so plainly rather than reporting an empty database.

Returns SchemaAdapter

Raises NotSupportedError if the adapter has no introspection.

DriverConnection.close()

sql.DriverConnection.close()

Closes the connection. Calling this more than once does nothing.

DriverConnection.is_closed()

sql.DriverConnection.is_closed() -> bool

Whether this connection has been closed.

Returns bool

DriverStatement

class sql.DriverStatement

A statement compiled once and run many times.

DriverStatement.execute()

sql.DriverStatement.execute(values: list) -> dict

Runs the statement for its effect.

Parameters

  • values (list)

Returns dict — { rows_affected, last_insert_id }

DriverStatement.select()

sql.DriverStatement.select(values: list) -> dict

Runs the statement and reads its whole result.

Parameters

  • values (list)

Returns dict — { columns, rows }

DriverStatement.open_cursor()

sql.DriverStatement.open_cursor(values: list, options: dict) -> DriverCursor

Runs the statement and returns a cursor over its result.

Parameters

  • values (list)
  • options (dict)

Returns DriverCursor

DriverStatement.columns()

sql.DriverStatement.columns() -> list[dict]

The columns this statement returns, as { name, type } dictionaries.

Returns list[dict]

DriverStatement.parameter_count()

sql.DriverStatement.parameter_count() -> number

How many parameters this statement binds.

Returns number

DriverStatement.close()

sql.DriverStatement.close()

Releases the statement. Calling this more than once does nothing.

DriverCursor

class sql.DriverCursor

A result being read a batch at a time rather than all at once.

DriverCursor.columns()

sql.DriverCursor.columns() -> list[dict]

The columns of this result, as { name, type } dictionaries.

Returns list[dict]

DriverCursor.next_row()

sql.DriverCursor.next_row() -> list|nil

The next row as a list of values, or nil once the result is exhausted.

Returns list|nil

DriverCursor.close()

sql.DriverCursor.close()

Releases the cursor. Calling this more than once does nothing.

SchemaAdapter

class sql.SchemaAdapter

The introspection queries for one engine.

Every method returns the same shape whichever engine answered, which is the whole point of routing introspection through here rather than letting each program write its own information_schema query.

SchemaAdapter.tables()

sql.SchemaAdapter.tables(schema) -> list[string]

The names of the tables the application created, leaving out the ones the engine keeps for itself.

Parameters

  • schema (string|nil)

Returns list[string]

SchemaAdapter.views()

sql.SchemaAdapter.views(schema) -> list[string]

The names of the views.

Parameters

  • schema (string|nil)

Returns list[string]

SchemaAdapter.columns()

sql.SchemaAdapter.columns(table, schema) -> list[dict]

The columns of table, in order, each shaped as schema.column_shape() describes.

Parameters

  • table (string)
  • schema (string|nil)

Returns list[dict]

SchemaAdapter.primary_key_columns()

sql.SchemaAdapter.primary_key_columns(table, schema) -> list[string]

The columns making up table’s primary key, in key order, or an empty list for a table with none.

Parameters

  • table (string)
  • schema (string|nil)

Returns list[string]

SchemaAdapter.indexes()

sql.SchemaAdapter.indexes(table, schema) -> list[dict]

The indexes on table, each { name, columns, unique }.

Parameters

  • table (string)
  • schema (string|nil)

Returns list[dict]

SchemaAdapter.foreign_keys()

sql.SchemaAdapter.foreign_keys(table, schema) -> list[dict]

The foreign keys on table, each { columns, references_table, references_columns, on_delete, on_update }.

Parameters

  • table (string)
  • schema (string|nil)

Returns list[dict]


2026, Richard Ore and Zuri contributors

sql.errors

import sql

Everything here is re-exported by sql, so import sql is enough and the names are called as sql.*. Importing sql.errors on its own works too and reaches the same definitions.

Every error a database raises, under one root, in one shape, whatever engine it came from.

This is the part of sql that pays for itself soonest. PostgreSQL reports a unique constraint violation as SQLSTATE 23505; SQLite reports it as extended result code 2067. Neither number means anything to the other, and code that matched on either would stop working the moment the adapter changed. Every adapter translates its engine’s own vocabulary into the classes below, so a program catches UniqueViolation and keeps catching it after a migration.

The original is never thrown away. code and sqlstate carry whatever the engine said, so anything genuinely engine specific is still reachable.

Classes

SqlError

class sql.SqlError < Error

Base class for every error this module and its adapters raise.

Catch this to catch anything a database can do. The subclasses below separate the cases worth handling differently, and every one of them is a SqlError, so a broad catch never misses one.

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
codeThe engine’s own error code, as the engine spells it.
sqlstateThe five character SQLSTATE, for engines that report one.
driverName of the adapter that raised this, such as 'sqlite', 'postgres' or 'mysql'.
queryThe statement that failed, as it was sent to the engine.

Constructor

sql.SqlError(message, details)

Builds an error, optionally carrying the engine’s own detail.

details is a dictionary; any of code, sqlstate, driver and query it holds are copied onto the error and the rest ignored. Adapters fill it in. Application code raising one of these by hand can leave it out entirely.

Parameters

  • message (string)
  • details (dict|nil)

SqlError.to_string()

sql.SqlError.to_string()

ConnectionError

class sql.ConnectionError < SqlError

The database could not be reached, or the connection dropped.

A network failure, a refused connection, a server that is not running, a database file that cannot be opened. Retrying is often reasonable; the same query against the same data may well succeed once the cause clears.

  • printable — has a @to_string(), so echo and print() show something useful

AuthenticationError

class sql.AuthenticationError < ConnectionError

The server rejected the credentials.

A ConnectionError, because the connection is what failed, but worth separating: no amount of retrying fixes a wrong password.

  • printable — has a @to_string(), so echo and print() show something useful

ProtocolError

class sql.ProtocolError < ConnectionError

The server sent something the adapter could not make sense of.

A ConnectionError because that is what it means in practice: the conversation has lost its place, so nothing further on this connection can be trusted and a pool holding it should discard it rather than lend it out again.

Seeing one means either a bug in the adapter or something between it and the server that is not the server.

  • printable — has a @to_string(), so echo and print() show something useful

TimeoutError

class sql.TimeoutError < ConnectionError

An operation ran out of time.

Raised for a connection that took too long to open, a statement that exceeded its deadline, and for SQLite’s busy timeout expiring while another writer held the database.

  • printable — has a @to_string(), so echo and print() show something useful

QueryError

class sql.QueryError < SqlError

The engine refused the statement.

Bad syntax, an unknown table or column, a type the engine would not accept, a wrong number of parameters. These are programming errors: the same statement will fail the same way every time.

  • printable — has a @to_string(), so echo and print() show something useful

IntegrityError

class sql.IntegrityError < SqlError

A constraint rejected the data.

Catch this to handle any constraint violation without caring which; the subclasses are there when the difference matters, as it does when a duplicate key means “already exists” and a foreign key violation means “referenced row is gone”.

  • printable — has a @to_string(), so echo and print() show something useful

UniqueViolation

class sql.UniqueViolation < IntegrityError

A unique index or primary key already holds this value.

  • printable — has a @to_string(), so echo and print() show something useful

ForeignKeyViolation

class sql.ForeignKeyViolation < IntegrityError

A foreign key has no matching row, or a referenced row is still in use by another table.

  • printable — has a @to_string(), so echo and print() show something useful

NotNullViolation

class sql.NotNullViolation < IntegrityError

A column declared NOT NULL was given nothing.

  • printable — has a @to_string(), so echo and print() show something useful

CheckViolation

class sql.CheckViolation < IntegrityError

A CHECK constraint rejected the row.

  • printable — has a @to_string(), so echo and print() show something useful

TransactionError

class sql.TransactionError < SqlError

A transaction could not be carried out as asked.

Raised for a commit that failed, a rollback with nothing to roll back, and for nesting mistakes such as releasing a savepoint that was never taken.

  • printable — has a @to_string(), so echo and print() show something useful

SerializationError

class sql.SerializationError < TransactionError

Two transactions could not both be treated as though they ran alone.

The engine gave up on one of them rather than produce a result that serial execution could not have produced. Retrying the whole transaction is the correct response, and usually succeeds.

  • printable — has a @to_string(), so echo and print() show something useful

DeadlockError

class sql.DeadlockError < TransactionError

Two transactions were each waiting on the other, and the engine broke the tie by aborting this one.

As with SerializationError, the response is to retry the transaction from the beginning.

  • printable — has a @to_string(), so echo and print() show something useful

PoolError

class sql.PoolError < SqlError

Something went wrong with a connection pool rather than with a database.

  • printable — has a @to_string(), so echo and print() show something useful

PoolExhaustedError

class sql.PoolExhaustedError < PoolError

Every connection was busy and none came free within the acquire timeout.

Either the pool is too small for the load or connections are being held longer than they should be; a connection acquired and not released is the usual cause.

  • printable — has a @to_string(), so echo and print() show something useful

NotSupportedError

class sql.NotSupportedError < SqlError

The engine cannot do what was asked, and no approximation would be honest.

Raised where an adapter would otherwise have to guess: asking PostgreSQL for a last insert id it does not report, asking SQLite for an isolation level it has no analogue for, connecting to PostgreSQL over a Unix domain socket. The message names both the adapter and the feature.

  • printable — has a @to_string(), so echo and print() show something useful

ClosedError

class sql.ClosedError < SqlError

The connection, statement, cursor, transaction or pool was already closed.

Closing something twice is always allowed and never raises this. Using something after closing it is what does.

  • printable — has a @to_string(), so echo and print() show something useful

2026, Richard Ore and Zuri contributors

sql.mysql

import sql.mysql

sql does not re-export this module, so it is reached only by importing it directly.

The MySQL and MariaDB adapter.

Reach it through sql, which is what keeps a program portable:

import sql

var db = sql.open('mysql://alice:secret@localhost/app')

The module underneath is here for the parts of MySQL that have no equivalent elsewhere: the server’s own settings, the connection’s id, and which of the two servers answered.

What this adapter speaks

The client/server protocol over TCP, optionally under TLS and optionally compressed with zlib or zstd, or over a unix domain socket. A socket option names one, and so does a host beginning with a slash. TLS is neither offered nor wanted there, since nothing sits in between, and the connection counts as private, which is what decides whether a plugin may send the password itself.

A statement with no values is sent as text, which is one round trip. A statement with values is prepared, so that the values travel in the server’s binary encoding rather than being written into the SQL. Prepared statements are kept and reused, so a query run in a loop is compiled once.

Authentication

mysql_native_password, caching_sha2_password (both the cached exchange and the slower one behind it), sha256_password, mysql_clear_password and MariaDB’s client_ed25519. On a connection that is not private, the two plugins that need the password itself encrypt it to the server’s public key rather than sending it, and mysql_clear_password is refused outright.

Submodules

ModuleReached asSummary
sql.mysql.authsql.mysql.auth.*The authentication plugins a MySQL or MariaDB server can ask for.
sql.mysql.connectionsql.mysql.connection.*One open MySQL connection, as the layer above it sees one.
sql.mysql.cursorsql.mysql.cursor.*Reading a MySQL result a batch at a time.
sql.mysql.driversql.mysql.driver.*Opening MySQL and MariaDB connections: reading a connection string, establishing the socket, upgrading it to…
sql.mysql.ed25519import sql.mysql.ed25519Ed25519 signing over a key derived from a password, which is what MariaDB’s client_ed25519 authentication…
sql.mysql.errorssql.mysql.errors.*Turning MySQL’s error numbers into the shared error classes.
sql.mysql.packetssql.mysql.packets.*The MySQL packet layer: framing, sequence numbering, compression and the integer and string encodings every…
sql.mysql.protocolsql.mysql.protocol.*The MySQL client/server protocol: the handshake that opens a connection and the command exchange that runs…
sql.mysql.resultsimport sql.mysql.resultsTurning what the server answered into the shapes the shared contract asks for.
sql.mysql.schemaimport sql.mysql.schemaIntrospection for MySQL, through information_schema.
sql.mysql.statementsql.mysql.statement.*A statement compiled once on the server and run many times.
sql.mysql.typessql.mysql.types.*What MySQL’s column types mean in Zuri, in both directions and in both of the protocol’s two encodings.

Constants

DRIVER

sql.mysql.DRIVER

The driver instance sql registers for MySQL.

MARIADB_DRIVER

sql.mysql.MARIADB_DRIVER

The driver instance sql registers for MariaDB.

Functions

connect()

sql.mysql.connect(dsn) -> MySqlConnection

Opens a MySQL connection directly, without going through sql.

Anything meant to stay portable should use sql.open() instead.

Parameters

  • dsn (string|dict) — A connection string or options.

Returns MySqlConnection


2026, Richard Ore and Zuri contributors

sql.mysql.auth

import sql.mysql.auth

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.mysql.auth.* needs import sql.mysql.auth.

The authentication plugins a MySQL or MariaDB server can ask for.

Every one of them works from the same starting point: the server sends a random scramble in its greeting, and the client answers with something that proves it knows the password without containing it. What differs is the proof.

PluginProof
mysql_native_passwordSHA-1 of the password, masked with the scramble.
caching_sha2_passwordThe same in SHA-256, with a fallback for an uncached password.
sha256_passwordThe password itself, under TLS or RSA.
client_ed25519A signature over the scramble, by a key derived from the password.
mysql_clear_passwordThe password itself. Refused unless the connection is private.

The two that send the password

sha256_password and the slow half of caching_sha2_password send the password rather than a proof, because the server needs it to compute the hash it will cache. That is safe over TLS and over a unix socket, and over neither otherwise, so on a plain TCP connection the password is encrypted to a public key the server hands over first. mysql_clear_password has no such fallback and is refused on a connection that is not already private.

Constants

NATIVE

sql.mysql.NATIVE = 'mysql_native_password'

MySQL’s original plugin, and still MariaDB’s default.

CACHING_SHA2

sql.mysql.CACHING_SHA2 = 'caching_sha2_password'

The default since MySQL 8.0.

SHA256

sql.mysql.SHA256 = 'sha256_password'

MySQL 5.7’s stronger option, superseded by the caching one.

CLEAR

sql.mysql.CLEAR = 'mysql_clear_password'

Sends the password as it is. Needs a private connection.

ED25519

sql.mysql.ED25519 = 'client_ed25519'

MariaDB’s signature-based plugin.

NONE

sql.mysql.auth.NONE = 'mysql_old_password'

Named in a handshake by a server that wants no password at all.

SUPPORTED

sql.mysql.auth.SUPPORTED = [...]

The plugins this adapter can answer.

Functions

supports()

sql.mysql.auth.supports(plugin: string) -> bool

Whether plugin is one this adapter knows how to answer.

Parameters

  • plugin (string)

Returns bool

native_password()

sql.mysql.auth.native_password(password: string, scramble) -> bytes

The mysql_native_password response.

The server stores SHA-1 of SHA-1 of the password. Hashing the scramble together with what the server stores produces a mask that both sides can compute, and the client returns its own first stage hash under that mask. The server unmasks it, hashes it once more, and compares. The password never crosses the wire and neither does anything that would work a second time.

Parameters

  • password (string)
  • scramble (bytes) — The 20 bytes from the greeting.

Returns bytes — The 20 byte response, or nothing for an empty password, which is what the server expects for an account with none.

caching_sha2_password()

sql.mysql.auth.caching_sha2_password(password: string, scramble) -> bytes

The fast caching_sha2_password response.

The same construction in SHA-256, with the scramble on the other side of the hash. It only succeeds where the server already holds this account’s hash in memory; a server that does not answers with a request for the password itself.

Parameters

  • password (string)
  • scramble (bytes)

Returns bytes — The 32 byte response, or nothing for an empty password.

ed25519_password()

sql.mysql.auth.ed25519_password(password: string, scramble) -> bytes

The client_ed25519 response, which MariaDB uses.

The password is hashed into an Ed25519 key and the scramble is signed with it. The server holds only the matching public key, so unlike every hash based plugin here, what the server stores cannot be replayed against it.

Parameters

  • password (string)
  • scramble (bytes)

Returns bytes — The 64 byte signature.

clear_password()

sql.mysql.auth.clear_password(password: string) -> bytes

The password as the server reads it when it is sent outright: the bytes and the zero that ends them.

Parameters

  • password (string)

Returns bytes

obfuscate()

sql.mysql.auth.obfuscate(password: string, scramble) -> bytes

The password masked with the scramble, which is the form the two RSA plugins encrypt.

Masking adds nothing against someone who can read the ciphertext, since they cannot. It matters because the scramble differs every connection, so the same password never encrypts to the same bytes twice, and a captured exchange cannot be replayed.

Parameters

  • password (string)
  • scramble (bytes)

Returns bytes

encrypt_password()

sql.mysql.auth.encrypt_password(password: string, scramble, public_pem: string) -> bytes

The password encrypted to the server’s public key, for the plugins that need the password itself over a connection that is not private.

MySQL decrypts this with OAEP under SHA-1, which is what its own server links against. The choice is the server’s, not this adapter’s.

Parameters

  • password (string)
  • scramble (bytes)
  • public_pem (string) — The key the server sent.

Returns bytes

Raises AuthenticationError if the key will not parse.

response_for()

sql.mysql.auth.response_for(plugin: string, password: string, scramble, private: bool) -> bytes

The first response to send for plugin, before any back and forth.

Parameters

  • plugin (string)
  • password (string)
  • scramble (bytes)
  • private (bool) — Whether the connection is already private, which decides whether a plugin may send the password outright.

Returns bytes

Raises AuthenticationError if the plugin is one this adapter cannot answer, or would have to answer unsafely.


2026, Richard Ore and Zuri contributors

sql.mysql.connection

import sql.mysql.connection

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.mysql.connection.* needs import sql.mysql.connection.

One open MySQL connection, as the layer above it sees one.

Why a statement with values is always prepared

MySQL has two ways to run a statement. Sent as text it is one round trip, but there is nowhere to put a value: it would have to be written into the SQL, which means quoting it, which means being wrong about quoting it eventually. Prepared, the values travel separately in the binary encoding and the question never arises.

So a statement with no values goes as text and a statement with values is prepared. Preparing costs a round trip, which is why a connection keeps the statements it has compiled and reuses them: a query run in a loop pays for it once.

Constants

CACHE_LIMIT

sql.mysql.connection.CACHE_LIMIT = 64

How many compiled statements a connection keeps before releasing the one it has gone longest without using.

Each costs a little memory on the server, so this is a ceiling rather than a target.

Classes

MySqlConnection

class sql.mysql.MySqlConnection < DriverConnection

A connection to MySQL or MariaDB.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

sql.mysql.MySqlConnection(driver, protocol, options)

Parameters

  • driver (Driver)
  • protocol (Protocol) — An authenticated protocol.
  • options (dict)

MySqlConnection.protocol()

sql.mysql.MySqlConnection.protocol() -> Protocol

The protocol underneath, for the MySQL-only features that are not part of the shared contract.

Returns Protocol

MySqlConnection.parameters()

sql.mysql.MySqlConnection.parameters() -> dict

What the server said about itself when it answered.

Returns dict

MySqlConnection.flavor()

sql.mysql.MySqlConnection.flavor() -> string

Which server this is: 'mysql' or 'mariadb'.

Returns string

MySqlConnection.connection_id()

sql.mysql.MySqlConnection.connection_id() -> number

The id the server gave this connection.

This is what KILL names, and what a connection shows as in SHOW PROCESSLIST, so it is the handle for stopping a statement running on this connection from another one.

Returns number

MySqlConnection.schema_adapter()

sql.mysql.MySqlConnection.schema_adapter()

MySqlConnection.execute()

sql.mysql.MySqlConnection.execute(sql: string, values: list)

MySqlConnection.select()

sql.mysql.MySqlConnection.select(sql: string, values: list)

MySqlConnection.open_cursor()

sql.mysql.MySqlConnection.open_cursor(sql: string, values: list, options: dict)

MySqlConnection.prepare()

sql.mysql.MySqlConnection.prepare(sql: string)

MySqlConnection.begin()

sql.mysql.MySqlConnection.begin(isolation)

Opens a transaction, or takes a savepoint inside the one already open.

Parameters

  • isolation (string|nil)

MySqlConnection.commit()

sql.mysql.MySqlConnection.commit()

Commits, or releases the innermost savepoint.

MySqlConnection.rollback()

sql.mysql.MySqlConnection.rollback()

Rolls back, or returns to the innermost savepoint and discards it.

MySqlConnection.savepoint()

sql.mysql.MySqlConnection.savepoint(name: string)

MySqlConnection.release_savepoint()

sql.mysql.MySqlConnection.release_savepoint(name: string)

MySqlConnection.rollback_to()

sql.mysql.MySqlConnection.rollback_to(name: string)

MySqlConnection.in_transaction()

sql.mysql.MySqlConnection.in_transaction()

MySqlConnection.last_insert_id()

sql.mysql.MySqlConnection.last_insert_id() -> number|bigint|nil

The id generated by the last insert on this connection.

Zero means the last statement generated none, which is what an insert into a table with no auto increment column reports, so it comes back as nil rather than as a row that does not exist.

Returns number|bigint|nil

MySqlConnection.ping()

sql.mysql.MySqlConnection.ping()

MySqlConnection.server_version()

sql.mysql.MySqlConnection.server_version()

MySqlConnection.reset()

sql.mysql.MySqlConnection.reset()

Returns the session to the state it had when it opened.

A pool calls this before handing the connection on, so that temporary tables, session variables and an unfinished transaction cannot leak from one borrower to the next.

MySqlConnection.close()

sql.mysql.MySqlConnection.close()

MySqlConnection.is_closed()

sql.mysql.MySqlConnection.is_closed()

MySqlConnection.run_simple()

sql.mysql.MySqlConnection.run_simple(sql: string) -> dict

Runs a statement as text, for the cases that have no values and so need no preparing.

Parameters

  • sql (string)

Returns dict

MySqlConnection.to_string()

sql.mysql.MySqlConnection.to_string()

2026, Richard Ore and Zuri contributors

sql.mysql.cursor

import sql.mysql.cursor

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.mysql.cursor.* needs import sql.mysql.cursor.

Reading a MySQL result a batch at a time.

The server keeps the result and sends as many rows as each fetch asks for, so nothing but the current batch is ever held on this side. Only a prepared statement can be read this way, which is why a cursor over a statement written as text still prepares it first.

One at a time

A cursor holds the connection: the server is part way through answering, and no other command may be sent until it has finished. Running a statement on the same connection while a cursor is open raises rather than corrupting the exchange.

Classes

MySqlCursor

class sql.mysql.MySqlCursor < DriverCursor

A result being read in batches.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

sql.mysql.MySqlCursor(connection, state)

Parameters

  • connection (MySqlConnection)
  • state (dict) — The statement to fetch from, its columns, the batch size, whether there is anything to fetch at all, and whether this cursor prepared the statement and so has to release it.

MySqlCursor.columns()

sql.mysql.MySqlCursor.columns()

MySqlCursor.next_row()

sql.mysql.MySqlCursor.next_row() -> list|nil

The next row as a list of values, or nil once the result is finished.

Returns list|nil

MySqlCursor.close()

sql.mysql.MySqlCursor.close()

Releases the cursor, and the statement if this cursor prepared one. Safe to call more than once.

MySqlCursor.is_closed()

sql.mysql.MySqlCursor.is_closed() -> bool

Whether the cursor has been closed or read to the end.

Returns bool

MySqlCursor.to_string()

sql.mysql.MySqlCursor.to_string()

2026, Richard Ore and Zuri contributors

sql.mysql.driver

import sql.mysql.driver

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.mysql.driver.* needs import sql.mysql.driver.

Opening MySQL and MariaDB connections: reading a connection string, establishing the socket, upgrading it to TLS when asked, and authenticating.

Constants

DEFAULT_PORT

sql.mysql.DEFAULT_PORT = 3306

The port a MySQL server listens on unless told otherwise.

DEFAULT_TIMEOUT

sql.mysql.driver.DEFAULT_TIMEOUT = 30000

How long to wait for the socket and for each read, in milliseconds.

MAX_PARAMETERS

sql.mysql.MAX_PARAMETERS = 65535

Most parameters one statement can bind. The protocol counts them in a 16 bit field, so this is a hard limit rather than a configured one.

SSL_MODES

sql.mysql.SSL_MODES = [...]

What sslmode can be.

ModeMeaning
disableNever use TLS.
preferWhere the server offers it, unverified; in the clear where it does not.
requireInsist on TLS, without checking who the server is.
verifyInsist on TLS and check the server’s certificate and host name.

MySQL’s own spellings are accepted too. DISABLED, PREFERRED and REQUIRED mean what they say; VERIFY_CA and VERIFY_IDENTITY both become verify. That makes VERIFY_CA stricter here than on the command line, where it checks the certificate but not the name: the difference can only cause a connection to be refused, never to be wrongly trusted.

Classes

MySqlDriver

class sql.mysql.MySqlDriver < Driver

Opens MySQL connections.

MySqlDriver.name()

sql.mysql.MySqlDriver.name()

MySqlDriver.schemes()

sql.mysql.MySqlDriver.schemes()

MySqlDriver.capabilities()

sql.mysql.MySqlDriver.capabilities() -> dict

What MySQL can do.

last_insert_id is true and returning is false, which together are the most visible difference from PostgreSQL: an insert that wants its id back reads it from what the server reported rather than from a RETURNING clause, and Connection.insert() picks between the two on its own.

Returns dict

MySqlDriver.parse_dsn()

sql.mysql.MySqlDriver.parse_dsn(dsn) -> dict

Reads a connection string, in either of the two forms MySQL’s own tooling accepts.

A URI:

mysql://alice:secret@db.example.com:3306/app?sslmode=verify
mysql://localhost/app

Or keywords, as my.cnf spells them:

host=localhost port=3306 database=app user=alice password=secret

Anything absent is filled in the way the command line client fills it: the user defaults to root, the host to 127.0.0.1 and the port to 3306. No database is selected unless one is named.

Parameters

  • dsn (string|dict)

Returns dict

Raises ConnectionError if the string cannot be read.

MySqlDriver.connect()

sql.mysql.MySqlDriver.connect(options: dict) -> MySqlConnection

Opens the connection and authenticates.

Parameters

  • options (dict) — From parse_dsn().

Returns MySqlConnection

MariaDbDriver

class sql.mysql.MariaDbDriver < MySqlDriver

Opens MariaDB connections.

MariaDB speaks the same protocol, so everything about the exchange is shared. It differs in what it can be asked to do: RETURNING works there and does not on MySQL, which is the one capability that changes what the layer above generates.

A MariaDB server reached through mysql:// works, and simply reports the smaller set of capabilities.

MariaDbDriver.name()

sql.mysql.MariaDbDriver.name()

MariaDbDriver.schemes()

sql.mysql.MariaDbDriver.schemes()

MariaDbDriver.capabilities()

sql.mysql.MariaDbDriver.capabilities()

2026, Richard Ore and Zuri contributors

sql.mysql.ed25519

import sql.mysql.ed25519

sql does not re-export this module, so it is reached only by importing it directly.

Ed25519 signing over a key derived from a password, which is what MariaDB’s client_ed25519 authentication is built on.

Why this is not crypto.ed25519

An ordinary Ed25519 private key is a 32 byte seed, and signing begins by hashing that seed into the 64 bytes the signature is actually computed from. MariaDB skips the seed: it hashes the password into those 64 bytes directly, so the user’s password takes the place of the expanded key rather than the place of the seed.

Nothing in crypto.ed25519 can express that, since it takes a PEM-encoded key and does its own expansion internally. So the curve arithmetic lives here, where the one thing that needs it is.

Correctness

Everything below the password derivation is Ed25519 exactly as RFC 8032 specifies it, and is checked against that document’s test vectors and against crypto.ed25519 over random keys.

Constants

P

sql.mysql.ed25519.P

The field prime, 2^255 - 19.

L

sql.mysql.ed25519.L

The order of the base point’s subgroup.

D

sql.mysql.ed25519.D

The curve constant, -121665/121666.

D2

sql.mysql.ed25519.D2

Twice the curve constant, which is what the addition uses.

Functions

password_key()

sql.mysql.ed25519.password_key(password: string) -> bytes

Expands a password into the 64 bytes MariaDB signs with.

Parameters

  • password (string)

Returns bytes

public_key()

sql.mysql.ed25519.public_key(expanded) -> bytes

The public key matching an expanded key.

Parameters

  • expanded (bytes) — The 64 bytes from password_key().

Returns bytes — The 32 byte encoded public key.

sign()

sql.mysql.ed25519.sign(expanded, message) -> bytes

Signs message with an expanded key.

Parameters

  • expanded (bytes) — The 64 bytes from password_key().
  • message (bytes)

Returns bytes — The 64 byte signature.


2026, Richard Ore and Zuri contributors

sql.mysql.errors

import sql.mysql.errors

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.mysql.errors.* needs import sql.mysql.errors.

Turning MySQL’s error numbers into the shared error classes.

MySQL reports two things about a failure: its own error number, such as 1062 for a duplicate key, and a five character SQLSTATE. The number is the specific one and the SQLSTATE is the portable one, so mapping works on the number where a case is worth telling apart and falls back to the SQLSTATE class otherwise. An error number this table has never seen still lands somewhere sensible.

MariaDB shares both schemes and most of the numbers, and the few it adds sit in the same SQLSTATE classes, so nothing here needs to know which server answered.

Constants

SPECIFIC

sql.mysql.errors.SPECIFIC = {...}

Error numbers specific enough to name a class of their own.

CLASSES

sql.mysql.errors.CLASSES = {...}

SQLSTATE classes, for numbers the table above does not name.

Functions

class_for()

sql.mysql.class_for(code, sqlstate) -> class

The class that best describes an error the server reported.

Parameters

  • code (number|nil) — MySQL’s own error number.
  • sqlstate (string|nil)

Returns class

raise_for()

sql.mysql.raise_for(report: dict, query)

Raises the right class for a server error report.

Parameters

  • report (dict) — { code, sqlstate, message }, as protocol.parse_error() returns it.
  • query (string|nil) — The statement that was running.

Raises SqlError always.


2026, Richard Ore and Zuri contributors

sql.mysql.packets

import sql.mysql.packets

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.mysql.packets.* needs import sql.mysql.packets.

The MySQL packet layer: framing, sequence numbering, compression and the integer and string encodings every message above is built from.

A MySQL connection is a stream of packets, each a three byte little-endian length, a one byte sequence number, and that many bytes of payload. The sequence number restarts at zero with every command a client sends and increments for each packet either side writes until that command is answered, which is how both ends notice a packet that went missing or arrived twice.

Why a packet is not always a message

The length field is three bytes, so a payload of 16777215 bytes is the largest that can be described. A message that long is split, and the split is signalled by a packet of exactly that size: the reader has to keep reading until it sees a shorter one. A message whose length is an exact multiple of the maximum therefore ends with an empty packet, which exists only to say the message is over.

Constants

MAX_PAYLOAD

sql.mysql.packets.MAX_PAYLOAD = 16777215

The largest payload a single packet can carry. A payload of exactly this size is always continued in the next packet.

COMPRESS_THRESHOLD

sql.mysql.packets.COMPRESS_THRESHOLD = 50

Below this, a compressed packet is sent uncompressed instead. Small payloads grow rather than shrink under zlib, and the protocol allows either, so the choice is the sender’s.

LENENC_NULL

sql.mysql.packets.LENENC_NULL = 251

The first byte of a length-encoded integer that means the value is NULL. Only meaningful where a column value is expected.

EXACT_INTEGER_LIMIT

sql.mysql.packets.EXACT_INTEGER_LIMIT = 9007199254740992

Largest integer a Zuri number holds exactly. A wider value read off the wire becomes a bigint rather than quietly losing its low bits.

Classes

Writer

class sql.mysql.packets.Writer

Builds one packet payload.

Bytes accumulate in a list, which payload() turns into the real thing. Every appending method returns the writer so that building a message reads as one expression.

Writer.byte()

sql.mysql.packets.Writer.byte(value: number) -> Writer

Appends one byte.

Parameters

  • value (number) — Only the low eight bits are used.

Returns Writer

Writer.int2()

sql.mysql.packets.Writer.int2(value: number) -> Writer

Appends a little-endian 16 bit integer.

Parameters

  • value (number)

Returns Writer

Writer.int3()

sql.mysql.packets.Writer.int3(value: number) -> Writer

Appends a little-endian 24 bit integer.

Parameters

  • value (number)

Returns Writer

Writer.int4()

sql.mysql.packets.Writer.int4(value: number) -> Writer

Appends a little-endian 32 bit integer.

Values are written unsigned. A negative number is written as its two’s complement, which is what the protocol expects wherever a signed field appears.

Parameters

  • value (number)

Returns Writer

Writer.int8()

sql.mysql.packets.Writer.int8(value) -> Writer

Appends a little-endian 64 bit integer.

Parameters

  • value (number|bigint)

Returns Writer

Writer.lenenc_int()

sql.mysql.packets.Writer.lenenc_int(value) -> Writer

Appends a length-encoded integer, in the shortest form that holds it.

Parameters

  • value (number|bigint)

Returns Writer

Writer.lenenc_string()

sql.mysql.packets.Writer.lenenc_string(value) -> Writer

Appends a string with its length in front of it.

Parameters

  • value (string|bytes)

Returns Writer

Writer.cstring()

sql.mysql.packets.Writer.cstring(value: string) -> Writer

Appends a string and the zero byte that ends it.

Parameters

  • value (string)

Returns Writer

Writer.string()

sql.mysql.packets.Writer.string(value) -> Writer

Appends a string with no length and no terminator, which only works where the reader knows the length some other way.

Parameters

  • value (string|bytes)

Returns Writer

Writer.zeros()

sql.mysql.packets.Writer.zeros(count: number) -> Writer

Appends count zero bytes, for the filler fields the protocol reserves and does not use.

Parameters

  • count (number)

Returns Writer

Writer.raw()

sql.mysql.packets.Writer.raw(values) -> Writer

Appends raw bytes.

Parameters

  • values (bytes|list)

Returns Writer

Writer.length()

sql.mysql.packets.Writer.length() -> number

How many bytes have accumulated.

Returns number

Writer.payload()

sql.mysql.packets.Writer.payload() -> bytes

The finished payload.

The buffer itself, not a copy, so the writer is done once this has been called.

Returns bytes

Cursor

class sql.mysql.packets.Cursor

Reads values out of one packet payload, keeping its own position.

Every read advances past what it read, so a message is decoded by naming its fields in order rather than by tracking offsets.

Constructor

sql.mysql.packets.Cursor(data)

Parameters

  • data (bytes) — One packet’s payload.

Cursor.remaining()

sql.mysql.packets.Cursor.remaining() -> number

How many bytes are left unread.

Returns number

Cursor.at_end()

sql.mysql.packets.Cursor.at_end() -> bool

Whether everything has been read.

Returns bool

Cursor.peek()

sql.mysql.packets.Cursor.peek() -> number|nil

The next byte without consuming it, or nil at the end.

Returns number|nil

Cursor.skip()

sql.mysql.packets.Cursor.skip(count: number) -> Cursor

Skips count bytes.

Parameters

  • count (number)

Returns Cursor

Cursor.byte()

sql.mysql.packets.Cursor.byte() -> number

Reads one byte.

Returns number

Cursor.int2()

sql.mysql.packets.Cursor.int2() -> number

Reads a little-endian 16 bit integer.

Returns number

Cursor.int3()

sql.mysql.packets.Cursor.int3() -> number

Reads a little-endian 24 bit integer.

Returns number

Cursor.int4()

sql.mysql.packets.Cursor.int4() -> number

Reads a little-endian 32 bit integer, unsigned.

Returns number

Cursor.int8()

sql.mysql.packets.Cursor.int8() -> number|bigint

Reads a little-endian 64 bit integer, unsigned.

Returns number|bigint — A bigint where the value will not fit in a number exactly.

Cursor.lenenc_int()

sql.mysql.packets.Cursor.lenenc_int() -> number|bigint|nil

Reads a length-encoded integer.

Returns number|bigint|nil — nil where the value is NULL, which only happens in a row.

Cursor.lenenc_bytes()

sql.mysql.packets.Cursor.lenenc_bytes() -> bytes|nil

Reads a length-encoded string as raw bytes.

Returns bytes|nil

Cursor.lenenc_string()

sql.mysql.packets.Cursor.lenenc_string() -> string|nil

Reads a length-encoded string as text.

Returns string|nil

Cursor.cstring()

sql.mysql.packets.Cursor.cstring() -> string

Reads up to the next zero byte, and past it.

A payload that simply ends is treated as terminated, since several messages end with a string whose terminator the server omits when nothing follows it.

Returns string

Cursor.fixed()

sql.mysql.packets.Cursor.fixed(count: number) -> bytes

Reads exactly count bytes.

Parameters

  • count (number)

Returns bytes

Cursor.rest()

sql.mysql.packets.Cursor.rest() -> bytes

Reads everything that is left.

Returns bytes


2026, Richard Ore and Zuri contributors

sql.mysql.protocol

import sql.mysql.protocol

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.mysql.protocol.* needs import sql.mysql.protocol.

The MySQL client/server protocol: the handshake that opens a connection and the command exchange that runs everything after it.

A connection is strictly one command at a time. The client sends a command packet, the server answers with one of three shapes, and nothing else may be sent until that answer is complete:

AnswerShape
OKThe command did something and returned no rows.
ERRThe command failed, with a number and a SQLSTATE.
A result setA column count, the columns, then the rows.

Why the end of a result set is awkward

A row and the marker that ends the rows are told apart by their first byte, and 0xfe means both “the rows are over” and “a length encoded integer of eight bytes follows”. The length decides: a genuine end marker is short, and a row that happens to start 0xfe is not. Servers that negotiate CLIENT_DEPRECATE_EOF send an OK packet there instead, which is what this adapter asks for, but the ambiguity has to be handled anyway for the ones that do not.

Constants

CAPABILITIES

sql.mysql.CAPABILITIES = {...}

What the client asks for and what the server offers.

STATUS

sql.mysql.protocol.STATUS = {...}

What the server says about the session after each command.

COMMANDS

sql.mysql.COMMANDS = {...}

The commands this adapter sends.

CURSOR_READ_ONLY

sql.mysql.protocol.CURSOR_READ_ONLY = 1

Asks the server to keep the result open rather than send it all.

CURSOR_NONE

sql.mysql.protocol.CURSOR_NONE = 0

No cursor: the whole result comes back at once.


2026, Richard Ore and Zuri contributors

sql.mysql.results

import sql.mysql.results

sql does not re-export this module, so it is reached only by importing it directly.

Turning what the server answered into the shapes the shared contract asks for.

These live apart from the connection so the statement can use them too. A statement that reached for them through the connection, which already reaches for the statement, would be a circular import.

Functions

exec_result()

sql.mysql.results.exec_result(response: dict) -> dict

What a statement run for its effect reports.

A statement that returned rows still has to answer this, since execute() may be called on anything. Its row count is zero, which is true: it changed nothing.

Parameters

  • response (dict) — From Protocol.read_response().

Returns dict — { rows_affected, last_insert_id }

select_result()

sql.mysql.results.select_result(response: dict) -> dict

What a statement run for its rows reports.

A statement that returned none still answers with the same shape and an empty result, so a caller never has to tell the two apart.

Parameters

  • response (dict) — From Protocol.read_response().

Returns dict — { columns, rows }


2026, Richard Ore and Zuri contributors

sql.mysql.schema

import sql.mysql.schema

sql does not re-export this module, so it is reached only by importing it directly.

Introspection for MySQL, through information_schema.

A schema is a database

MySQL has no layer between the server and a database: what the SQL standard calls a schema is what MySQL calls a database, and information_schema.table_schema holds the database name. So the schema argument every method here takes names a database, and leaving it out means the one the connection is using.

Why nothing is grouped by the server

An index over several columns arrives as one row per column, and the obvious group_concat would gather them server side. It also truncates silently at group_concat_max_len, which is a kilobyte by default, so a wide index would come back missing columns with nothing to say it had. The rows are grouped here instead.

Classes

MySqlSchema

class sql.mysql.schema.MySqlSchema < SchemaAdapter

Answers introspection questions about a MySQL database.

Constructor

sql.mysql.schema.MySqlSchema(connection)

Parameters

  • connection (MySqlConnection)

MySqlSchema.tables()

sql.mysql.schema.MySqlSchema.tables(schema) -> list[string]

The tables the application created.

Parameters

  • schema (string|nil) — The database to look in, or nil for the one in use.

Returns list[string]

MySqlSchema.views()

sql.mysql.schema.MySqlSchema.views(schema) -> list[string]

The views.

Parameters

  • schema (string|nil)

Returns list[string]

MySqlSchema.columns()

sql.mysql.schema.MySqlSchema.columns(table: string, schema) -> list[dict]

The columns of table, in order.

The type is the one the table was declared with, varchar(64) rather than varchar, since on MySQL the width is frequently the part worth knowing.

Parameters

  • table (string)
  • schema (string|nil)

Returns list[dict]

MySqlSchema.primary_key_columns()

sql.mysql.schema.MySqlSchema.primary_key_columns(table: string, schema) -> list[string]

The primary key columns, in key order.

MySQL names every primary key PRIMARY, whatever the table.

Parameters

  • table (string)
  • schema (string|nil)

Returns list[string]

MySqlSchema.indexes()

sql.mysql.schema.MySqlSchema.indexes(table: string, schema) -> list[dict]

The indexes on table, each { name, columns, unique }.

The primary key is among them, under the name MySQL gives it.

Parameters

  • table (string)
  • schema (string|nil)

Returns list[dict]

MySqlSchema.foreign_keys()

sql.mysql.schema.MySqlSchema.foreign_keys(table: string, schema) -> list[dict]

The foreign keys declared on table.

Parameters

  • table (string)
  • schema (string|nil)

Returns list[dict]


2026, Richard Ore and Zuri contributors

sql.mysql.statement

import sql.mysql.statement

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.mysql.statement.* needs import sql.mysql.statement.

A statement compiled once on the server and run many times.

Preparing sends the statement, and the server answers with the types it will bind and the columns it will return. Every execution after that sends only the values, in the binary encoding, which is both smaller than the text one and free of any question of quoting.

Classes

MySqlStatement

class sql.mysql.MySqlStatement < DriverStatement

A prepared statement.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

sql.mysql.MySqlStatement(connection, prepared, sql)

Parameters

  • connection (MySqlConnection)
  • prepared (dict) — What the server said when it compiled the statement.
  • sql (string)

MySqlStatement.id()

sql.mysql.MySqlStatement.id() -> number

The statement’s id on the server.

Returns number

MySqlStatement.sql()

sql.mysql.MySqlStatement.sql() -> string

The statement as it was compiled.

Returns string

MySqlStatement.raw_columns()

sql.mysql.MySqlStatement.raw_columns() -> list[dict]

The columns as the server described them, with everything it said about each rather than the two fields the contract asks for.

Returns list[dict]

MySqlStatement.columns()

sql.mysql.MySqlStatement.columns()

MySqlStatement.parameter_count()

sql.mysql.MySqlStatement.parameter_count()

MySqlStatement.execute()

sql.mysql.MySqlStatement.execute(values: list)

MySqlStatement.select()

sql.mysql.MySqlStatement.select(values: list)

MySqlStatement.open_cursor()

sql.mysql.MySqlStatement.open_cursor(values: list, options: dict)

MySqlStatement.close()

sql.mysql.MySqlStatement.close()

Releases the statement on the server. Safe to call more than once.

MySqlStatement.is_closed()

sql.mysql.MySqlStatement.is_closed() -> bool

Whether this statement has been released.

Returns bool

MySqlStatement.to_string()

sql.mysql.MySqlStatement.to_string()

2026, Richard Ore and Zuri contributors

sql.mysql.types

import sql.mysql.types

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.mysql.types.* needs import sql.mysql.types.

What MySQL’s column types mean in Zuri, in both directions and in both of the protocol’s two encodings.

Two encodings for the same values

A statement sent as text answers with every value as text, however it is declared: an INT arrives as '42'. A prepared statement answers in the binary encoding instead, where an INT is four bytes. Both reach the same Zuri values, and the two paths are checked against each other rather than written to agree by hand: dates and times decode by way of the same text form whichever encoding carried them.

Where a type has no Zuri counterpart

BIT and GEOMETRY arrive as bytes, since neither has a shape Zuri could represent without inventing one. SET arrives as the comma separated text the server stores rather than as a list, because a list going back in would be written as JSON and the column would quietly stop matching.

Time zones

A DATETIME has no zone and a TIMESTAMP is converted to the session’s. The adapter puts the session in UTC when it connects, so both arrive as UTC and a date.Date written back converts from whatever offset it carries. Turning that off is what the time_zone connection option is for.

Constants

TYPES

sql.mysql.TYPES = {...}

The type byte the server puts on a column, and that a parameter carries back.

FLAGS

sql.mysql.FLAGS = {...}

What the server says about a column beyond its type.

BINARY_CHARSET

sql.mysql.types.BINARY_CHARSET = 63

The collation id that means a column holds bytes rather than text. It is the only thing separating BLOB from TEXT on the wire, and BINARY from CHAR.

UNSIGNED_FLAG

sql.mysql.types.UNSIGNED_FLAG = 128

The flag byte that goes with a parameter’s type to mark it unsigned.

Functions

is_binary()

sql.mysql.types.is_binary(column: dict) -> bool

Whether a column holds bytes rather than text.

Parameters

  • column (dict)

Returns bool

is_unsigned()

sql.mysql.types.is_unsigned(column: dict) -> bool

Whether a column’s values are unsigned.

Parameters

  • column (dict)

Returns bool

type_name_of()

sql.mysql.type_name_of(column: dict) -> string

The name of a column’s type, as MySQL itself would write it.

Parameters

  • column (dict)

Returns string

describe_columns()

sql.mysql.types.describe_columns(columns: list) -> list[dict]

Reduces the server’s column descriptors to the two fields the shared contract asks for.

Parameters

  • columns (list)

Returns list[dict]

decode_text()

sql.mysql.types.decode_text(raw, column: dict) -> any

Reads one value out of a text protocol row.

Parameters

  • raw (bytes|nil) — The value as the server wrote it, or nil for NULL.
  • column (dict)

Returns any

decode_binary()

sql.mysql.types.decode_binary(cursor, column: dict) -> any

Reads one value out of a binary protocol row.

The NULL bitmap is the caller’s business; this is only reached for a value that is present.

Parameters

  • cursor (Cursor) — Positioned at the value.
  • column (dict)

Returns any

parameter_type()

sql.mysql.types.parameter_type(value) -> dict

The type byte and flag a value is sent as.

write_parameter() has to agree with this exactly, since a statement sends every type first and then every value.

Parameters

  • value (any)

Returns dict — { type, unsigned }

Raises QueryError for a value with no MySQL representation.

write_parameter()

sql.mysql.types.write_parameter(writer, value)

Appends a value in the form its type byte promises.

Parameters

  • writer (Writer)
  • value (any)

2026, Richard Ore and Zuri contributors

sql.params

import sql.params

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.params.* needs import sql.params.

Rewriting a statement’s placeholders into whatever the active adapter expects.

Engines disagree about how a parameter is spelled. PostgreSQL wants $1, SQLite accepts ?1, others want a bare ?. Without a translation step, switching adapters would mean rewriting every statement in a program, which is most of what makes changing database an ordeal.

So sql defines one spelling and translates. Statements are written with ? for positional parameters or :name for named ones, and each adapter’s driver declares which form it needs.

translate('select * from t where a = ? and b = ?', [1, 2], NUMBERED)
# { sql: 'select * from t where a = $1 and b = $2', values: [1, 2] }

The scanner understands enough SQL to know when a ? or a : is not a placeholder: inside a string, a quoted identifier, a bracketed identifier, a comment, or a PostgreSQL dollar-quoted body, and in the :: cast operator. A statement that needs the literal character rather than a placeholder writes ??, which becomes a single ?. That matters for PostgreSQL, where ? is a JSON containment operator.

Nothing here escapes a value or builds SQL out of one. Values are bound by the engine, always.

Constants

QUESTION

sql.QUESTION = 'question'

A bare ? for every parameter, in order. MySQL and MariaDB.

NUMBERED

sql.NUMBERED = 'numbered'

$1, $2 and so on, numbered from one in binding order. PostgreSQL.

INDEXED

sql.INDEXED = 'indexed'

?1, ?2 and so on. SQLite, which accepts a bare ? too but is given explicit indices so a repeated named parameter can be bound once rather than once per mention.

STYLES

sql.params.STYLES = [...]

Every style a driver may declare.

Functions

placeholder()

sql.placeholder(style: string, position: number) -> string

The placeholder text for the parameter at position, counting from one.

Used by the statement builders in crud, which generate SQL and so need to spell placeholders themselves rather than translate them.

Parameters

  • style (string) — One of QUESTION, NUMBERED or INDEXED.
  • position (number) — The parameter’s 1 based binding position.

Returns string

Raises QueryError if style is not one this module defines.

tokenize()

sql.params.tokenize(sql: string) -> list[dict]

Splits sql into the pieces that matter, in order.

Each token is a dictionary with a kind:

  • 'text' is SQL to copy through, text holding it.
  • 'opaque' is a run nothing inside counts in: a string, a quoted or bracketed identifier, a comment, or a dollar-quoted body. Also copied through, but never looked inside.
  • 'question' is a positional placeholder.
  • 'named' is a named one, name holding the name without its colon.
  • 'separator' is a semicolon at statement level.

Everything in this module reads the same statement the same way because everything in this module goes through here.

Parameters

  • sql (string)

Returns list[dict]

scan()

sql.scan(sql: string) -> dict

What placeholders sql uses, without needing any values.

prepare() needs this: it translates a statement before it has anything to bind, so it has to know what the statement asks for.

Parameters

  • sql (string)

Returns dict — { positional, names }, positional the count of ? and names the :name names in first-mention order.

split_statements()

sql.split_statements(sql: string) -> list[string]

Splits a script into its statements.

Only semicolons outside strings, identifiers and comments separate, so a semicolon inside a string literal or a trigger body does not split the statement it belongs to. Empty pieces, which a trailing semicolon leaves behind, are dropped.

Parameters

  • sql (string)

Returns list[string]

translate()

sql.translate(sql: string, params, style: string) -> dict

Rewrites sql into style and puts the values in binding order.

Accepts two forms, and only one of them per statement:

  • Positional. ? in the statement, params a list. Values bind in the order they appear.
  • Named. :name in the statement, params a dictionary. A name used more than once binds once under NUMBERED and INDEXED, and once per mention under QUESTION, which has no way to refer back.

Mixing the two in one statement raises, as does supplying a list for a named statement or a dictionary for a positional one. Each of those is a mistake with a silent wrong answer available, so none of them is guessed at.

Parameters

  • sql (string) — The statement, written with ? or :name.
  • params (list|dict|nil) — The values to bind. nil means none.
  • style (string) — The active driver’s placeholder style.

Returns dict — { sql, values }, where values is always a list in binding order.

Raises QueryError if the two forms are mixed, if params is the wrong shape for the form used, if a positional statement and its list disagree about how many values there are, or if a named statement mentions a name the dictionary does not hold.


2026, Richard Ore and Zuri contributors

sql.pool

import sql.pool

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.pool.* needs import sql.pool.

Keeping connections open and lending them out.

Opening a connection is expensive: for PostgreSQL it is a TCP connection, a TLS handshake and an authentication exchange before the first statement runs. A server that opened one per request would spend most of its time connecting. A pool opens a few and lends them out.

var db = sql.pool('postgres://localhost/app', { max: 10 })

var posts = db.fetch_all('select * from posts')

Used that way the pool takes a connection, runs the statement and gives it back. Where several statements have to run on the same connection, which a transaction requires, acquire() holds one until it is released and with_connection() does the releasing.

Sizing

A pool wants to be as small as the work allows. Connections are not free at the other end either, and a pool larger than the database can usefully serve turns a queue in the application into a queue in the database, where it is harder to see.

SQLite wants a smaller pool than a server engine does. Readers are concurrent but writers are not, so past a handful of connections the writes queue on the database’s own lock rather than running. An in-memory SQLite database goes further: it belongs to the connection that opened it, so a pool of several would be several different empty databases. The pool clamps that case to one connection.

Isolates

A pool belongs to the isolate that made it. An isolate that needs database access makes its own.

Constants

DEFAULT_MAX

sql.pool.DEFAULT_MAX = 10

How many connections a pool opens at most, when it is not told.

DEFAULT_IDLE_TIMEOUT

sql.pool.DEFAULT_IDLE_TIMEOUT = 300000

How long an idle connection is kept before being closed, in milliseconds. Zero keeps them indefinitely.

DEFAULT_MAX_LIFETIME

sql.pool.DEFAULT_MAX_LIFETIME = 1800000

How long any connection is kept before being replaced, in milliseconds. Zero keeps them indefinitely.

Recycling connections on a schedule is what stops a pool holding one that a database restart, a failover or a firewall has quietly invalidated.

Classes

Pool

class sql.Pool

A pool of connections to one database.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

sql.Pool(driver, options, settings)

Parameters

  • driver (Driver)
  • options (dict) — The adapter’s connection options.
  • settings (dict) — The pool’s own settings: min, max, idle_timeout, max_lifetime, validate_on_acquire and on_connect.

Pool.size()

sql.Pool.size() -> number

How many connections exist, lent out or not.

Returns number

Pool.available()

sql.Pool.available() -> number

How many are available right now.

Returns number

Pool.in_use()

sql.Pool.in_use() -> number

How many are currently lent out.

Returns number

Pool.stats()

sql.Pool.stats() -> dict

A snapshot of the pool: { size, idle, in_use, max }.

Returns dict

Pool.acquire()

sql.Pool.acquire() -> Connection

Takes a connection, waiting for one if they are all busy.

The caller has to give it back. close() on the connection does that, which is what makes a pooled connection a drop-in for one that is not pooled, and with_connection() does it whatever happens.

There is no waiting when the pool is empty, and no timeout to set. A pool belongs to one isolate, so nothing else can release a connection while this call is running; blocking would never end. Running out means the pool is too small for the work or a connection is being held longer than it should be, and both of those are better reported than waited on.

Returns Connection

Raises PoolExhaustedError if every connection is in use.

Raises ClosedError if the pool has been closed.

Pool.release()

sql.Pool.release(connection)

Gives a connection back.

Called for you by Connection.close() on a pooled connection, so there is rarely a reason to call it directly.

A connection that has been closed for real, or that has outlived max_lifetime, is discarded rather than kept.

Parameters

  • connection (Connection)

Pool.with_connection()

sql.Pool.with_connection(body: function) -> any

Runs body with a connection, giving it back whatever happens.

db.with_connection(@(connection) {
  connection.transaction(@(tx) {
    tx.exec('...')
  })
})

Parameters

  • body (function) — Called with a Connection.

Returns any — Whatever body returned.

Pool.transaction()

sql.Pool.transaction(body: function, isolation) -> any

Runs body in a transaction on a connection of its own.

Parameters

  • body (function) — Called with a Transaction.
  • isolation (string|nil)

Returns any — Whatever body returned.

Pool.query()

sql.Pool.query(sql: string, params) -> ResultSet

Runs a query on a connection from the pool.

Parameters

  • sql (string)
  • params (list|dict|nil)

Returns ResultSet

Pool.exec()

sql.Pool.exec(sql: string, params) -> ExecResult

Runs a statement for its effect on a connection from the pool.

Parameters

  • sql (string)
  • params (list|dict|nil)

Returns ExecResult

Pool.fetch_one()

sql.Pool.fetch_one(sql: string, params) -> dict|nil

The first row of a query, or nil.

Parameters

  • sql (string)
  • params (list|dict|nil)

Returns dict|nil

Pool.fetch_all()

sql.Pool.fetch_all(sql: string, params) -> list[dict]

Every row of a query.

Parameters

  • sql (string)
  • params (list|dict|nil)

Returns list[dict]

Pool.fetch_value()

sql.Pool.fetch_value(sql: string, params, fallback) -> any

The first column of the first row.

Parameters

  • sql (string)
  • params (list|dict|nil)
  • fallback (any)

Returns any

Pool.close()

sql.Pool.close()

Closes every connection and refuses further use.

Connections currently lent out are closed as they come back.

Safe to call more than once.

Pool.is_closed()

sql.Pool.is_closed() -> bool

Whether this pool has been closed.

Returns bool

Pool.to_string()

sql.Pool.to_string()

2026, Richard Ore and Zuri contributors

sql.postgres

import sql.postgres

sql does not re-export this module, so it is reached only by importing it directly.

The PostgreSQL adapter.

Reach it through sql, which is what keeps a program portable:

import sql

var db = sql.open('postgres://alice:secret@localhost/app')

The module underneath is here for the parts of PostgreSQL that have no equivalent elsewhere: LISTEN and NOTIFY, the server’s own settings, and the type registry, which is how a program teaches the adapter about a type its database defines.

What this adapter speaks

Version 3 of the wire protocol, over TCP, optionally under TLS, or over a unix domain socket. A host beginning with a slash is a socket directory, as libpq spells it, and the socket inside it is named after the port. TLS is neither offered nor wanted there, since nothing sits in between, so sslmode is ignored.

Statements go through the extended protocol, which compiles them on the server and binds values rather than substituting them. Values travel in the server’s own binary form wherever this adapter has a codec for the type, and as text otherwise, so a type it has never heard of still arrives readable.

Submodules

ModuleReached asSummary
sql.postgres.authimport sql.postgres.authThe authentication exchanges a PostgreSQL server can ask for.
sql.postgres.connectionsql.postgres.connection.*A PostgreSQL connection, as the layer above the adapters sees one.
sql.postgres.cursorsql.postgres.cursor.*Reading a PostgreSQL result a batch at a time.
sql.postgres.driversql.postgres.driver.*Opening PostgreSQL connections: reading a connection string, establishing the socket, upgrading it to TLS…
sql.postgres.errorssql.postgres.errors.*Turning PostgreSQL’s SQLSTATE codes into the shared error classes.
sql.postgres.messagesimport sql.postgres.messagesFraming for the PostgreSQL wire protocol, version 3.
sql.postgres.notifysql.postgres.notify.*LISTEN and NOTIFY, PostgreSQL’s own publish and subscribe.
sql.postgres.protocolsql.postgres.protocol.*The PostgreSQL frontend/backend protocol, version 3.
sql.postgres.resultssql.postgres.results.*Reading what the server says about a result it has just produced.
sql.postgres.schemaimport sql.postgres.schemaIntrospection for PostgreSQL, through the standard information_schema views and the catalogue behind them.
sql.postgres.statementsql.postgres.statement.*A PostgreSQL statement compiled once and run many times.
sql.postgres.typessql.postgres.types.*Encoding and decoding PostgreSQL’s binary value formats.

Constants

DRIVER

sql.postgres.DRIVER

The driver instance sql registers for this engine.

Functions

connect()

sql.postgres.connect(dsn) -> PostgresConnection

Opens a PostgreSQL connection directly, without going through sql.

Anything meant to stay portable should use sql.open() instead.

Parameters

  • dsn (string|dict) — A connection string or options.

Returns PostgresConnection


2026, Richard Ore and Zuri contributors

sql.postgres.auth

import sql.postgres.auth

sql does not re-export this module, so it is reached only by importing it directly.

The authentication exchanges a PostgreSQL server can ask for.

Which one happens is the server’s choice, made from its pg_hba.conf. A modern server asks for SCRAM-SHA-256; older ones ask for MD5, which PostgreSQL itself now discourages; and a server set to trust asks for nothing at all.

Constants

SCRAM_SHA_256

sql.postgres.auth.SCRAM_SHA_256 = 'SCRAM-SHA-256'

The mechanism this adapter implements. Channel binding is not offered, so the server sees plain SCRAM-SHA-256 rather than the -PLUS variant.

Functions

md5_response()

sql.postgres.auth.md5_response(user: string, password: string, salt) -> string

Builds the response to an MD5 password request.

The scheme is md5(md5(password + user) + salt), hex encoded, with a literal md5 in front. It is weak and PostgreSQL says so; it is here because servers still ask for it.

Parameters

  • user (string)
  • password (string)
  • salt (bytes) — The four bytes the server sent.

Returns string

nonce()

sql.postgres.auth.nonce() -> string

A fresh SCRAM nonce.

It has to be unpredictable: the nonce is what stops a recorded exchange being replayed, so it comes from the system’s random source rather than from rand().

Returns string

parse_scram()

sql.postgres.auth.parse_scram(message: string) -> dict

Splits a SCRAM message into its key=value parts.

Parameters

  • message (string)

Returns dict

offers_scram()

sql.postgres.auth.offers_scram(payload) -> bool

Whether the server offered SCRAM-SHA-256 among its mechanisms.

Parameters

  • payload (bytes) — The mechanism list, zero-terminated names.

Returns bool

client_first()

sql.postgres.auth.client_first(client_nonce: string) -> dict

The client’s opening SCRAM message.

The username is left empty, which is what PostgreSQL expects: it takes the user from the startup message and ignores this one.

Parameters

  • client_nonce (string)

Returns dict — { bare, full }, the second with the channel binding header the exchange is signed over.

client_proof()

sql.postgres.auth.client_proof(password: string, client_nonce: string, client_first_bare: string, server_first: string) -> dict

Works out the client’s proof from the server’s challenge.

This is SCRAM as RFC 5802 defines it: the password is salted and iterated into a salted password, which yields a client key, whose hash the server already holds. Signing the whole exchange with that hash and returning the signature XORed with the client key proves the password without sending it.

Parameters

  • password (string)
  • client_nonce (string) — The nonce sent in the first message.
  • client_first_bare (string) — The first message without its header.
  • server_first (string) — The server’s reply, verbatim.

Returns dict — { message, server_signature }, the message to send and the signature the server’s own reply has to match.

Raises AuthenticationError if the server’s reply is malformed or its nonce does not extend the client’s.

verify_server()

sql.postgres.auth.verify_server(server_final: string, expected: string)

Checks the server’s closing message really is from a server that knows the password.

Skipping this would leave the exchange one-way, proving the client to the server but not the server to the client.

Parameters

  • server_final (string)
  • expected (string) — From client_proof().

Raises AuthenticationError if the signature is absent or wrong.


2026, Richard Ore and Zuri contributors

sql.postgres.connection

import sql.postgres.connection

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.postgres.connection.* needs import sql.postgres.connection.

A PostgreSQL connection, as the layer above the adapters sees one.

Statements are compiled once

Every statement run here goes through the extended protocol, which compiles it on the server and then executes the compiled form. The compiled form is kept, keyed by the statement’s text, so a statement run in a loop is compiled once. That is also what makes binary parameters and results possible: both need to know the types, and compiling is what reports them.

Cursors need a transaction

A PostgreSQL portal is destroyed when its transaction ends, and a statement outside a transaction is its own transaction. So a cursor opened outside one would be closed by the server before the first row was read. This opens a transaction for the cursor’s own sake in that case and commits it when the cursor closes.

Constants

CACHE_LIMIT

sql.postgres.connection.CACHE_LIMIT = 64

How many compiled statements a connection keeps.

Classes

PostgresConnection

class sql.postgres.PostgresConnection < DriverConnection

An open PostgreSQL connection.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

sql.postgres.PostgresConnection(driver, protocol, options)

Parameters

  • driver (Driver)
  • protocol (Protocol) — An authenticated protocol.
  • options (dict)

PostgresConnection.protocol()

sql.postgres.PostgresConnection.protocol() -> Protocol

The protocol underneath, for the PostgreSQL-only features that are not part of the shared contract.

Returns Protocol

PostgresConnection.parameters()

sql.postgres.PostgresConnection.parameters() -> dict

The settings the server reported when the connection opened.

Returns dict

PostgresConnection.schema_adapter()

sql.postgres.PostgresConnection.schema_adapter() -> PostgresSchema

This connection’s introspection.

Returns PostgresSchema

PostgresConnection.execute()

sql.postgres.PostgresConnection.execute(sql: string, values: list) -> dict

Runs a statement for its effect.

Parameters

  • sql (string)
  • values (list)

Returns dict — { rows_affected, last_insert_id }

PostgresConnection.select()

sql.postgres.PostgresConnection.select(sql: string, values: list) -> dict

Runs a statement and reads its whole result.

Parameters

  • sql (string)
  • values (list)

Returns dict — { columns, rows }

PostgresConnection.open_cursor()

sql.postgres.PostgresConnection.open_cursor(sql: string, values: list, options: dict) -> PostgresCursor

Runs a statement and hands back a cursor over its result.

Parameters

  • sql (string)
  • values (list)
  • options (dict) — { batch }, how many rows to fetch at a time. Defaults to 256.

Returns PostgresCursor

PostgresConnection.prepare()

sql.postgres.PostgresConnection.prepare(sql: string) -> PostgresStatement

Compiles a statement for repeated use.

Parameters

  • sql (string)

Returns PostgresStatement

PostgresConnection.begin()

sql.postgres.PostgresConnection.begin(isolation)

Opens a transaction, or takes a savepoint when one is open.

Parameters

  • isolation (string|nil)

PostgresConnection.commit()

sql.postgres.PostgresConnection.commit()

Commits, or releases the innermost savepoint.

PostgresConnection.rollback()

sql.postgres.PostgresConnection.rollback()

Rolls back, or undoes the innermost savepoint.

PostgresConnection.savepoint()

sql.postgres.PostgresConnection.savepoint(name: string)

PostgresConnection.release_savepoint()

sql.postgres.PostgresConnection.release_savepoint(name: string)

PostgresConnection.rollback_to()

sql.postgres.PostgresConnection.rollback_to(name: string)

PostgresConnection.in_transaction()

sql.postgres.PostgresConnection.in_transaction() -> bool

Whether a transaction is open.

The server reports this with every result, so this is what it last said rather than a guess kept on this side.

Returns bool

PostgresConnection.in_failed_transaction()

sql.postgres.PostgresConnection.in_failed_transaction() -> bool

Whether the open transaction has failed and can now only be rolled back, which is the state PostgreSQL puts one in after an error.

Returns bool

PostgresConnection.last_insert_id()

sql.postgres.PostgresConnection.last_insert_id()

Always raises: PostgreSQL does not report the id of an inserted row.

Raises NotSupportedError always.

PostgresConnection.ping()

sql.postgres.PostgresConnection.ping() -> bool

Checks the connection is usable.

Returns bool

PostgresConnection.server_version()

sql.postgres.PostgresConnection.server_version() -> string

The server’s version, such as '17.2'.

Returns string

PostgresConnection.listener()

sql.postgres.PostgresConnection.listener() -> Listener

A listener for LISTEN and NOTIFY on this connection.

var listener = db.native().listener()
listener.listen('jobs')

Returns Listener

PostgresConnection.notifications()

sql.postgres.PostgresConnection.notifications() -> list[dict]

Notifications received on this connection since the last call.

Returns list[dict] — Each { channel, payload, pid }.

PostgresConnection.close()

sql.postgres.PostgresConnection.close()

Closes the connection. Safe to call more than once.

PostgresConnection.is_closed()

sql.postgres.PostgresConnection.is_closed()

PostgresConnection.to_string()

sql.postgres.PostgresConnection.to_string()

2026, Richard Ore and Zuri contributors

sql.postgres.cursor

import sql.postgres.cursor

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.postgres.cursor.* needs import sql.postgres.cursor.

Reading a PostgreSQL result a batch at a time.

The server holds the result in a portal and sends as many rows as each Execute asks for. When more remain it says so, and the next Execute continues where the last stopped. Nothing but the current batch is ever held on this side.

Classes

PostgresCursor

class sql.postgres.PostgresCursor < DriverCursor

A portal being read.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

sql.postgres.PostgresCursor(connection, state)

Parameters

  • connection (PostgresConnection)
  • state (dict) — The portal’s name, its columns, the first batch of rows, whether more remain, and whether this cursor opened the transaction it is running in.

PostgresCursor.columns()

sql.postgres.PostgresCursor.columns()

PostgresCursor.next_row()

sql.postgres.PostgresCursor.next_row() -> list|nil

The next row as a list of values, or nil once the result is finished.

Returns list|nil

PostgresCursor.close()

sql.postgres.PostgresCursor.close()

Releases the portal, and the transaction if this cursor opened one. Safe to call more than once.

PostgresCursor.is_closed()

sql.postgres.PostgresCursor.is_closed() -> bool

Whether the cursor has been closed or read to the end.

Returns bool

PostgresCursor.to_string()

sql.postgres.PostgresCursor.to_string()

2026, Richard Ore and Zuri contributors

sql.postgres.driver

import sql.postgres.driver

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.postgres.driver.* needs import sql.postgres.driver.

Opening PostgreSQL connections: reading a connection string, establishing the socket, upgrading it to TLS when asked, and authenticating.

Constants

DEFAULT_PORT

sql.postgres.DEFAULT_PORT = 5432

The port a PostgreSQL server listens on unless told otherwise.

DEFAULT_TIMEOUT

sql.postgres.driver.DEFAULT_TIMEOUT = 30000

How long to wait for the socket and for each read, in milliseconds.

MAX_PARAMETERS

sql.postgres.MAX_PARAMETERS = 65535

Most parameters one statement can bind. The protocol counts them in a signed 16 bit field, so this is a hard limit rather than a configured one.

SSL_MODES

sql.postgres.SSL_MODES = [...]

What sslmode can be.

disable never uses TLS. prefer uses it when the server offers it and carries on without when it does not. require insists, and fails when the server refuses.

Classes

PostgresDriver

class sql.postgres.PostgresDriver < Driver

Opens PostgreSQL connections.

PostgresDriver.name()

sql.postgres.PostgresDriver.name()

PostgresDriver.schemes()

sql.postgres.PostgresDriver.schemes()

PostgresDriver.capabilities()

sql.postgres.PostgresDriver.capabilities() -> dict

What PostgreSQL can do.

last_insert_id is false, which is the one difference most visible from above: an insert that wants its id back gets a RETURNING clause instead, which Connection.insert() adds.

Returns dict

PostgresDriver.parse_dsn()

sql.postgres.PostgresDriver.parse_dsn(dsn) -> dict

Reads a connection string, in either of the two forms PostgreSQL’s own tools accept.

A URI:

postgres://alice:secret@db.example.com:5432/app?sslmode=require
postgresql://localhost/app

Or keywords:

host=localhost port=5432 dbname=app user=alice password=secret

Anything absent is filled in the way libpq fills it: the user defaults to postgres, the database to the user’s name, the host to localhost and the port to 5432.

Parameters

  • dsn (string|dict)

Returns dict

Raises ConnectionError if the string cannot be read.

PostgresDriver.connect()

sql.postgres.PostgresDriver.connect(options: dict) -> PostgresConnection

Opens the connection and authenticates.

Parameters

  • options (dict) — From parse_dsn().

Returns PostgresConnection


2026, Richard Ore and Zuri contributors

sql.postgres.errors

import sql.postgres.errors

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.postgres.errors.* needs import sql.postgres.errors.

Turning PostgreSQL’s SQLSTATE codes into the shared error classes.

Every error PostgreSQL reports carries a five character SQLSTATE. The first two characters are its class and the rest narrow it down, so 23505 is class 23, integrity constraint violation, specifically a unique violation. Mapping works on the specific code where one is worth telling apart and falls back to the class otherwise, which means a code this table has never seen still lands somewhere sensible.

Constants

SPECIFIC

sql.postgres.errors.SPECIFIC = {...}

Codes specific enough to name a class of their own.

CLASSES

sql.postgres.errors.CLASSES = {...}

SQLSTATE classes, for codes the table above does not name.

Functions

class_for()

sql.postgres.class_for(sqlstate) -> class

The class that best describes sqlstate.

Parameters

  • sqlstate (string|nil)

Returns class

raise_for()

sql.postgres.raise_for(report: dict, query)

Raises the right class for a server error report.

Parameters

  • report (dict) — The fields the server sent, as protocol.parse_error() returns them.
  • query (string|nil) — The statement that was running.

Raises SqlError always.


2026, Richard Ore and Zuri contributors

sql.postgres.messages

import sql.postgres.messages

sql does not re-export this module, so it is reached only by importing it directly.

Framing for the PostgreSQL wire protocol, version 3.

Every message is a one byte tag, a four byte big-endian length that counts itself, and then the body. The one exception is the startup message, which has no tag because the server has not yet been told which protocol version is in use.

Constants

AUTHENTICATION

sql.postgres.messages.AUTHENTICATION = 'R'

Messages the server sends, by tag.

BACKEND_KEY_DATA

sql.postgres.messages.BACKEND_KEY_DATA = 'K'

BIND_COMPLETE

sql.postgres.messages.BIND_COMPLETE = '2'

CLOSE_COMPLETE

sql.postgres.messages.CLOSE_COMPLETE = '3'

COMMAND_COMPLETE

sql.postgres.messages.COMMAND_COMPLETE = 'C'

COPY_IN_RESPONSE

sql.postgres.messages.COPY_IN_RESPONSE = 'G'

COPY_OUT_RESPONSE

sql.postgres.messages.COPY_OUT_RESPONSE = 'H'

COPY_DATA

sql.postgres.messages.COPY_DATA = 'd'

COPY_DONE

sql.postgres.messages.COPY_DONE = 'c'

DATA_ROW

sql.postgres.messages.DATA_ROW = 'D'

EMPTY_QUERY_RESPONSE

sql.postgres.messages.EMPTY_QUERY_RESPONSE = 'I'

ERROR_RESPONSE

sql.postgres.messages.ERROR_RESPONSE = 'E'

NO_DATA

sql.postgres.messages.NO_DATA = 'n'

NOTICE_RESPONSE

sql.postgres.messages.NOTICE_RESPONSE = 'N'

NOTIFICATION_RESPONSE

sql.postgres.messages.NOTIFICATION_RESPONSE = 'A'

PARAMETER_DESCRIPTION

sql.postgres.messages.PARAMETER_DESCRIPTION = 't'

PARAMETER_STATUS

sql.postgres.messages.PARAMETER_STATUS = 'S'

PARSE_COMPLETE

sql.postgres.messages.PARSE_COMPLETE = '1'

PORTAL_SUSPENDED

sql.postgres.messages.PORTAL_SUSPENDED = 's'

READY_FOR_QUERY

sql.postgres.messages.READY_FOR_QUERY = 'Z'

ROW_DESCRIPTION

sql.postgres.messages.ROW_DESCRIPTION = 'T'

AUTH_OK

sql.postgres.messages.AUTH_OK = 0

Authentication requests, by the number that follows the tag.

AUTH_CLEARTEXT

sql.postgres.messages.AUTH_CLEARTEXT = 3

AUTH_MD5

sql.postgres.messages.AUTH_MD5 = 5

AUTH_SASL

sql.postgres.messages.AUTH_SASL = 10

AUTH_SASL_CONTINUE

sql.postgres.messages.AUTH_SASL_CONTINUE = 11

AUTH_SASL_FINAL

sql.postgres.messages.AUTH_SASL_FINAL = 12

PROTOCOL_VERSION

sql.postgres.messages.PROTOCOL_VERSION = 196608

The protocol version this adapter speaks: 3.0, as a single number with the major version in the high half.

SSL_REQUEST

sql.postgres.messages.SSL_REQUEST = 80877103

The number the server recognises as a request to start TLS. It takes the place of a protocol version in a startup-shaped message.

Functions

read_cstring()

sql.postgres.messages.read_cstring(payload, at: number) -> dict

Splits a run of zero-terminated strings.

Several messages are built this way, including the error report, where each field is a one byte code followed by its text.

Parameters

  • payload (bytes)
  • at (number) — Where to start.

Returns dict — { value, next }

Classes

Writer

class sql.postgres.messages.Writer

Builds one outgoing message.

Bytes accumulate in a list and frame() puts the tag and length on the front, since the length cannot be known until the body is finished.

Writer.byte()

sql.postgres.messages.Writer.byte(value: number) -> Writer

Appends one byte.

Parameters

  • value (number)

Returns Writer — This writer, so calls chain.

Writer.int16()

sql.postgres.messages.Writer.int16(value: number) -> Writer

Appends a big-endian 16 bit integer.

Parameters

  • value (number)

Returns Writer

Writer.int32()

sql.postgres.messages.Writer.int32(value: number) -> Writer

Appends a big-endian 32 bit integer.

Parameters

  • value (number)

Returns Writer

Writer.cstring()

sql.postgres.messages.Writer.cstring(value: string) -> Writer

Appends a string and the zero byte that ends it.

Parameters

  • value (string)

Returns Writer

Writer.raw()

sql.postgres.messages.Writer.raw(values: list) -> Writer

Appends raw bytes.

Parameters

  • values (list)

Returns Writer

Writer.length()

sql.postgres.messages.Writer.length() -> number

How many bytes the body holds so far.

Returns number

Writer.frame()

sql.postgres.messages.Writer.frame(tag) -> bytes

The finished message, ready to send.

Parameters

  • tag (string|nil) — The message’s tag, or nil for a startup message, which has none.

Returns bytes

Reader

class sql.postgres.messages.Reader

Reads framed messages from a connected stream.

Works over a plain TcpStream or a TlsStream without caring which, since both read the same way.

Constructor

sql.postgres.messages.Reader(stream)

Parameters

  • stream (TcpStream|TlsStream) — An already-connected stream.

Reader.upgrade()

sql.postgres.messages.Reader.upgrade(stream)

Swaps the stream, for the point in the handshake where a plain connection becomes a TLS one.

Parameters

  • stream (TlsStream)

Reader.read_exact()

sql.postgres.messages.Reader.read_exact(count: number) -> bytes

Reads exactly count bytes, waiting for as many as it takes.

Parameters

  • count (number)

Returns bytes

Raises ConnectionError if the server closes first.

Reader.read_message()

sql.postgres.messages.Reader.read_message() -> dict

Reads the next message.

Returns dict — { tag, payload }


2026, Richard Ore and Zuri contributors

sql.postgres.notify

import sql.postgres.notify

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.postgres.notify.* needs import sql.postgres.notify.

LISTEN and NOTIFY, PostgreSQL’s own publish and subscribe.

A connection that has run LISTEN channel receives a message whenever anything runs NOTIFY channel, including from another connection or another process entirely. It is the one part of this adapter with no equivalent in SQLite, so it lives here rather than in the shared contract and is reached through db.native().

var listener = db.native().listener()

listener.listen('jobs')

while true {
  for message in listener.poll() {
    handle(message.payload)
  }
}

Notifications arrive between other messages, so a connection that is busy running statements collects them as it goes and poll() hands over whatever has accumulated. A connection doing nothing else has to ask, which is what wait() does.

Classes

Listener

class sql.postgres.Listener

Subscribes a connection to channels and collects what arrives.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

sql.postgres.Listener(connection)

Parameters

  • connection (PostgresConnection)

Listener.channels()

sql.postgres.Listener.channels() -> list[string]

The channels this listener is subscribed to.

Returns list[string]

Listener.listen()

sql.postgres.Listener.listen(channel: string)

Subscribes to channel.

Parameters

  • channel (string)

Listener.unlisten()

sql.postgres.Listener.unlisten(channel: string)

Unsubscribes from channel.

Parameters

  • channel (string)

Listener.notify()

sql.postgres.Listener.notify(channel: string, payload)

Sends a notification.

The payload is bound rather than pasted in, so it can hold anything including quotes.

Parameters

  • channel (string)
  • payload (string|nil)

Listener.poll()

sql.postgres.Listener.poll() -> list[dict]

Whatever has arrived since the last call, without waiting.

Returns list[dict] — Each { channel, payload, pid }.

Listener.wait()

sql.postgres.Listener.wait(rounds) -> list[dict]

Waits for at least one notification, checking the connection each time round.

There is no way to block on a socket and a database at once here, so this asks the server a trivial question on each pass, which is what carries any pending notification back with it.

Parameters

  • rounds (number|nil) — How many times to check before giving up. Unlimited when nil.

Returns list[dict] — The notifications received, which is empty only if rounds ran out.

Listener.close()

sql.postgres.Listener.close()

Unsubscribes from everything. Safe to call more than once.

Listener.to_string()

sql.postgres.Listener.to_string()

2026, Richard Ore and Zuri contributors

sql.postgres.protocol

import sql.postgres.protocol

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.postgres.protocol.* needs import sql.postgres.protocol.

The PostgreSQL frontend/backend protocol, version 3.

A connection is a sequence of messages in both directions. The client sends a request and then a Sync, and the server answers with some number of messages ending in ReadyForQuery. Everything between those two points belongs to the request, and nothing else may be sent until it arrives, which is what makes this a state machine rather than a set of independent calls.

Why statements are described before they are bound

A value can be sent and received either as text or in the server’s own binary form, chosen per parameter and per column, and the choice is made in the Bind message. To choose well, the types have to be known first, which is what Describe reports. So a statement is parsed and described once, and every execution after that reuses what was learned. A statement run in a loop pays for this once.


2026, Richard Ore and Zuri contributors

sql.postgres.results

import sql.postgres.results

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.postgres.results.* needs import sql.postgres.results.

Reading what the server says about a result it has just produced.

These live apart from the connection so the statement can use them too. A statement that reached for them through the connection, which already reaches for the statement, would be a circular import.

Functions

affected_rows()

sql.postgres.affected_rows(tag) -> number

The number of rows a command tag reports.

The tag is the command’s name followed by its counts. INSERT 0 3 carries an unused OID before the row count and everything else carries the count alone, so the last field is the one to read. A tag with no count at all, as CREATE TABLE has, means no rows.

Parameters

  • tag (string)

Returns number

type_name_of()

sql.postgres.type_name_of(oid: number) -> string

The name of the type an OID stands for, or the OID as text for one this adapter has no name for, which is what an extension’s type will be.

Parameters

  • oid (number)

Returns string

describe_columns()

sql.postgres.describe_columns(columns: list) -> list[dict]

Reduces the server’s column descriptors to the two fields the shared contract asks for.

Parameters

  • columns (list)

Returns list[dict]


2026, Richard Ore and Zuri contributors

sql.postgres.schema

import sql.postgres.schema

sql does not re-export this module, so it is reached only by importing it directly.

Introspection for PostgreSQL, through the standard information_schema views and the catalogue behind them.

information_schema covers tables, views and columns and is the SQL standard, so those queries would work on other engines too. Constraints and indexes are read from pg_catalog instead, because the standard views describe them in a form that loses which columns an index actually covers and in what order.

Where a caller names no schema, the server’s own search_path decides, which is what makes an unqualified table name mean the same here as it does in a query.

Classes

PostgresSchema

class sql.postgres.schema.PostgresSchema < SchemaAdapter

Answers introspection questions about a PostgreSQL database.

Constructor

sql.postgres.schema.PostgresSchema(connection)

Parameters

  • connection (PostgresConnection)

PostgresSchema.tables()

sql.postgres.schema.PostgresSchema.tables(schema) -> list[string]

The tables the application created.

Parameters

  • schema (string|nil) — The schema to look in, or nil for whatever search_path resolves to.

Returns list[string]

PostgresSchema.views()

sql.postgres.schema.PostgresSchema.views(schema) -> list[string]

The views.

Parameters

  • schema (string|nil)

Returns list[string]

PostgresSchema.columns()

sql.postgres.schema.PostgresSchema.columns(table: string, schema) -> list[dict]

The columns of table, in order.

Parameters

  • table (string)
  • schema (string|nil)

Returns list[dict]

PostgresSchema.primary_key_columns()

sql.postgres.schema.PostgresSchema.primary_key_columns(table: string, schema) -> list[string]

The primary key columns, in key order.

pg_index records the key’s own column order, which is not the table’s, so the order here is the one the index was declared with.

Parameters

  • table (string)
  • schema (string|nil)

Returns list[string]

PostgresSchema.indexes()

sql.postgres.schema.PostgresSchema.indexes(table: string, schema) -> list[dict]

The indexes on table, each { name, columns, unique }.

Parameters

  • table (string)
  • schema (string|nil)

Returns list[dict]

PostgresSchema.foreign_keys()

sql.postgres.schema.PostgresSchema.foreign_keys(table: string, schema) -> list[dict]

The foreign keys declared on table.

Parameters

  • table (string)
  • schema (string|nil)

Returns list[dict]


2026, Richard Ore and Zuri contributors

sql.postgres.statement

import sql.postgres.statement

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.postgres.statement.* needs import sql.postgres.statement.

A PostgreSQL statement compiled once and run many times.

Every statement this adapter runs is compiled on the server, so a prepared statement is not doing anything a plain query does not. The difference is ownership: this one has a name of its own and is released when the program says so, rather than living in the connection’s cache and being evicted when the cache fills.

Classes

PostgresStatement

class sql.postgres.PostgresStatement < DriverStatement

A compiled PostgreSQL statement.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

sql.postgres.PostgresStatement(connection, compiled)

Parameters

  • connection (PostgresConnection)
  • compiled (dict) — name, sql, parameters and columns as the server described them.

PostgresStatement.columns()

sql.postgres.PostgresStatement.columns()

PostgresStatement.parameter_count()

sql.postgres.PostgresStatement.parameter_count()

PostgresStatement.execute()

sql.postgres.PostgresStatement.execute(values: list) -> dict

Runs the statement for its effect.

Parameters

  • values (list)

Returns dict — { rows_affected, last_insert_id }

PostgresStatement.select()

sql.postgres.PostgresStatement.select(values: list) -> dict

Runs the statement and reads its whole result.

Parameters

  • values (list)

Returns dict — { columns, rows }

PostgresStatement.open_cursor()

sql.postgres.PostgresStatement.open_cursor(values: list, options: dict) -> PostgresCursor

Runs the statement and hands back a cursor over its result.

Parameters

  • values (list)
  • options (dict) — { batch }

Returns PostgresCursor

PostgresStatement.close()

sql.postgres.PostgresStatement.close()

Releases the statement on the server. Safe to call more than once.

PostgresStatement.is_closed()

sql.postgres.PostgresStatement.is_closed() -> bool

Whether this statement has been closed.

Returns bool

PostgresStatement.to_string()

sql.postgres.PostgresStatement.to_string()

2026, Richard Ore and Zuri contributors

sql.postgres.types

import sql.postgres.types

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.postgres.types.* needs import sql.postgres.types.

Encoding and decoding PostgreSQL’s binary value formats.

PostgreSQL will send a value either as text or as the server’s own binary representation, chosen per column. Binary is what this adapter asks for wherever it has a decoder, because it is exact: a float8 arrives as the same eight bytes the server holds rather than as a decimal rendering that has to be parsed back, and a numeric arrives as its actual digits rather than as text that a double would round.

Where there is no decoder, text is asked for instead and the value arrives as a string. That is the important half of the design: a type this adapter has never heard of, including one an extension added, still comes back as something readable rather than as bytes nobody can interpret.

Constants

OIDS

sql.postgres.OIDS = {...}

PostgreSQL’s built-in type OIDs, by name.

An array type has an OID of its own, listed here as <Type>Array.

ARRAY_ELEMENTS

sql.postgres.types.ARRAY_ELEMENTS = {...}

The element type each array OID holds.

EPOCH_OFFSET

sql.postgres.types.EPOCH_OFFSET = 946684800

Seconds between the Unix epoch and PostgreSQL’s, which is 2000-01-01 rather than 1970-01-01.

DEFAULT_REGISTRY

sql.postgres.DEFAULT_REGISTRY

The registry every connection uses unless given one of its own.

Functions

read_int16()

sql.postgres.types.read_int16(buffer, at: number) -> number

Reads a signed 16 bit big-endian integer.

Parameters

  • buffer (bytes)
  • at (number)

Returns number

read_uint16()

sql.postgres.types.read_uint16(buffer, at: number) -> number

Reads an unsigned 16 bit big-endian integer.

Parameters

  • buffer (bytes)
  • at (number)

Returns number

read_int32()

sql.postgres.types.read_int32(buffer, at: number) -> number

Reads a signed 32 bit big-endian integer.

Parameters

  • buffer (bytes)
  • at (number)

Returns number

read_uint32()

sql.postgres.types.read_uint32(buffer, at: number) -> number

Reads an unsigned 32 bit big-endian integer.

Parameters

  • buffer (bytes)
  • at (number)

Returns number

read_int64()

sql.postgres.types.read_int64(buffer, at: number) -> number

Reads a signed 64 bit big-endian integer.

Built from the two halves rather than shifted in one go, because the top bits of a 64 bit value do not survive a bitwise shift on a double.

Parameters

  • buffer (bytes)
  • at (number)

Returns number

write_int16()

sql.postgres.types.write_int16(value: number) -> list[number]

Writes a signed 16 bit big-endian integer.

Parameters

  • value (number)

Returns list[number]

write_int32()

sql.postgres.types.write_int32(value: number) -> list[number]

Writes a signed 32 bit big-endian integer.

Parameters

  • value (number)

Returns list[number]

write_int64()

sql.postgres.types.write_int64(value: number) -> list[number]

Writes a signed 64 bit big-endian integer.

Parameters

  • value (number)

Returns list[number]

text_of()

sql.postgres.types.text_of(buffer) -> string

A bytes slice as a UTF-8 string.

Parameters

  • buffer (bytes)

Returns string

decode_numeric()

sql.postgres.types.decode_numeric(buffer) -> Decimal|number

Decodes PostgreSQL’s numeric into an exact Decimal.

The wire form is a count of base 10000 digits, the position of the first of them relative to the decimal point, a sign, the number of places to display, and then the digits. Reassembling it as text and handing that to Decimal keeps every digit; going through a double anywhere in here would defeat the point of the type.

Parameters

  • buffer (bytes)

Returns Decimal|number

encode_numeric()

sql.postgres.types.encode_numeric(value) -> list[number]

Encodes a Decimal, a number or a bigint as numeric.

Parameters

  • value (Decimal|number|bigint)

Returns list[number]

date_from_micros()

sql.postgres.types.date_from_micros(micros: number) -> date.Date

Turns a count of microseconds since the PostgreSQL epoch into a date.Date in UTC.

Parameters

  • micros (number)

Returns date.Date

micros_from_date()

sql.postgres.types.micros_from_date(moment) -> number

The microseconds since the PostgreSQL epoch a date.Date stands for.

Parameters

  • moment (date.Date)

Returns number

decode_bool()

sql.postgres.types.decode_bool(buffer, _oid)

A decoder reads one column value from its binary form.

Each takes the value’s bytes and the OID it arrived under, and returns a Zuri value.

decode_int2()

sql.postgres.types.decode_int2(buffer, _oid)

decode_int4()

sql.postgres.types.decode_int4(buffer, _oid)

decode_int8()

sql.postgres.types.decode_int8(buffer, _oid)

decode_oid()

sql.postgres.types.decode_oid(buffer, _oid)

decode_float4()

sql.postgres.types.decode_float4(buffer, _oid)

decode_float8()

sql.postgres.types.decode_float8(buffer, _oid)

decode_text()

sql.postgres.types.decode_text(buffer, _oid)

decode_bytea()

sql.postgres.types.decode_bytea(buffer, _oid)

decode_json()

sql.postgres.types.decode_json(buffer, _oid)

decode_jsonb()

sql.postgres.types.decode_jsonb(buffer, _oid)

jsonb carries a version byte in front of the text, which every server so far sets to 1.

decode_uuid()

sql.postgres.types.decode_uuid(buffer, _oid)

decode_numeric_value()

sql.postgres.types.decode_numeric_value(buffer, _oid)

decode_date()

sql.postgres.types.decode_date(buffer, _oid)

date is a count of days from 2000-01-01, with no time part.

decode_time()

sql.postgres.types.decode_time(buffer, _oid)

time is microseconds since midnight, with no date part. It comes back as a dictionary rather than a date.Date, because a time of day is not a point in time and giving it an arbitrary date would be inventing information.

decode_timetz()

sql.postgres.types.decode_timetz(buffer, _oid)

timetz is a time followed by its offset in seconds west of UTC.

decode_timestamp()

sql.postgres.types.decode_timestamp(buffer, _oid)

Both timestamp and timestamptz are microseconds from the PostgreSQL epoch.

The server holds a timestamptz in UTC and converts on the way in and out, so what arrives is already UTC and is handed back as such. A timestamp has no zone at all; it is read as UTC because a date.Date has to say something, and UTC is the only answer that does not invent a zone the value never had.

decode_interval()

sql.postgres.types.decode_interval(buffer, _oid)

interval is a span rather than a point, so it comes back as its parts. Months and days are kept separate from the time because neither has a fixed length: a month is 28 to 31 days, and a day across a daylight saving change is not 24 hours.

decode_inet()

sql.postgres.types.decode_inet(buffer, _oid)

inet and cidr share a form: address family, prefix bits, a flag saying which of the two it is, then the address bytes.

decode_macaddr()

sql.postgres.types.decode_macaddr(buffer, _oid)

decode_point()

sql.postgres.types.decode_point(buffer, _oid)

decode_lseg()

sql.postgres.types.decode_lseg(buffer, _oid)

decode_box()

sql.postgres.types.decode_box(buffer, _oid)

decode_circle()

sql.postgres.types.decode_circle(buffer, _oid)

encode_bool()

sql.postgres.types.encode_bool(value)

An encoder turns a Zuri value into the bytes of its binary form.

encode_int2()

sql.postgres.types.encode_int2(value)

encode_int4()

sql.postgres.types.encode_int4(value)

encode_int8()

sql.postgres.types.encode_int8(value)

encode_float4()

sql.postgres.types.encode_float4(value)

encode_float8()

sql.postgres.types.encode_float8(value)

encode_text()

sql.postgres.types.encode_text(value)

encode_bytea()

sql.postgres.types.encode_bytea(value)

encode_json()

sql.postgres.types.encode_json(value)

encode_jsonb()

sql.postgres.types.encode_jsonb(value)

jsonb takes the same text behind the version byte the server expects.

encode_uuid()

sql.postgres.types.encode_uuid(value)

encode_date()

sql.postgres.types.encode_date(value)

encode_timestamp()

sql.postgres.types.encode_timestamp(value)

encode_array()

sql.postgres.types.encode_array(value, element_oid, registry)

Builds the binary form of an array.

One dimension only. PostgreSQL’s arrays can nest, but a nested Zuri list would have to be rectangular to become one, and quietly refusing a ragged list is worse than not offering it.

decode_array()

sql.postgres.types.decode_array(buffer, oid, registry)

Reads an array back, however many dimensions it has.

Classes

TypeRegistry

class sql.postgres.TypeRegistry

Which decoder and encoder each OID uses.

A registry can be extended, which is how a program teaches the adapter about a type its database defines that PostgreSQL does not ship:

registry.register(oid, decoder, encoder)

The important question a registry answers is knows(): the adapter asks for a column in binary only when the answer is yes, and asks for text otherwise, so an unregistered type arrives as a readable string rather than as bytes.

Constructor

sql.postgres.TypeRegistry()

TypeRegistry.register()

sql.postgres.TypeRegistry.register(oid: number, decoder, encoder)

Adds or replaces the codec for one OID.

Parameters

  • oid (number)
  • decoder (function) — Takes (bytes, oid) and returns a value.
  • encoder (function) — Takes a value and returns a list of bytes.

TypeRegistry.knows()

sql.postgres.TypeRegistry.knows(oid: number) -> bool

Whether this registry can read oid in binary.

Parameters

  • oid (number)

Returns bool

TypeRegistry.can_encode()

sql.postgres.TypeRegistry.can_encode(oid: number) -> bool

Whether this registry can write oid in binary.

A parameter whose type has no encoder is sent as text and left to the server to read, which it can do for every type it knows.

Parameters

  • oid (number)

Returns bool

TypeRegistry.decode()

sql.postgres.TypeRegistry.decode(buffer, oid: number) -> any

Reads a value from its binary form.

Parameters

  • buffer (bytes)
  • oid (number)

Returns any

TypeRegistry.encode()

sql.postgres.TypeRegistry.encode(value, oid: number) -> list[number]

Writes a value in binary form.

Parameters

  • value (any)
  • oid (number)

Returns list[number]

TypeRegistry.infer()

sql.postgres.TypeRegistry.infer(value) -> number

The OID that best carries value when the server has not said which type it expects.

Parameters

  • value (any)

Returns number


2026, Richard Ore and Zuri contributors

sql.result

import sql.result

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.result.* needs import sql.result.

What a statement hands back: the rows of a query, or the count of a statement that changed something.

Rows are dictionaries keyed by column name, which is what makes row.title read the way it does. Where that is not enough, because a join selected two columns of the same name or because position is what matters, columns and tuples() give the ordered view of the same result.

Classes

ResultSet

class sql.ResultSet

The rows a query returned, with the shape of the result beside them.

Iterating a result set walks its rows, so the common case needs nothing else:

for row in db.query('select id, title from posts') {
  echo '${row.id}: ${row.title}'
}
  • printable — has a @to_string(), so echo and print() show something useful
  • iterable — can be walked with for and iter

Fields

FieldTypeDescription
columnslist[dict]The result’s columns in order, each a dictionary with name and type.
rowslist[dict]The rows, each a dictionary keyed by column name.
commandThe engine’s description of what ran, such as 'SELECT 3', or nil for an engine that reports nothing.

Constructor

sql.ResultSet(columns, rows, command)

Parameters

  • columns (list) — Column descriptors.
  • rows (list) — Rows as lists of values, in column order.
  • command (string|nil) — The engine’s command tag.

ResultSet.length()

sql.ResultSet.length() -> number

How many rows the result holds.

Returns number

ResultSet.is_empty()

sql.ResultSet.is_empty() -> bool

Whether the result holds no rows at all.

Returns bool

ResultSet.first()

sql.ResultSet.first() -> dict|nil

The first row, or nil for an empty result.

Returns dict|nil

ResultSet.last()

sql.ResultSet.last() -> dict|nil

The last row, or nil for an empty result.

Returns dict|nil

ResultSet.column_names()

sql.ResultSet.column_names() -> list[string]

The names of the result’s columns, in order.

Returns list[string]

ResultSet.column()

sql.ResultSet.column(column) -> list

Every value of one column, in row order.

Parameters

  • column (string|number) — A column name, or its position.

Returns list

Raises QueryError if the result has no such column.

ResultSet.scalar()

sql.ResultSet.scalar(fallback) -> any

The first column of the first row, for a query written to return exactly one value.

var total = db.query('select count(*) from posts').scalar()

Parameters

  • fallback (any) — What to return for an empty result.

Returns any

ResultSet.tuples()

sql.ResultSet.tuples() -> list[list]

The rows as lists of values in column order, rather than as dictionaries.

This is the view that survives duplicate column names.

Returns list[list]

ResultSet.to_string()

sql.ResultSet.to_string()

ExecResult

class sql.ExecResult

What a statement that changed something reports.

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
rows_affectednumberHow many rows the statement inserted, updated or deleted.
last_insert_idThe id of the row just inserted, where the engine reports one.

Constructor

sql.ExecResult(rows_affected, last_insert_id)

Parameters

  • rows_affected (number)
  • last_insert_id (number|bigint|nil)

ExecResult.to_string()

sql.ExecResult.to_string()

2026, Richard Ore and Zuri contributors

sql.schema

import sql.schema

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.schema.* needs import sql.schema.

Asking a database what is in it.

Every engine can list its tables and describe its columns, and every engine does it differently: PostgreSQL has information_schema, SQLite has PRAGMA table_info. The queries have nothing in common, so each adapter writes its own and they all return the same shape.

for table in db.schema.tables() {
  echo table
}

for column in db.schema.columns('posts') {
  echo '${column.name} ${column.type}${column.nullable ? "" : " not null"}'
}

This is what makes insert() portable: on an engine with no last insert id, the insert needs a RETURNING clause, and the column to return is the table’s primary key, which is asked for here.

Functions

column_shape()

sql.column_shape() -> dict

The description of one column, as every adapter returns it.

KeyMeaning
nameThe column’s name.
typeThe engine’s own name for its type.
nullableWhether it accepts NULL.
default_valueIts default as SQL text, or nil for none.
primary_keyWhether it is part of the primary key.
positionIts position in the table, counting from one.

Returns dict

Classes

Schema

class sql.Schema

Introspection for one connection.

The methods here delegate to the adapter, which is where the queries that differ per engine live. An adapter that has not implemented introspection raises NotSupportedError rather than returning something empty, so “this engine cannot tell you” never looks like “there is nothing there”.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

sql.Schema(connection)

Parameters

  • connection (Connection)

Schema.tables()

sql.Schema.tables(schema) -> list[string]

The names of the tables in the database.

Tables the engine keeps for itself are left out, so what comes back is what the application created.

Parameters

  • schema (string|nil) — Which schema to look in, on an engine that has them. The default schema when nil.

Returns list[string]

Schema.views()

sql.Schema.views(schema) -> list[string]

The names of the views in the database.

Parameters

  • schema (string|nil)

Returns list[string]

Schema.has_table()

sql.Schema.has_table(table: string, schema) -> bool

Whether table exists.

Parameters

  • table (string)
  • schema (string|nil)

Returns bool

Schema.columns()

sql.Schema.columns(table: string, schema) -> list[dict]

The columns of table, in order.

Parameters

  • table (string)
  • schema (string|nil)

Returns list[dict] — Each as described by column_shape().

Schema.column_names()

sql.Schema.column_names(table: string, schema) -> list[string]

The names of table’s columns, in order.

Parameters

  • table (string)
  • schema (string|nil)

Returns list[string]

Schema.has_column()

sql.Schema.has_column(table: string, column: string, schema) -> bool

Whether table has a column called column.

Parameters

  • table (string)
  • column (string)
  • schema (string|nil)

Returns bool

Schema.primary_key()

sql.Schema.primary_key(table: string, schema) -> string|nil

The name of table’s primary key column, or nil.

Nil for a table with no primary key and for one whose primary key spans several columns, since neither has a single column to name. primary_key_columns() covers the composite case.

Parameters

  • table (string)
  • schema (string|nil)

Returns string|nil

Schema.primary_key_columns()

sql.Schema.primary_key_columns(table: string, schema) -> list[string]

The columns making up table’s primary key, in key order.

Parameters

  • table (string)
  • schema (string|nil)

Returns list[string]

Schema.indexes()

sql.Schema.indexes(table: string, schema) -> list[dict]

The indexes on table.

Each is { name, columns, unique }.

Parameters

  • table (string)
  • schema (string|nil)

Returns list[dict]

Schema.foreign_keys()

sql.Schema.foreign_keys(table: string, schema) -> list[dict]

The foreign keys declared on table.

Each is { columns, references_table, references_columns, on_delete, on_update }.

Parameters

  • table (string)
  • schema (string|nil)

Returns list[dict]

Schema.to_string()

sql.Schema.to_string()

2026, Richard Ore and Zuri contributors

sql.sqlite

import sql.sqlite

sql does not re-export this module, so it is reached only by importing it directly.

The SQLite adapter.

SQLite is a database that is a file. There is no server to run, no port to open and no user to create, which is what makes it the right choice for an application that ships with its data, a test suite that wants a real database per run, and anything that would otherwise have invented a file format of its own.

Reach it through sql, which is what makes the same program work against another engine later:

import sql

var db = sql.open('sqlite://./app.db')

The module underneath is here for the parts of SQLite that have no equivalent anywhere else: incremental blob access, online backup, functions and collations written in Zuri, and the hooks that report what the engine is doing.

Submodules

ModuleReached asSummary
sql.sqlite.backupsql.sqlite.backup.*Copying a live SQLite database without stopping it.
sql.sqlite.blobsql.sqlite.blob.*Reading and writing a single blob-valued cell a piece at a time.
sql.sqlite.connectionsql.sqlite.connection.*A SQLite connection, as the layer above the adapters sees one.
sql.sqlite.cursorsql.sqlite.cursor.*Reading a SQLite result one row at a time.
sql.sqlite.driversql.sqlite.driver.*Opening SQLite databases: reading a connection string, choosing the open flags, and putting a new connection…
sql.sqlite.errorssql.sqlite.errors.*Turning SQLite’s result codes into the shared error classes.
sql.sqlite.schemaimport sql.sqlite.schemaIntrospection for SQLite, which answers through pragmas rather than through catalogue tables.
sql.sqlite.statementsql.sqlite.statement.*A SQLite statement compiled once and run many times.
sql.sqlite.typessql.sqlite.types.*Reading SQLite values back as the types a column was declared to hold.

Constants

DRIVER

sql.sqlite.DRIVER

The driver instance sql registers for this engine.

A driver holds no state of its own, so one serves every connection.

Functions

connect()

sql.sqlite.connect(dsn) -> SqliteConnection

Opens a SQLite database directly, without going through sql.

Useful when a program has settled on SQLite and wants the adapter’s own extras without the indirection. Anything meant to stay portable should use sql.open() instead.

Parameters

  • dsn (string|dict) — A path, a sqlite: string, or options.

Returns SqliteConnection


2026, Richard Ore and Zuri contributors

sql.sqlite.backup

import sql.sqlite.backup

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.sqlite.backup.* needs import sql.sqlite.backup.

Copying a live SQLite database without stopping it.

Copying the file with the filesystem is only safe when nothing is writing, which for a running application is never guaranteed. SQLite’s backup interface copies page by page and notices when the source changes underneath, restarting the copy rather than producing a file that is half of one state and half of another.

db.native().backup_to('./snapshot.db')

That copies everything in one call. For a large database where the pause matters, Backup copies in steps so other work can happen in between.

Constants

DEFAULT_PAGES

sql.sqlite.backup.DEFAULT_PAGES = 256

How many pages a step copies when no size is given.

Classes

Backup

class sql.sqlite.Backup

A copy in progress between two connections.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

sql.sqlite.Backup(handle)

Parameters

  • ptr — handle The native backup handle.

Backup.step()

sql.sqlite.Backup.step(pages) -> string

Copies up to pages pages.

Parameters

  • pages (number|nil) — How many, or nil for DEFAULT_PAGES. A negative number copies everything remaining.

Returns string — 'done' when the copy is complete, 'ok' when more remains, and 'busy' or 'locked' when the source moved and the step should be tried again.

Backup.remaining()

sql.sqlite.Backup.remaining() -> number

Pages still to copy, as of the last step.

Returns number

Backup.total()

sql.sqlite.Backup.total() -> number

Pages in the source, as of the last step.

Returns number

Backup.progress()

sql.sqlite.Backup.progress() -> number

How much of the copy is done, from 0 to 1.

Returns number

Backup.run()

sql.sqlite.Backup.run(pages, attempts)

Runs the copy to completion, retrying the steps the source interrupts.

Parameters

  • pages (number|nil) — How many pages per step.
  • attempts (number|nil) — How many times to retry a busy step before giving up. 100 when nil.

Raises SqlError if the source stays busy for that many attempts.

Backup.close()

sql.sqlite.Backup.close()

Ends the copy. Safe to call more than once, and legitimate on an unfinished copy, which abandons it and leaves the destination as it was.

Backup.is_closed()

sql.sqlite.Backup.is_closed() -> bool

Whether this backup has finished or been abandoned.

Returns bool

Backup.to_string()

sql.sqlite.Backup.to_string()

2026, Richard Ore and Zuri contributors

sql.sqlite.blob

import sql.sqlite.blob

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.sqlite.blob.* needs import sql.sqlite.blob.

Reading and writing a single blob-valued cell a piece at a time.

A large value stored in a database is usually read with a select, which means the whole of it arrives at once. For a hundred megabyte file that is a hundred megabytes of memory to move a copy from one place to another. A blob handle opens one cell and reads or writes windows of it, so the memory needed is the size of the window.

var blob = db.native().blob('main', 'files', 'content', id, false)

var at = 0
while at < blob.length() {
  var chunk = blob.read(at, 65536.min(blob.length() - at))
  out.write(chunk)
  at += chunk.length()
}

blob.close()

The cell has to exist and already be the right size: SQLite cannot grow a blob through this interface. The usual approach is to insert zeroblob(n) for the final size and then write into it.

Classes

Blob

class sql.sqlite.Blob

An open blob handle.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

sql.sqlite.Blob(handle, writable)

Parameters

  • ptr — handle The native blob handle.
  • writable (bool) — Whether it was opened for writing.

Blob.length()

sql.sqlite.Blob.length() -> number

How many bytes the blob holds. Fixed for the handle’s lifetime.

Returns number

Blob.is_writable()

sql.sqlite.Blob.is_writable() -> bool

Whether this handle may write.

Returns bool

Blob.read()

sql.sqlite.Blob.read(offset: number, length: number) -> bytes

Reads length bytes starting at offset.

Parameters

  • offset (number)
  • length (number)

Returns bytes

Raises SqlError if the window runs past the end of the blob.

Blob.read_all()

sql.sqlite.Blob.read_all() -> bytes

Reads the whole blob. Defeats the point of a blob handle, and is here for the case where one turns out to be small.

Returns bytes

Blob.write()

sql.sqlite.Blob.write(data, offset: number)

Writes data starting at offset.

Parameters

  • data (bytes)
  • offset (number)

Raises SqlError if the handle is read-only or the write would run past the end.

Blob.reopen()

sql.sqlite.Blob.reopen(rowid: number)

Points this handle at the same column of a different row.

Cheaper than closing and opening again, which is why it exists.

Parameters

  • rowid (number)

Blob.close()

sql.sqlite.Blob.close()

Releases the handle. Safe to call more than once.

Blob.is_closed()

sql.sqlite.Blob.is_closed() -> bool

Whether this handle has been closed.

Returns bool

Blob.to_string()

sql.sqlite.Blob.to_string()

2026, Richard Ore and Zuri contributors

sql.sqlite.connection

import sql.sqlite.connection

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.sqlite.connection.* needs import sql.sqlite.connection.

A SQLite connection, as the layer above the adapters sees one.

SQLite has no server, so a connection here is an open database file (or an in-memory database) and the handle onto it. That difference shows through in a few places, and this is where each of them is dealt with.

Transactions

SQLite has one transaction per connection and no nesting, so nested transactions are savepoints, as they are for every other adapter. Its isolation levels are not levels but locking modes: BEGIN DEFERRED takes no lock until the first statement, BEGIN IMMEDIATE takes the write lock at once. Serializable maps onto IMMEDIATE, which is what actually delivers it, and the levels SQLite cannot approximate raise rather than being quietly accepted.

One writer

A SQLite database takes one writer at a time. Where another engine would queue, SQLite returns SQLITE_BUSY, which this adapter turns into a TimeoutError once the busy timeout expires. Setting a busy timeout is what turns a burst of contention into a wait rather than an error, so connections open with one already set.

Constants

CACHE_LIMIT

sql.sqlite.connection.CACHE_LIMIT = 64

How many compiled statements a connection keeps for reuse.

Statements run through execute() and select() are cached by their text, since those compile, run and reset within the one call and are never handed out. A cursor’s statement is never cached: it stays live between calls, and lending the same handle to two readers would have them stepping each other’s result.

Classes

SqliteConnection

class sql.sqlite.SqliteConnection < DriverConnection

An open SQLite database.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

sql.sqlite.SqliteConnection(driver, handle, options)

Parameters

  • driver (Driver) — The driver that opened this.
  • ptr — handle The native connection.
  • options (dict) — The options it was opened with.

SqliteConnection.handle()

sql.sqlite.SqliteConnection.handle()

The native handle, for the parts of SQLite that are not part of the shared contract: blobs, backups, user-defined functions and hooks.

Returns — ptr

SqliteConnection.schema_adapter()

sql.sqlite.SqliteConnection.schema_adapter() -> SqliteSchema

This connection’s introspection, built once and reused.

Returns SqliteSchema

SqliteConnection.execute()

sql.sqlite.SqliteConnection.execute(sql: string, values: list) -> dict

Runs a statement for its effect.

Parameters

  • sql (string)
  • values (list)

Returns dict — { rows_affected, last_insert_id }

SqliteConnection.select()

sql.sqlite.SqliteConnection.select(sql: string, values: list) -> dict

Runs a statement and reads its whole result.

Parameters

  • sql (string)
  • values (list)

Returns dict — { columns, rows }

SqliteConnection.open_cursor()

sql.sqlite.SqliteConnection.open_cursor(sql: string, values: list, options: dict) -> SqliteCursor

Runs a statement and hands back a cursor over its result.

The statement is compiled fresh rather than taken from the cache, because the cursor keeps it for as long as it is reading.

Parameters

  • sql (string)
  • values (list)
  • options (dict) — Unused; SQLite produces rows on demand already, so there is no batch size to choose.

Returns SqliteCursor

SqliteConnection.prepare()

sql.sqlite.SqliteConnection.prepare(sql: string) -> SqliteStatement

Compiles a statement for repeated use.

Parameters

  • sql (string)

Returns SqliteStatement

SqliteConnection.begin()

sql.sqlite.SqliteConnection.begin(isolation)

Opens a transaction, or takes a savepoint when one is already open.

Parameters

  • isolation (string|nil)

Raises NotSupportedError for a level SQLite has no analogue for.

SqliteConnection.commit()

sql.sqlite.SqliteConnection.commit()

Commits the transaction, or releases the innermost savepoint.

SqliteConnection.rollback()

sql.sqlite.SqliteConnection.rollback()

Rolls the transaction back, or undoes the innermost savepoint.

SqliteConnection.savepoint()

sql.sqlite.SqliteConnection.savepoint(name: string)

SqliteConnection.release_savepoint()

sql.sqlite.SqliteConnection.release_savepoint(name: string)

SqliteConnection.rollback_to()

sql.sqlite.SqliteConnection.rollback_to(name: string)

SqliteConnection.in_transaction()

sql.sqlite.SqliteConnection.in_transaction() -> bool

Whether a transaction is open.

Returns bool

SqliteConnection.depth()

sql.sqlite.SqliteConnection.depth() -> number

How deeply transactions are currently nested: zero outside one, one inside the outermost, and one more per savepoint.

Returns number

SqliteConnection.changes()

sql.sqlite.SqliteConnection.changes() -> number

How many rows the last statement changed.

Returns number

SqliteConnection.last_insert_id()

sql.sqlite.SqliteConnection.last_insert_id() -> number|bigint|nil

The row id of the last insert on this connection, or nil if there has not been one.

Returns number|bigint|nil

SqliteConnection.ping()

sql.sqlite.SqliteConnection.ping() -> bool

Checks the connection is usable.

Returns bool

SqliteConnection.server_version()

sql.sqlite.SqliteConnection.server_version() -> string

SQLite’s version, such as '3.53.2'.

Returns string

SqliteConnection.interrupt()

sql.sqlite.SqliteConnection.interrupt()

Asks a running statement on this connection to stop.

The statement fails rather than being killed, and the connection stays usable. Meant for a long query on a connection another isolate is waiting on.

SqliteConnection.set_busy_timeout()

sql.sqlite.SqliteConnection.set_busy_timeout(milliseconds: number)

How long a statement waits for another writer before giving up.

Parameters

  • milliseconds (number) — Zero waits not at all.

SqliteConnection.blob()

sql.sqlite.SqliteConnection.blob(database: string, table: string, column: string, rowid: number, writable) -> Blob

Opens one blob-valued cell for piecewise reading and writing.

Parameters

  • database (string) — The attached database, usually 'main'.
  • table (string)
  • column (string)
  • rowid (number)
  • writable (bool|nil) — Read-only when nil or false.

Returns Blob

SqliteConnection.backup()

sql.sqlite.SqliteConnection.backup(destination, options) -> Backup

Starts a copy of this database into destination.

Parameters

  • destination (SqliteConnection) — An open connection to copy into, which must not be this one.
  • options (dict|nil) — source and target name which attached database on each side; both default to 'main'.

Returns Backup

SqliteConnection.backup_to()

sql.sqlite.SqliteConnection.backup_to(path: string, options)

Copies this database to a file, all in one call.

The file is created if it is not there. An existing one is copied over rather than appended to, so a snapshot taken repeatedly to the same path replaces itself.

db.native().backup_to('./snapshot.db', nil)

For a copy that is not a file, or one taken in steps, open the destination yourself and use backup().

Parameters

  • path (string)
  • options (dict|nil) — As backup() takes.

SqliteConnection.create_function()

sql.sqlite.SqliteConnection.create_function(name: string, arity: number, body: function, options)

Adds a function to this connection, written in Zuri.

db.native().create_function('initials', 1, @(name) {
  return ''.join(name.split(' ').map(@(part) => part[0, 1]))
})

db.query('select initials(name) from authors')

The function runs inside the engine, once per row, so it can be used anywhere an expression can. It must not touch the connection it was registered on; SQLite is in the middle of a statement when it calls, and reentering would deadlock.

Parameters

  • name (string)
  • arity (number) — How many arguments, or -1 for any number.
  • body (function)
  • options (dict|nil) — deterministic says the result depends only on the arguments, which lets SQLite use the function in an index and hoist it out of loops. True unless said otherwise.

SqliteConnection.create_aggregate()

sql.sqlite.SqliteConnection.create_aggregate(name: string, arity: number, step: function, finish: function)

Adds an aggregate function, written as a fold.

step is called once per row with the accumulator so far followed by the row’s arguments, and returns the next accumulator. finish is called once per group with the last accumulator and returns the group’s value. The accumulator starts as nil, which is also what finish sees for a group with no rows in it.

db.native().create_aggregate('longest', 1,
  @(longest, word) {
    if longest == nil or word.length() > longest.length() {
      return word
    }

    return longest
  },
  @(longest) => longest
)

Parameters

  • name (string)
  • arity (number)
  • step (function)
  • finish (function)

SqliteConnection.create_collation()

sql.sqlite.SqliteConnection.create_collation(name: string, compare: function)

Adds a collation, which is an ordering for text.

The callable is passed two strings and returns a negative number, zero or a positive number, the same shape a sort comparator takes. A statement reaches it with order by column collate name.

Parameters

  • name (string)
  • compare (function)

SqliteConnection.delete_function()

sql.sqlite.SqliteConnection.delete_function(name: string, arity: number)

Removes a function or aggregate registered under this name and arity.

Parameters

  • name (string)
  • arity (number)

SqliteConnection.on_change()

sql.sqlite.SqliteConnection.on_change(handler)

Called after each row an INSERT, UPDATE or DELETE changes, with the operation name, the database, the table and the row id.

Rows a trigger or a foreign key action changes are reported too. Each change is reported as it is made, so one a transaction later rolls back has still been reported. A DELETE with no WHERE clause empties the table without visiting its rows, and a WITHOUT ROWID table has no rowid to report, so neither is reported at all.

Parameters

  • handler (function|nil) — Nil clears it.

SqliteConnection.on_commit()

sql.sqlite.SqliteConnection.on_commit(handler)

Called just before each commit. Returning false turns the commit into a rollback.

Parameters

  • handler (function|nil) — Nil clears it.

SqliteConnection.on_rollback()

sql.sqlite.SqliteConnection.on_rollback(handler)

Called whenever a transaction rolls back, however it was caused.

Parameters

  • handler (function|nil) — Nil clears it.

SqliteConnection.set_authorizer()

sql.sqlite.SqliteConnection.set_authorizer(handler)

Consulted for every action a statement wants to take, as the statement is compiled rather than as it runs.

The callable is passed the action code and the four strings SQLite supplies about it, and answers 'allow', 'deny' or 'ignore'. Anything else counts as denial, so a handler that falls off the end fails closed.

Parameters

  • handler (function|nil) — Nil clears it.

SqliteConnection.on_progress()

sql.sqlite.SqliteConnection.on_progress(instructions: number, handler)

Called every instructions steps of a long statement. Returning false interrupts it.

Parameters

  • instructions (number)
  • handler (function|nil) — Nil clears it.

SqliteConnection.close()

sql.sqlite.SqliteConnection.close()

Closes the connection, releasing every statement it cached.

Safe to call more than once.

SqliteConnection.is_closed()

sql.sqlite.SqliteConnection.is_closed()

SqliteConnection.to_string()

sql.sqlite.SqliteConnection.to_string()

2026, Richard Ore and Zuri contributors

sql.sqlite.cursor

import sql.sqlite.cursor

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.sqlite.cursor.* needs import sql.sqlite.cursor.

Reading a SQLite result one row at a time.

SQLite computes rows as they are asked for rather than building the whole result first, so a cursor here is the engine’s own behaviour exposed rather than a batching scheme layered on top. A query over a large table holds one row at a time however long the table is.

The statement a cursor is stepping belongs to it until it closes. That is why the connection compiles a separate statement for each cursor rather than lending one from its cache.

Classes

SqliteCursor

class sql.sqlite.SqliteCursor < DriverCursor

A result being stepped.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

sql.sqlite.SqliteCursor(statement, columns, conversions, query, owned)

Parameters

  • ptr — statement A prepared native statement, already bound.
  • columns (list) — Column descriptors.
  • conversions (list) — Per column conversions.
  • query (string) — The statement, for error messages.
  • owned (bool) — Whether closing the cursor finalizes the statement, which it does for a cursor the connection compiled and does not for one belonging to a prepared statement the caller still holds.

SqliteCursor.columns()

sql.sqlite.SqliteCursor.columns()

SqliteCursor.next_row()

sql.sqlite.SqliteCursor.next_row() -> list|nil

The next row, or nil once the result is finished.

Reaching the end closes the cursor, so a loop that runs to exhaustion releases the statement without having to remember to.

Returns list|nil

SqliteCursor.close()

sql.sqlite.SqliteCursor.close()

Releases the cursor. Safe to call more than once, and safe to call on a cursor that already ran to the end.

SqliteCursor.is_closed()

sql.sqlite.SqliteCursor.is_closed() -> bool

Whether this cursor has been closed or run to exhaustion.

Returns bool

SqliteCursor.to_string()

sql.sqlite.SqliteCursor.to_string()

2026, Richard Ore and Zuri contributors

sql.sqlite.driver

import sql.sqlite.driver

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.sqlite.driver.* needs import sql.sqlite.driver.

Opening SQLite databases: reading a connection string, choosing the open flags, and putting a new connection into a sane state.

Constants

MEMORY

sql.sqlite.MEMORY = ':memory:'

The path that means a private database held in memory, which is discarded when the connection closes.

OPEN_READONLY

sql.sqlite.driver.OPEN_READONLY = 1

OPEN_READWRITE

sql.sqlite.driver.OPEN_READWRITE = 2

OPEN_CREATE

sql.sqlite.driver.OPEN_CREATE = 4

OPEN_URI

sql.sqlite.driver.OPEN_URI = 64

OPEN_MEMORY

sql.sqlite.driver.OPEN_MEMORY = 128

OPEN_NOMUTEX

sql.sqlite.driver.OPEN_NOMUTEX = 32768

OPEN_FULLMUTEX

sql.sqlite.driver.OPEN_FULLMUTEX = 65536

OPEN_SHAREDCACHE

sql.sqlite.driver.OPEN_SHAREDCACHE = 131072

OPEN_PRIVATECACHE

sql.sqlite.driver.OPEN_PRIVATECACHE = 262144

DEFAULT_BUSY_TIMEOUT

sql.sqlite.DEFAULT_BUSY_TIMEOUT = 5000

How long a statement waits for another writer by default.

SQLite’s own default is not to wait at all, which turns any contention into an immediate error. Five seconds is long enough to ride out the short bursts that make up most contention and short enough that a genuine deadlock still surfaces.

MAX_PARAMETERS

sql.sqlite.MAX_PARAMETERS = 32766

Most parameters one statement can bind, which is SQLite’s own limit.

Classes

SqliteDriver

class sql.sqlite.SqliteDriver < Driver

Opens SQLite databases.

SqliteDriver.name()

sql.sqlite.SqliteDriver.name()

SqliteDriver.schemes()

sql.sqlite.SqliteDriver.schemes()

SqliteDriver.capabilities()

sql.sqlite.SqliteDriver.capabilities() -> dict

What SQLite can do.

Two of these are worth reading twice. concurrent_writers is false: a SQLite database takes one writer at a time whatever the journal mode, and a pool over it serialises writes rather than parallelising them. json is false because SQLite has JSON functions but no JSON type, so a list or dictionary is stored as text and comes back as text unless the column says otherwise.

Returns dict

SqliteDriver.parse_dsn()

sql.sqlite.SqliteDriver.parse_dsn(dsn) -> dict

Reads a connection string, or passes an options dictionary through with its defaults filled in.

These all name the same database:

./app.db
sqlite:app.db
sqlite://./app.db
sqlite:///absolute/path/app.db

and these all mean a private in-memory database:

:memory:
sqlite::memory:
sqlite://:memory:

A file: URI is handed to SQLite’s own URI handling, which is how the options SQLite spells itself are reached:

file:app.db?mode=ro&cache=shared

Anything after ? on a non-URI form is read as this adapter’s own options instead. The recognised keys are mode (ro, rw, rwc or memory), busy_timeout in milliseconds, foreign_keys, journal_mode and cache (shared or private).

Parameters

  • dsn (string|dict)

Returns dict

Raises ConnectionError if the string names no database.

SqliteDriver.connect()

sql.sqlite.SqliteDriver.connect(options: dict) -> SqliteConnection

Opens the database and puts it into the state the options ask for.

Parameters

  • options (dict) — From parse_dsn().

Returns SqliteConnection


2026, Richard Ore and Zuri contributors

sql.sqlite.errors

import sql.sqlite.errors

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.sqlite.errors.* needs import sql.sqlite.errors.

Turning SQLite’s result codes into the shared error classes.

SQLite reports failures as small integers. The native layer puts the extended code in front of every message it raises, in the form [2067] UNIQUE constraint failed: t.a, and this module reads it back out and picks the class that says the same thing in the vocabulary every adapter shares.

Extended codes rather than primary ones, because the distinctions worth acting on live there: every constraint failure is the primary code 19, and only the extended code says whether it was a unique index, a foreign key or a NOT NULL column.

Functions

primary()

sql.sqlite.errors.primary(code: number) -> number

An extended code’s primary code, which is its low byte.

Parameters

  • code (number)

Returns number

class_for()

sql.sqlite.class_for(code: number) -> class

The class that best describes code.

Parameters

  • code (number) — An extended result code.

Returns class

split_message()

sql.sqlite.split_message(message: string) -> dict

Splits the [code] message form the native layer raises.

A message without the prefix comes back with a nil code and its text unchanged, so an error raised anywhere other than the native layer still passes through this cleanly.

Parameters

  • message (string)

Returns dict — { code, text }

reraise()

sql.sqlite.reraise(error, query)

Re-raises an error from the native layer as the right class.

Anything that is already a SqlError passes through untouched, so wrapping a call that itself went through here does not bury the specific class under a general one.

Parameters

  • error (Error) — The error the native layer raised.
  • query (string|nil) — The statement that was running, for context.

Raises SqlError always.


2026, Richard Ore and Zuri contributors

sql.sqlite.schema

import sql.sqlite.schema

sql does not re-export this module, so it is reached only by importing it directly.

Introspection for SQLite, which answers through pragmas rather than through catalogue tables.

PRAGMA table_info, index_list, index_info and foreign_key_list are functions that return result sets, so each of these is an ordinary query. The one catalogue table SQLite does have, sqlite_master, is where the list of tables comes from.

SQLite has no schemas. It has attached databases, which the schema argument here names: nil and 'main' both mean the database the connection opened, and 'temp' holds temporary tables.

Classes

SqliteSchema

class sql.sqlite.schema.SqliteSchema < SchemaAdapter

Answers introspection questions about a SQLite database.

Constructor

sql.sqlite.schema.SqliteSchema(connection)

Parameters

  • connection (SqliteConnection)

SqliteSchema.tables()

sql.sqlite.schema.SqliteSchema.tables(schema) -> list[string]

The tables the application created.

SQLite keeps its own bookkeeping in tables whose names begin with sqlite_, so those are left out.

Parameters

  • schema (string|nil) — An attached database name.

Returns list[string]

SqliteSchema.views()

sql.sqlite.schema.SqliteSchema.views(schema) -> list[string]

The views.

Parameters

  • schema (string|nil)

Returns list[string]

SqliteSchema.columns()

sql.sqlite.schema.SqliteSchema.columns(table: string, schema) -> list[dict]

The columns of table, in declaration order.

Parameters

  • table (string)
  • schema (string|nil)

Returns list[dict]

SqliteSchema.primary_key_columns()

sql.sqlite.schema.SqliteSchema.primary_key_columns(table: string, schema) -> list[string]

The primary key columns, in key order.

table_info reports each key column’s position within the key, so a composite key comes back in the order it was declared rather than in column order.

Parameters

  • table (string)
  • schema (string|nil)

Returns list[string]

SqliteSchema.indexes()

sql.sqlite.schema.SqliteSchema.indexes(table: string, schema) -> list[dict]

The indexes on table.

The implicit index behind a column’s UNIQUE or PRIMARY KEY declaration is included, as SQLite reports it, since it is a real index and constrains the table the same way a named one does.

Parameters

  • table (string)
  • schema (string|nil)

Returns list[dict]

SqliteSchema.foreign_keys()

sql.sqlite.schema.SqliteSchema.foreign_keys(table: string, schema) -> list[dict]

The foreign keys declared on table.

A key spanning several columns is reported by SQLite as one row per column sharing an id, so the rows are gathered back into one entry per key.

Parameters

  • table (string)
  • schema (string|nil)

Returns list[dict]


2026, Richard Ore and Zuri contributors

sql.sqlite.statement

import sql.sqlite.statement

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.sqlite.statement.* needs import sql.sqlite.statement.

A SQLite statement compiled once and run many times.

Compiling a statement is the expensive half of running one, and a statement run in a loop pays that cost once here rather than on every pass. The saving is real enough to be worth the extra object: an insert of ten thousand rows through a prepared statement does one compile instead of ten thousand.

A statement holds one native handle, so it can only be doing one thing at a time. Opening a cursor over it takes that handle until the cursor closes, and running it again in the meantime raises rather than silently resetting the cursor underneath.

Functions

describe()

sql.sqlite.statement.describe(statement) -> dict

Describes the columns a compiled statement returns.

Parameters

  • ptr — statement

Returns dict — { columns, conversions }

bind_all()

sql.sqlite.statement.bind_all(statement, values: list, query: string)

Binds values to a reset statement, in order.

Parameters

  • ptr — statement
  • values (list)
  • query (string) — For error messages.

Raises QueryError if the count does not match what the statement expects.

Classes

SqliteStatement

class sql.sqlite.SqliteStatement < DriverStatement

A prepared SQLite statement.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

sql.sqlite.SqliteStatement(connection, statement, query)

Parameters

  • connection (SqliteConnection) — The connection that owns this.
  • ptr — statement The compiled native statement.
  • query (string) — The statement text, as sent to the engine.

SqliteStatement.columns()

sql.sqlite.SqliteStatement.columns()

SqliteStatement.parameter_count()

sql.sqlite.SqliteStatement.parameter_count()

SqliteStatement.execute()

sql.sqlite.SqliteStatement.execute(values: list) -> dict

Runs the statement for its effect.

Parameters

  • values (list)

Returns dict — { rows_affected, last_insert_id }

SqliteStatement.select()

sql.sqlite.SqliteStatement.select(values: list) -> dict

Runs the statement and reads its whole result.

Parameters

  • values (list)

Returns dict — { columns, rows }

SqliteStatement.open_cursor()

sql.sqlite.SqliteStatement.open_cursor(values: list, options: dict) -> SqliteCursor

Runs the statement and hands back a cursor over its result.

The cursor holds this statement until it closes.

Parameters

  • values (list)
  • options (dict) — Unused here; SQLite streams natively.

Returns SqliteCursor

SqliteStatement.close()

sql.sqlite.SqliteStatement.close()

Releases the statement. Safe to call more than once.

SqliteStatement.is_closed()

sql.sqlite.SqliteStatement.is_closed() -> bool

Whether this statement has been closed.

Returns bool

SqliteStatement.to_string()

sql.sqlite.SqliteStatement.to_string()

2026, Richard Ore and Zuri contributors

sql.sqlite.types

import sql.sqlite.types

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.sqlite.types.* needs import sql.sqlite.types.

Reading SQLite values back as the types a column was declared to hold.

SQLite stores five things and remembers nothing about intent. A boolean goes in as 1 and comes back as the number 1; a timestamp goes in as text and comes back as text. What it does keep is the type each column was declared with, and that is enough to put the value back the way it went in.

So decoding here is driven by the declaration and never by the value. A column declared BOOLEAN yields true and false; one declared INTEGER yields the number 1 even when every row in it happens to be 0 or 1. Guessing from content would make the type of a result depend on the rows it happened to return, which is the kind of thing that works until the day a table has different data in it.

A column with no declared type, which is what an expression has, comes back exactly as SQLite stored it.

Constants

BOOLEAN_TYPES

sql.sqlite.types.BOOLEAN_TYPES = [...]

Declared types that mean a boolean.

TEMPORAL_TYPES

sql.sqlite.types.TEMPORAL_TYPES = [...]

Declared types that mean a point in time.

JSON_TYPES

sql.sqlite.types.JSON_TYPES = [...]

Declared types that mean JSON held in a text column.

Functions

base_type()

sql.sqlite.base_type(declared) -> string

The bare type name from a declaration, upper cased and without any size or precision.

VARCHAR(255) is VARCHAR, decimal(10, 2) is DECIMAL, and an undeclared column is the empty string.

Parameters

  • declared (string|nil)

Returns string

conversion_for()

sql.sqlite.conversion_for(declared) -> string|nil

Which conversion a column’s declaration calls for: 'bool', 'date', 'json', or nil for a column that needs none.

Parameters

  • declared (string|nil) — The column’s declared type.

Returns string|nil

decode()

sql.sqlite.types.decode(value, conversion) -> any

Applies one column’s conversion to one stored value.

NULL stays nil whatever the column was declared as. A value that will not convert, such as text in a DATETIME column that is not a timestamp, comes back as it was stored rather than as nil: losing the value would be a worse answer than handing back the text that is actually in the database.

Parameters

  • value (any) — The value as SQLite stored it.
  • conversion (string|nil) — From conversion_for().

Returns any

conversions_for()

sql.sqlite.types.conversions_for(declared: list) -> list

The conversions for a whole result, one per column, worked out once so each row does not have to look at the declarations again.

Parameters

  • declared (list) — The declared types, in column order.

Returns list

decode_row()

sql.sqlite.types.decode_row(values: list, conversions: list) -> list

Applies a result’s conversions to one row of stored values.

Parameters

  • values (list)
  • conversions (list)

Returns list


2026, Richard Ore and Zuri contributors

sql.statement

import sql.statement

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.statement.* needs import sql.statement.

A statement compiled once and run many times.

Every engine here splits running a statement into compiling it and executing it, and the compile is the expensive half. A statement run in a loop should pay for it once:

var insert = db.prepare('insert into points (x, y) values (?, ?)')

for point in points {
  insert.exec([point.x, point.y])
}

insert.close()

The placeholders are translated once, when the statement is prepared, so the loop does no string work at all.

Classes

Statement

class sql.Statement

A prepared statement.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

sql.Statement(connection, statement, source, sql, style)

Parameters

  • connection (Connection) — The connection that prepared this.
  • statement (DriverStatement) — The adapter’s own statement.
  • source (string) — The statement as it was written.
  • sql (string) — The statement as the engine received it.
  • style (string) — The driver’s placeholder style.

Statement.source()

sql.Statement.source() -> string

The statement as it was written, with ? or :name in it.

Returns string

Statement.sql()

sql.Statement.sql() -> string

The statement as the engine received it, with the engine’s own placeholders.

Returns string

Statement.columns()

sql.Statement.columns() -> list[dict]

The columns this statement returns, each a dictionary with name and type. Empty for a statement that returns no rows.

Returns list[dict]

Statement.parameter_count()

sql.Statement.parameter_count() -> number

How many parameters this statement binds.

Returns number

Statement.query()

sql.Statement.query(params) -> ResultSet

Runs the statement and reads its whole result.

Parameters

  • params (list|dict|nil)

Returns ResultSet

Statement.exec()

sql.Statement.exec(params) -> ExecResult

Runs the statement for its effect.

Parameters

  • params (list|dict|nil)

Returns ExecResult

Statement.stream()

sql.Statement.stream(params, options) -> Cursor

Runs the statement and returns a cursor over its result.

The cursor holds this statement until it closes, so the statement cannot be run again in the meantime.

Parameters

  • params (list|dict|nil)
  • options (dict|nil) — { batch } on engines that fetch in batches; ignored where the engine streams already.

Returns Cursor

Statement.fetch_one()

sql.Statement.fetch_one(params) -> dict|nil

The first row, or nil when the statement returns none.

Parameters

  • params (list|dict|nil)

Returns dict|nil

Statement.fetch_all()

sql.Statement.fetch_all(params) -> list[dict]

Every row, as dictionaries.

Parameters

  • params (list|dict|nil)

Returns list[dict]

Statement.fetch_value()

sql.Statement.fetch_value(params, fallback) -> any

The first column of the first row.

Parameters

  • params (list|dict|nil)
  • fallback (any) — What to return when the statement returns no rows.

Returns any

Statement.close()

sql.Statement.close()

Releases the statement. Safe to call more than once.

Statement.is_closed()

sql.Statement.is_closed() -> bool

Whether this statement has been closed.

Returns bool

Statement.to_string()

sql.Statement.to_string()

2026, Richard Ore and Zuri contributors

sql.transaction

import sql.transaction

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.transaction.* needs import sql.transaction.

Transactions, and the nesting that savepoints make possible.

The shape worth using is the closure form, because it cannot be got wrong:

db.transaction(@(tx) {
  tx.exec('update accounts set balance = balance - ? where id = ?', [100, 1])
  tx.exec('update accounts set balance = balance + ? where id = ?', [100, 2])
})

It commits when the closure returns and rolls back when the closure raises, and there is no path through it that leaves a transaction open. The manual form exists for the cases the closure form cannot express, such as a transaction whose lifetime is a request rather than a block.

Nesting

No engine here supports a transaction inside a transaction, but both support savepoints, which is the same thing under a different name. A transaction() inside another takes a savepoint, so a helper that opens one works the same whether it was called on its own or from inside a larger piece of work. Only the outermost actually commits.

Classes

Transaction

class sql.Transaction

An open transaction.

It carries the same query, exec and CRUD methods a connection does, so code inside a transaction reads the same as code outside one. Every statement it runs goes to the connection the transaction is on.

  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
connectionConnectionThe connection this transaction is running on.
nestedboolWhether this transaction is a savepoint inside another rather than a transaction of its own.

Constructor

sql.Transaction(connection, nested)

Parameters

  • connection (Connection)
  • nested (bool)

Transaction.is_finished()

sql.Transaction.is_finished() -> bool

Whether this transaction has already committed or rolled back.

Returns bool

Transaction.query()

sql.Transaction.query(sql: string, params) -> ResultSet

Runs a query on this transaction’s connection.

Parameters

  • sql (string)
  • params (list|dict|nil)

Returns ResultSet

Transaction.exec()

sql.Transaction.exec(sql: string, params) -> ExecResult

Runs a statement for its effect.

Parameters

  • sql (string)
  • params (list|dict|nil)

Returns ExecResult

Transaction.fetch_one()

sql.Transaction.fetch_one(sql: string, params) -> dict|nil

The first row of a query, or nil.

Parameters

  • sql (string)
  • params (list|dict|nil)

Returns dict|nil

Transaction.fetch_all()

sql.Transaction.fetch_all(sql: string, params) -> list[dict]

Every row of a query.

Parameters

  • sql (string)
  • params (list|dict|nil)

Returns list[dict]

Transaction.fetch_value()

sql.Transaction.fetch_value(sql: string, params, fallback) -> any

The first column of the first row.

Parameters

  • sql (string)
  • params (list|dict|nil)
  • fallback (any)

Returns any

Transaction.fetch_column()

sql.Transaction.fetch_column(sql: string, params, column) -> list

One column’s values, as a list.

Parameters

  • sql (string)
  • params (list|dict|nil)
  • column (string|number|nil) — Which column, the first by default.

Returns list

Transaction.stream()

sql.Transaction.stream(sql: string, params, options) -> Cursor

Runs a query and returns a cursor over its result rather than reading all of it. The cursor belongs to the transaction’s connection, so it has to be read to the end or closed before the transaction commits.

Parameters

  • sql (string)
  • params (list|dict|nil)
  • options (dict|nil) — { batch } on engines that fetch in batches.

Returns Cursor

Transaction.prepare()

sql.Transaction.prepare(sql: string) -> Statement

Compiles a statement for repeated use inside this transaction.

Parameters

  • sql (string)

Returns Statement

Transaction.exec_script()

sql.Transaction.exec_script(sql: string)

Runs one or more statements for their effect, as a script: a schema file, a migration, or a batch of settings. It binds no parameters and returns no rows.

On PostgreSQL and SQLite the whole script commits or rolls back with the transaction. MySQL commits a statement that changes a table’s definition as it runs it, whatever transaction it is in, so a script of those is only as atomic as MySQL makes it.

Parameters

  • sql (string)

Transaction.insert()

sql.Transaction.insert(table: string, values: dict, options) -> any

Inserts a row and returns its id.

Parameters

  • table (string)
  • values (dict)
  • options (dict|nil)

Returns any

Transaction.insert_many()

sql.Transaction.insert_many(table: string, rows: list) -> number

Inserts several rows in one statement.

Parameters

  • table (string)
  • rows (list) — Dictionaries of column to value.

Returns number — How many rows were inserted.

Transaction.update()

sql.Transaction.update(table: string, values: dict, where) -> number

Updates the rows matching where.

Parameters

  • table (string)
  • values (dict)
  • where (dict|nil)

Returns number

Transaction.delete()

sql.Transaction.delete(table: string, where) -> number

Deletes the rows matching where.

Parameters

  • table (string)
  • where (dict|nil)

Returns number

Transaction.find()

sql.Transaction.find(table: string, where, options) -> ResultSet

Selects the rows matching where.

Parameters

  • table (string)
  • where (dict|nil)
  • options (dict|nil) — columns, order, limit and offset.

Returns ResultSet

Transaction.find_one()

sql.Transaction.find_one(table: string, where, options) -> dict|nil

The first row matching where, or nil.

Parameters

  • table (string)
  • where (dict|nil)
  • options (dict|nil)

Returns dict|nil

Transaction.count()

sql.Transaction.count(table: string, where) -> number

How many rows match where.

Parameters

  • table (string)
  • where (dict|nil)

Returns number

Transaction.commit()

sql.Transaction.commit()

Commits this transaction, or releases its savepoint when it is nested.

Raises TransactionError if it has already finished.

Transaction.rollback()

sql.Transaction.rollback()

Rolls this transaction back, or undoes its savepoint when it is nested.

Raises TransactionError if it has already finished.

Transaction.to_string()

sql.Transaction.to_string()

2026, Richard Ore and Zuri contributors

sql.types

import sql.types

sql lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled sql.types.* needs import sql.types.

How Zuri values and database values correspond, for the parts that are the same whichever engine is underneath.

Databases agree on very little. They all store numbers and text; past that, one has a native JSON type and another stores JSON in a text column, one has a date type with a time zone and another has no date type at all. The adapters differ where the engines differ, and share the conversions that do not have to.

The correspondence

ZuriDatabase
nilNULL
boola boolean where the engine has one, otherwise 0 and 1
numberan integer where the value is whole, otherwise a float
biginta 64 bit integer, or an error if it will not fit in one
stringtext
bytesa blob
date.Datea timestamp, or ISO 8601 text on an engine with no date type
Timea time interval on an engine that has one, otherwise text
list, dictJSON, native where the engine has it and text where it does not

A value of any other type is refused rather than guessed at. An instance of a class in particular has no obvious storage form, and inventing one would make the round trip lossy in a way that only showed up on the way back out.

Time

Timestamps written as text use ISO 8601 with microseconds and an explicit offset, which every engine here parses and which sorts correctly as text. A date.Date carries an offset of its own, so nothing is assumed about the local zone at either end.

Constants

ISO_FORMAT

sql.types.ISO_FORMAT = 'Y-m-d\TH:i:s.uP'

The format this module writes timestamps in: ISO 8601 with microsecond precision and an explicit UTC offset.

ISO_DATE_FORMAT

sql.types.ISO_DATE_FORMAT = 'Y-m-d'

The same, without the time, for a column that holds only a date.

Functions

is_temporal()

sql.types.is_temporal(value) -> bool

Whether value is a date.Date.

Parameters

  • value (any)

Returns bool

is_interval()

sql.types.is_interval(value) -> bool

Whether value is a Time.

Parameters

  • value (any)

Returns bool

is_structured()

sql.types.is_structured(value) -> bool

Whether value is something this module stores as JSON.

Parameters

  • value (any)

Returns bool

to_iso()

sql.types.to_iso(value) -> string

Formats a date.Date as ISO 8601 text.

Parameters

  • value (date.Date)

Returns string

from_iso()

sql.types.from_iso(text) -> date.Date|nil

Reads ISO 8601 text back into a date.Date.

Accepts what the engines actually write, which is more than one shape: with or without a T, with or without fractional seconds, and with or without an offset. Text that is not a timestamp at all comes back as nil rather than raising, so a column whose declared type promises a date but whose contents do not can still be read.

Parameters

  • text (string)

Returns date.Date|nil

to_json()

sql.types.to_json(value) -> string

Encodes value as JSON text.

Parameters

  • value (list|dict)

Returns string

from_json()

sql.types.from_json(text) -> any

Decodes JSON text, returning nil for anything that will not parse.

Parameters

  • text (string)

Returns any

flatten()

sql.types.flatten(value) -> nil|bool|number|bigint|string|bytes

Reduces value to something an engine with no richer type can store: a timestamp becomes ISO text, a list or dictionary becomes JSON text, and everything else is already storable and passes through.

Adapters for engines that do have those types encode them natively instead and never call this.

Parameters

  • value (any)

Returns nil|bool|number|bigint|string|bytes

Raises QueryError for a value with no storable form.

time()

sql.time(hours, minutes, seconds, microseconds, negative) -> Time

Builds a Time.

Parameters

  • hours (number|nil)
  • minutes (number|nil)
  • seconds (number|nil)
  • microseconds (number|nil)
  • negative (bool|nil)

Returns Time

time_from_seconds()

sql.time_from_seconds(total: number) -> Time

Builds a Time of total seconds, splitting it into parts.

Parameters

  • total (number) — Negative for a span that runs backwards.

Returns Time

parse_time()

sql.parse_time(text) -> Time|nil

Reads a SQL TIME literal.

Accepts HH:MM:SS, HH:MM, and either with fractional seconds, in each case optionally signed. Text that is not a time comes back as nil rather than raising, matching from_iso().

Parameters

  • text (string)

Returns Time|nil

integer_from_text()

sql.types.integer_from_text(text) -> number|bigint|nil

Reads an integer, however long, as a number where one holds it exactly and a bigint where it does not.

Parameters

  • text (string) — An optionally signed run of digits.

Returns number|bigint|nil — nil where text is not an integer.

Classes

Time

class sql.Time

A signed span of time, as a SQL TIME column holds one.

A TIME is not a point in the day. MySQL’s range runs from -838:59:59.999999 to 838:59:59.999999, so a value can be negative and can exceed twenty four hours, and reading one into a date.Date would quietly lose both of those. It is a duration: the length of a shift, the lateness of a delivery, the gap between two events.

The parts are kept as they were written rather than normalised, so a value built as 90 minutes stays 90 minutes and does not silently become an hour and a half. total_seconds() is there for arithmetic and comparison, and compare() uses it, so two values that mean the same length compare equal whichever way each was built.

Fields

FieldTypeDescription
negativeboolWhether the span runs backwards.
hoursnumberWhole hours.
minutesnumber
secondsnumber
microsecondsnumberFractional seconds, in millionths.

Constructor

sql.Time(hours, minutes, seconds, microseconds, negative)

Parameters

  • hours (number|nil)
  • minutes (number|nil)
  • seconds (number|nil)
  • microseconds (number|nil)
  • negative (bool|nil)

Time.total_seconds()

sql.Time.total_seconds() -> number

The whole span in seconds, negative where the span is.

Returns number — Fractional where there are microseconds.

Time.is_negative()

sql.Time.is_negative() -> bool

Whether the span runs backwards.

A zero span is never negative, whichever way it was built.

Returns bool

Time.is_zero()

sql.Time.is_zero() -> bool

Whether the span is of no length.

Returns bool

Time.compare()

sql.Time.compare(other) -> number

Orders this span against another by length.

Parameters

  • other (Time)

Returns number — Negative, zero or positive.

Time.equals()

sql.Time.equals(other) -> bool

Whether two spans are of the same length, however each was built.

Parameters

  • other (any)

Returns bool

Time.to_string()

sql.Time.to_string() -> string

The span as a SQL TIME literal, such as '-12:30:00'.

Microseconds appear only where there are some, which is the same choice the engines make when they print one.

Returns string


2026, Richard Ore and Zuri contributors

mail

import mail

Mail, from the message to the socket.

mail builds and reads messages, and speaks the three protocols that move them: SMTP to send, IMAP and POP3 to read. It also runs the other end, with an SMTP server that accepts mail and an IMAP server that serves it. All of it is written in Zuri.

import mail

var note = mail.message()
  .set_from('reports@example.com')
  .add_to('ann@example.com')
  .set_subject('Quarterly report')
  .set_text('The numbers are in.')

mail.send('smtp://mail.example.com', note, {
  username: 'reports',
  password: secret,
})

What is here

message()starts a message, built up one call at a time
parse()reads one back
send()sends one, opening and closing the connection
Messagea message, or one part of one
Addressone address and the name written with it
Headersthe header block, in order and case-insensitive
SmtpClienta connection to a server that sends mail
ImapClienta connection to a server that stores it
Pop3Clientthe older way of collecting it
SmtpServerthe receiving end of SMTP
ImapServerthe serving end of IMAP
MailStorewhere an ImapServer keeps mail
MailErrorthe root of every error raised here

Reading mail

import mail

var inbox = mail.imap('imaps://mail.example.com', {
  username: 'ann',
  password: secret,
})

inbox.select('INBOX')

for id in inbox.search('UNSEEN') {
  echo inbox.fetch_message(id).subject()
}

inbox.logout()

Running a server

The servers are the same shape as http.HttpServer: bind, listen, and hand each connection to a worker. An SmtpServer decides what to accept through handlers, and an ImapServer answers from a MailStore, of which two ship: one on disk in Maildir format and one in memory.

Security

Every client can start in TLS or negotiate it with STARTTLS, and refuses to send credentials over a connection that is neither. mail.dkim signs outgoing mail and verifies incoming mail.

The mail API

Every public name in mail, wherever it is declared. Each links to the page that documents it.

NameKindSummary
mail.ALLOWED_FLAGSconstant
mail.AddressclassOne address, with the display name that was written alongside it.
mail.AttachmentclassSomething a message carries alongside what it says: a file to offer, or an image the HTML shows.
mail.AuthenticationErrorclassThe credentials were not accepted, or no mechanism both ends understand was on offer.
mail.BASE_CAPABILITIESconstant
mail.BodyPartclassWhat one message is made of, without fetching it: the type of each part, its size, and where in the message…
mail.ConnectionClosedclassThe connection closed while a conversation was still going on, or a command was issued on one that had…
mail.ContentDispositionclassWhat a part is for: shown where it sits, or offered as a file.
mail.ContentTypeclassA media type and the parameters that came with it.
mail.DEFAULT_CHARSETconstant
mail.DEFAULT_MAX_RECIPIENTSconstant
mail.DEFAULT_MAX_SIZEconstant
mail.DEFAULT_TIMEOUTconstant
mail.DEFAULT_TYPEconstant
mail.DELIMITERconstant
mail.EntryclassOne message in the mailbox, as list() reports it.
mail.EnvelopeclassThe addresses and dates out of a message’s headers, as the server parsed them, so a list of messages can be…
mail.GroupclassA named group of addresses, as Managers: ann@example.com, bob@example.com; writes one.
mail.HeadersclassThe headers of a message or of one of its parts.
mail.IMAP_SCHEMESconstant
mail.INDEX_FILEconstant
mail.INFO_SEPARATORconstant
mail.ImapClientclassA connection to a server that stores mail.
mail.ImapErrorclassThe IMAP server answered a command with NO or BAD.
mail.ImapServerclassA server that serves mail.
mail.ImapSessionclassOne connection, and what it has got as far as doing.
mail.MAILDIR_FLAGSconstant
mail.MAILDIR_PARTSconstant
mail.MAX_DATA_LINEconstant
mail.MAX_ERRORSconstant
mail.MAX_REPLY_LINEconstant
mail.MECHANISMSconstant
mail.MailErrorclassBase class for every error this module raises.
mail.MailStoreclassWhat a store has to be able to do.
mail.MailboxclassAn open mailbox, and what the server said about it when it opened.
mail.MailboxErrorclassSomething went wrong in a mail store: a mailbox that does not exist, one that cannot be created, a message…
mail.MailboxInfoclassOne mailbox as list() reports it: its name, the character that separates the levels of it, and what the…
mail.MaildirStoreclassA store that keeps mail on disk in the Maildir layout.
mail.MemoryStoreclassA store that keeps everything in the process and nothing on disk.
mail.MessageclassOne message, or one part of one.
mail.MessageErrorclassThe bytes handed over are not a message, or are one that contradicts itself: a header with no colon in it, a…
mail.MessageInfoclassWhat a FETCH returned about one message.
mail.POP3_SCHEMESconstant
mail.Pop3ClientclassA connection to a server that hands mail over.
mail.Pop3ErrorclassThe POP3 server answered a command with -ERR.
mail.ProtocolErrorclassThe server said something the protocol does not allow: a greeting that is not a greeting, a response with no…
mail.ReplyclassOne reply from the server: its code, and whatever it said with it.
mail.SMTP_SCHEMESconstant
mail.STATESconstant
mail.SmtpClientclassA connection to a server that sends mail.
mail.SmtpErrorclassThe server rejected a command, and said so with a reply code.
mail.SmtpPermanentErrorclassA 5xx reply: the server will not accept the message, and trying again changes nothing.
mail.SmtpServerclassA server that accepts mail.
mail.SmtpSessionclassOne connection, and everything known about it so far.
mail.SmtpTransientErrorclassA 4xx reply: the server could not accept the message now, and the sender should try again later.
mail.StateErrorclassA command was issued that makes no sense in the state the connection is in: a fetch before a mailbox has been…
mail.StoredMessageclassOne message in a store.
mail.address.addressfunctionBuilds an address without parsing anything.
mail.address.format_listfunctionWrites a list of addresses as a header value.
mail.address.parsefunctionReads exactly one address.
mail.address.parse_groupsfunctionReads every address in a header value, keeping the groups.
mail.address.parse_listfunctionReads every address in a header value.
mail.attachmentfunctionStarts an attachment from what is in it.
mail.dkim.ALGORITHMSconstantThe signing algorithms this implements.
mail.dkim.CANONICALISATIONSconstantThe canonicalisations this implements, on either headers or body.
mail.dkim.DEFAULT_HEADERSconstantThe headers signed when the signer is not told which to sign.
mail.dkim.DkimErrorclassRaised when a signature cannot be built: an algorithm this does not implement, a key that will not parse, a…
mail.dkim.ResultclassWhat checking one signature came to.
mail.dkim.SignatureclassOne DKIM-Signature header, read into its parts.
mail.dkim.SignerclassSigns outgoing messages for one domain with one key.
mail.dkim.canonicalise_bodyfunctionCanonicalises a body.
mail.dkim.canonicalise_headerfunctionCanonicalises one header.
mail.dkim.is_signedfunctionWhether a message carries at least one signature that checks out.
mail.dkim.public_key_pemfunctionTurns the p= value of a key record into a PEM the crypto module will accept.
mail.dkim.verifyfunctionChecks every signature on a message.
mail.encoding.TRANSFER_ENCODINGSconstant
mail.encoding.decode_base64functionDecodes base64, ignoring the line breaks and stray whitespace a message carries it with.
mail.encoding.decode_bodyfunctionDecodes a part’s body the way its Content-Transfer-Encoding says.
mail.encoding.decode_parametersfunctionPuts the parameters of a header back together.
mail.encoding.decode_quoted_printablefunctionDecodes quoted-printable.
mail.encoding.decode_textfunctionTurns bytes into text, reading them as the character set names.
mail.encoding.decode_wordsfunctionReads the encoded words out of a header value and puts back the text they stand for.
mail.encoding.encode_base64functionEncodes data as base64, wrapped into lines.
mail.encoding.encode_bodyfunctionEncodes a part’s body the way its Content-Transfer-Encoding says.
mail.encoding.encode_parameterfunctionWrites one parameter of a header, choosing the form its value needs.
mail.encoding.encode_quoted_printablefunctionEncodes data as quoted-printable.
mail.encoding.encode_wordfunctionEncodes text as one or more RFC 2047 encoded words, so that a header can carry characters a header is not…
mail.encoding.guess_encodingfunctionPicks the transfer encoding a body should use.
mail.encoding.is_asciifunctionWhether every byte is plain ASCII, which decides whether a header or a body needs encoding at all.
mail.format_addressesfunctionWrites a list of addresses as a header value.
mail.headers.ADDRESS_HEADERSconstant
mail.headers.LINE_WIDTHconstant
mail.headers.foldfunctionWrites one header, folded so that no line runs past the width.
mail.headers.unfoldfunction
mail.imapfunctionOpens a connection to a server that stores mail.
mail.messagefunctionStarts a message.
mail.multipartfunction
mail.new_message_idfunctionAn identifier for a message, unique enough that no two ever collide.
mail.parsefunctionReads a message.
mail.parse_addressfunctionReads one address out of text.
mail.parse_address_groupsfunctionReads every address out of text, keeping the groups as groups.
mail.parse_address_listfunctionReads every address out of text, with any groups flattened into their members.
mail.parser.LiteralclassA run of bytes a server sent as a literal.
mail.parser.ReaderclassReads responses off a connection, one at a time.
mail.parser.ResponseclassOne response from the server.
mail.parser.STATUSESconstant
mail.parser.SYSTEM_FLAGSconstant
mail.parser.quotefunctionWrites a value the way a command has to carry it.
mail.parser.tokenizefunctionReads a run of IMAP values out of text.
mail.pool.ClusterclassA running pool: the socket, the workers, and the loop feeding them.
mail.pool.servefunctionStarts a pool and runs it until something closes it.
mail.pool.startfunctionStarts a pool without running the accept loop, so the address is known before the first connection.
mail.pool.worker_mainfunctionWhat one worker isolate runs: build a server of its own, then serve whatever connections the acceptor hands…
mail.pop3functionOpens a connection to a server that hands mail over.
mail.sasl.AnonymousclassANONYMOUS: no identity at all, with an optional note saying who is knocking.
mail.sasl.CramMd5classCRAM-MD5: a keyed digest of the server’s challenge, so the password never travels.
mail.sasl.ExternalclassEXTERNAL: the connection already established who this is, usually with a client certificate.
mail.sasl.LoginclassLOGIN: the username and the password again, one prompt at a time.
mail.sasl.MechanismclassWhat every mechanism looks like from the outside.
mail.sasl.NEEDS_TLSconstantThe mechanisms that put the password itself on the wire, and so must never be used without TLS.
mail.sasl.OAuthBearerclassOAUTHBEARER: the standardised form of the same idea, from RFC 7628.
mail.sasl.PREFERENCEconstantThe mechanisms this implements, strongest first, which is the order a client picks from what a server offers.
mail.sasl.PlainclassPLAIN: the username and the password, separated by zero bytes.
mail.sasl.ScramclassSCRAM-SHA-1 and SCRAM-SHA-256: the password is never sent, the server never has to store it, and the…
mail.sasl.XOAuth2classXOAUTH2: a bearer token rather than a password, which is what Google and Microsoft accept.
mail.sasl.choosefunctionPicks the mechanism to use.
mail.sasl.offunctionBuilds a mechanism by name.
mail.sasl.preparefunctionPrepares a username or password the way RFC 4013 says to, so that two spellings of the same characters…
mail.sasl.responsefunctionThe answer to a CRAM-MD5 challenge.
mail.sendfunctionSends one message, opening the connection and closing it again.
mail.smtpfunctionOpens a connection to a server that sends mail.
mail.stream.LineStreamclassA connection that reads and writes lines.
mail.stream.MAX_LINEconstant
mail.stream.TLS_MODESconstantHow TLS is treated on a connection that did not start encrypted: require refuses to go on without it,…
mail.stream.connectfunctionOpens a connection to a host, in TLS or in the clear.
mail.stream.endpointfunctionReads a connection string into the pieces needed to open it.
mail.stream.start_tlsfunctionWraps a connection that started in the clear in TLS, which is what every STARTTLS comes down to.
mail.text_partfunctionBuilds one text part.

Submodules

ModuleReached asSummary
mail.addressmail.address.*Reading and writing the addresses in a mail header.
mail.contentmail.*The two headers that say what a part holds and what to do with it.
mail.dkimmail.dkim.*DomainKeys Identified Mail: signing outgoing messages so a receiver can tell they came from the domain they…
mail.encodingmail.encoding.*The encodings a message uses to get eight bit data through a seven bit pipe.
mail.errorsmail.*Every error the mail stack raises, under one root.
mail.headersmail.headers.*The header block of a message: what is in it, in what order, and how it is written back out.
mail.imapmail.*IMAP: reading mail where it is kept, rather than taking it away.
mail.messagemail.*A mail message: its headers, its body, and the tree of parts a body turns into once there is more than one…
mail.poolmail.pool.*Running a mail server on more than one connection at a time.
mail.pop3mail.*POP3: the client end of collecting mail.
mail.saslmail.sasl.*The authentication mechanisms all three mail protocols share.
mail.smtpmail.*SMTP: the protocol that moves mail from where it was written to where it is kept.
mail.streammail.stream.*A line-oriented connection, which is what all three mail protocols are underneath.

Functions

parse_address()

mail.parse_address(text: string) -> Address

Reads one address out of text.

import mail

echo mail.parse_address('Ann <ann@example.com>').address
ann@example.com

Parameters

  • text (string)

Returns Address

Raises MessageError if the text holds no address, or several.

parse_address_list()

mail.parse_address_list(text: string) -> list

Reads every address out of text, with any groups flattened into their members.

Parameters

  • text (string)

Returns list — of Address

parse_address_groups()

mail.parse_address_groups(text: string) -> list

Reads every address out of text, keeping the groups as groups.

Parameters

  • text (string)

Returns list — of Address and Group

format_addresses()

mail.format_addresses(people: list) -> string

Writes a list of addresses as a header value.

Parameters

  • people (list) — Address values, Group values, or text to be parsed first.

Returns string

send()

mail.send(url: string, note, options: ?dict) -> Reply

Sends one message, opening the connection and closing it again.

This is the whole of sending mail for a program that sends one at a time. A program sending many wants an SmtpClient of its own, kept open across all of them.

import mail

mail.send('smtp://mail.example.com', mail.message()
  .set_from('reports@example.com')
  .add_to('ann@example.com')
  .set_subject('Quarterly report')
  .set_text('The numbers are in.'),
  { username: 'reports', password: secret })

Parameters

  • url (string) — smtp://host or smtps://host.
  • note (Message)
  • options (?dict) — Everything SmtpClient.connect() takes, and from and to to override the envelope.

Returns Reply — the server’s verdict on the message.

Raises SmtpError if the server refuses any part of it.

smtp()

mail.smtp(url: string, options: ?dict) -> SmtpClient

Opens a connection to a server that sends mail.

Parameters

  • url (string)
  • options (?dict)

Returns SmtpClient

imap()

mail.imap(url: string, options: ?dict) -> ImapClient

Opens a connection to a server that stores mail.

Parameters

  • url (string)
  • options (?dict)

Returns ImapClient

pop3()

mail.pop3(url: string, options: ?dict) -> Pop3Client

Opens a connection to a server that hands mail over.

Parameters

  • url (string)
  • options (?dict)

Returns Pop3Client


2026, Richard Ore and Zuri contributors

mail.address

import mail.address

mail lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled mail.address.* needs import mail.address.

Reading and writing the addresses in a mail header.

An address in a header is rarely just an address. It can carry a display name, the name can be quoted or encoded, either can have comments buried in it, and a header can list several of them or gather them into a named group. This module turns all of that into Address objects, and turns them back into a header a server will accept.

import mail.address

var who = address.parse('"Ore, Richard" <richard@example.com> (author)')

echo who.name
echo who.address
echo who.domain
Ore, Richard
richard@example.com
example.com

Functions

parse_groups()

mail.address.parse_groups(text: string) -> list

Reads every address in a header value, keeping the groups.

Entries that are not addresses at all are left out rather than raising, because one unusable entry in a To header is no reason to refuse the whole message.

Parameters

  • text (string)

Returns list — of Address and Group

parse_list()

mail.address.parse_list(text: string) -> list

Reads every address in a header value.

A group’s members come back alongside the rest, since a program sending mail cares who the recipients are and not how they were gathered.

import mail.address

var people = address.parse_list('Ann <ann@example.com>, bob@example.com')

echo people.map(@(person) => person.address)
[ann@example.com, bob@example.com]

Parameters

  • text (string)

Returns list — of Address

parse()

mail.address.parse(text: string) -> Address

Reads exactly one address.

Parameters

  • text (string)

Returns Address

Raises MessageError if text holds no address, or more than one.

address()

mail.address.address(spec: string, name: ?string) -> Address

Builds an address without parsing anything.

Parameters

  • spec (string) — The address, as local@domain.
  • name (?string) — The display name.

Returns Address

format_list()

mail.address.format_list(items: list) -> string

Writes a list of addresses as a header value.

Parameters

  • items (list) — Address or Group values, or plain strings, which are parsed first.

Returns string

Classes

Address

class mail.Address

One address, with the display name that was written alongside it.

name is the display name, already decoded from whatever encoding the header carried it in, and is an empty string when there was none. address is the address itself, local the part before the @ and domain the part after it.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.Address(spec: string, name: ?string)

Builds an address.

import mail.address { Address }

echo Address('richard@example.com', 'Richard Ore').to_string()
Richard Ore <richard@example.com>

Parameters

  • spec (string) — The address, as local@domain.
  • name (?string) — The display name. Empty when not given.

Raises MessageError if spec has no local part.

Address.is_routable()

mail.Address.is_routable() -> bool

Whether this address has a domain. An address without one is legal in a header but cannot be delivered to over the network.

Returns bool

Address.to_string()

mail.Address.to_string() -> string

The address as a header would carry it, with the display name quoted or encoded as it needs to be.

Returns string

Address.equals()

mail.Address.equals(other) -> bool

Whether two addresses are the same one.

The domain is compared without regard to case, since domains are case-insensitive. The local part is compared exactly, since it is the receiving server’s to interpret and some of them do distinguish case.

Parameters

  • other (Address)

Returns bool

Group

class mail.Group

A named group of addresses, as Managers: ann@example.com, bob@example.com; writes one.

Groups are rare, and a program that does not care about them can use parse_list(), which hands back the members and forgets the name.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.Group(name: string, members: list)

Parameters

  • name (string) — The group’s name.
  • members (list) — The Address values in it, possibly empty.

Group.to_string()

mail.Group.to_string()

2026, Richard Ore and Zuri contributors

mail.content

import mail

Everything here is re-exported by mail, so import mail is enough and the names are called as mail.*. Importing mail.content on its own works too and reaches the same definitions.

The two headers that say what a part holds and what to do with it.

Content-Type names the media type and carries the parameters that go with it: the character set of text, the boundary of a multipart, the name of a file. Content-Disposition says whether the part is meant to be shown where it sits or offered as an attachment, and carries the filename to offer it under.

import mail.content { ContentType }

var type = ContentType.parse('text/plain; charset="utf-8"; format=flowed')

echo type.mime_type()
echo type.charset()
echo type.get('format')
text/plain
utf-8
flowed

Constants

DEFAULT_TYPE

mail.DEFAULT_TYPE = 'text/plain'

DEFAULT_CHARSET

mail.DEFAULT_CHARSET = 'us-ascii'

Classes

ContentType

class mail.ContentType

A media type and the parameters that came with it.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.ContentType(type: string, subtype: string, parameters: ?dict)

Parameters

  • type (string) — The type, such as text.
  • subtype (string) — The subtype, such as plain.
  • parameters (?dict) — The parameters, already decoded.

ContentType.parse()

mail.ContentType.parse(text: ?string) -> ContentType

Reads a Content-Type header.

A header that is missing or empty gives text/plain, which is what a part with no type is defined to be.

Parameters

  • text (?string)

Returns ContentType

ContentType.mime_type()

mail.ContentType.mime_type() -> string

The type and subtype together, as text/plain.

Returns string

ContentType.get()

mail.ContentType.get(name: string, fallback) -> any

One parameter’s value.

Parameters

  • name (string) — Matched without regard to case.
  • fallback (?any) — Returned when the parameter is absent.

Returns any

ContentType.charset()

mail.ContentType.charset() -> string

The character set of a text part.

Returns string — us-ascii when the header does not say, which is the defined default.

ContentType.boundary()

mail.ContentType.boundary() -> string|nil

The boundary that separates the parts of a multipart, or nil when this is not one.

Returns string|nil

ContentType.name()

mail.ContentType.name() -> string|nil

The filename this part suggests, taken from the name parameter.

Content-Disposition is the header that is supposed to carry it, and Message.filename() looks there first.

Returns string|nil

ContentType.is_multipart()

mail.ContentType.is_multipart() -> bool

Whether this part contains other parts.

Returns bool

ContentType.is_text()

mail.ContentType.is_text() -> bool

Whether this part is text, and so has a character set worth applying.

Returns bool

ContentType.to_string()

mail.ContentType.to_string() -> string

The header value, with every parameter written in the form it needs.

Returns string

ContentDisposition

class mail.ContentDisposition

What a part is for: shown where it sits, or offered as a file.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.ContentDisposition(kind: string, parameters: ?dict)

Parameters

  • kind (string) — inline or attachment.
  • parameters (?dict)

ContentDisposition.parse()

mail.ContentDisposition.parse(text: ?string) -> ContentDisposition

Reads a Content-Disposition header.

Parameters

  • text (?string)

Returns ContentDisposition

ContentDisposition.get()

mail.ContentDisposition.get(name: string, fallback) -> any

One parameter’s value.

Parameters

  • name (string)
  • fallback (?any)

Returns any

ContentDisposition.filename()

mail.ContentDisposition.filename() -> string|nil

The filename the part should be saved under, or nil.

Returns string|nil

ContentDisposition.is_attachment()

mail.ContentDisposition.is_attachment() -> bool

Whether the part is meant to be offered as a file rather than shown in place.

Returns bool

ContentDisposition.to_string()

mail.ContentDisposition.to_string()

2026, Richard Ore and Zuri contributors

mail.dkim

import mail

mail exposes this as mail.dkim, so import mail is enough and the names are called as mail.dkim.*. import mail.dkim reaches the same definitions directly.

DomainKeys Identified Mail: signing outgoing messages so a receiver can tell they came from the domain they claim to, and checking the signatures on incoming ones.

A signature covers the body and a chosen set of headers. The public key lives in the domain’s own DNS, under the selector the signature names, which is what ties the message to the domain.

import mail
import mail.dkim { Signer }

var signer = Signer('example.com', 'default', private_key)

signer.sign(note)

mail.send('smtp://mail.example.com', note)

Verification looks the key up through net.resolver unless handed something else to look it up with:

import mail.dkim

for result in dkim.verify(incoming) {
  echo '${result.domain}: ${result.valid ? 'ok' : result.reason}'
}

What is covered

Both canonicalisations, simple and relaxed, on headers and on bodies, and both signing algorithms in use: rsa-sha256 and the ed25519-sha256 of RFC 8463.

A signature says nothing about who wrote the message, only that the domain that signed it takes responsibility for it. Deciding what a valid signature from a given domain is worth is a policy question, and the answer is usually DMARC.

Constants

ALGORITHMS

mail.dkim.ALGORITHMS: list = [...]

The signing algorithms this implements.

CANONICALISATIONS

mail.dkim.CANONICALISATIONS: list = [...]

The canonicalisations this implements, on either headers or body.

simple covers the bytes as they are, which means any change at all on the way breaks the signature. relaxed forgives the whitespace and folding changes a mail server may make in passing, and is what almost everything uses.

DEFAULT_HEADERS

mail.dkim.DEFAULT_HEADERS: list = [...]

The headers signed when the signer is not told which to sign.

A header that is not in the message is skipped rather than signed as empty, and one that is listed twice is signed twice, which is how a signature says “there was exactly one of these”.

Functions

canonicalise_body()

mail.dkim.canonicalise_body(raw, method: string) -> bytes

Canonicalises a body.

Parameters

  • raw (string|bytes)
  • method (string) — simple or relaxed.

Returns bytes

Raises DkimError if method is neither.

canonicalise_header()

mail.dkim.canonicalise_header(name: string, value: string, method: string)

Canonicalises one header.

Parameters

  • name (string)
  • value (string) — The value as it was written, folding and all.
  • method (string) — simple or relaxed.

Returns — string, without the line break that ends it.

Raises DkimError if method is neither.

public_key_pem()

mail.dkim.public_key_pem(encoded: string, algorithm: string) -> string

Turns the p= value of a key record into a PEM the crypto module will accept.

An RSA key is published as the whole SubjectPublicKeyInfo structure and only needs wrapping. An Ed25519 key is published as the 32 raw bytes, and has that structure put around it here.

Parameters

  • encoded (string) — The base64 from the record’s p= tag.
  • algorithm (string) — One of ALGORITHMS.

Returns string

Raises DkimError if the key will not decode.

verify()

mail.dkim.verify(message, lookup) -> list

Checks every signature on a message.

One result comes back per DKIM-Signature header, in the order they appear, whether it passed or not. A message with no signatures gives an empty list, which is not a failure: it is a message nobody signed.

import mail.dkim

for result in dkim.verify(incoming) {
  echo '${result.domain}: ${result.valid ? 'pass' : result.reason}'
}

Parameters

  • message (Message)
  • lookup (?function) — Given a name, returns the text records published under it. net.resolver is used when not given.

Returns list — of Result

Note: A message that was parsed is checked against the bytes it was parsed from, which is the only thing a signature can be checked against. Changing a parsed message and checking it again reports on the message that arrived, not the one now in hand.

is_signed()

mail.dkim.is_signed(message, lookup) -> bool

Whether a message carries at least one signature that checks out.

Parameters

  • message (Message)
  • lookup (?function)

Returns bool

Classes

DkimError

class mail.dkim.DkimError < MailError

Raised when a signature cannot be built: an algorithm this does not implement, a key that will not parse, a canonicalisation that is not one of the two.

  • printable — has a @to_string(), so echo and print() show something useful

DkimError.to_string()

mail.dkim.DkimError.to_string()

Signature

class mail.dkim.Signature

One DKIM-Signature header, read into its parts.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.dkim.Signature(tags: dict, raw: ?string)

Parameters

  • tags (dict) — The tags of the header, by name.
  • raw (?string) — The header value it was read from.

Signature.parse()

mail.dkim.Signature.parse(value: string) -> Signature

Reads a DKIM-Signature header value.

Parameters

  • value (string)

Returns Signature

Signature.record_name()

mail.dkim.Signature.record_name() -> string

The name the public key is published under.

Returns string

Signature.to_string()

mail.dkim.Signature.to_string()

Signer

class mail.dkim.Signer

Signs outgoing messages for one domain with one key.

Build one and keep it: a signer holds no connection and no state beyond its key, and signing is the only thing it does.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.dkim.Signer(domain: string, selector: string, private_key: string, options: ?dict)
optiondefaultwhat it does
algorithmrsa-sha256or ed25519-sha256
canonicalisationrelaxed/relaxedheaders then body
headersDEFAULT_HEADERSwhich headers to cover
identitynonethe i= tag, an address within the domain
expires_innoneseconds until the signature stops counting
timestamptruewhether to record when it was signed

Parameters

  • domain (string) — The domain taking responsibility.
  • selector (string) — Which of the domain’s keys this is.
  • private_key (string) — The key, PEM encoded.
  • options (?dict)

Raises DkimError if an option names something not implemented.

Signer.sign()

mail.dkim.Signer.sign(message) -> string

Signs a message, adding the DKIM-Signature header to the top of it.

The message is signed as it stands, so everything else about it has to be settled first. Changing a signed header afterwards breaks the signature.

Parameters

  • message (Message)

Returns string — the header value that was added.

Raises DkimError if the key will not sign.

Signer.to_string()

mail.dkim.Signer.to_string()

Result

class mail.dkim.Result

What checking one signature came to.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.dkim.Result(signature, valid: bool, reason: ?string)

Parameters

  • signature (Signature) — The signature that was checked.
  • valid (bool)
  • reason (?string) — Why not, when it is not valid.

Result.to_string()

mail.dkim.Result.to_string()

2026, Richard Ore and Zuri contributors

mail.encoding

import mail

mail exposes this as mail.encoding, so import mail is enough and the names are called as mail.encoding.*. import mail.encoding reaches the same definitions directly.

The encodings a message uses to get eight bit data through a seven bit pipe.

Mail headers are ASCII and mail bodies were historically ASCII too, so everything else has to be spelled out in it. Bodies use base64 or quoted-printable; headers use the encoded words of RFC 2047 and, for parameters, the continuations of RFC 2231. This module implements all four in both directions, plus the character sets a message is likely to name.

import mail.encoding

echo encoding.encode_word('Grüße', 'utf-8', 'Q')
echo encoding.decode_words('=?utf-8?Q?Gr=C3=BC=C3=9Fe?=')
=?utf-8?Q?Gr=C3=BC=C3=9Fe?=
Grüße

Constants

TRANSFER_ENCODINGS

mail.encoding.TRANSFER_ENCODINGS = [...]

Functions

is_ascii()

mail.encoding.is_ascii(data) -> bool

Whether every byte is plain ASCII, which decides whether a header or a body needs encoding at all.

Parameters

  • data (string|bytes)

Returns bool

encode_base64()

mail.encoding.encode_base64(data, width: ?number) -> string

Encodes data as base64, wrapped into lines.

Parameters

  • data (string|bytes)
  • width (?number) — Characters per line, 76 by default. Pass 0 for one unbroken line, which is what a header needs.

Returns string

decode_base64()

mail.encoding.decode_base64(text) -> bytes

Decodes base64, ignoring the line breaks and stray whitespace a message carries it with.

Parameters

  • text (string|bytes)

Returns bytes

Raises MessageError if what is left is not base64.

encode_quoted_printable()

mail.encoding.encode_quoted_printable(data) -> string

Encodes data as quoted-printable.

Bytes that can stand for themselves do; the rest become =XX. Lines are kept within 76 characters with the soft breaks the encoding defines, and whitespace at the end of a line is encoded so that a transport which trims it cannot change what was sent.

import mail.encoding

echo encoding.encode_quoted_printable('café = good')
caf=C3=A9 =3D good

Parameters

  • data (string|bytes)

Returns string

decode_quoted_printable()

mail.encoding.decode_quoted_printable(text) -> bytes

Decodes quoted-printable.

Tolerant of what real messages contain: a soft break with a bare newline rather than a carriage return and newline, and a lone = that is not followed by two hex digits, which is passed through rather than treated as a failure.

Parameters

  • text (string|bytes)

Returns bytes

decode_text()

mail.encoding.decode_text(raw, charset: ?string) -> string

Turns bytes into text, reading them as the character set names.

The sets a message is likely to name are understood outright: UTF-8, US-ASCII, the ISO 8859 Latin sets 1 and 15, windows-1252, and UTF-16 in either byte order. A set this does not know is read as UTF-8, which leaves the ASCII in it intact rather than discarding the part that would have been readable.

Parameters

  • raw (string|bytes)
  • charset (?string) — utf-8 when not given.

Returns string

encode_word()

mail.encoding.encode_word(text: string, charset: ?string, method: ?string) -> string

Encodes text as one or more RFC 2047 encoded words, so that a header can carry characters a header is not allowed to contain.

Text that is already ASCII is returned untouched, since a word that encodes nothing only makes the header harder to read.

Long text becomes several words separated by a space, because one word may not exceed 75 characters. Words are split on character boundaries, never in the middle of one.

Parameters

  • text (string)
  • charset (?string) — utf-8 when not given.
  • method (?string) — B or Q. Chosen by what the text looks like when not given: Q when most of it is ASCII, B otherwise.

Returns string

decode_words()

mail.encoding.decode_words(text: string) -> string

Reads the encoded words out of a header value and puts back the text they stand for.

Whitespace between two adjacent encoded words is dropped, which is what lets a long subject be split across several of them without a space appearing where none was written.

Anything that is not a well-formed encoded word is left exactly as it is, including text that merely begins with =?.

import mail.encoding

echo encoding.decode_words('=?utf-8?B?SGVsbG8=?= =?utf-8?B?IHdvcmxk?=')
Hello world

Parameters

  • text (string)

Returns string

encode_parameter()

mail.encoding.encode_parameter(name: string, value: string) -> list

Writes one parameter of a header, choosing the form its value needs.

A short ASCII value is quoted and written as it is. A value with characters a header cannot carry is written in the extended form of RFC 2231, which names the character set. A long value is split across numbered continuations, because a parameter may not run past the end of a line.

import mail.encoding

echo '; '.join(encoding.encode_parameter('filename', 'report.pdf'))
echo '; '.join(encoding.encode_parameter('filename', 'résumé.pdf'))
filename="report.pdf"
filename*=utf-8''r%C3%A9sum%C3%A9.pdf

Parameters

  • name (string)
  • value (string)

Returns list — of string, each key=value ready to be joined with '; '

decode_parameters()

mail.encoding.decode_parameters(raw: dict) -> dict

Puts the parameters of a header back together.

Takes them as they were written, continuations and extended forms and all, and hands back one value per parameter with the pieces joined and the character set applied.

Parameters

  • raw (dict) — Parameters by the name they were written under, including the *0* and * suffixes.

Returns dict

encode_body()

mail.encoding.encode_body(data, encoding: string) -> string

Encodes a part’s body the way its Content-Transfer-Encoding says.

7bit, 8bit and binary describe the data rather than change it, so they hand it back as it is.

Parameters

  • data (string|bytes)
  • encoding (string)

Returns string

Raises MessageError if encoding is not one this understands.

decode_body()

mail.encoding.decode_body(data, encoding: ?string) -> bytes

Decodes a part’s body the way its Content-Transfer-Encoding says.

An encoding this does not understand is treated as 8bit and handed back untouched, because a part whose encoding cannot be read is still better delivered than discarded.

Parameters

  • data (string|bytes)
  • encoding (?string) — 7bit when not given.

Returns bytes

guess_encoding()

mail.encoding.guess_encoding(data) -> string

Picks the transfer encoding a body should use.

ASCII text that fits inside the line limit needs none. Text that is mostly ASCII is cheaper as quoted-printable, which leaves it readable; anything else is smaller as base64.

Parameters

  • data (string|bytes)

Returns string — one of 7bit, quoted-printable or base64


2026, Richard Ore and Zuri contributors

mail.errors

import mail

Everything here is re-exported by mail, so import mail is enough and the names are called as mail.*. Importing mail.errors on its own works too and reaches the same definitions.

Every error the mail stack raises, under one root.

MailError is that root. The subclasses separate the cases worth handling differently: a message that will not parse, a server that refused, credentials that were not accepted, a connection that went away mid-conversation.

The protocols each add one of their own, carrying the reply the server actually sent. SMTP splits its into two, because the difference between “not now” and “not ever” is the difference between queueing a message and bouncing it.

Classes

MailError

class mail.MailError < Error

Base class for every error this module raises.

  • printable — has a @to_string(), so echo and print() show something useful

MailError.to_string()

mail.MailError.to_string()

MessageError

class mail.MessageError < MailError

The bytes handed over are not a message, or are one that contradicts itself: a header with no colon in it, a multipart body whose boundary never appears, a transfer encoding that decodes to nothing.

  • printable — has a @to_string(), so echo and print() show something useful

MessageError.to_string()

mail.MessageError.to_string()

ProtocolError

class mail.ProtocolError < MailError

The server said something the protocol does not allow: a greeting that is not a greeting, a response with no tag, a line longer than the specification permits.

  • printable — has a @to_string(), so echo and print() show something useful

ProtocolError.to_string()

mail.ProtocolError.to_string()

ConnectionClosed

class mail.ConnectionClosed < MailError

The connection closed while a conversation was still going on, or a command was issued on one that had already closed.

  • printable — has a @to_string(), so echo and print() show something useful

ConnectionClosed.to_string()

mail.ConnectionClosed.to_string()

AuthenticationError

class mail.AuthenticationError < MailError

The credentials were not accepted, or no mechanism both ends understand was on offer.

  • printable — has a @to_string(), so echo and print() show something useful

AuthenticationError.to_string()

mail.AuthenticationError.to_string()

StateError

class mail.StateError < MailError

A command was issued that makes no sense in the state the connection is in: a fetch before a mailbox has been selected, a recipient before a sender.

  • printable — has a @to_string(), so echo and print() show something useful

StateError.to_string()

mail.StateError.to_string()

SmtpError

class mail.SmtpError < MailError

The server rejected a command, and said so with a reply code.

code is the three digit code, and enhanced the finer grained one from RFC 3463 when the server sends one, as '5.7.1'. Servers differ in how much detail they put in text, and none of it is meant to be matched on.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.SmtpError(message: string, code: number, enhanced: ?string, lines: ?list)

Parameters

  • message (string)
  • code (number) — The three digit reply code.
  • enhanced (?string) — The enhanced status code, when sent.
  • lines (?list) — Every line of the reply, without its code.

SmtpError.to_string()

mail.SmtpError.to_string()

SmtpTransientError

class mail.SmtpTransientError < SmtpError

A 4xx reply: the server could not accept the message now, and the sender should try again later. A queue retries these.

  • printable — has a @to_string(), so echo and print() show something useful

SmtpTransientError.to_string()

mail.SmtpTransientError.to_string()

SmtpPermanentError

class mail.SmtpPermanentError < SmtpError

A 5xx reply: the server will not accept the message, and trying again changes nothing. A queue bounces these.

  • printable — has a @to_string(), so echo and print() show something useful

SmtpPermanentError.to_string()

mail.SmtpPermanentError.to_string()

ImapError

class mail.ImapError < MailError

The IMAP server answered a command with NO or BAD.

status is which of the two it was. NO means the command was understood and refused; BAD means it was not understood, which points at the client rather than the request.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.ImapError(message: string, status: string, code: ?string)

Parameters

  • message (string)
  • status (string) — NO or BAD.
  • code (?string) — The bracketed response code, such as TRYCREATE, when the server sent one.

ImapError.to_string()

mail.ImapError.to_string()

Pop3Error

class mail.Pop3Error < MailError

The POP3 server answered a command with -ERR.

  • printable — has a @to_string(), so echo and print() show something useful

Pop3Error.to_string()

mail.Pop3Error.to_string()

MailboxError

class mail.MailboxError < MailError

Something went wrong in a mail store: a mailbox that does not exist, one that cannot be created, a message that has gone from under a session that was reading it.

  • printable — has a @to_string(), so echo and print() show something useful

MailboxError.to_string()

mail.MailboxError.to_string()

2026, Richard Ore and Zuri contributors

mail.headers

import mail.headers

mail lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled mail.headers.* needs import mail.headers.

The header block of a message: what is in it, in what order, and how it is written back out.

Mail headers are not a dictionary. The same name can appear more than once and the order matters, which is why Received traces a route and why a signature covers the headers it covers. Headers keeps them as they were, matches names without regard to case, and hands back one value or all of them as the caller asks.

import mail.headers { Headers }

var headers = Headers()

headers.set('Subject', 'Quarterly report')
headers.add('Received', 'from a.example.com')
headers.add('Received', 'from b.example.com')

echo headers.get('subject', nil)
echo headers.get_all('Received').length()
Quarterly report
2

Constants

LINE_WIDTH

mail.headers.LINE_WIDTH = 78

ADDRESS_HEADERS

mail.headers.ADDRESS_HEADERS = [...]

Functions

unfold()

mail.headers.unfold(text: string)

fold()

mail.headers.fold(name: string, value: string, width: ?number) -> string

Writes one header, folded so that no line runs past the width.

Folding happens at whitespace, which is the only place it is allowed to. A single run of characters with no whitespace in it cannot be folded and is left to overrun, because breaking it would change what it says.

import mail.headers

var folded = headers.fold('Subject', ('word ' * 20).trim(), 40)

echo folded.lines().length()
3

Parameters

  • name (string)
  • value (string)
  • width (?number) — 78 when not given.

Returns string — with \r\n between the lines it produced

Classes

Headers

class mail.Headers

The headers of a message or of one of its parts.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.Headers(entries: ?list)

Parameters

  • entries (?list) — Pairs of [name, value] to start with.

Headers.parse()

mail.Headers.parse(text) -> Headers

Reads a header block.

Lines that continue the one before them are joined back together. A line that is not a header and does not continue one is skipped, which is what lets a message with an mbox separator in front of it still be read.

Parameters

  • text (string|bytes) — The block, without the blank line that ends it.

Returns Headers

Headers.add()

mail.Headers.add(name: string, value) -> Headers

Adds a header, leaving any already there alone.

Parameters

  • name (string)
  • value (string)

Returns Headers — itself.

Headers.prepend()

mail.Headers.prepend(name: string, value) -> Headers

Adds a header in front of every other one.

A header that records what happened to a message on its way goes at the top, so that reading down the block reads backwards through the message’s history. Received and DKIM-Signature are both added this way.

Parameters

  • name (string)
  • value (string)

Returns Headers — itself.

Headers.set()

mail.Headers.set(name: string, value) -> Headers

Sets a header, removing any already there.

Parameters

  • name (string)
  • value (string)

Returns Headers — itself.

Headers.get()

mail.Headers.get(name: string, fallback) -> string|any

The first value of a header, or fallback when it is not there.

The value comes back as it was written. Use decoded() when it may carry encoded words.

Parameters

  • name (string)
  • fallback (?any)

Returns string|any

Headers.get_all()

mail.Headers.get_all(name: string) -> list

Every value of a header, in the order they appear.

Parameters

  • name (string)

Returns list — of string

Headers.decoded()

mail.Headers.decoded(name: string, fallback) -> string|any

The first value of a header with its encoded words decoded.

Parameters

  • name (string)
  • fallback (?any)

Returns string|any

Headers.remove()

mail.Headers.remove(name: string) -> number

Removes every instance of a header.

Parameters

  • name (string)

Returns number — how many were removed.

Headers.contains()

mail.Headers.contains(name: string) -> bool

Whether a header is present.

Parameters

  • name (string)

Returns bool

Headers.names()

mail.Headers.names() -> list

The names of every header, in order and as they were written. A name that appears more than once appears here more than once.

Returns list — of string

Headers.entries()

mail.Headers.entries() -> list

Every header as a { name, value } dictionary, in order.

Returns list — of dict

Headers.length()

mail.Headers.length() -> number

How many headers there are, counting repeats separately.

Returns number

Headers.is_empty()

mail.Headers.is_empty() -> bool

Returns bool

Headers.clone()

mail.Headers.clone() -> Headers

A copy that can be changed without changing this one.

Returns Headers

Headers.addresses()

mail.Headers.addresses(name: string) -> list

The addresses in a header, across every instance of it.

import mail.headers { Headers }

var headers = Headers([['To', 'Ann <ann@example.com>, bob@example.com']])

echo headers.addresses('to').map(@(person) => person.address)
[ann@example.com, bob@example.com]

Parameters

  • name (string)

Returns list — of Address

Headers.set_addresses()

mail.Headers.set_addresses(name: string, people) -> Headers

Sets a header to a list of addresses.

Parameters

  • name (string)
  • people (list|string|Address) — One address or several, as Address values or as text to be parsed.

Returns Headers — itself.

Headers.date()

mail.Headers.date(name: ?string) -> Date|nil

A header read as a date, or nil when it is absent or unreadable.

Parameters

  • name (?string) — Date when not given.

Returns Date|nil

Headers.set_date()

mail.Headers.set_date(name: string, moment) -> Headers

Sets a header to a date, written the way RFC 5322 wants one.

Parameters

  • name (string)
  • moment (?Date) — The current time when not given.

Returns Headers — itself.

Headers.content_type()

mail.Headers.content_type() -> ContentType

The Content-Type of this part.

Returns ContentType

Headers.content_disposition()

mail.Headers.content_disposition() -> ContentDisposition

The Content-Disposition of this part.

Returns ContentDisposition

Headers.to_string()

mail.Headers.to_string() -> string

The whole block, folded, with every line ended by a carriage return and newline. The blank line that separates the headers from the body is not included.

Returns string


2026, Richard Ore and Zuri contributors

mail.imap

import mail

Everything here is re-exported by mail, so import mail is enough and the names are called as mail.*. Importing mail.imap on its own works too and reaches the same definitions.

IMAP: reading mail where it is kept, rather than taking it away.

Both ends are here. ImapClient talks to a server; ImapServer is one, answering out of a MailStore of which two ship.

import mail.imap { ImapClient }

var inbox = ImapClient.connect('imaps://mail.example.com', {
  username: 'ann',
  password: secret,
})

inbox.select('INBOX')

echo inbox.search('UNSEEN', true).length()

inbox.logout()

Submodules

ModuleReached asSummary
mail.imap.clientmail.*The reading end of IMAP.
mail.imap.parserimport mail.imap.parserThe IMAP grammar: turning what a server says into something a program can read.
mail.imap.servermail.*The serving end of IMAP.
mail.imap.storemail.*Where an ImapServer keeps mail.

2026, Richard Ore and Zuri contributors

mail.imap.client

import mail

Everything here is re-exported by mail, so import mail is enough and the names are called as mail.*. Importing mail.imap.client on its own works too and reaches the same definitions.

The reading end of IMAP.

ImapClient connects to a server, opens a mailbox, and reads what is in it. Unlike POP3, nothing is downloaded that was not asked for: a message’s headers, one part of it, or the whole of it, as needed.

import mail.imap { ImapClient }

var inbox = ImapClient.connect('imaps://mail.example.com', {
  username: 'ann',
  password: secret,
})

inbox.select('INBOX')

for id in inbox.search('UNSEEN') {
  echo inbox.fetch_message(id).subject()
}

inbox.logout()

Constants

IMAP_SCHEMES

mail.IMAP_SCHEMES = {...}

STATES

mail.STATES = [...]

Classes

Mailbox

class mail.Mailbox

An open mailbox, and what the server said about it when it opened.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.Mailbox(name: string)

Parameters

  • name (string)

Mailbox.to_string()

mail.Mailbox.to_string()

MailboxInfo

class mail.MailboxInfo

One mailbox as list() reports it: its name, the character that separates the levels of it, and what the server says about it.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.MailboxInfo(name: string, flags: list, delimiter: ?string)

Parameters

  • name (string)
  • flags (list) — The attributes, such as \HasChildren.
  • delimiter (?string) — The hierarchy separator, or nil for a server with no hierarchy at all.

MailboxInfo.is_selectable()

mail.MailboxInfo.is_selectable() -> bool

Whether the server says this name cannot be selected, which is what a folder that only holds other folders looks like.

Returns bool

MailboxInfo.to_string()

mail.MailboxInfo.to_string()

Envelope

class mail.Envelope

The addresses and dates out of a message’s headers, as the server parsed them, so a list of messages can be shown without fetching any of them.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.Envelope(values: list)

Envelope.sent_at()

mail.Envelope.sent_at() -> Date|nil

The date as a Date, or nil when the server sent none or it cannot be read.

Returns Date|nil

Envelope.to_string()

mail.Envelope.to_string()

BodyPart

class mail.BodyPart

What one message is made of, without fetching it: the type of each part, its size, and where in the message it sits.

section is the number a BODY[...] fetch uses to ask for that part on its own.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.BodyPart(type: string, subtype: string, parameters: dict, size: ?number, section: string)

Parameters

  • type (string)
  • subtype (string)
  • parameters (dict)
  • size (?number)
  • section (string)

BodyPart.mime_type()

mail.BodyPart.mime_type() -> string

The media type, as text/plain.

Returns string

BodyPart.walk()

mail.BodyPart.walk() -> list

Every part inside this one and itself, outermost first.

Returns list — of BodyPart

BodyPart.to_string()

mail.BodyPart.to_string()

MessageInfo

class mail.MessageInfo

What a FETCH returned about one message.

Which fields are filled in depends on what was asked for. parts holds each BODY[...] section that came back, keyed by the section that was requested.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.MessageInfo(sequence: number)

Parameters

  • sequence (number) — The message’s position in the mailbox.

MessageInfo.message()

mail.MessageInfo.message() -> Message|nil

The whole message, when the fetch asked for it.

Returns Message|nil

MessageInfo.is_seen()

mail.MessageInfo.is_seen() -> bool

Whether the message has been read.

Returns bool

MessageInfo.to_string()

mail.MessageInfo.to_string()

ImapClient

class mail.ImapClient

A connection to a server that stores mail.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.ImapClient(connection, options: ?dict)

Builds a client around a connection that is already open.

Parameters

  • connection (LineStream)
  • options (?dict) — As connect().

ImapClient.connect()

mail.ImapClient.connect(url: string, options: ?dict) -> ImapClient

Opens a connection and gets as far as being able to select a mailbox.

optiondefaultwhat it does
username, passwordnoneauthenticate when both are given
tokennoneauthenticate with a bearer token instead
tlsrequirerequire, prefer or disable, when not already encrypted
tls_configa default onethe trust settings for TLS
mechanismsall of themrestricts which are acceptable
timeout60000milliseconds to wait on the socket

Parameters

  • url (string) — imaps://host, imap://host, or a bare host.
  • options (?dict)

Returns ImapClient

Raises ImapError if the server refuses.

Raises AuthenticationError if it refuses the credentials.

ImapClient.begin()

mail.ImapClient.begin() -> ImapClient

Reads the greeting, negotiates TLS and authenticates.

Returns ImapClient — itself.

Raises ImapError if the greeting says the server is not available.

ImapClient.supports()

mail.ImapClient.supports(name: string) -> bool

Whether the server offers a capability.

Parameters

  • name (string) — Matched without regard to case.

Returns bool

ImapClient.is_secure()

mail.ImapClient.is_secure() -> bool

Whether the connection is encrypted.

Returns bool

ImapClient.refresh_capabilities()

mail.ImapClient.refresh_capabilities() -> list

Asks the server what it can do, replacing what is known.

Returns list — of string

ImapClient.start_tls()

mail.ImapClient.start_tls() -> ImapClient

Negotiates TLS over a connection that started in the clear, and asks what the server can do again, because nothing it said before the handshake was protected.

Returns ImapClient — itself.

Raises ImapError if the server refuses.

ImapClient.login()

mail.ImapClient.login(mechanism: ?string) -> ImapClient

Authenticates, choosing the strongest mechanism both ends know.

A server may forbid a mechanism outright by advertising LOGINDISABLED, which is how it says the connection is not private enough; this refuses to try in that case rather than sending the password anyway.

Parameters

  • mechanism (?string) — Forces one rather than choosing.

Returns ImapClient — itself.

Raises AuthenticationError if there is no mechanism in common or the credentials are refused.

ImapClient.authenticate()

mail.ImapClient.authenticate(mechanism) -> ImapClient

Runs one authentication exchange with a mechanism already built.

Parameters

  • mechanism (Mechanism)

Returns ImapClient — itself.

Raises AuthenticationError if the server refuses.

ImapClient.run()

mail.ImapClient.run(name: string, arguments: list) -> list

Sends a command and reads everything it draws, up to and including the tagged answer.

Arguments are written as they are: a plain string goes unquoted, so quote anything that needs it with parser.quote() first. A Literal is sent as a literal, waiting for the server’s permission unless it said permission is not needed.

Parameters

  • name (string)
  • arguments (list)

Returns list — of the untagged responses the command produced.

Raises ImapError if the server answers NO or BAD.

ImapClient.on_event()

mail.ImapClient.on_event(listener) -> ImapClient

Calls a function with every untagged response the server sends.

This is how a program watches a mailbox: the server announces new mail and changed flags whenever it likes, and idle() exists so there is something to announce them during.

Parameters

  • listener (?function(1)) — nil stops listening.

Returns ImapClient — itself.

ImapClient.select()

mail.ImapClient.select(name: string) -> Mailbox

Opens a mailbox for reading and writing.

Parameters

  • name (string)

Returns Mailbox

Raises ImapError if there is no such mailbox.

ImapClient.examine()

mail.ImapClient.examine(name: string) -> Mailbox

Opens a mailbox without being able to change anything in it, which also means reading a message does not mark it read.

Parameters

  • name (string)

Returns Mailbox

Raises ImapError if there is no such mailbox.

ImapClient.close_mailbox()

mail.ImapClient.close_mailbox() -> ImapClient

Closes the open mailbox, removing the messages marked deleted on the way out.

Returns ImapClient — itself.

ImapClient.unselect()

mail.ImapClient.unselect() -> ImapClient

Closes the open mailbox without removing anything, which is what CLOSE cannot do.

Returns ImapClient — itself.

Raises StateError if the server does not offer UNSELECT.

ImapClient.list()

mail.ImapClient.list(reference: ?string, pattern: ?string) -> list

The mailboxes matching a pattern.

* matches anything including the hierarchy separator, and % matches anything except it, which is what lists one level.

for box in inbox.list('', '*') {
  echo box.name
}

Parameters

  • reference (?string) — The prefix to search under. Empty when not given.
  • pattern (?string) — * when not given.

Returns list — of MailboxInfo

ImapClient.lsub()

mail.ImapClient.lsub(reference: ?string, pattern: ?string) -> list

The mailboxes matching a pattern that this account is subscribed to.

Parameters

  • reference (?string)
  • pattern (?string)

Returns list — of MailboxInfo

ImapClient.status()

mail.ImapClient.status(name: string, items: ?list) -> dict

What is in a mailbox without opening it.

echo inbox.status('INBOX', ['MESSAGES', 'UNSEEN'])

Parameters

  • name (string)
  • items (?list) — MESSAGES, RECENT, UIDNEXT, UIDVALIDITY and UNSEEN. All five when not given.

Returns dict — of the items asked for, by name.

ImapClient.create()

mail.ImapClient.create(name: string) -> ImapClient

Creates a mailbox.

Parameters

  • name (string)

Returns ImapClient — itself.

Raises ImapError if it exists already or the name is not allowed.

ImapClient.delete()

mail.ImapClient.delete(name: string) -> ImapClient

Deletes a mailbox and everything in it.

Parameters

  • name (string)

Returns ImapClient — itself.

Raises ImapError if there is no such mailbox.

ImapClient.rename()

mail.ImapClient.rename(from: string, to: string) -> ImapClient

Renames a mailbox.

Parameters

  • from (string)
  • to (string)

Returns ImapClient — itself.

ImapClient.subscribe()

mail.ImapClient.subscribe(name: string) -> ImapClient

Marks a mailbox as one this account wants to see.

Parameters

  • name (string)

Returns ImapClient — itself.

ImapClient.unsubscribe()

mail.ImapClient.unsubscribe(name: string) -> ImapClient

Undoes subscribe().

Parameters

  • name (string)

Returns ImapClient — itself.

ImapClient.search()

mail.ImapClient.search(criteria: string, by_uid: ?bool) -> list

The messages in the open mailbox matching a search.

The criteria are IMAP’s own, which read close to English: 'UNSEEN', 'FROM ann@example.com', 'SINCE 1-Jan-2026 SMALLER 10000'.

for uid in inbox.search('UNSEEN SINCE 1-Jan-2026', true) {
  echo uid
}

Parameters

  • criteria (string)
  • by_uid (?bool) — Return unique identifiers rather than positions, which is what anything that outlives the connection wants.

Returns list — of number

Raises StateError if no mailbox is open.

ImapClient.fetch()

mail.ImapClient.fetch(ids, items: ?string, by_uid: ?bool) -> list

Fetches what the server knows about some messages.

for info in inbox.fetch('1:10', 'ENVELOPE FLAGS', false) {
  echo '${info.sequence} ${info.envelope.subject}'
}

Parameters

  • ids (string|list|number) — A set, as '1:10' or '1,3,5', or a list of numbers, or one number.
  • items (?string) — What to fetch, in IMAP’s own words. ENVELOPE FLAGS INTERNALDATE RFC822.SIZE when not given.
  • by_uid (?bool) — Whether ids are unique identifiers.

Returns list — of MessageInfo

Raises StateError if no mailbox is open.

ImapClient.fetch_message()

mail.ImapClient.fetch_message(id: number, by_uid: ?bool, mark_seen: ?bool) -> Message

Fetches one message whole.

Reading a message this way does not mark it read, because a program that goes through a mailbox should not change what a person sees when they next open it. Pass mark_seen to say otherwise, which is what a mail client showing a message wants.

echo inbox.fetch_message(uid, true, false).subject()

Parameters

  • id (number)
  • by_uid (?bool)
  • mark_seen (?bool) — Whether to set \Seen on the way.

Returns Message

Raises ImapError if there is no such message.

ImapClient.fetch_headers()

mail.ImapClient.fetch_headers(ids, by_uid: ?bool) -> list

Fetches only the headers of some messages, which is enough to show a list without pulling the bodies across.

Parameters

  • ids (string|list|number)
  • by_uid (?bool)

Returns list — of MessageInfo, each with parts holding the header block.

ImapClient.store()

mail.ImapClient.store(ids, action: string, flags: list, by_uid: ?bool) -> list

Changes the flags on some messages.

Parameters

  • ids (string|list|number)
  • action (string) — FLAGS to replace, +FLAGS to add, -FLAGS to remove. Add .SILENT to any of them to stop the server reporting the result back.
  • flags (list) — The flags, as ['\\Seen'].
  • by_uid (?bool)

Returns list — of MessageInfo describing what changed.

ImapClient.add_flags()

mail.ImapClient.add_flags(ids, flags: list, by_uid: ?bool) -> list

Adds flags to some messages, leaving the rest alone.

Parameters

  • ids (string|list|number)
  • flags (list)
  • by_uid (?bool)

Returns list — of MessageInfo

ImapClient.remove_flags()

mail.ImapClient.remove_flags(ids, flags: list, by_uid: ?bool) -> list

Removes flags from some messages.

Parameters

  • ids (string|list|number)
  • flags (list)
  • by_uid (?bool)

Returns list — of MessageInfo

ImapClient.mark_seen()

mail.ImapClient.mark_seen(ids, by_uid: ?bool) -> list

Marks messages read.

Parameters

  • ids (string|list|number)
  • by_uid (?bool)

Returns list — of MessageInfo

ImapClient.mark_deleted()

mail.ImapClient.mark_deleted(ids, by_uid: ?bool) -> list

Marks messages for removal. Nothing goes until expunge().

Parameters

  • ids (string|list|number)
  • by_uid (?bool)

Returns list — of MessageInfo

ImapClient.copy()

mail.ImapClient.copy(ids, target: string, by_uid: ?bool) -> ImapClient

Copies messages into another mailbox.

Parameters

  • ids (string|list|number)
  • target (string)
  • by_uid (?bool)

Returns ImapClient — itself.

ImapClient.move()

mail.ImapClient.move(ids, target: string, by_uid: ?bool) -> ImapClient

Moves messages into another mailbox.

Uses the server’s own MOVE where there is one, and otherwise does what MOVE was invented to replace: copy, mark deleted, expunge.

Parameters

  • ids (string|list|number)
  • target (string)
  • by_uid (?bool)

Returns ImapClient — itself.

ImapClient.expunge()

mail.ImapClient.expunge() -> list

Removes every message marked deleted in the open mailbox.

Returns list — of number: the positions that went, highest first, which is the order they have to be applied in.

ImapClient.append()

mail.ImapClient.append(mailbox: string, note, flags: ?list, received) -> ImapClient

Adds a message to a mailbox without sending it anywhere, which is how a sent message gets into the Sent folder.

Parameters

  • mailbox (string)
  • note (Message|string|bytes)
  • flags (?list) — ['\\Seen'] is the usual one for a sent message.
  • received (?Date) — The internal date to file it under. The time of arrival when not given.

Returns ImapClient — itself.

ImapClient.idle()

mail.ImapClient.idle(timeout: ?number) -> list

Waits for the server to say something, which is how a program learns about new mail without asking over and over.

Returns when the server reports anything, or when the wait runs out. A server may drop a connection that idles for too long, so even a program with nothing else to do should come back around every twenty minutes or so; RFC 2177 says as much.

inbox.on_event(@(response) {
  echo 'server said ${response.name()}'
})

while true {
  inbox.idle(1500000)
}

Parameters

  • timeout (?number) — Milliseconds to wait. 1500000, which is twenty-five minutes, when not given.

Returns list — of the untagged responses that arrived.

Raises StateError if the server does not offer IDLE.

ImapClient.noop()

mail.ImapClient.noop() -> list

Asks the server for nothing, which both keeps the connection alive and gives it a chance to report anything that has changed.

Returns list — of the untagged responses that came with it.

ImapClient.namespace()

mail.ImapClient.namespace()

The namespaces this account has: its own, other people’s, and the shared ones.

Returns — list, as the server sent it.

Raises StateError if the server does not offer NAMESPACE.

ImapClient.enable()

mail.ImapClient.enable(names: list) -> ImapClient

Turns on an extension the server offers but does not use until asked, such as UTF8=ACCEPT.

Parameters

  • names (list)

Returns ImapClient — itself.

Raises StateError if the server does not offer ENABLE.

ImapClient.id()

mail.ImapClient.id(fields: ?dict) -> dict

Tells the server what this client is, and reads back what the server is. Both sides may say nothing at all.

Parameters

  • fields (?dict) — name, version and whatever else is worth saying. A name of Zuri when not given.

Returns dict — of what the server said about itself.

ImapClient.logout()

mail.ImapClient.logout() -> ImapClient

Says goodbye and closes the connection.

Returns ImapClient — itself.

ImapClient.close()

mail.ImapClient.close() -> ImapClient

Closes the connection without saying goodbye.

Returns ImapClient — itself.

ImapClient.to_string()

mail.ImapClient.to_string()

2026, Richard Ore and Zuri contributors

mail.imap.parser

import mail.imap.parser

mail does not re-export this module, so it is reached only by importing it directly.

The IMAP grammar: turning what a server says into something a program can read.

IMAP does not speak in lines alone. A response may carry a literal, which is a byte count followed by exactly that many bytes of anything at all, including line breaks, so reading one response can mean reading several lines and a block of raw data. That is why this reads from the connection rather than from a string.

import mail.imap.parser

var tokens = parser.tokenize('(\\Seen \\Answered) "/" "INBOX"')

echo tokens[0]
echo tokens[2]
[\Seen, \Answered]
INBOX

Constants

STATUSES

mail.parser.STATUSES = [...]

SYSTEM_FLAGS

mail.parser.SYSTEM_FLAGS = [...]

Functions

tokenize()

mail.parser.tokenize(text: string, source) -> list

Reads a run of IMAP values out of text.

source is only needed when the text can contain a literal, which is to say whenever it came from a server rather than from a test.

Parameters

  • text (string)
  • source (?LineStream) — Where to read a literal’s bytes from.

Returns list

Raises ProtocolError if the text is not well formed.

quote()

mail.parser.quote(value: string) -> string|Literal

Writes a value the way a command has to carry it.

A name that is plain enough goes as it is, one that is not is quoted, and one that cannot be quoted at all, because it holds a line break or characters outside ASCII, is sent as a literal.

Parameters

  • value (string)

Returns string|Literal

Classes

Literal

class mail.parser.Literal

A run of bytes a server sent as a literal.

Kept apart from an ordinary string because a literal is where a message body arrives, and a body is bytes: decoding it as text would corrupt anything that is not text.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.parser.Literal(data)

Parameters

  • data (bytes)

Literal.to_text()

mail.parser.Literal.to_text() -> string

The literal read as UTF-8 text.

Returns string

Literal.length()

mail.parser.Literal.length() -> number

How many bytes it holds.

Returns number

Literal.to_string()

mail.parser.Literal.to_string()

Response

class mail.parser.Response

One response from the server.

kind is tagged for the answer to a command, untagged for everything a server says on its own, and continuation for the invitation to send the rest of a command.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.parser.Response(kind: string, tag: ?string, tokens: list, line: string)

Parameters

  • kind (string)
  • tag (?string)
  • tokens (list) — The values in the response, in order.
  • line (string) — The first line it was read from.

Response.is_ok()

mail.parser.Response.is_ok() -> bool

Whether this is a command’s answer and the answer was yes.

Returns bool

Response.name()

mail.parser.Response.name() -> string|nil

The name of an untagged data response, such as EXISTS, FETCH or LIST, or nil when it is not one.

A numbered response puts the number first, so the name is the second value in those and the first in the rest.

Returns string|nil

Response.number()

mail.parser.Response.number() -> number|nil

The number a numbered response carries, such as the message number in * 12 FETCH, or nil when it carries none.

Returns number|nil

Response.arguments()

mail.parser.Response.arguments() -> list

The values after the response’s name.

Returns list

Response.to_string()

mail.parser.Response.to_string()

Reader

class mail.parser.Reader

Reads responses off a connection, one at a time.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.parser.Reader(source)

Parameters

  • source (LineStream)

Reader.next()

mail.parser.Reader.next() -> Response

Reads the next response, however many lines and literals it takes.

Returns Response

Raises ProtocolError if what arrives is not a response.

Raises ConnectionClosed if the connection ends first.

Reader.to_string()

mail.parser.Reader.to_string()

2026, Richard Ore and Zuri contributors

mail.imap.server

import mail

Everything here is re-exported by mail, so import mail is enough and the names are called as mail.*. Importing mail.imap.server on its own works too and reaches the same definitions.

The serving end of IMAP.

ImapServer accepts connections, runs the session state machine, and answers every command out of a MailStore. What mail there is and where it lives is the store’s business; this knows only the protocol.

import mail.imap { ImapServer, MaildirStore }

var store = MaildirStore('/var/mail')

store.add_account('ann', secret)

ImapServer({ port: 143 }, store).listen()

Constants

BASE_CAPABILITIES

mail.BASE_CAPABILITIES = [...]

ALLOWED_FLAGS

mail.ALLOWED_FLAGS = [...]

DEFAULT_TIMEOUT

mail.DEFAULT_TIMEOUT = 1800000

Classes

ImapSession

class mail.ImapSession

One connection, and what it has got as far as doing.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.ImapSession(peer: string, secure: bool)

Parameters

  • peer (string)
  • secure (bool)

ImapSession.is_selected()

mail.ImapSession.is_selected() -> bool

Whether a mailbox is open.

Returns bool

ImapSession.to_string()

mail.ImapSession.to_string()

ImapServer

class mail.ImapServer

A server that serves mail.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.ImapServer(options: ?dict, store)
optiondefaultwhat it does
host127.0.0.1the address to bind
port143the port to bind
greetingZuri IMAP readywhat to say on connecting
timeout1800000milliseconds a client may go quiet
require_tlsfalserefuse to authenticate without TLS

Parameters

  • options (?dict)
  • store (MailStore) — Where the mail is.

ImapServer.on_connect()

mail.ImapServer.on_connect(handler) -> ImapServer

Called with the session when a connection opens.

Parameters

  • handler (function(1))

Returns ImapServer — itself.

ImapServer.on_close()

mail.ImapServer.on_close(handler) -> ImapServer

Called with the session when a connection closes.

Parameters

  • handler (function(1))

Returns ImapServer — itself.

ImapServer.on_error()

mail.ImapServer.on_error(handler) -> ImapServer

Called with the error and the session when something inside the server fails.

Parameters

  • handler (function(2))

Returns ImapServer — itself.

ImapServer.use_tls()

mail.ImapServer.use_tls(cert_chain: string, private_key: string) -> ImapServer

Gives the server a certificate, which is what lets it offer STARTTLS.

Parameters

  • cert_chain (string)
  • private_key (string)

Returns ImapServer — itself.

ImapServer.set_tls_config()

mail.ImapServer.set_tls_config(config) -> ImapServer

Gives the server a TLS configuration built elsewhere.

Parameters

  • config (TlsConfig)

Returns ImapServer — itself.

ImapServer.bind()

mail.ImapServer.bind() -> ImapServer

Binds the listening socket without accepting anything yet.

Returns ImapServer — itself.

ImapServer.address()

mail.ImapServer.address() -> SocketAddr

The address the server is listening on.

Returns SocketAddr

ImapServer.accept()

mail.ImapServer.accept() -> ImapServer

Accepts one connection and serves it to the end.

Returns ImapServer — itself.

ImapServer.listen()

mail.ImapServer.listen() -> ImapServer

Accepts connections one after another until close().

One at a time: an IMAP connection is long-lived, so a server expecting more than one client at once wants a worker per connection.

Returns ImapServer — itself.

ImapServer.close()

mail.ImapServer.close() -> ImapServer

Stops the accept loop and closes the listening socket.

Returns ImapServer — itself.

ImapServer.is_listening()

mail.ImapServer.is_listening() -> bool

Whether the accept loop is running.

Returns bool

ImapServer.serve_connection()

mail.ImapServer.serve_connection(client) -> ImapServer

Serves one already-accepted connection to the end.

Parameters

  • client (TcpStream)

Returns ImapServer — itself.

ImapServer.to_string()

mail.ImapServer.to_string()

2026, Richard Ore and Zuri contributors

mail.imap.store

import mail

Everything here is re-exported by mail, so import mail is enough and the names are called as mail.*. Importing mail.imap.store on its own works too and reaches the same definitions.

Where an ImapServer keeps mail.

MailStore is the contract: what a store has to be able to do for the server to answer with it. Two are provided. MaildirStore keeps mail on disk in the Maildir layout, which every other mail tool can read, so a mailbox written here is not trapped here. MemoryStore keeps it in the process and forgets it on exit, which is what a test and an embedded server want.

import mail.imap { ImapServer, MaildirStore }

var store = MaildirStore('/var/mail/ann')

store.add_account('ann', 'secret')

ImapServer({ port: 143 }, store).listen()

A store of your own only has to answer the same calls. Nothing here is required to come from a file.

Constants

MAILDIR_PARTS

mail.MAILDIR_PARTS = [...]

INFO_SEPARATOR

mail.INFO_SEPARATOR

MAILDIR_FLAGS

mail.MAILDIR_FLAGS = {...}

DELIMITER

mail.DELIMITER = '.'

INDEX_FILE

mail.INDEX_FILE = 'zuri-uidlist'

Classes

StoredMessage

class mail.StoredMessage

One message in a store.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.StoredMessage(uid: number, raw, flags: list, received)

Parameters

  • uid (number) — Unique within its mailbox, and never reused.
  • raw (bytes) — The message exactly as it arrived.
  • flags (list)
  • received (Date) — When the store took it.

StoredMessage.size()

mail.StoredMessage.size() -> number

How large the message is.

Returns number

StoredMessage.has_flag()

mail.StoredMessage.has_flag(flag: string) -> bool

Whether a flag is set.

Parameters

  • flag (string)

Returns bool

StoredMessage.to_string()

mail.StoredMessage.to_string()

MailStore

class mail.MailStore

What a store has to be able to do.

Every method raises here; a store is something that answers them. Both of the ones that ship do, and so can one of your own.

  • printable — has a @to_string(), so echo and print() show something useful

MailStore.authenticate()

mail.MailStore.authenticate(username: string, password: string) -> bool

Whether these credentials name an account.

Parameters

  • username (string)
  • password (string)

Returns bool

MailStore.password_of()

mail.MailStore.password_of(username: string) -> string|nil

The password of an account, for the mechanisms that prove it without sending it, or nil when there is no such account or the store cannot produce one.

A store that keeps only hashed passwords returns nil, and the server then does not offer those mechanisms.

Parameters

  • username (string)

Returns string|nil

MailStore.has_passwords()

mail.MailStore.has_passwords() -> bool

Whether this store can produce a password rather than only check one.

A server offers the mechanisms that prove a password without sending it only when it can work out the same proof, which means knowing the password. A store that keeps only hashes, or that checks credentials somewhere else entirely, says no here and the server stops offering them.

Returns bool

MailStore.mailboxes()

mail.MailStore.mailboxes(username: string) -> list

The mailboxes an account has.

Parameters

  • username (string)

Returns list — of string

MailStore.exists()

mail.MailStore.exists(username: string, mailbox: string) -> bool

Whether a mailbox exists.

Parameters

  • username (string)
  • mailbox (string)

Returns bool

MailStore.create()

mail.MailStore.create(username: string, mailbox: string)

Creates a mailbox.

Parameters

  • username (string)
  • mailbox (string)

Raises MailboxError if it exists already.

MailStore.remove()

mail.MailStore.remove(username: string, mailbox: string)

Removes a mailbox and everything in it.

Parameters

  • username (string)
  • mailbox (string)

Raises MailboxError if there is no such mailbox.

MailStore.rename()

mail.MailStore.rename(username: string, from: string, to: string)

Renames a mailbox.

Parameters

  • username (string)
  • from (string)
  • to (string)

Raises MailboxError if there is no such mailbox, or the new name is taken.

MailStore.messages()

mail.MailStore.messages(username: string, mailbox: string) -> list

Every message in a mailbox, oldest first.

Parameters

  • username (string)
  • mailbox (string)

Returns list — of StoredMessage

Raises MailboxError if there is no such mailbox.

MailStore.append()

mail.MailStore.append(username: string, mailbox: string, raw, flags: ?list, received) -> StoredMessage

Adds a message to a mailbox.

Parameters

  • username (string)
  • mailbox (string)
  • raw (bytes)
  • flags (?list)
  • received (?Date)

Returns StoredMessage

Raises MailboxError if there is no such mailbox.

MailStore.set_flags()

mail.MailStore.set_flags(username: string, mailbox: string, uid: number, flags: list)

Replaces the flags on one message.

Parameters

  • username (string)
  • mailbox (string)
  • uid (number)
  • flags (list)

Raises MailboxError if there is no such message.

MailStore.expunge()

mail.MailStore.expunge(username: string, mailbox: string) -> list

Removes every message in a mailbox marked \Deleted.

Parameters

  • username (string)
  • mailbox (string)

Returns list — of number: the identifiers that went.

MailStore.counters()

mail.MailStore.counters(username: string, mailbox: string) -> dict

What the next message added to a mailbox will be numbered, and how to tell that the numbering has been reset.

Parameters

  • username (string)
  • mailbox (string)

Returns dict — of uidnext and uidvalidity.

MailStore.to_string()

mail.MailStore.to_string()

MemoryStore

class mail.MemoryStore < MailStore

A store that keeps everything in the process and nothing on disk.

Fast, and gone when the program is. This is what a test wants, and what a server embedded in something else wants when the mail it holds is not the point.

import mail.imap { MemoryStore }

var store = MemoryStore()

store.add_account('ann', 'secret')
store.append('ann', 'INBOX', 'From: a@b.com\r\n\r\nhello', nil, nil)

echo store.mailboxes('ann')
echo store.messages('ann', 'INBOX').length()
[INBOX]
1
  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.MemoryStore(mailboxes: ?list)

Parameters

  • mailboxes (?list) — The mailboxes every new account starts with. ['INBOX'] when not given.

MemoryStore.add_account()

mail.MemoryStore.add_account(username: string, password: string) -> MemoryStore

Adds an account, with the mailboxes a new account starts with.

Parameters

  • username (string)
  • password (string)

Returns MemoryStore — itself.

MemoryStore.authenticate()

mail.MemoryStore.authenticate(username: string, password: string)

MemoryStore.password_of()

mail.MemoryStore.password_of(username: string)

MemoryStore.has_passwords()

mail.MemoryStore.has_passwords()

MemoryStore.mailboxes()

mail.MemoryStore.mailboxes(username: string)

MemoryStore.exists()

mail.MemoryStore.exists(username: string, mailbox: string)

MemoryStore.create()

mail.MemoryStore.create(username: string, mailbox: string)

MemoryStore.remove()

mail.MemoryStore.remove(username: string, mailbox: string)

MemoryStore.rename()

mail.MemoryStore.rename(username: string, from: string, to: string)

MemoryStore.messages()

mail.MemoryStore.messages(username: string, mailbox: string)

MemoryStore.append()

mail.MemoryStore.append(username: string, mailbox: string, raw, flags: ?list, received)

MemoryStore.set_flags()

mail.MemoryStore.set_flags(username: string, mailbox: string, uid: number, flags: list)

MemoryStore.expunge()

mail.MemoryStore.expunge(username: string, mailbox: string)

MemoryStore.counters()

mail.MemoryStore.counters(username: string, mailbox: string)

MemoryStore.to_string()

mail.MemoryStore.to_string()

MaildirStore

class mail.MaildirStore < MailStore

A store that keeps mail on disk in the Maildir layout.

Each account is a directory under root, holding its INBOX directly and every other mailbox as .Name beside it, which is the Maildir++ arrangement every other mail tool understands. A mailbox written here can be read by anything else, and mail delivered by anything else turns up here.

Flags are kept the Maildir way, as letters at the end of a message’s filename after :2,. A colon cannot appear in a Windows filename, so on Windows the flags follow ;2, instead, which is what mbsync writes there too. A Maildir moved between Windows and any other system keeps its messages but has to have their flags renamed to match.

Mail is on disk; accounts are not. The store asks the program who its users are, either as a password given to add_account() or through set_authenticator(), because where an application keeps its passwords is the application’s business and not a mail library’s.

import mail.imap { MaildirStore }

var store = MaildirStore('/var/mail')

store.add_account('ann', secret)
store.append('ann', 'INBOX', raw, nil, nil)
  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.MaildirStore(root: string)

Parameters

  • root (string) — The directory the accounts live under. It is created if it is not there.

MaildirStore.add_account()

mail.MaildirStore.add_account(username: string, password: string) -> MaildirStore

Adds an account with a password this store will check itself, and makes its mail directory if it is not there.

Parameters

  • username (string)
  • password (string)

Returns MaildirStore — itself.

MaildirStore.set_authenticator()

mail.MaildirStore.set_authenticator(checker) -> MaildirStore

Hands credential checking to a function of your own, which is what a program with its own user table wants.

A store with an authenticator can no longer produce a password, so the server stops offering the mechanisms that need one.

Parameters

  • checker (function(2)) — Given a username and a password, returns whether they are right.

Returns MaildirStore — itself.

MaildirStore.authenticate()

mail.MaildirStore.authenticate(username: string, password: string)

MaildirStore.password_of()

mail.MaildirStore.password_of(username: string)

MaildirStore.has_passwords()

mail.MaildirStore.has_passwords()

MaildirStore.mailboxes()

mail.MaildirStore.mailboxes(username: string)

MaildirStore.exists()

mail.MaildirStore.exists(username: string, mailbox: string)

MaildirStore.create()

mail.MaildirStore.create(username: string, mailbox: string)

MaildirStore.remove()

mail.MaildirStore.remove(username: string, mailbox: string)

MaildirStore.rename()

mail.MaildirStore.rename(username: string, from: string, to: string)

MaildirStore.messages()

mail.MaildirStore.messages(username: string, mailbox: string)

MaildirStore.append()

mail.MaildirStore.append(username: string, mailbox: string, raw, flags: ?list, received)

MaildirStore.set_flags()

mail.MaildirStore.set_flags(username: string, mailbox: string, uid: number, flags: list)

MaildirStore.expunge()

mail.MaildirStore.expunge(username: string, mailbox: string)

MaildirStore.counters()

mail.MaildirStore.counters(username: string, mailbox: string)

MaildirStore.to_string()

mail.MaildirStore.to_string()

2026, Richard Ore and Zuri contributors

mail.message

import mail

Everything here is re-exported by mail, so import mail is enough and the names are called as mail.*. Importing mail.message on its own works too and reaches the same definitions.

A mail message: its headers, its body, and the tree of parts a body turns into once there is more than one thing in it.

Message is both what comes off the wire and what goes onto it. Parsing one gives the whole MIME tree with every part decoded on demand; building one from text, HTML and files gives back a message any mail server will accept.

import mail

var note = mail.message()
  .set_from('Richard Ore <richard@example.com>')
  .add_to('ann@example.com')
  .set_subject('Quarterly report')
  .set_text('The numbers are attached.')

echo note.subject()
echo note.content_type().mime_type()
Quarterly report
text/plain

A message with both text and HTML in it becomes a multipart/alternative; add a file and the whole thing is wrapped in a multipart/mixed. None of that has to be spelled out: the shape follows from what was put in.

Functions

text_part()

mail.text_part(text: string, charset: ?string, subtype: ?string) -> Message

Builds one text part.

Parameters

  • text (string)
  • charset (?string) — utf-8, or us-ascii when the text needs nothing more. No other set can be written.
  • subtype (?string) — plain when not given.

Returns Message

Raises MessageError if charset names a set this cannot write.

attachment()

mail.attachment(data) -> Attachment

Starts an attachment from what is in it.

import mail

echo mail.attachment('some bytes').set_filename('notes.txt').to_string()
<Attachment notes.txt, 10 bytes>

Parameters

  • data (string|bytes)

Returns Attachment

multipart()

mail.multipart(subtype: string, parts: list)

new_message_id()

mail.new_message_id(domain: ?string) -> string

An identifier for a message, unique enough that no two ever collide.

Parameters

  • domain (?string) — The right hand side. The machine’s own name when not given.

Returns string — including the angle brackets a header needs.

parse()

mail.parse(data) -> Message

Reads a message.

Parameters

  • data (string|bytes)

Returns Message

message()

mail.message() -> Message

Starts a message.

What goes in it is said one call at a time, and every call hands the message back so they read as one:

import mail

var note = mail.message()
  .set_from('Richard Ore <richard@example.com>')
  .add_to('ann@example.com')
  .set_subject('Quarterly report')
  .set_text('The numbers are in.')

echo note.subject()
echo note.content_type().mime_type()
Quarterly report
text/plain

The shape follows from what is said. Text alone is a text/plain message; adding HTML makes it a multipart/alternative; attaching a file wraps the whole thing in a multipart/mixed. None of that has to be arranged by hand.

A message starts dated, since the time it was written is the time it was written. It gets its identifier when it is sent, because that is when the domain it belongs to is settled.

Returns Message

Classes

Message

class mail.Message

One message, or one part of one.

A part is a message too: it has headers and a body, and its body may be more parts. The same class covers both, so walking a message and reading a standalone one are the same code.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.Message(headers, source)

Builds a message.

Most callers want mail.message(), which fills one in, or mail.parse(), which reads one off the wire.

Parameters

  • headers (?Headers) — The headers to start with.
  • source (?any) — Bytes or text to read the message from, in which case headers is ignored.

Message.parse()

mail.Message.parse(data) -> Message

Reads a message.

Nothing is decoded up front. The tree is built, and each part decodes its own body the first time something asks for it, so reading the subject of a message with a large attachment in it costs nothing more than reading the headers.

import mail

var note = mail.parse(
  'From: a@example.com\r\n'
  + 'Subject: Hi\r\n'
  + '\r\n'
  + 'Hello there.\r\n'
)

echo note.subject()
echo note.text()
Hi
Hello there.

Parameters

  • data (string|bytes)

Returns Message

Raises MessageError if the headers cannot be read.

Message.content_type()

mail.Message.content_type() -> ContentType

The Content-Type of this part.

Returns ContentType

Message.is_multipart()

mail.Message.is_multipart() -> bool

Whether this part holds other parts.

Returns bool

Message.parts()

mail.Message.parts() -> list

The parts directly inside this one, which is empty for a part that holds content rather than other parts.

Returns list — of Message

Message.walk()

mail.Message.walk() -> list

Every part of the message including this one, outermost first.

for part in note.walk() {
  echo part.content_type().mime_type()
}

Returns list — of Message

Message.body_bytes()

mail.Message.body_bytes() -> bytes

This part’s body, decoded from whatever transfer encoding it arrived in.

A part that holds other parts has no body of its own and gives back nothing.

Returns bytes

Message.text()

mail.Message.text() -> string

This part’s body as text, decoded from its transfer encoding and then from its character set.

Returns string

Message.raw_body()

mail.Message.raw_body() -> bytes

The body still encoded, exactly as it sits in the message.

Returns bytes

Message.set_raw_body()

mail.Message.set_raw_body(data) -> Message

Replaces the body with bytes that are already in the transfer encoding the headers name, and drops any parts this held.

The encoding is not applied here and not checked. set_text() and attach() are the calls that do both.

Parameters

  • data (string|bytes)

Returns Message — itself.

Message.set_parts()

mail.Message.set_parts(parts: list) -> Message

Replaces the parts this one holds.

Parameters

  • parts (list) — Message values.

Returns Message — itself.

Message.is_empty()

mail.Message.is_empty() -> bool

Whether this part holds nothing at all: no body and no parts.

Returns bool

Message.source_headers()

mail.Message.source_headers() -> string|nil

The bytes this part’s headers were read from, or nil for a part that was built rather than parsed.

A signature covers the message as it was written, not as it would be written again, which is what this is for.

Returns string|nil

Message.source_body()

mail.Message.source_body() -> bytes|nil

The bytes this part’s body was read from, or nil for a part that was built rather than parsed.

Returns bytes|nil

Message.subject()

mail.Message.subject() -> string

The subject, with any encoded words in it decoded.

Returns string — empty when the message has no subject.

Message.set_subject()

mail.Message.set_subject(text: string) -> Message

Sets the subject, encoding it if it holds characters a header cannot carry.

Parameters

  • text (string)

Returns Message — itself.

Message.sender()

mail.Message.sender() -> Address|nil

The sender, or nil when the message has no From.

Returns Address|nil

Message.to()

mail.Message.to() -> list

The addresses in To.

Returns list — of Address

Message.cc()

mail.Message.cc() -> list

The addresses in Cc.

Returns list — of Address

Message.bcc()

mail.Message.bcc() -> list

The addresses in Bcc.

A message that has been sent should not have this header at all, since the whole point of a blind copy is that the other recipients cannot see it. mail.send() strips it before handing the message over, after taking the recipients from it.

Returns list — of Address

Message.reply_to()

mail.Message.reply_to() -> list

The addresses in Reply-To, falling back to the sender when there is no such header, which is what a mail client does.

Returns list — of Address

Message.recipients()

mail.Message.recipients() -> list

Everyone the message is addressed to: To, Cc and Bcc together, with duplicates removed.

Returns list — of Address

Message.sent_at()

mail.Message.sent_at() -> Date|nil

The Date header as a date, or nil when it is absent or unreadable.

Returns Date|nil

Message.message_id()

mail.Message.message_id() -> string|nil

The Message-ID, with the angle brackets left on, or nil.

Returns string|nil

Message.references()

mail.Message.references() -> list

The message identifiers in References, oldest first, which is the chain of replies this message belongs to.

Returns list — of string

Message.filename()

mail.Message.filename() -> string|nil

The name this part suggests being saved under, from its disposition or, failing that, its type. nil when it suggests none.

Returns string|nil

Message.is_attachment()

mail.Message.is_attachment() -> bool

Whether this part is meant to be offered as a file rather than shown in place.

A part is an attachment when it says so, and also when it says nothing but carries a filename, which is what older mailers do.

Returns bool

Message.find_part()

mail.Message.find_part(mime_type: string) -> Message|nil

The first part of a given type anywhere in the message, or nil.

Parameters

  • mime_type (string) — As text/html.

Returns Message|nil

Message.text_body()

mail.Message.text_body() -> string|nil

The plain text of the message, wherever in the tree it is, or nil when there is none.

Returns string|nil

Message.html_body()

mail.Message.html_body() -> string|nil

The HTML of the message, wherever in the tree it is, or nil when there is none.

Returns string|nil

Message.attachments()

mail.Message.attachments() -> list

Every part of the message that is an attachment.

Returns list — of Message

Message.take_content()

mail.Message.take_content() -> Message

Moves this part’s content into a part of its own and hands it back, leaving this one holding the message-level headers and nothing else.

This is what turns a message into the first child of the multipart that is about to replace it, which is why attach() can wrap a message without the caller rebuilding it.

Returns Message

Message.become_multipart()

mail.Message.become_multipart(subtype: string, parts: list) -> Message

Turns this part into a multipart holding the given parts, under a boundary nothing inside it can contain.

Parameters

  • subtype (string) — mixed, alternative, related or digest.
  • parts (list) — The Message values to put in it.

Returns Message — itself.

Message.set_from()

mail.Message.set_from(person) -> Message

Sets who the message is from, replacing whoever was there.

note.set_from('Richard Ore <richard@example.com>')

Parameters

  • person (string|Address)

Returns Message — itself.

Message.set_sender()

mail.Message.set_sender(person) -> Message

Sets the Sender header, which says who actually put the message on the wire when that is not who it is from.

Parameters

  • person (string|Address)

Returns Message — itself.

Message.add_to()

mail.Message.add_to(people) -> Message

Adds recipients, keeping any already there.

note.add_to('ann@example.com').add_to('bob@example.com')

Parameters

  • people (string|Address|list)

Returns Message — itself.

Message.add_cc()

mail.Message.add_cc(people) -> Message

Adds copied recipients, keeping any already there.

Parameters

  • people (string|Address|list)

Returns Message — itself.

Message.add_bcc()

mail.Message.add_bcc(people) -> Message

Adds blind copied recipients.

The header is removed before the message is handed to a server, after the recipients have been taken from it, which is the whole point of a blind copy.

Parameters

  • people (string|Address|list)

Returns Message — itself.

Message.set_to()

mail.Message.set_to(people) -> Message

Sets the recipients, replacing any already there.

Parameters

  • people (string|Address|list)

Returns Message — itself.

Message.set_cc()

mail.Message.set_cc(people) -> Message

Sets the copied recipients, replacing any already there.

Parameters

  • people (string|Address|list)

Returns Message — itself.

Message.set_bcc()

mail.Message.set_bcc(people) -> Message

Sets the blind copied recipients, replacing any already there.

Parameters

  • people (string|Address|list)

Returns Message — itself.

Message.set_reply_to()

mail.Message.set_reply_to(people) -> Message

Sets where replies should go, when that is not the sender.

Parameters

  • people (string|Address|list)

Returns Message — itself.

Message.set_header()

mail.Message.set_header(name: string, value) -> Message

Sets any header at all, replacing one already there.

The value is written exactly as given, so a header that needs encoding needs it applied first. set_subject() and the address setters do that for the headers where it matters.

Parameters

  • name (string)
  • value (any)

Returns Message — itself.

Message.add_header()

mail.Message.add_header(name: string, value) -> Message

Adds a header, keeping any already there. Received is the header this is for.

Parameters

  • name (string)
  • value (any)

Returns Message — itself.

Message.set_date()

mail.Message.set_date(moment) -> Message

Sets when the message was written.

A message built by mail.message() is dated for you, so this is for saying otherwise.

Parameters

  • moment (?Date) — The current time when not given.

Returns Message — itself.

Message.set_message_id()

mail.Message.set_message_id(id: ?string) -> Message

Sets the message’s own identifier.

Parameters

  • id (?string) — A fresh one, built from the sender’s domain, when not given.

Returns Message — itself.

Message.set_in_reply_to()

mail.Message.set_in_reply_to(id: string) -> Message

Marks the message as a reply to another, which is what puts the two in the same conversation in a mail client.

reply.set_in_reply_to(original.message_id())
     .set_references(original.references() + [original.message_id()])

Parameters

  • id (string) — The identifier of the message being answered.

Returns Message — itself.

Message.set_references()

mail.Message.set_references(ids) -> Message

Sets the chain of identifiers this message belongs to, oldest first.

Parameters

  • ids (list|string)

Returns Message — itself.

Message.set_text()

mail.Message.set_text(text: string, charset: ?string) -> Message

Sets the plain text of the message.

Replaces the text that is already there, wherever in the tree it is. A message that has HTML but no text becomes a multipart/alternative holding both.

Parameters

  • text (string)
  • charset (?string) — utf-8 when not given.

Returns Message — itself.

Message.set_html()

mail.Message.set_html(html: string, charset: ?string) -> Message

Sets the HTML of the message.

Parameters

  • html (string)
  • charset (?string) — utf-8 when not given.

Returns Message — itself.

Message.attach()

mail.Message.attach(part) -> Message

Adds an attachment to the message.

The message becomes a multipart/mixed if it is not one already, with whatever it held before as the first part. That happens once, however many attachments are added.

note.attach(mail.attachment(figures).set_filename('figures.csv'))
note.attach(mail.Attachment.from_file('reports/q3.pdf'))

Parameters

  • part (Attachment|Message) — An attachment, or a part that is already built.

Returns Message — itself.

Raises MessageError if handed anything else.

Message.embed()

mail.Message.embed(part) -> string

Adds something to be shown inside the HTML rather than offered separately, and returns the reference to point an <img> at.

The message becomes a multipart/related around the content it already held, which is what tells a reader the two belong together.

var source = note.embed(mail.attachment(logo).set_filename('logo.png'))

note.set_html('<img src="${source}">')

Parameters

  • part (Attachment|Message)

Returns string — the cid: URL that refers to it.

Raises MessageError if handed anything else.

Message.to_bytes()

mail.Message.to_bytes() -> bytes

The whole message, ready to be written to a socket or a file.

Lines end with a carriage return and a newline, which is what the protocols require whatever the local convention is.

Returns bytes

Message.to_string()

mail.Message.to_string() -> string

The whole message as text.

Returns string

Attachment

class mail.Attachment

Something a message carries alongside what it says: a file to offer, or an image the HTML shows.

Built one call at a time, the way a message is, and handed to Message.attach() or Message.embed().

import mail

var figures = mail.attachment('name,total\nengines,41')
  .set_filename('figures.csv')

echo figures.filename()
echo figures.content_type()
echo figures.size()
figures.csv
text/csv
24

The media type follows from the filename unless it is set. The transfer encoding follows from the contents, and a file is always encoded rather than sent as it is, because bytes that happen to look like text are still bytes.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.Attachment(data)

Parameters

  • data (string|bytes) — The contents.

Attachment.from_file()

mail.Attachment.from_file(path: string) -> Attachment

Reads the contents from a file on disk, taking the name from the path.

note.attach(Attachment.from_file('reports/q3.pdf'))

Parameters

  • path (string)

Returns Attachment

Raises Error if the file cannot be read.

Attachment.set_filename()

mail.Attachment.set_filename(name: string) -> Attachment

Sets the name to offer the file under.

A name with characters a header cannot carry is encoded, and a long one is split across continuations, on the way out.

Parameters

  • name (string)

Returns Attachment — itself.

Attachment.set_content_type()

mail.Attachment.set_content_type(type: string) -> Attachment

Sets the media type, for when the filename does not settle it.

Parameters

  • type (string) — As application/pdf.

Returns Attachment — itself.

Attachment.set_disposition()

mail.Attachment.set_disposition(kind: string) -> Attachment

Sets whether the part is offered as a file or shown where it sits.

embed() sets this to inline itself, so it is only worth setting for a part that is neither quite.

Parameters

  • kind (string) — attachment or inline.

Returns Attachment — itself.

Attachment.set_cid()

mail.Attachment.set_cid(id: string) -> Attachment

Sets the identifier HTML refers to this part by.

embed() invents one when there is none, so this is for a reference that has to be predictable.

Parameters

  • id (string) — With or without the angle brackets.

Returns Attachment — itself.

Attachment.set_encoding()

mail.Attachment.set_encoding(name: string) -> Attachment

Sets the transfer encoding, overriding the one the contents would have chosen.

Parameters

  • name (string) — One of mail.encoding.TRANSFER_ENCODINGS.

Returns Attachment — itself.

Attachment.set_description()

mail.Attachment.set_description(text: string) -> Attachment

Sets a human-readable description, which some mail clients show beside the file.

Parameters

  • text (string)

Returns Attachment — itself.

Attachment.filename()

mail.Attachment.filename() -> string|nil

The name this will be offered under, or nil.

Returns string|nil

Attachment.content_type()

mail.Attachment.content_type() -> string

The media type this will be sent as, worked out from the filename when it was not set.

Returns string

Attachment.disposition()

mail.Attachment.disposition() -> string

Whether the part is offered as a file or shown where it sits.

Returns string

Attachment.cid()

mail.Attachment.cid() -> string|nil

The identifier HTML refers to this part by, or nil.

Returns string|nil

Attachment.size()

mail.Attachment.size() -> number

How many bytes the contents are.

Returns number

Attachment.data()

mail.Attachment.data() -> bytes

The contents.

Returns bytes

Attachment.to_part()

mail.Attachment.to_part() -> Message

Turns this into the message part that goes on the wire.

attach() and embed() call it. It is here for a program building a tree by hand.

Returns Message

Attachment.to_string()

mail.Attachment.to_string()

2026, Richard Ore and Zuri contributors

mail.pool

import mail

mail exposes this as mail.pool, so import mail is enough and the names are called as mail.pool.*. import mail.pool reaches the same definitions directly.

Running a mail server on more than one connection at a time.

Both servers here serve one connection to the end before taking the next, which is the right shape for the protocols and the wrong shape for more than one client. serve() puts a pool of isolates behind one listening socket: the main isolate accepts, and each worker takes a connection and sees it through.

import mail.pool
import .my_server

pool.serve(my_server.build, { host: '0.0.0.0', port: 143, workers: 8 })

build is a function in a module of its own, not a closure, because an isolate resolves a module’s function by name on its own side and cannot be handed one that closes over anything here.

# my_server.zu
import mail.imap { ImapServer, MaildirStore }

def build() {
  var store = MaildirStore('/var/mail')

  store.add_account('ann', 'secret')

  return ImapServer({}, store)
}

An IMAP connection can be open for hours, so the pool is a ceiling on how many clients can be served at once, not on how fast they are served. Size it accordingly.

Functions

worker_main()

mail.pool.worker_main(connections, build)

What one worker isolate runs: build a server of its own, then serve whatever connections the acceptor hands it.

Called by serve(). It is public because an isolate has to be able to find it by name.

Parameters

  • connections (Channel) — Accepted sockets arrive here.
  • build (function(0)) — Returns this worker’s server.

start()

mail.pool.start(build, options: ?dict) -> Cluster

Starts a pool without running the accept loop, so the address is known before the first connection.

Parameters

  • build (function(0)) — A module’s function returning a server.
  • options (?dict) — host, port and workers.

Returns Cluster

serve()

mail.pool.serve(build, options: ?dict) -> Cluster

Starts a pool and runs it until something closes it.

Parameters

  • build (function(0))
  • options (?dict) — As start().

Returns Cluster

Classes

Cluster

class mail.pool.Cluster

A running pool: the socket, the workers, and the loop feeding them.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.pool.Cluster(listener, connections, workers: list)

Parameters

  • listener (TcpStream)
  • connections (Channel)
  • workers (list) — The isolates serving them.

Cluster.address()

mail.pool.Cluster.address() -> SocketAddr

The address the pool is listening on.

Returns SocketAddr

Cluster.run()

mail.pool.Cluster.run() -> Cluster

Accepts connections and hands them out until close().

Returns Cluster — itself.

Cluster.accept_one()

mail.pool.Cluster.accept_one() -> Cluster

Accepts exactly one connection and hands it to a worker, for a program that wants to drive the loop itself.

Returns Cluster — itself.

Cluster.close()

mail.pool.Cluster.close(timeout: ?number) -> Cluster

Stops accepting, tells the workers there is nothing more coming, and waits for them to finish what they hold.

Parameters

  • timeout (?number) — Milliseconds to wait for each worker.

Returns Cluster — itself.

Cluster.to_string()

mail.pool.Cluster.to_string()

2026, Richard Ore and Zuri contributors

mail.pop3

import mail

Everything here is re-exported by mail, so import mail is enough and the names are called as mail.*. Importing mail.pop3 on its own works too and reaches the same definitions.

POP3: the client end of collecting mail.

import mail.pop3 { Pop3Client }

var mailbox = Pop3Client.connect('pop3s://mail.example.com', {
  username: 'ann',
  password: secret,
})

echo mailbox.stat().count

mailbox.quit()

Submodules

ModuleReached asSummary
mail.pop3.clientmail.*POP3: collecting mail and taking it away.

2026, Richard Ore and Zuri contributors

mail.pop3.client

import mail

Everything here is re-exported by mail, so import mail is enough and the names are called as mail.*. Importing mail.pop3.client on its own works too and reaches the same definitions.

POP3: collecting mail and taking it away.

Where IMAP leaves mail on the server and lets a client work with it in place, POP3 hands it over and, usually, forgets it. That makes it the wrong protocol for reading mail on more than one device and the right one for a program whose job is to drain a mailbox.

import mail.pop3 { Pop3Client }

var mailbox = Pop3Client.connect('pop3s://mail.example.com', {
  username: 'ann',
  password: secret,
})

for entry in mailbox.list() {
  var note = mailbox.retrieve(entry.number)

  archive(note)
  mailbox.delete(entry.number)
}

mailbox.quit()

Nothing is actually removed until quit(): delete() only marks, and reset() undoes every mark. A connection that drops without quit() leaves the mailbox as it was, which is the protocol protecting against a client that fails halfway.

There is no POP3 server here. A POP3 server is an IMAP server with almost everything taken away, and mail.imap is the one to run.

Constants

POP3_SCHEMES

mail.POP3_SCHEMES = {...}

Classes

Entry

class mail.Entry

One message in the mailbox, as list() reports it.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.Entry(number: number, size: number, uid: ?string)

Parameters

  • number (number) — Its position, which holds for this session only.
  • size (number) — How many bytes it is.
  • uid (?string) — The identifier that outlives the session, when the server offers one.

Entry.to_string()

mail.Entry.to_string()

Pop3Client

class mail.Pop3Client

A connection to a server that hands mail over.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.Pop3Client(connection, options: ?dict)

Builds a client around a connection that is already open.

Parameters

  • connection (LineStream)
  • options (?dict) — As connect().

Pop3Client.connect()

mail.Pop3Client.connect(url: string, options: ?dict) -> Pop3Client

Opens a connection and authenticates.

optiondefaultwhat it does
username, passwordnoneauthenticate when both are given
tlsrequirerequire, prefer or disable, when not already encrypted
tls_configa default onethe trust settings for TLS
mechanismsall of themrestricts which are acceptable
timeout60000milliseconds to wait on the socket

Parameters

  • url (string) — pop3s://host, pop3://host, or a bare host.
  • options (?dict)

Returns Pop3Client

Raises Pop3Error if the server refuses.

Raises AuthenticationError if it refuses the credentials.

Pop3Client.begin()

mail.Pop3Client.begin() -> Pop3Client

Reads the greeting, negotiates TLS and authenticates.

Returns Pop3Client — itself.

Pop3Client.supports()

mail.Pop3Client.supports(name: string) -> bool

Whether the server offers a capability.

Parameters

  • name (string)

Returns bool

Pop3Client.is_secure()

mail.Pop3Client.is_secure() -> bool

Whether the connection is encrypted.

Returns bool

Pop3Client.refresh_capabilities()

mail.Pop3Client.refresh_capabilities() -> list

Asks the server what it can do.

A server old enough not to know CAPA reports nothing, which is not an error: it simply predates the command.

Returns list — of string

Pop3Client.start_tls()

mail.Pop3Client.start_tls() -> Pop3Client

Negotiates TLS over a connection that started in the clear.

Returns Pop3Client — itself.

Raises Pop3Error if the server refuses.

Pop3Client.login()

mail.Pop3Client.login(mechanism: ?string) -> Pop3Client

Authenticates, choosing the strongest mechanism both ends know.

APOP is preferred over sending the password where the server’s greeting offered it, and USER/PASS is the last resort and only over TLS.

Parameters

  • mechanism (?string) — Forces one rather than choosing.

Returns Pop3Client — itself.

Raises AuthenticationError if the credentials are refused or nothing usable is on offer.

Pop3Client.authenticate()

mail.Pop3Client.authenticate(mechanism) -> Pop3Client

Runs one authentication exchange with a mechanism already built.

Parameters

  • mechanism (Mechanism)

Returns Pop3Client — itself.

Raises AuthenticationError if the server refuses.

Pop3Client.stat()

mail.Pop3Client.stat() -> dict

How many messages are waiting and how large they are together.

Returns dict — of count and size.

Pop3Client.list()

mail.Pop3Client.list() -> list

Every message waiting, with its size and, where the server offers one, its lasting identifier.

The number is only good for this session: deleting a message renumbers the rest on the next connection. The identifier is what a program that runs twice should remember.

Returns list — of Entry

Pop3Client.uidl()

mail.Pop3Client.uidl()

The lasting identifier of every message, by its number in this session.

Returns — dict, empty when the server does not offer UIDL.

Pop3Client.retrieve()

mail.Pop3Client.retrieve(number: number) -> Message

Fetches one message whole.

Parameters

  • number (number)

Returns Message

Raises Pop3Error if there is no such message.

Pop3Client.retrieve_raw()

mail.Pop3Client.retrieve_raw(number: number) -> bytes

Fetches one message as it arrived, without parsing it.

Parameters

  • number (number)

Returns bytes

Raises Pop3Error if there is no such message.

Pop3Client.top()

mail.Pop3Client.top(number: number, lines: ?number) -> Message

Fetches a message’s headers and the first few lines of its body, which is enough to decide whether to fetch the rest.

Parameters

  • number (number)
  • lines (?number) — Body lines to include. None when not given.

Returns Message

Raises Pop3Error if the server does not offer TOP.

Pop3Client.delete()

mail.Pop3Client.delete(number: number) -> Pop3Client

Marks a message for removal. Nothing goes until quit().

Parameters

  • number (number)

Returns Pop3Client — itself.

Raises Pop3Error if there is no such message.

Pop3Client.reset()

mail.Pop3Client.reset() -> Pop3Client

Unmarks everything marked for removal in this session.

Returns Pop3Client — itself.

Pop3Client.noop()

mail.Pop3Client.noop() -> Pop3Client

Asks the server for nothing, which keeps the connection alive.

Returns Pop3Client — itself.

Pop3Client.quit()

mail.Pop3Client.quit() -> Pop3Client

Says goodbye, which is when the server actually removes whatever was marked, and closes the connection.

Returns Pop3Client — itself.

Pop3Client.close()

mail.Pop3Client.close() -> Pop3Client

Closes the connection without saying goodbye, which leaves the mailbox exactly as it was.

Returns Pop3Client — itself.

Pop3Client.to_string()

mail.Pop3Client.to_string()

2026, Richard Ore and Zuri contributors

mail.sasl

import mail

mail exposes this as mail.sasl, so import mail is enough and the names are called as mail.sasl.*. import mail.sasl reaches the same definitions directly.

The authentication mechanisms all three mail protocols share.

SMTP, IMAP and POP3 each have their own way of starting an authentication exchange and their own way of carrying the bytes, but what travels inside is the same in all three. This module holds the mechanisms themselves, so none of the protocols has to.

import mail.sasl

var mechanism = sasl.of('PLAIN', { username: 'ann', password: 'secret' })

echo mechanism.name()
echo mechanism.start().to_string().replace('/\0/', '|')
PLAIN
|ann|secret

What is here

mechanismwhat it sends
PLAINthe password, so it needs TLS under it
LOGINthe same, one prompt at a time, for servers that only do this
CRAM-MD5a keyed digest of the server’s challenge
SCRAM-SHA-1, SCRAM-SHA-256a proof the server checks without the password, and one back
XOAUTH2, OAUTHBEARERa bearer token, which is what the large providers want
EXTERNALnothing: the client certificate already said who this is
ANONYMOUSnothing, on purpose

The clients pick the strongest mechanism both ends offer, in the order PREFERENCE gives.

Constants

PREFERENCE

mail.sasl.PREFERENCE: list = [...]

The mechanisms this implements, strongest first, which is the order a client picks from what a server offers.

SCRAM proves the password without sending it and proves the server knew it too. CRAM-MD5 proves it without sending it but proves nothing about the server, and its digest is old. The rest send something worth protecting, which is why every client here refuses to use them without TLS.

NEEDS_TLS

mail.sasl.NEEDS_TLS: list = [...]

The mechanisms that put the password itself on the wire, and so must never be used without TLS.

Functions

prepare()

mail.sasl.prepare(text: string) -> string

Prepares a username or password the way RFC 4013 says to, so that two spellings of the same characters authenticate the same.

The space mappings, the deletions and the prohibited characters are all applied. Unicode normalisation is not, so text that needs it must arrive already normalised; for anything typed on a keyboard in any Latin script this makes no difference at all.

Parameters

  • text (string)

Returns string

Raises AuthenticationError if the text holds a character that may not appear in a credential.

response()

mail.sasl.response(username: string, password: string, challenge) -> string

The answer to a CRAM-MD5 challenge.

Used by the mechanism, and by a server checking one, which has to work out the same answer to compare against.

Parameters

  • username (string)
  • password (string)
  • challenge (string|bytes)

Returns string

of()

mail.sasl.of(name: string, credentials: dict) -> Mechanism

Builds a mechanism by name.

credentialused by
username, passwordPLAIN, LOGIN, CRAM-MD5, SCRAM-*
username, tokenXOAUTH2, OAUTHBEARER
authorize_asPLAIN, EXTERNAL, SCRAM-*
host, portOAUTHBEARER
traceANONYMOUS

Parameters

  • name (string) — One of PREFERENCE.
  • credentials (dict)

Returns Mechanism

Raises AuthenticationError if the name is not one of them, or a credential it needs is missing.

choose()

mail.sasl.choose(offered: list, credentials: dict, secure: bool, allowed: ?list) -> string|nil

Picks the mechanism to use.

Takes what the server offers and what the caller can supply, and returns the strongest name both sides have, or nil when there is none.

Mechanisms that put the password on the wire are left out entirely when the connection is not encrypted, which is why a client that refuses to authenticate in the clear does not have to check for it separately.

import mail.sasl

echo sasl.choose(['PLAIN', 'LOGIN', 'CRAM-MD5'], { password: 'x' }, false)
echo sasl.choose(['PLAIN', 'LOGIN'], { password: 'x' }, true)
echo sasl.choose(['PLAIN', 'LOGIN'], { password: 'x' }, false)
CRAM-MD5
PLAIN
nil

Parameters

  • offered (list) — The names the server advertised.
  • credentials (dict) — What the caller has to offer.
  • secure (bool) — Whether the connection is encrypted.
  • allowed (?list) — Restricts the choice to these names.

Returns string|nil

Classes

Mechanism

class mail.sasl.Mechanism

What every mechanism looks like from the outside.

A client calls start() once, sends whatever comes back if anything does, and then calls step() with each challenge the server sends until is_done().

  • printable — has a @to_string(), so echo and print() show something useful

Mechanism.name()

mail.sasl.Mechanism.name() -> string

The name to offer the server.

Returns string

Mechanism.start()

mail.sasl.Mechanism.start() -> bytes|nil

The response to send before the server has said anything, or nil for a mechanism that waits to be asked.

Returns bytes|nil

Mechanism.step()

mail.sasl.Mechanism.step(challenge) -> bytes|nil

The response to one challenge.

Parameters

  • challenge (bytes) — The server’s challenge, already decoded.

Returns bytes|nil

Mechanism.is_done()

mail.sasl.Mechanism.is_done() -> bool

Whether the exchange is over as far as this mechanism is concerned. A server may still have the last word.

Returns bool

Mechanism.to_string()

mail.sasl.Mechanism.to_string()

Plain

class mail.sasl.Plain < Mechanism

PLAIN: the username and the password, separated by zero bytes.

Everything worth having is on the wire, so this belongs under TLS and nowhere else.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.sasl.Plain(username: string, password: string, authorize_as: ?string)

Parameters

  • username (string)
  • password (string)
  • authorize_as (?string) — The account to act as, when it is not the one being authenticated.

Plain.name()

mail.sasl.Plain.name()

Plain.start()

mail.sasl.Plain.start()

Login

class mail.sasl.Login < Mechanism

LOGIN: the username and the password again, one prompt at a time.

Never standardised and entirely superseded by PLAIN, but some servers offer nothing else.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.sasl.Login(username: string, password: string)

Parameters

  • username (string)
  • password (string)

Login.name()

mail.sasl.Login.name()

Login.step()

mail.sasl.Login.step(challenge)

Login.is_done()

mail.sasl.Login.is_done()

CramMd5

class mail.sasl.CramMd5 < Mechanism

CRAM-MD5: a keyed digest of the server’s challenge, so the password never travels.

The digest is old and the exchange proves nothing about the server, so prefer SCRAM where there is a choice.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.sasl.CramMd5(username: string, password: string)

Parameters

  • username (string)
  • password (string)

CramMd5.name()

mail.sasl.CramMd5.name()

CramMd5.step()

mail.sasl.CramMd5.step(challenge)

CramMd5.is_done()

mail.sasl.CramMd5.is_done()

XOAuth2

class mail.sasl.XOAuth2 < Mechanism

XOAUTH2: a bearer token rather than a password, which is what Google and Microsoft accept.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.sasl.XOAuth2(username: string, token: string)

Parameters

  • username (string) — The account the token is for.
  • token (string) — The access token, without the Bearer word.

XOAuth2.name()

mail.sasl.XOAuth2.name()

XOAuth2.start()

mail.sasl.XOAuth2.start()

XOAuth2.step()

mail.sasl.XOAuth2.step(challenge)

OAuthBearer

class mail.sasl.OAuthBearer < Mechanism

OAUTHBEARER: the standardised form of the same idea, from RFC 7628.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.sasl.OAuthBearer(username: string, token: string, host: ?string, port: ?number)

Parameters

  • username (string)
  • token (string)
  • host (?string) — The server being reached, which the token may be bound to.
  • port (?number)

OAuthBearer.name()

mail.sasl.OAuthBearer.name()

OAuthBearer.start()

mail.sasl.OAuthBearer.start()

OAuthBearer.step()

mail.sasl.OAuthBearer.step(challenge)

External

class mail.sasl.External < Mechanism

EXTERNAL: the connection already established who this is, usually with a client certificate.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.sasl.External(authorize_as: ?string)

Parameters

  • authorize_as (?string) — The account to act as. Empty means whichever one the certificate names.

External.name()

mail.sasl.External.name()

External.start()

mail.sasl.External.start()

Anonymous

class mail.sasl.Anonymous < Mechanism

ANONYMOUS: no identity at all, with an optional note saying who is knocking.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.sasl.Anonymous(trace: ?string)

Parameters

  • trace (?string) — An address or a token, for the server’s log.

Anonymous.name()

mail.sasl.Anonymous.name()

Anonymous.start()

mail.sasl.Anonymous.start()

Scram

class mail.sasl.Scram < Mechanism

SCRAM-SHA-1 and SCRAM-SHA-256: the password is never sent, the server never has to store it, and the server proves it knew it too.

This is the mechanism to use wherever a server offers it. Channel binding, the -PLUS form, is not offered: it needs a value out of the TLS session that nothing here exposes.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.sasl.Scram(username: string, password: string, digest: ?string, authorize_as: ?string, nonce: ?string)

Parameters

  • username (string)
  • password (string)
  • digest (?string) — sha256 when not given, or sha1.
  • authorize_as (?string)
  • nonce (?string) — The client nonce. A fresh random one is generated when not given, which is what every real exchange wants; passing one is for reproducing a known exchange.

Raises AuthenticationError if digest is neither.

Scram.name()

mail.sasl.Scram.name()

Scram.start()

mail.sasl.Scram.start()

Scram.step()

mail.sasl.Scram.step(challenge)

Scram.is_done()

mail.sasl.Scram.is_done()

2026, Richard Ore and Zuri contributors

mail.smtp

import mail

Everything here is re-exported by mail, so import mail is enough and the names are called as mail.*. Importing mail.smtp on its own works too and reaches the same definitions.

SMTP: the protocol that moves mail from where it was written to where it is kept.

Both ends are here. SmtpClient hands messages to a server; SmtpServer is the server, and decides what to accept through handlers of your own.

import mail.smtp { SmtpClient }

var server = SmtpClient.connect('smtp://mail.example.com', {
  username: 'reports',
  password: secret,
})

server.send(note, nil)
server.quit()

Submodules

ModuleReached asSummary
mail.smtp.clientmail.*The sending end of SMTP.
mail.smtp.servermail.*The receiving end of SMTP.

2026, Richard Ore and Zuri contributors

mail.smtp.client

import mail

Everything here is re-exported by mail, so import mail is enough and the names are called as mail.*. Importing mail.smtp.client on its own works too and reaches the same definitions.

The sending end of SMTP.

SmtpClient opens a connection, negotiates what the server can do, authenticates, and hands messages over. Most programs want mail.send(), which does all of that for one message and closes the connection again; the client itself is for sending many, or for needing the steps apart.

import mail.smtp { SmtpClient }

var server = SmtpClient.connect('smtp://mail.example.com', {
  username: 'reports',
  password: secret,
})

for note in queue {
  server.send(note, nil)
}

server.quit()

Constants

SMTP_SCHEMES

mail.SMTP_SCHEMES = {...}

MAX_REPLY_LINE

mail.MAX_REPLY_LINE = 1002

Classes

Reply

class mail.Reply

One reply from the server: its code, and whatever it said with it.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.Reply(code: number, lines: list)

Parameters

  • code (number) — The three digit reply code.
  • lines (list) — Each line of the reply, without its code.

Reply.is_positive()

mail.Reply.is_positive() -> bool

Whether the server accepted: a code in the 200s or 300s.

Returns bool

Reply.is_intermediate()

mail.Reply.is_intermediate() -> bool

Whether the server wants more from the client before it will say, which is the 300s.

Returns bool

Reply.is_transient()

mail.Reply.is_transient() -> bool

Whether this is a refusal that may not be one tomorrow.

Returns bool

Reply.is_permanent()

mail.Reply.is_permanent() -> bool

Whether this is a refusal that trying again will not change.

Returns bool

Reply.ok()

mail.Reply.ok(doing: ?string) -> Reply

Raises the error this reply stands for, or returns it when the server accepted.

Parameters

  • doing (?string) — What was being attempted, for the message.

Returns Reply — itself.

Raises SmtpTransientError|SmtpPermanentError

Reply.to_string()

mail.Reply.to_string()

SmtpClient

class mail.SmtpClient

A connection to a server that sends mail.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.SmtpClient(connection, options: ?dict)

Builds a client around a connection that is already open.

connect() is what opens one; this is for a connection made some other way, and for tests that hand it a transcript instead of a socket.

Parameters

  • connection (LineStream)
  • options (?dict) — As connect().

SmtpClient.connect()

mail.SmtpClient.connect(url: string, options: ?dict) -> SmtpClient

Opens a connection and gets as far as being ready to send.

The greeting is read, capabilities are asked for, TLS is negotiated if the connection did not start with it, and the client authenticates when given something to authenticate with.

optiondefaultwhat it does
username, passwordnoneauthenticate when both are given
tokennoneauthenticate with a bearer token instead
tlsrequirerequire, prefer or disable, when not already encrypted
tls_configa default onethe trust settings for TLS
mechanismsall of themrestricts which are acceptable
client_namethe local hostnamethe name to greet the server with
timeout30000milliseconds to wait on the socket

Parameters

  • url (string) — smtp://host, smtps://host, or a bare host. A username and password in the string are used when the options do not carry them.
  • options (?dict)

Returns SmtpClient

Raises SmtpError if the server refuses the greeting.

Raises AuthenticationError if it refuses the credentials.

Raises ProtocolError if TLS is required and not offered.

SmtpClient.begin()

mail.SmtpClient.begin() -> SmtpClient

Reads the greeting, introduces the client, negotiates TLS and authenticates. connect() calls this; a client built around an existing connection has to.

Returns SmtpClient — itself.

SmtpClient.greeting()

mail.SmtpClient.greeting() -> Reply|nil

What the server said when the connection opened.

Returns Reply|nil

SmtpClient.capabilities()

mail.SmtpClient.capabilities() -> dict

What the server said it can do, by name. A capability with no value of its own maps to true.

Returns dict

SmtpClient.supports()

mail.SmtpClient.supports(name: string) -> bool

Whether the server offers a capability.

Parameters

  • name (string) — Matched without regard to case.

Returns bool

SmtpClient.is_secure()

mail.SmtpClient.is_secure() -> bool

Whether the connection is encrypted.

Returns bool

SmtpClient.is_authenticated()

mail.SmtpClient.is_authenticated() -> bool

Whether the client has authenticated.

Returns bool

SmtpClient.max_size()

mail.SmtpClient.max_size() -> number|nil

The largest message the server said it will take, or nil when it did not say.

Returns number|nil

SmtpClient.read_reply()

mail.SmtpClient.read_reply() -> Reply

Reads one reply, however many lines it runs to.

Returns Reply

Raises ProtocolError if what arrives is not a reply.

SmtpClient.command()

mail.SmtpClient.command(line: string) -> Reply

Sends a command and reads the reply it draws.

Parameters

  • line (string)

Returns Reply

SmtpClient.ehlo()

mail.SmtpClient.ehlo(name: ?string) -> Reply

Introduces the client and reads back what the server can do.

Falls back to the older HELO when the server does not understand EHLO, in which case there are no capabilities to read.

Parameters

  • name (?string) — The name to give. The one from the options, or the local hostname, when not given.

Returns Reply

Raises SmtpError if the server refuses both greetings.

SmtpClient.start_tls()

mail.SmtpClient.start_tls() -> SmtpClient

Negotiates TLS over a connection that started in the clear, and introduces the client again, because everything the server said before the handshake has to be thrown away.

Returns SmtpClient — itself.

Raises SmtpError if the server refuses.

SmtpClient.login()

mail.SmtpClient.login(mechanism: ?string) -> SmtpClient

Authenticates, choosing the strongest mechanism both ends know.

Parameters

  • mechanism (?string) — Forces one rather than choosing.

Returns SmtpClient — itself.

Raises AuthenticationError if there is no mechanism in common, if the credentials are refused, or if the only ones on offer would put the password on an unencrypted connection.

SmtpClient.authenticate()

mail.SmtpClient.authenticate(mechanism) -> SmtpClient

Runs one authentication exchange with a mechanism already built.

Parameters

  • mechanism (Mechanism)

Returns SmtpClient — itself.

Raises AuthenticationError if the server refuses.

SmtpClient.mail_from()

mail.SmtpClient.mail_from(sender, options: ?dict) -> Reply

Starts a transaction by naming the sender.

Parameters

  • sender (string|Address) — The return path. An empty string is the null sender a bounce is sent from.
  • options (?dict) — size to declare the message’s size, and body for 8BITMIME or BINARYMIME.

Returns Reply

Raises SmtpError if the server refuses the sender.

SmtpClient.rcpt_to()

mail.SmtpClient.rcpt_to(recipient, options: ?dict) -> Reply

Adds one recipient to the transaction.

Parameters

  • recipient (string|Address)
  • options (?dict) — notify and original for DSN.

Returns Reply

Raises SmtpError if the server refuses the recipient.

SmtpClient.data()

mail.SmtpClient.data(body) -> Reply

Hands over the message itself and ends the transaction.

Line endings are normalised and a leading dot on any line is doubled, so a body containing a line with one dot on it cannot end the message early.

Parameters

  • body (string|bytes)

Returns Reply — the server’s verdict on the whole message.

Raises SmtpError if the server refuses either the command or the message.

SmtpClient.bdat()

mail.SmtpClient.bdat(chunk, last: bool) -> Reply

Hands the message over in chunks, which is what CHUNKING is for.

Nothing is escaped, because a chunk carries its own length and has no terminator to collide with. Only works where the server offers CHUNKING.

Parameters

  • chunk (string|bytes)
  • last (bool) — Whether this is the final chunk.

Returns Reply

Raises StateError if the server does not offer CHUNKING.

Raises SmtpError if the server refuses.

SmtpClient.send()

mail.SmtpClient.send(message, options: ?dict) -> Reply

Sends one message: the sender, the recipients, and the message itself, in one transaction.

The sender comes from the message’s From and the recipients from its To, Cc and Bcc unless the options say otherwise. Bcc is removed before the message goes, which is the whole point of it.

server.send(note, nil)
server.send(note, { from: 'bounces@example.com' })

Parameters

  • message (Message)
  • options (?dict) — from and to to override the envelope, and anything mail_from() and rcpt_to() accept.

Returns Reply — the server’s verdict.

Raises StateError if there is no sender or no recipient.

Raises SmtpError if the server refuses any part of it.

SmtpClient.send_raw()

mail.SmtpClient.send_raw(sender, recipients: list, body, options: ?dict) -> Reply

Sends a message that is already bytes, to recipients given here.

Parameters

  • sender (string|Address)
  • recipients (list)
  • body (string|bytes)
  • options (?dict)

Returns Reply

Raises StateError if there are no recipients.

SmtpClient.rset()

mail.SmtpClient.rset() -> Reply

Abandons whatever transaction is in progress.

Returns Reply

SmtpClient.noop()

mail.SmtpClient.noop() -> Reply

Asks the server for nothing, which is how a connection is kept from going idle.

Returns Reply

SmtpClient.verify()

mail.SmtpClient.verify(address: string) -> Reply

Asks whether the server knows an address.

Most servers on the public internet refuse to answer this, because answering it hands an address list to whoever asks. A refusal is returned rather than raised, since it is an answer of a kind.

Parameters

  • address (string)

Returns Reply

SmtpClient.quit()

mail.SmtpClient.quit() -> Reply|nil

Says goodbye and closes the connection.

Returns Reply|nil — the server’s farewell, or nil when it had already gone.

SmtpClient.close()

mail.SmtpClient.close() -> SmtpClient

Closes the connection without saying goodbye.

Returns SmtpClient — itself.

SmtpClient.to_string()

mail.SmtpClient.to_string()

2026, Richard Ore and Zuri contributors

mail.smtp.server

import mail

Everything here is re-exported by mail, so import mail is enough and the names are called as mail.*. Importing mail.smtp.server on its own works too and reaches the same definitions.

The receiving end of SMTP.

SmtpServer accepts connections, runs the ESMTP conversation, and asks handlers of your own what to do at each stage: whether to take this sender, this recipient, this message. It knows the protocol; everything about policy is yours.

import mail.smtp { SmtpServer }

var server = SmtpServer({ port: 2525, hostname: 'mail.example.com' })

server.on_rcpt(@(session, recipient) {
  if !recipient.domain.ends_with('example.com') {
    return { code: 550, message: 'not a local address' }
  }
})

server.on_data(@(session, raw) {
  store.deliver(session.recipients, raw)
})

server.listen()

A handler that returns nothing accepts. One that returns a code and a message refuses, with those. One that raises is a failure on the server’s side, and the sender is told to try again later.

Constants

MECHANISMS

mail.MECHANISMS = [...]

DEFAULT_MAX_SIZE

mail.DEFAULT_MAX_SIZE = 35882577

DEFAULT_MAX_RECIPIENTS

mail.DEFAULT_MAX_RECIPIENTS = 100

MAX_ERRORS

mail.MAX_ERRORS = 10

MAX_DATA_LINE

mail.MAX_DATA_LINE = 4096

Classes

SmtpSession

class mail.SmtpSession

One connection, and everything known about it so far.

Handlers are given this and may put their own values on it through state, which starts empty and lives as long as the connection.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.SmtpSession(peer: string, secure: bool)

sender is the Address the transaction is from, nil before one has started, and an empty string for the null path a bounce comes from. recipients is the Address values accepted so far.

Parameters

  • peer (string) — The address the connection came from.
  • secure (bool) — Whether it is encrypted.

SmtpSession.is_authenticated()

mail.SmtpSession.is_authenticated() -> bool

Whether the client has authenticated.

Returns bool

SmtpSession.is_secure()

mail.SmtpSession.is_secure() -> bool

Whether the connection is encrypted.

Returns bool

SmtpSession.reset()

mail.SmtpSession.reset()

SmtpSession.to_string()

mail.SmtpSession.to_string()

SmtpServer

class mail.SmtpServer

A server that accepts mail.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.SmtpServer(options: ?dict)
optiondefaultwhat it does
host127.0.0.1the address to bind
port25the port to bind
hostnamethe machine’s ownthe name to greet clients with
max_size35 MBthe largest message to take
max_recipients100recipients one message may have
timeout300000milliseconds a client may go quiet
require_tlsfalserefuse mail on an unencrypted connection
require_authfalserefuse mail from a client that has not authenticated
bannernoneextra text on the greeting line

Parameters

  • options (?dict)

SmtpServer.on_connect()

mail.SmtpServer.on_connect(handler) -> SmtpServer

Called when a connection opens, with the session. Refusing here closes the connection before the greeting.

Parameters

  • handler (function(1))

Returns SmtpServer — itself.

SmtpServer.on_mail()

mail.SmtpServer.on_mail(handler) -> SmtpServer

Called with the session and the sender Address when a transaction starts.

Parameters

  • handler (function(2))

Returns SmtpServer — itself.

SmtpServer.on_rcpt()

mail.SmtpServer.on_rcpt(handler) -> SmtpServer

Called with the session and each recipient Address. This is where a server decides whether it has anywhere to put the message.

Parameters

  • handler (function(2))

Returns SmtpServer — itself.

SmtpServer.on_data()

mail.SmtpServer.on_data(handler) -> SmtpServer

Called with the session and the message, as bytes, once the whole of it has arrived. This is where a message is delivered.

Parameters

  • handler (function(2))

Returns SmtpServer — itself.

SmtpServer.on_auth()

mail.SmtpServer.on_auth(handler) -> SmtpServer

Called with the session and { mechanism, username, password }. Returning nothing accepts.

Without this handler the server advertises no authentication at all, and a client that tries anyway is told so.

Parameters

  • handler (function(2))

Returns SmtpServer — itself.

SmtpServer.on_password()

mail.SmtpServer.on_password(handler) -> SmtpServer

Called with a username, and returns that account’s password or nil.

CRAM-MD5 proves the password without sending it, which means the server has to work out the same digest and therefore has to know the password. A server that cannot supply one does not offer the mechanism.

Parameters

  • handler (function(1))

Returns SmtpServer — itself.

SmtpServer.on_close()

mail.SmtpServer.on_close(handler) -> SmtpServer

Called with the session when a connection closes, however it closed.

Parameters

  • handler (function(1))

Returns SmtpServer — itself.

SmtpServer.on_error()

mail.SmtpServer.on_error(handler) -> SmtpServer

Called with the error and the session when something inside the server fails. Without it, failures are silent and the client is told to try again later.

Parameters

  • handler (function(2))

Returns SmtpServer — itself.

SmtpServer.use_tls()

mail.SmtpServer.use_tls(cert_chain: string, private_key: string) -> SmtpServer

Gives the server a certificate, which is what lets it offer STARTTLS.

Parameters

  • cert_chain (string) — The certificate chain, PEM encoded.
  • private_key (string) — The key, PEM encoded.

Returns SmtpServer — itself.

SmtpServer.set_tls_config()

mail.SmtpServer.set_tls_config(config) -> SmtpServer

Gives the server a TLS configuration built elsewhere.

Parameters

  • config (TlsConfig)

Returns SmtpServer — itself.

SmtpServer.is_secure()

mail.SmtpServer.is_secure() -> bool

Whether the server can offer STARTTLS.

Returns bool

SmtpServer.bind()

mail.SmtpServer.bind() -> SmtpServer

Binds the listening socket without accepting anything yet, so that the address is known before the first connection.

Returns SmtpServer — itself.

SmtpServer.address()

mail.SmtpServer.address() -> SocketAddr

The address the server is listening on, which is how a port of 0 is turned into the one the system chose.

Returns SocketAddr

SmtpServer.accept()

mail.SmtpServer.accept() -> SmtpServer

Accepts one connection and serves it to the end.

Returns SmtpServer — itself.

SmtpServer.listen()

mail.SmtpServer.listen() -> SmtpServer

Accepts connections one after another until close().

One connection is served at a time. To serve several at once, run serve() instead, which puts a pool of isolates behind the same socket.

Returns SmtpServer — itself.

SmtpServer.close()

mail.SmtpServer.close() -> SmtpServer

Stops the accept loop and closes the listening socket.

Returns SmtpServer — itself.

SmtpServer.is_listening()

mail.SmtpServer.is_listening() -> bool

Whether the accept loop is running.

Returns bool

SmtpServer.serve_connection()

mail.SmtpServer.serve_connection(client) -> SmtpServer

Serves one already-accepted connection to the end, then closes it.

This is the whole conversation: the greeting, every command, and the close. Call it directly to put a server behind a socket something else accepted.

Parameters

  • client (TcpStream)

Returns SmtpServer — itself.

SmtpServer.to_string()

mail.SmtpServer.to_string()

2026, Richard Ore and Zuri contributors

mail.stream

import mail

mail exposes this as mail.stream, so import mail is enough and the names are called as mail.stream.*. import mail.stream reaches the same definitions directly.

A line-oriented connection, which is what all three mail protocols are underneath.

SMTP, IMAP and POP3 all speak in lines ending with a carriage return and a newline, mixed with runs of raw bytes whose length something else announced. LineStream puts that over any of the transports net provides, and lets one be swapped for another mid-conversation, which is what STARTTLS is.

Constants

TLS_MODES

mail.stream.TLS_MODES: list = [...]

How TLS is treated on a connection that did not start encrypted: require refuses to go on without it, prefer takes it when the server offers it, and disable does not ask.

MAX_LINE

mail.stream.MAX_LINE = 65536

Functions

connect()

mail.stream.connect(host: string, port: number, options: dict) -> LineStream

Opens a connection to a host, in TLS or in the clear.

Parameters

  • host (string)
  • port (number)
  • options (dict) — timeout in milliseconds, tls to handshake immediately, tls_config and server_name.

Returns LineStream

Raises Error if the connection or the handshake fails.

start_tls()

mail.stream.start_tls(stream, server_name: string, config) -> LineStream

Wraps a connection that started in the clear in TLS, which is what every STARTTLS comes down to.

Parameters

  • stream (LineStream)
  • server_name (string) — The name to check the certificate against.
  • config (?TlsConfig)

Returns LineStream — the same one, now encrypted.

Raises Error if the handshake fails.

endpoint()

mail.stream.endpoint(url: string, schemes: dict) -> dict

Reads a connection string into the pieces needed to open it.

Accepts scheme://[user[:password]@]host[:port], and a bare host[:port] for which the first scheme in schemes is assumed.

import mail.stream

var where = stream.endpoint('smtps://ann:secret@mail.example.com', {
  smtp: { port: 587, tls: false },
  smtps: { port: 465, tls: true },
})

echo '${where.host} ${where.port} ${where.tls} ${where.username}'
mail.example.com 465 true ann

Parameters

  • url (string)
  • schemes (dict) — Each scheme’s default port and whether it means TLS from the first byte.

Returns dict — of scheme, host, port, tls, username and password, the last two nil when the string carries none.

Raises ProtocolError if the scheme is not one of those given, or there is no host.

Classes

LineStream

class mail.stream.LineStream

A connection that reads and writes lines.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

mail.stream.LineStream(transport, secure: ?bool)

Parameters

  • transport (any) — Anything with read(), write_all(), flush() and close(). Every socket in net qualifies.
  • secure (?bool) — Whether the transport is already encrypted. Worked out from the transport when not given.

LineStream.transport()

mail.stream.LineStream.transport() -> any

The transport underneath, for the calls this does not wrap.

Returns any

LineStream.is_secure()

mail.stream.LineStream.is_secure() -> bool

Whether what goes over this connection is encrypted.

Returns bool

LineStream.upgrade()

mail.stream.LineStream.upgrade(transport, secure: ?bool) -> LineStream

Replaces the transport with another, which is what a protocol does once it has negotiated TLS over a connection that started in the clear.

Parameters

  • transport (any)
  • secure (?bool)

Returns LineStream — itself.

Raises ProtocolError if anything is still buffered, since bytes read before the handshake cannot belong after it.

LineStream.read_line()

mail.stream.LineStream.read_line(limit: ?number) -> string

Reads one line.

The line break comes off, and a lone newline is accepted in place of the pair, because servers send one often enough that refusing would be refusing real mail.

Parameters

  • limit (?number) — The longest line to accept. MAX_LINE when not given.

Returns string

Raises ConnectionClosed if the connection ends mid-line.

Raises ProtocolError if the line runs past the limit.

LineStream.read_bytes()

mail.stream.LineStream.read_bytes(count: number) -> bytes

Reads exactly count bytes, whatever they contain.

Parameters

  • count (number)

Returns bytes

Raises ConnectionClosed if the connection ends first.

LineStream.has_buffered()

mail.stream.LineStream.has_buffered() -> bool

Whether anything is waiting in the buffer, without touching the transport.

Returns bool

LineStream.write()

mail.stream.LineStream.write(data) -> LineStream

Writes bytes as they are.

Parameters

  • data (string|bytes)

Returns LineStream — itself.

LineStream.write_line()

mail.stream.LineStream.write_line(text: string) -> LineStream

Writes one line and the break that ends it.

Parameters

  • text (string)

Returns LineStream — itself.

LineStream.flush()

mail.stream.LineStream.flush() -> LineStream

Pushes anything buffered by the transport out to the peer.

Returns LineStream — itself.

LineStream.close()

mail.stream.LineStream.close() -> LineStream

Closes the connection. Closing one that is already closed does nothing.

Returns LineStream — itself.

LineStream.is_closed()

mail.stream.LineStream.is_closed() -> bool

Whether close() has been called.

Returns bool

LineStream.to_string()

mail.stream.LineStream.to_string()

2026, Richard Ore and Zuri contributors

ffi

import ffi

Calling C and Rust libraries, and being called by them.

ffi loads a shared library, describes the functions in it, and calls them with ordinary Zuri values. Every conversion across the boundary is checked against the C type, so a value that does not fit is an error at the call rather than a corrupted argument inside the library. The module reads C and Rust declarations as their headers and crates write them, links static libraries into loadable ones, and turns Zuri functions into C function pointers.

import ffi

var c = ffi.open(ffi.LIBC).declare('
  size_t strlen(const char *s);
  int abs(int n);
')

echo c.strlen('hello')
echo c.abs(-7)

Values at the boundary

Numbers become any C integer or floating-point type, with integers range-checked against their type. Integers too large for a number to hold exactly, 64- and 128-bit ones, travel as bigints both ways. A string passed to a char * becomes a NUL-terminated copy for the duration of the call; bytes pass their own storage with nothing copied; a list or dictionary passed to a pointer becomes a temporary array or struct, written back afterwards unless the pointer is to const. Structs and unions by value are dictionaries. nil is a null pointer, and a null pointer coming back is nil.

Memory

alloc() and its siblings hand out memory that knows its own size, and every access through a pointer into it is bounds-checked and refused once the memory is freed. Memory C returns can be taken over with Pointer.own(), which releases it with the right function when the pointer is collected.

Callbacks

A Zuri function passed to a function-pointer parameter works as a callback for that call. callback() makes one that lasts, for a library that keeps the pointer. A callback can be called from any thread: from another thread, the call waits until the isolate that made the callback runs it.

The ffi API

Every public name in ffi, wherever it is declared. Each links to the page that documents it.

NameKindSummary
ffi.CallbackclassA Zuri function behind a C function pointer, made by ffi.callback().
ffi.CallbackErrorclassA callback could not be created, or was used after its release.
ffi.DeclarationErrorclassC or Rust source handed to a declaration could not be read.
ffi.DeclarationsclassA set of declarations: types, functions, variables and constants, read from any mix of C and Rust source.
ffi.EnumTypeclassA C enum, or a Rust enum without fields.
ffi.FfiErrorclassBase class for every error this module raises.
ffi.LIBCconstantThe C runtime library of this platform, as open() takes it: libc.so.6 on Linux, the system library on…
ffi.LIBMconstantThe C maths library, which on macOS and Windows is the same library as LIBC.
ffi.LibraryclassA shared library loaded into the process, or the process itself.
ffi.LinkErrorclassA static library could not be linked into a loadable one.
ffi.LoadErrorclassA library could not be found or loaded.
ffi.PointerclassAn address in native memory.
ffi.PointerErrorclassMemory was touched in a way that would have been undefined in C.
ffi.RecordTypeclassA struct or a union, built member by member.
ffi.StructTypeclassA C struct, or a Rust #[repr(C)] struct or enum with fields.
ffi.SymbolErrorclassA library does not export a symbol that was asked for.
ffi.TypeclassA C type.
ffi.TypedclassA value marked with the C type it should travel as.
ffi.UnionTypeclassA C union.
ffi.allocfunctionZeroed memory for count values of type, typed as type.
ffi.alloc_bytesfunctionsize zeroed bytes, as an untyped pointer.
ffi.alloc_stringfunctionA NUL-terminated copy of text in memory that lasts, typed as its code unit: char, char16_t, wchar_t…
ffi.arrayfunctionAn array type.
ffi.atfunctionA pointer to a known address.
ffi.boolconstantbool: C’s _Bool and Rust’s bool, one byte; a Zuri bool.
ffi.callbackfunctionA lasting C function pointer that calls function.
ffi.charconstantchar: signed on x86-64 and on Apple’s Arm platforms, unsigned on Arm Linux, as the platform’s C compiler…
ffi.char16_tconstantchar16_t, a UTF-16 code unit.
ffi.char32_tconstantchar32_t, a UTF-32 code unit.
ffi.complex_doubleconstantdouble _Complex, as complex_float with doubles.
ffi.complex_floatconstantfloat _Complex: a list of two numbers, [real, imaginary].
ffi.declarationsfunctionAn empty set of declarations to add C and Rust source to.
ffi.declarefunctionA set of declarations read from C source; the same as declarations().declare(source).
ffi.declare_rustfunctionA set of declarations read from Rust source; the same as declarations().declare_rust(source).
ffi.default_link_cachefunctionThe directory link() keeps linked libraries in by default: $XDG_CACHE_HOME/zuri/ffi (or…
ffi.describefunctionWhat a foreign function is: { name, pointer, type, threaded, library }, where pointer is its address as a…
ffi.doubleconstantdouble, 64 bits.
ffi.enumfunctionA new enum type to add constants to.
ffi.errnofunctionerrno as the most recent foreign call on this isolate left it.
ffi.f32constantRust’s f32.
ffi.f64constantRust’s f64.
ffi.findfunctionWhere name would be loaded from, without loading it, or nil when it cannot be found.
ffi.floatconstantfloat, 32 bits.
ffi.functionfunctionA Zuri function that calls the C function at pointer with the signature of type.
ffi.function_typefunctionA function type, for callbacks and for calling function pointers.
ffi.i128constantRust’s i128.
ffi.i16constantRust’s i16.
ffi.i32constantRust’s i32.
ffi.i64constantRust’s i64.
ffi.i8constantRust’s i8.
ffi.intconstantint, 32 bits.
ffi.int128constant__int128, a 128-bit integer; Rust’s i128.
ffi.int16constantint16_t.
ffi.int32constantint32_t.
ffi.int64constantint64_t.
ffi.int8constantint8_t.
ffi.intptr_tconstantintptr_t, 64 bits.
ffi.is_foreignfunctionWhether value is a foreign function this module made.
ffi.isizeconstantRust’s isize.
ffi.last_errorfunctionOn Windows, GetLastError() as the most recent foreign call on this isolate left it, cleared before the call…
ffi.linkfunctionLinks static libraries into a shared one with the platform’s linker, and loads it.
ffi.longconstantlong: 64 bits, except on Windows, where it is 32.
ffi.longdoubleconstantlong double: 80-bit extended precision on x86-64 Linux and macOS, 128-bit quad precision on Arm Linux, and…
ffi.longlongconstantlong long, 64 bits.
ffi.mallocfunctionsize zeroed bytes from the C allocator, as an untyped pointer.
ffi.openfunctionLoads a shared library.
ffi.platformfunctionThe facts about this platform that decide how C types are laid out: `{ os, arch, pointer_size, long_size,…
ffi.pointerfunctionA pointer type.
ffi.ptrconstantvoid *, the untyped pointer.
ffi.ptrdiff_tconstantptrdiff_t, 64 bits.
ffi.rust_charconstantRust’s char: four bytes holding a Unicode scalar value, which is a one-character string on the Zuri side.
ffi.scharconstantsigned char.
ffi.servefunctionRuns every callback called from another thread that is waiting for this isolate, returning how many ran.
ffi.set_errnofunctionSets errno, for the rare API that reads it.
ffi.shortconstantshort, 16 bits.
ffi.size_tconstantsize_t, 64 bits.
ffi.slicefunctionThe #[repr(C)] struct of a pointer and a length that a Rust slice is passed across the C ABI as: `{ ptr,…
ffi.ssize_tconstantssize_t, 64 bits.
ffi.stringconstantconst char * that converts to and from a string: a string passed in becomes a NUL-terminated UTF-8 copy for…
ffi.structfunctionA new struct type to add members to.
ffi.threadedfunctionThe same foreign function, run on a helper thread while the calling isolate keeps answering callbacks called…
ffi.typefunctionA type written as C writes it, using only built-in type names: 'unsigned long', 'const char *',…
ffi.u128constantRust’s u128.
ffi.u16constantRust’s u16.
ffi.u32constantRust’s u32.
ffi.u64constantRust’s u64.
ffi.u8constantRust’s u8.
ffi.ucharconstantunsigned char.
ffi.uintconstantunsigned int, 32 bits.
ffi.uint128constantunsigned __int128; Rust’s u128.
ffi.uint16constantuint16_t.
ffi.uint32constantuint32_t.
ffi.uint64constantuint64_t.
ffi.uint8constantuint8_t.
ffi.uintptr_tconstantuintptr_t, 64 bits.
ffi.ulongconstantunsigned long: 64 bits, except on Windows, where it is 32.
ffi.ulonglongconstantunsigned long long, 64 bits.
ffi.unionfunctionA new union type to add members to.
ffi.ushortconstantunsigned short, 16 bits.
ffi.usizeconstantRust’s usize.
ffi.voidconstantvoid: the return type of a function that returns nothing.
ffi.wchar_tconstantwchar_t: 16 bits and unsigned on Windows, 32 bits elsewhere.
ffi.wstringconstantconst wchar_t * that converts to and from a string, as string does in wchar_t’s encoding: UTF-16 on…

Submodules

ModuleReached asSummary
ffi.callbackffi.callback.*Zuri functions that C can call.
ffi.declareffi.declare.*Declarations read from C and Rust source.
ffi.errorsffi.*Every error the ffi module raises, under one root.
ffi.libraryffi.library.*A loaded shared library, and binding what it exports.
ffi.pointerffi.pointer.*Addresses in native memory, and reading and writing through them.
ffi.typesffi.types.*C types as values.

Constants

LIBC

ffi.LIBC: string

The C runtime library of this platform, as open() takes it: libc.so.6 on Linux, the system library on macOS, and the Universal C Runtime, ucrtbase.dll, on Windows.

LIBM

ffi.LIBM: string

The C maths library, which on macOS and Windows is the same library as LIBC.

void

ffi.void: Type

void: the return type of a function that returns nothing.

bool

ffi.bool: Type

bool: C’s _Bool and Rust’s bool, one byte; a Zuri bool.

char

ffi.char: Type

char: signed on x86-64 and on Apple’s Arm platforms, unsigned on Arm Linux, as the platform’s C compiler has it.

schar

ffi.schar: Type

signed char.

uchar

ffi.uchar: Type

unsigned char.

short

ffi.short: Type

short, 16 bits.

ushort

ffi.ushort: Type

unsigned short, 16 bits.

int

ffi.int: Type

int, 32 bits.

uint

ffi.uint: Type

unsigned int, 32 bits.

long

ffi.long: Type

long: 64 bits, except on Windows, where it is 32.

ulong

ffi.ulong: Type

unsigned long: 64 bits, except on Windows, where it is 32.

longlong

ffi.longlong: Type

long long, 64 bits.

ulonglong

ffi.ulonglong: Type

unsigned long long, 64 bits.

int8

ffi.int8: Type

int8_t.

uint8

ffi.uint8: Type

uint8_t.

int16

ffi.int16: Type

int16_t.

uint16

ffi.uint16: Type

uint16_t.

int32

ffi.int32: Type

int32_t.

uint32

ffi.uint32: Type

uint32_t.

int64

ffi.int64: Type

int64_t. Values beyond 2^53 in magnitude read back as bigints.

uint64

ffi.uint64: Type

uint64_t. Values above 2^53 read back as bigints.

int128

ffi.int128: Type

__int128, a 128-bit integer; Rust’s i128.

uint128

ffi.uint128: Type

unsigned __int128; Rust’s u128.

size_t

ffi.size_t: Type

size_t, 64 bits.

ssize_t

ffi.ssize_t: Type

ssize_t, 64 bits.

ptrdiff_t

ffi.ptrdiff_t: Type

ptrdiff_t, 64 bits.

intptr_t

ffi.intptr_t: Type

intptr_t, 64 bits.

uintptr_t

ffi.uintptr_t: Type

uintptr_t, 64 bits.

wchar_t

ffi.wchar_t: Type

wchar_t: 16 bits and unsigned on Windows, 32 bits elsewhere.

char16_t

ffi.char16_t: Type

char16_t, a UTF-16 code unit.

char32_t

ffi.char32_t: Type

char32_t, a UTF-32 code unit.

float

ffi.float: Type

float, 32 bits.

double

ffi.double: Type

double, 64 bits.

longdouble

ffi.longdouble: Type

long double: 80-bit extended precision on x86-64 Linux and macOS, 128-bit quad precision on Arm Linux, and the same as double on Windows and Apple’s Arm platforms. A Zuri number is a double, so a wider value is rounded to the nearest double when read.

complex_float

ffi.complex_float: Type

float _Complex: a list of two numbers, [real, imaginary]. Passing one by value is not available on Windows, whose C compiler has no _Complex; in memory it works everywhere.

complex_double

ffi.complex_double: Type

double _Complex, as complex_float with doubles.

ptr

ffi.ptr: Type

void *, the untyped pointer.

string

ffi.string: Type

const char * that converts to and from a string: a string passed in becomes a NUL-terminated UTF-8 copy for the call, and a pointer coming back is read up to its terminator into a string, or is nil when null. Use pointer(char) to receive the pointer itself.

wstring

ffi.wstring: Type

const wchar_t * that converts to and from a string, as string does in wchar_t’s encoding: UTF-16 on Windows and UTF-32 elsewhere.

i8

ffi.i8: Type

Rust’s i8.

u8

ffi.u8: Type

Rust’s u8.

i16

ffi.i16: Type

Rust’s i16.

u16

ffi.u16: Type

Rust’s u16.

i32

ffi.i32: Type

Rust’s i32.

u32

ffi.u32: Type

Rust’s u32.

i64

ffi.i64: Type

Rust’s i64.

u64

ffi.u64: Type

Rust’s u64.

i128

ffi.i128: Type

Rust’s i128.

u128

ffi.u128: Type

Rust’s u128.

isize

ffi.isize: Type

Rust’s isize.

usize

ffi.usize: Type

Rust’s usize.

f32

ffi.f32: Type

Rust’s f32.

f64

ffi.f64: Type

Rust’s f64.

rust_char

ffi.rust_char: Type

Rust’s char: four bytes holding a Unicode scalar value, which is a one-character string on the Zuri side.

Functions

open()

ffi.open(name, options) -> Library

Loads a shared library.

name is a path, a file name, or a bare name the platform’s naming convention completes: sqlite3 is tried as libsqlite3.so and then as the versioned file the loader’s cache lists on Linux, libsqlite3.dylib and sqlite3.framework on macOS, and sqlite3.dll on Windows. The directories in paths are searched first, then the platform’s own search order.

With no name, the result is the running process itself, whose symbols include everything already loaded into it.

var libc = ffi.open(ffi.LIBC)
var local = ffi.open('mylib', { paths: ['./build'] })

Parameters

  • name (string|nil)
  • options (dict|nil) — paths, a list of directories searched first; lazy, resolve symbols as they are first used rather than at load (default false); global, make the library’s symbols visible to libraries loaded after it (default false). lazy and global have no effect on Windows.

Returns Library

Raises LoadError when the library cannot be found or loaded.

find()

ffi.find(name: string, paths) -> string|nil

Where name would be loaded from, without loading it, or nil when it cannot be found. Resolves names the same way open() does.

Parameters

  • name (string)
  • paths (list|nil) — Directories searched first.

Returns string|nil

declarations()

ffi.declarations() -> Declarations

An empty set of declarations to add C and Rust source to.

Returns Declarations

declare()

ffi.declare(source: string) -> Declarations

A set of declarations read from C source; the same as declarations().declare(source).

Parameters

  • source (string)

Returns Declarations

Raises DeclarationError when the source cannot be read.

declare_rust()

ffi.declare_rust(source: string) -> Declarations

A set of declarations read from Rust source; the same as declarations().declare_rust(source).

Parameters

  • source (string)

Returns Declarations

Raises DeclarationError when the source cannot be read.

struct()

ffi.struct(name) -> StructType

A new struct type to add members to.

Parameters

  • name (string|nil) — The name it prints as.

Returns StructType

union()

ffi.union(name) -> UnionType

A new union type to add members to.

Parameters

  • name (string|nil) — The name it prints as.

Returns UnionType

enum()

ffi.enum(name, type) -> EnumType

A new enum type to add constants to.

Parameters

  • name (string|nil) — The name it prints as.
  • type (Type|nil) — The integer type values are stored as. Defaults to int, which is what C uses.

Returns EnumType

pointer()

ffi.pointer(type, options) -> Type

A pointer type.

Parameters

  • type (Type) — What it points at; void for an untyped pointer.
  • options (dict|nil) — nonnull, true to refuse nil where one is passed, as a Rust reference does (default false); text, an encoding ('utf-8', 'utf-16', 'utf-32' or 'wide') that makes the pointer convert to and from a string the way ffi.string does.

Returns Type

array()

ffi.array(type, length) -> Type

An array type.

Parameters

  • type (Type) — The element type.
  • length (number|nil) — Leave out for an array of unknown length.

Returns Type

function_type()

ffi.function_type(returns, params: list, options) -> Type

A function type, for callbacks and for calling function pointers.

var compare = ffi.function_type(ffi.int, [ffi.ptr, ffi.ptr])

Parameters

  • returns (Type)
  • params (list) — The parameter types.
  • options (dict|nil) — variadic (default false) and abi, as for Library.function().

Returns Type

slice()

ffi.slice(type, mutable) -> StructType

The #[repr(C)] struct of a pointer and a length that a Rust slice is passed across the C ABI as: { ptr, len }, with len a usize counting elements.

Parameters

  • type (Type) — The element type.
  • mutable (bool|nil) — Whether the pointer is *mut; defaults to a *const one.

Returns StructType

type()

ffi.type(spelling: string) -> Type

A type written as C writes it, using only built-in type names: 'unsigned long', 'const char *', 'int[4]', 'void (*)(int)'. Declarations.type() resolves names declared there too.

Parameters

  • spelling (string)

Returns Type

Raises DeclarationError when it does not name a type.

alloc()

ffi.alloc(type, count) -> Pointer

Zeroed memory for count values of type, typed as type.

Freed when the last pointer into it is collected, or by Pointer.free().

var numbers = ffi.alloc(ffi.int, 4)
numbers.set(2, 99)

Parameters

  • type (Type)
  • count (number|nil) — Defaults to 1.

Returns Pointer

Raises FfiError when the type has no size.

alloc_bytes()

ffi.alloc_bytes(size, align) -> Pointer

size zeroed bytes, as an untyped pointer.

Parameters

  • size (number)
  • align (number|nil) — A power of two. Defaults to 16, enough for any C type.

Returns Pointer

malloc()

ffi.malloc(size) -> Pointer

size zeroed bytes from the C allocator, as an untyped pointer.

Unlike alloc(), this memory is never freed when the pointer is collected, because the usual reason to want it is to hand it to C code that frees it with free(). Free it with Pointer.free() otherwise.

Parameters

  • size (number)

Returns Pointer

alloc_string()

ffi.alloc_string(text: string, encoding) -> Pointer

A NUL-terminated copy of text in memory that lasts, typed as its code unit: char, char16_t, wchar_t or char32_t.

Needed where a string must outlive a call: stored in a struct, or kept by the library.

Parameters

  • text (string)
  • encoding (string|nil) — 'utf-8' (the default), 'utf-16', 'utf-32' or 'wide'.

Returns Pointer

at()

ffi.at(address, type) -> Pointer

A pointer to a known address. Nothing about the address is checked.

Parameters

  • address (number|bigint)
  • type (Type|nil) — What it points at.

Returns Pointer

function()

ffi.function(pointer, type, name) -> function

A Zuri function that calls the C function at pointer with the signature of type.

Parameters

  • pointer (Pointer)
  • type (Type) — A function type, or a pointer to one.
  • name (string|nil) — The name the function reports.

Returns function

Raises PointerError when the pointer is null.

threaded()

ffi.threaded(function) -> function

The same foreign function, run on a helper thread while the calling isolate keeps answering callbacks called from other threads.

This is for a function that waits on threads of its own which call back into Zuri. Called the ordinary way, such a function would wait forever, because the isolate that has to run the callbacks is the one waiting for it.

Parameters

  • function (function) — A foreign function.

Returns function

Raises TypeError when function is not a foreign function.

is_foreign()

ffi.is_foreign(value) -> bool

Whether value is a foreign function this module made.

Parameters

  • value (any)

Returns bool

describe()

ffi.describe(function) -> dict

What a foreign function is: { name, pointer, type, threaded, library }, where pointer is its address as a Pointer, type its function pointer type, and library the path of the library it came from, or nil.

Parameters

  • function (function) — A foreign function.

Returns dict

callback()

ffi.callback(function, type, options) -> Callback

A lasting C function pointer that calls function.

var on_event = ffi.callback(@(code) {
  echo 'event ${code}'
}, ffi.function_type(ffi.void, [ffi.int]))

lib.set_handler(on_event)

The callback lives until Callback.release(). When the Zuri function raises, C gets error_value (or zero), and the error is raised again as soon as control returns to Zuri.

Parameters

  • function (function)
  • type (Type) — A function type, or a pointer to one.
  • options (dict|nil) — error_value, returned to C when function raises. Defaults to zero, or nothing for a void callback.

Returns Callback

Raises TypeError when type is not a function type or is variadic.

Raises CallbackError when 256 callbacks returning a 128-bit integer under the Microsoft x64 convention are alive already; each one released makes room for another.

serve()

ffi.serve(timeout) -> number

Runs every callback called from another thread that is waiting for this isolate, returning how many ran.

Such calls also run at the isolate’s own safepoints, during foreign calls and in threaded calls, so this is for a program that wants them answered at a point of its choosing, or wants to wait for one.

Parameters

  • timeout (number|nil) — Milliseconds to wait for a call when none is waiting. Returns at once when left out.

Returns number

errno()

ffi.errno() -> number

errno as the most recent foreign call on this isolate left it.

errno is cleared before every foreign call and read immediately after it, so this is always what that one call set, or 0.

Returns number

set_errno()

ffi.set_errno(value)

Sets errno, for the rare API that reads it.

Parameters

  • value (number)

last_error()

ffi.last_error() -> number

On Windows, GetLastError() as the most recent foreign call on this isolate left it, cleared before the call and read after it as errno is. Always 0 on other platforms.

Returns number

ffi.link(archives, options) -> Library

Links static libraries into a shared one with the platform’s linker, and loads it.

var lib = ffi.link('target/release/libgeometry.a')

Every object in the archives is linked in. The result is cached under a name derived from the archives’ contents and the options, so the linker runs once for a given input. An archive holding Rust code is recognised and linked against the system libraries Rust’s standard library needs.

The linker is the C compiler, cc or $CC, on Linux and macOS, and MSVC’s link.exe on Windows, found through the Visual Studio installation. On Windows a static library’s functions are not exported by default, so every symbol the archives define with an unmangled name is.

Parameters

  • archives (string|list) — One path or a list of them.
  • options (dict|nil) — libraries, names to link against (['m']); search_paths, directories to find them in; exports, on Windows the exact symbols to export; linker, the program to run; flags, further arguments for it; cache, the directory results are kept in (default default_link_cache()); rust, whether the archives hold Rust code (detected when left out).

Returns Library

Raises LinkError with the linker’s output when linking fails.

Raises LoadError when the linked library cannot be loaded.

ffi.default_link_cache() -> string

The directory link() keeps linked libraries in by default: $XDG_CACHE_HOME/zuri/ffi (or ~/.cache/zuri/ffi) on Linux, ~/Library/Caches/zuri/ffi on macOS, and %LOCALAPPDATA%\zuri\ffi on Windows.

Returns string

platform()

ffi.platform() -> dict

The facts about this platform that decide how C types are laid out: { os, arch, pointer_size, long_size, wchar_size, char_signed, long_double, complex, layout }.

long_double is 'x87', 'binary128' or 'double'; complex says whether complex numbers can be passed by value; layout is 'msvc' on Windows and 'itanium' elsewhere, the rules records follow.

Returns dict


2026, Richard Ore and Zuri contributors

ffi.callback

import ffi.callback

ffi lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled ffi.callback.* needs import ffi.callback.

Zuri functions that C can call.

Classes

Callback

class ffi.Callback

A Zuri function behind a C function pointer, made by ffi.callback().

A callback lives until release(). Nothing else frees it, because nothing can know when the C library has finished with the pointer; a callback that is never released is kept for the life of the program, and so is the function behind it.

A Zuri function passed straight to a function-pointer parameter needs none of this: it becomes a callback for the length of that one call. Make one explicitly when C keeps the pointer past the call.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

ffi.Callback()

Callback.pointer()

ffi.Callback.pointer() -> Pointer

The C function pointer, typed with the callback’s signature.

Returns Pointer

Raises CallbackError when the callback has been released.

Callback.type()

ffi.Callback.type() -> Type

The callback’s type: a pointer to its function type.

Returns Type

Callback.release()

ffi.Callback.release()

Frees the callback’s code and lets go of its function.

C must not call the pointer again. Passing the callback to a foreign function after this raises CallbackError; a call C still makes through a pointer it kept is undefined, exactly as a call through any freed function pointer is. Releasing twice does nothing.

Callback.is_released()

ffi.Callback.is_released() -> bool

Whether release() has been called.

Returns bool

Callback.to_string()

ffi.Callback.to_string()

2026, Richard Ore and Zuri contributors

ffi.declare

import ffi.declare

ffi lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled ffi.declare.* needs import ffi.declare.

Declarations read from C and Rust source.

Classes

Declarations

class ffi.Declarations

A set of declarations: types, functions, variables and constants, read from any mix of C and Rust source.

Each source added sees everything added before it, and anything in the declarations it includes. Nothing is bound to a library until bind(), so one set of declarations serves any number of libraries.

var api = ffi.declarations()
  .declare('typedef struct { double x, y; } point;')
  .declare('double length(point p);')

var geometry = api.bind(ffi.open('geometry'))
echo geometry.length({ x: 3, y: 4 })
  • printable — has a @to_string(), so echo and print() show something useful

Constructor

ffi.Declarations()

Declarations.declare()

ffi.Declarations.declare(source: string)

Reads C declarations into the set, returning it for chaining.

Typedefs, structs, unions, enums, function prototypes, extern variables and _Static_asserts are read. So are object-like #defines, which become constants, and #if and its relatives, which are evaluated for this platform. #pragma pack is honoured. Including a C standard header is accepted, since everything it declares is already known; including any other file is an error, as is expanding a function-like macro.

A function returning const char * returns a string. A function with a body, as an inline helper in a header has, is read and not bound, since the library need not export it.

Parameters

  • source (string)

Returns — self

Raises DeclarationError with the line and column of the problem.

Declarations.declare_rust()

ffi.Declarations.declare_rust(source: string)

Reads Rust declarations into the set, returning it for chaining.

Functions in extern "C" blocks, and extern "C" fn definitions marked #[no_mangle] or #[export_name], are read with their bodies skipped. So are #[repr(C)], #[repr(transparent)], packed and align structs and unions, enums with #[repr(C)] or an integer #[repr] (with or without fields), type aliases, const items and extern statics. #[cfg(...)] is evaluated for this platform. Anything else, from use lines to impl blocks, is skipped, and a type may be used before it is declared.

Parameters

  • source (string)

Returns — self

Raises DeclarationError with the line and column of the problem.

Declarations.include()

ffi.Declarations.include(other)

Makes the types and constants of other usable by sources added to this set from now on, returning this set for chaining.

Parameters

  • other (Declarations)

Returns — self

Raises ValueError when that would make a set include itself.

Declarations.type()

ffi.Declarations.type(spelling: string) -> Type

A type written in C, resolved against these declarations: 'struct stat *', 'point[4]', 'int (*)(const void *, const void *)'.

Parameters

  • spelling (string)

Returns Type

Raises DeclarationError when it does not name a type.

Declarations.constant()

ffi.Declarations.constant(name: string) -> number|bigint|string|Pointer|nil

The value of a declared constant: an enum constant, a #define, or a Rust const.

A #define casting a constant to a pointer type, as a header spells a sentinel address, is a Pointer of that type at that address: ((void *) -1) is the all-ones address, and a null one is nil. Passed where C takes that pointer, it is exactly the value the C code would pass.

Parameters

  • name (string)

Returns number|bigint|string|Pointer|nil

Raises ValueError when no constant of that name was declared.

Declarations.constants()

ffi.Declarations.constants() -> dict

Every declared constant, by name, in the order declared.

Returns dict

Declarations.types()

ffi.Declarations.types() -> dict

Every named type: typedefs and Rust type names under their names, and tagged C types under their tags, such as 'struct point'.

Returns dict

Declarations.functions()

ffi.Declarations.functions() -> dict

Every declared function’s type, by name.

Returns dict

Declarations.variables()

ffi.Declarations.variables() -> dict

Every declared variable’s type, by name.

Returns dict

Declarations.bind()

ffi.Declarations.bind(library, options) -> module

Binds the declarations to library, returning a namespace.

The namespace holds a function for each declared function, a Pointer for each declared variable, each constant, and each type that has a name of its own (a typedef or a Rust type); tagged C types are reached with type(). Members are read the way a module’s are:

var sqlite = api.bind(ffi.open('sqlite3'))
echo sqlite.sqlite3_libversion()

Parameters

  • library (Library)
  • options (dict|nil) — allow_missing: when true, a declared function or variable the library does not export is left out of the namespace instead of being an error. Defaults to false.

Returns module

Raises SymbolError naming every missing symbol, unless allow_missing is set.

Declarations.to_string()

ffi.Declarations.to_string()

2026, Richard Ore and Zuri contributors

ffi.errors

import ffi

Everything here is re-exported by ffi, so import ffi is enough and the names are called as ffi.*. Importing ffi.errors on its own works too and reaches the same definitions.

Every error the ffi module raises, under one root.

FfiError is that root, and each subclass names the stage that failed: finding a library, finding a symbol in it, reading a declaration, touching memory, running a callback, or linking a static library. A value that cannot be converted to the C type it is headed for is a TypeError or RangeError from the prelude instead, the same errors any other function raises for a bad argument, and the message names the parameter and the type.

catch {
  var sqlite = ffi.open('sqlite3')
} as error {
  if instance_of(error, ffi.LoadError) {
    echo 'SQLite is not installed: ${error.message}'
  }
}

Classes

FfiError

class ffi.FfiError < Error

Base class for every error this module raises.

Catch this to catch anything ffi itself can do. Raised directly for a type that cannot be used the way it was asked to be: a record with no size passed by value, a signature libffi cannot call.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

ffi.FfiError(message)

Parameters

  • message (string)

FfiError.to_string()

ffi.FfiError.to_string()

LoadError

class ffi.LoadError < FfiError

A library could not be found or loaded.

The message carries the loader’s own explanation when it gave one: a missing dependency, a library built for another architecture, a file that is not a library at all. Also raised when a function is called after its library was closed.

  • printable — has a @to_string(), so echo and print() show something useful

SymbolError

class ffi.SymbolError < FfiError

A library does not export a symbol that was asked for.

Raised by Library.function(), Library.variable() and by binding declarations, which lists every name it could not find in one error.

  • printable — has a @to_string(), so echo and print() show something useful

DeclarationError

class ffi.DeclarationError < FfiError

C or Rust source handed to a declaration could not be read.

line and column are both 1-based and point at the token the reader stopped at, counted within the source string that was passed in.

catch {
  ffi.declare('int add(int a, int b)')
} as error {
  echo '${error.message} at ${error.line}:${error.column}'
}
  • printable — has a @to_string(), so echo and print() show something useful

Fields

FieldTypeDescription
linenumberThe 1-based line the reader stopped at.
columnnumberThe 1-based column within that line.

Constructor

ffi.DeclarationError(message, line, column)

Parameters

  • message (string)
  • line (number|nil)
  • column (number|nil)

DeclarationError.to_string()

ffi.DeclarationError.to_string()

PointerError

class ffi.PointerError < FfiError

Memory was touched in a way that would have been undefined in C.

Reading or writing through a null pointer, past the end of a block the module allocated or was told the size of, through memory that has already been freed, freeing twice, or freeing memory this program does not own. The access is refused before it happens, so nothing has been corrupted when this is raised.

  • printable — has a @to_string(), so echo and print() show something useful

CallbackError

class ffi.CallbackError < FfiError

A callback could not be created, or was used after its release.

An error raised by the Zuri function behind a callback is not wrapped in this. It is raised again, as it was, once the C code that called the callback returns.

  • printable — has a @to_string(), so echo and print() show something useful

LinkError

class ffi.LinkError < FfiError

A static library could not be linked into a loadable one.

The message includes the linker’s own output.

  • printable — has a @to_string(), so echo and print() show something useful

2026, Richard Ore and Zuri contributors

ffi.library

import ffi.library

ffi lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled ffi.library.* needs import ffi.library.

A loaded shared library, and binding what it exports.

Classes

Library

class ffi.Library

A shared library loaded into the process, or the process itself.

Libraries come from ffi.open() and ffi.link(). Functions are bound from one either one at a time with function(), or in bulk from declarations with declare(), declare_rust() and bind().

A library stays loaded while anything bound from it is still reachable, even after close(): closing only stops further use.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

ffi.Library()

Library.path()

ffi.Library.path() -> string|nil

The path the library was loaded from, as the loader was given it, or nil for the running process.

Returns string|nil

Library.close()

ffi.Library.close()

Stops the library being used. Every function bound from it raises LoadError when called from now on. The code is unloaded once nothing refers to the library any longer.

Library.is_closed()

ffi.Library.is_closed() -> bool

Whether close() has been called.

Returns bool

Library.function()

ffi.Library.function(name: string, returns, params: list, options) -> function

Binds one function by name and signature, returning it as a Zuri function.

var strlen = libc.function('strlen', ffi.size_t, [ffi.string])
echo strlen('hello')

For a variadic function, params lists only the fixed parameters. Each extra argument travels as the type its value suggests: an integral number that fits an int as int, a larger one as long long, any other number as double, a bool as int, a string as const char *, and a pointer, bytes or nil as void *. Give any other type with Type.of().

On glibc, name@VERSION binds that version of a versioned symbol.

Parameters

  • name (string)
  • returns (Type)
  • params (list) — The parameter types.
  • options (dict|nil) — variadic (default false) and abi, the calling convention: 'default', or on x86-64 'win64' or 'sysv64'.

Returns function

Raises SymbolError when the library does not export name.

Raises FfiError when libffi cannot call the signature.

Library.variable()

ffi.Library.variable(name: string, type) -> Pointer

A pointer to a global variable the library exports, typed as the variable, so get() reads it and set() writes it.

Parameters

  • name (string)
  • type (Type)

Returns Pointer

Raises SymbolError when the library does not export name.

Library.symbol()

ffi.Library.symbol(name: string, type) -> Pointer|nil

The address of an exported symbol, or nil when there is none.

Parameters

  • name (string)
  • type (Type|nil) — The type to give the pointer.

Returns Pointer|nil

Library.has()

ffi.Library.has(name: string) -> bool

Whether the library exports name.

Parameters

  • name (string)

Returns bool

Library.declare()

ffi.Library.declare(source: string, options) -> module

Reads C declarations and binds them to this library in one step, returning the namespace Declarations.bind() does.

var c = libc.declare('
  size_t strlen(const char *s);
  int abs(int n);
')

echo c.strlen('hello')

Parameters

  • source (string)
  • options (dict|nil) — allow_missing as for bind(), and include, a list of Declarations whose types the source may use.

Returns module

Raises DeclarationError when the source cannot be read.

Raises SymbolError when a declared function or variable is missing.

Library.declare_rust()

ffi.Library.declare_rust(source: string, options) -> module

Reads Rust declarations and binds them to this library in one step.

var geometry = ffi.open('geometry').declare_rust('
  #[repr(C)]
  pub struct Point { pub x: f64, pub y: f64 }

  #[no_mangle]
  pub extern "C" fn distance(a: Point, b: Point) -> f64 { 0.0 }
')

Parameters

  • source (string)
  • options (dict|nil) — As for declare().

Returns module

Raises DeclarationError when the source cannot be read.

Raises SymbolError when a declared function or variable is missing.

Library.bind()

ffi.Library.bind(declarations, options) -> module

Binds everything declarations holds to this library; the same as declarations.bind(library, options).

Parameters

  • declarations (Declarations)
  • options (dict|nil) — As for Declarations.bind().

Returns module

Library.to_string()

ffi.Library.to_string()

2026, Richard Ore and Zuri contributors

ffi.pointer

import ffi.pointer

ffi lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled ffi.pointer.* needs import ffi.pointer.

Addresses in native memory, and reading and writing through them.

A Pointer is an address with, optionally, the type of what it points at. The type is what get(), set() and the field methods read and write, and the unit add() steps by; read() and write() take a type of their own and work on any pointer.

Memory this module allocated knows its own extent, and every access through a pointer into it is checked against that extent and against the memory having been freed. A pointer C handed back carries no extent, so only a null pointer is caught; reading past what the C library says is there is as undefined as it is in C.

Classes

Pointer

class ffi.Pointer

An address in native memory.

Pointers come from ffi.alloc() and its siblings, from functions that return one, and from reading a pointer out of memory. A null pointer returned from C arrives as nil instead, so if ptr is how a program checks for one.

Memory from ffi.alloc() is freed when the last pointer into it is collected. Keep a pointer reachable for as long as C code holds the address.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

ffi.Pointer()

Pointer.address()

ffi.Pointer.address() -> number|bigint

The address, as a number, or as a bigint where it is too large for a number to hold exactly.

Returns number|bigint

Pointer.is_null()

ffi.Pointer.is_null() -> bool

Whether the address is zero. A null pointer only exists as the result of arithmetic or ffi.at(0); C returning one gives nil.

Returns bool

Pointer.type()

ffi.Pointer.type() -> Type|nil

The type of what the pointer points at, or nil for an untyped pointer such as ffi.alloc_bytes() returns.

Returns Type|nil

Pointer.cast()

ffi.Pointer.cast(type) -> Pointer

The same address, pointing at type instead. Bounds and ownership carry over. Pass nil for an untyped pointer.

Parameters

  • type (Type|nil)

Returns Pointer

Pointer.offset()

ffi.Pointer.offset(bytes) -> Pointer

The pointer bytes further on, negative for back. Keeps the type.

Parameters

  • bytes (number)

Returns Pointer

Pointer.add()

ffi.Pointer.add(count) -> Pointer

The pointer count elements further on, as C’s ptr + count is.

Parameters

  • count (number) — Negative steps back.

Returns Pointer

Raises TypeError when the pointer has no element type, or its element type has no size.

Pointer.size()

ffi.Pointer.size() -> number|nil

How many bytes are known to be addressable from here to the end of the block, or nil when the extent is unknown, as it is for any pointer C handed back.

Returns number|nil

Pointer.read()

ffi.Pointer.read(type, offset) -> any

Reads a value of type at offset bytes from the pointer.

var header = ffi.alloc_bytes(16)
var magic = header.read(ffi.uint32)
var length = header.read(ffi.uint64, 8)

Parameters

  • type (Type)
  • offset (number|nil) — Defaults to 0.

Returns any

Raises PointerError when the pointer is null, the memory is freed, or the read runs outside a known block.

Pointer.write()

ffi.Pointer.write(type, value, offset)

Writes value as type at offset bytes from the pointer.

Nothing is written when the value does not convert, so a failed write never leaves half a value behind. Writing a record or array as a whole replaces every member; members the value leaves out keep what they held.

Parameters

  • type (Type)
  • value (any)
  • offset (number|nil) — Defaults to 0.

Raises PointerError for the reasons read() does.

Raises TypeError when value cannot be stored as type.

Raises RangeError when a number does not fit type.

Pointer.get()

ffi.Pointer.get(index) -> any

Reads element index of the pointer’s type, as C’s ptr[index].

Parameters

  • index (number|nil) — Defaults to 0.

Returns any

Raises TypeError when the pointer has no element type.

Raises PointerError for the reasons read() does.

Pointer.set()

ffi.Pointer.set(index, value)

Writes element index of the pointer’s type, as C’s ptr[index] = value.

Parameters

  • index (number)
  • value (any)

Raises TypeError when the pointer has no element type or the value does not convert.

Raises PointerError for the reasons read() does.

Pointer.get_field()

ffi.Pointer.get_field(name) -> any

Reads one member of the struct or union the pointer points at, as C’s ptr->name. Bitfields read like any other member.

Parameters

  • name (string)

Returns any

Raises TypeError when the pointer does not point at a record.

Raises ValueError when the record has no such member.

Pointer.set_field()

ffi.Pointer.set_field(name, value)

Writes one member of the struct or union the pointer points at, as C’s ptr->name = value, leaving the other members alone.

Parameters

  • name (string)
  • value (any)

Raises TypeError when the pointer does not point at a record, or the value does not convert.

Raises ValueError when the record has no such member.

Raises RangeError when a number does not fit, including a bitfield’s width.

Pointer.field()

ffi.Pointer.field(name) -> Pointer

A pointer to one member, typed as the member, as C’s &ptr->name. Reaching into nested records is a chain of these.

An array member gives a pointer to its first element instead, typed as the element, as the member itself decays to in C. That is how a flexible array member’s elements are reached.

Parameters

  • name (string)

Returns Pointer

Raises TypeError for a bitfield, which has no address.

Pointer.read_string()

ffi.Pointer.read_string(length, encoding, offset) -> string

Reads text.

With a length, exactly that many code units; without one, up to the first zero code unit, which must lie within the block when the block’s extent is known.

Parameters

  • length (number|nil) — In code units, not bytes.
  • encoding (string|nil) — 'utf-8' (the default), 'utf-16', 'utf-32', or 'wide' for wchar_t, which is UTF-16 on Windows and UTF-32 elsewhere.
  • offset (number|nil) — In bytes. Defaults to 0.

Returns string

Raises ValueError when the text is not valid in its encoding.

Raises PointerError when there is no terminator inside a known block.

Pointer.write_string()

ffi.Pointer.write_string(text, encoding, offset) -> number

Writes text and a terminating zero, returning the bytes written.

Parameters

  • text (string)
  • encoding (string|nil) — As for read_string().
  • offset (number|nil) — In bytes. Defaults to 0.

Returns number

Raises PointerError when the text and terminator do not fit a known block.

Pointer.read_bytes()

ffi.Pointer.read_bytes(length, offset) -> bytes

Copies length bytes out.

Parameters

  • length (number)
  • offset (number|nil) — Defaults to 0.

Returns bytes

Pointer.write_bytes()

ffi.Pointer.write_bytes(data, offset) -> number

Copies data in, returning how many bytes were written.

Parameters

  • data (bytes)
  • offset (number|nil) — Defaults to 0.

Returns number

Pointer.to_list()

ffi.Pointer.to_list(count) -> list

Reads count elements of the pointer’s type into a list.

Parameters

  • count (number)

Returns list

Pointer.copy_from()

ffi.Pointer.copy_from(source, length)

Copies length bytes from source, a pointer or bytes, to here. The two ranges may overlap.

Parameters

  • source (Pointer|bytes)
  • length (number)

Pointer.fill()

ffi.Pointer.fill(byte, length)

Sets length bytes to byte.

Parameters

  • byte (number) — 0 to 255.
  • length (number)

Pointer.compare()

ffi.Pointer.compare(other, length) -> number

Compares length bytes here with length bytes at other, as memcmp does: -1, 0 or 1.

Parameters

  • other (Pointer)
  • length (number)

Returns number

Pointer.own()

ffi.Pointer.own(destructor)

Takes ownership of memory C allocated, so it is released when the pointer is collected.

destructor is the C function that releases it, taking the address as its one argument. Leave it out for memory from the C allocator, which free releases. Pointers derived from this one afterwards share the ownership.

A destructor run by the collector cannot call back into Zuri: any callback it calls returns zero to C, or its error_value. free() runs the destructor with callbacks working as usual.

var text = c.strdup('hello').own()
var db = sqlite.open_handle(path).own(sqlite.sqlite3_close)

Parameters

  • destructor (function|nil) — A foreign function of one pointer.

Returns — self

Raises PointerError when the pointer is null or already owned.

Raises TypeError when the destructor is not a foreign function of one pointer.

Pointer.free()

ffi.Pointer.free()

Releases owned memory now rather than when it is collected: memory from ffi.alloc() or ffi.malloc(), or taken over with own(). Every pointer into it refuses access from then on.

Raises PointerError when the pointer does not own its memory, does not point at the start of it, or it has already been freed.

Pointer.is_freed()

ffi.Pointer.is_freed() -> bool

Whether the memory this pointer owns has been freed.

Returns bool

Pointer.is_owned()

ffi.Pointer.is_owned() -> bool

Whether this pointer owns its memory: it came from ffi.alloc() or ffi.malloc(), or own() was called on it or a pointer it came from.

Returns bool

Pointer.equals()

ffi.Pointer.equals(other) -> bool

Whether other is a pointer to the same address.

Parameters

  • other (any)

Returns bool

Pointer.to_string()

ffi.Pointer.to_string()

2026, Richard Ore and Zuri contributors

ffi.types

import ffi.types

ffi lifts part of this module out to its own top level; each name below is shown with the path that reaches it. Anything still spelled ffi.types.* needs import ffi.types.

C types as values.

Every type the module works with is a Type: the built-in ones the module exports (ffi.int, ffi.double, ffi.size_t), the ones built from them (ffi.pointer(ffi.char), ffi.array(ffi.int, 4)), records built member by member, and every type a declaration names. A type knows its size and alignment on this platform, and it is what every conversion between a Zuri value and C memory is driven by.

Records are built with chained calls and are laid out the first time anything needs their size, after which they can no longer change:

var Point = ffi.struct('Point')
  .add_field('x', ffi.double)
  .add_field('y', ffi.double)

echo Point.size()

Classes

Type

class ffi.Type

A C type.

Types are never constructed directly. They come from the module’s constants and builders, and from declarations.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

ffi.Type()

Type.name()

ffi.Type.name() -> string

The type as C spells it: int, const char *, struct point, or the name a typedef or a Rust declaration gave it.

Returns string

Type.kind()

ffi.Type.kind() -> string

What sort of type this is.

One of 'void', 'bool', 'int' (every integer and character type), 'float' (float, double and long double), 'complex', 'char' (a Rust char), 'pointer', 'string' (a pointer that converts to and from a string), 'function pointer', 'array', 'struct', 'union', 'enum' (C enums and Rust enums alike) and 'function'.

Returns string

Type.size()

ffi.Type.size() -> number

Size in bytes on this platform, as C’s sizeof gives it.

Asking for a record’s size lays the record out, after which no member can be added to it.

Returns number

Raises FfiError when the type has no size: void, a function, an array without a length, or a record that was declared and never defined.

Type.align()

ffi.Type.align() -> number

Alignment in bytes on this platform, as C’s _Alignof gives it.

Returns number

Raises FfiError when the type has no alignment, for the same types that have no size.

Type.is_const()

ffi.Type.is_const() -> bool

Whether the type is const-qualified.

A pointer to a const type is never written through, so a list or dictionary passed for one is not updated after the call.

Returns bool

Type.target()

ffi.Type.target() -> Type|nil

The type this one is made from: what a pointer points at, what an array holds, or the integer type an enum is stored as. nil for every other type.

Returns Type|nil

Type.length()

ffi.Type.length() -> number|nil

An array type’s number of elements, or nil for any other type and for an array declared without a length.

Returns number|nil

Type.signature()

ffi.Type.signature() -> dict|nil

The signature of a function type or a function pointer type, as { returns, params, variadic, abi }, or nil for any other type.

returns is a Type, params a list of them, variadic whether arguments may follow the fixed ones, and abi the calling convention’s name: 'default', 'win64' or 'sysv64'.

Returns dict|nil

Type.pointer()

ffi.Type.pointer() -> Type

A pointer to this type; the same as ffi.pointer(type).

Returns Type

Type.array()

ffi.Type.array(length) -> Type

An array of length values of this type; the same as ffi.array(type, length).

Parameters

  • length (number|nil) — Leave out for an array of unknown length, as a flexible array member is declared.

Returns Type

Type.as_const()

ffi.Type.as_const() -> Type

This type with const added.

Returns Type

Type.of()

ffi.Type.of(value) -> Typed

value marked as this type, for passing to a variadic function.

A variadic function’s extra arguments have no declared types, so each one otherwise travels as the type its value suggests (see Library.function()). This fixes it instead:

c.printf('%f %ld\n', ffi.double.of(1), ffi.long.of(7))

C’s default promotions still apply on top: a float travels as a double, and an integer narrower than int as an int.

Parameters

  • value (any)

Returns Typed

Type.equals()

ffi.Type.equals(other) -> bool

Whether other is the same type.

Scalars, pointers and arrays compare by shape, so ffi.int32 equals ffi.int, and a typedef equals what it names. Records and enums compare by identity: two structs built separately are different types even with the same members, as they are in C.

Parameters

  • other (any)

Returns bool

Type.to_string()

ffi.Type.to_string()

RecordType

class ffi.RecordType < Type

A struct or a union, built member by member.

Members go in declaration order, each at the offset the platform’s C compiler would give it: GCC and Clang’s rules on Linux and macOS, MSVC’s on Windows. The two differ only for bitfields. The record is laid out the first time anything needs its size, including passing a value of it, and from then on it cannot change.

  • printable — has a @to_string(), so echo and print() show something useful

RecordType.add_field()

ffi.RecordType.add_field(name: string, type, align)

Adds a member, returning the record for chaining.

var Header = ffi.struct('Header')
  .add_field('magic', ffi.uint32)
  .add_field('payload', ffi.uint8.array(16))
  .add_field('checksum', ffi.uint64, 16)

An array type without a length is a flexible array member and must come last. A struct or union added with an empty name is an anonymous member, whose own members are reached as this record’s.

Parameters

  • name (string)
  • type (Type)
  • align (number|nil) — A minimum alignment in bytes, as _Alignas gives a member. A power of two. Defaults to the type’s own.

Returns — self

Raises FfiError when the record has already been laid out, the name is taken, or a member follows a flexible array member.

RecordType.add_bitfield()

ffi.RecordType.add_bitfield(name: string, type, bits: number)

Adds a bitfield, returning the record for chaining.

var Flags = ffi.struct('Flags')
  .add_bitfield('ready', ffi.uint, 1)
  .add_bitfield('mode', ffi.uint, 3)
  .add_bitfield('', ffi.uint, 0)
  .add_bitfield('count', ffi.uint, 12)

An empty name is an unnamed bitfield, which only takes up space; one of width zero ends the current storage unit, as in C.

Parameters

  • name (string)
  • type (Type) — An integer, bool or enum type.
  • bits (number) — The width, at most the type’s own.

Returns — self

Raises FfiError when the type is not an integer type or the width does not fit it.

RecordType.set_packed()

ffi.RecordType.set_packed(bytes)

Packs the members to at most bytes alignment, as #pragma pack(n) does, or tightly when bytes is left out, as __attribute__((packed)) does.

Parameters

  • bytes (number|nil) — 1, 2, 4, 8 or 16. Defaults to 1.

Returns — self

RecordType.set_align()

ffi.RecordType.set_align(bytes: number)

Raises the record’s own alignment to at least bytes, as __attribute__((aligned(n))) on the record does. Its size grows to a multiple of the new alignment.

Parameters

  • bytes (number) — A power of two.

Returns — self

RecordType.fields()

ffi.RecordType.fields() -> list

Every member in declaration order, with the members of anonymous members listed in their place.

Each is { name, type, offset, bits, bit_offset }. offset is in bytes from the start of the record. For a bitfield, bits is its width and bit_offset the position of its first bit from the start of the record; both are nil for every other member.

Returns list

RecordType.offset_of()

ffi.RecordType.offset_of(name: string) -> number

A member’s offset in bytes, as C’s offsetof gives it. For a bitfield, the byte its first bit is in.

Parameters

  • name (string)

Returns number

Raises ValueError when there is no such member.

RecordType.has_field()

ffi.RecordType.has_field(name: string) -> bool

Whether the record has a member called name.

Parameters

  • name (string)

Returns bool

RecordType.variants()

ffi.RecordType.variants() -> dict|nil

For a Rust enum with fields, each variant’s name and discriminant; nil for an ordinary record.

Returns dict|nil

StructType

class ffi.StructType < RecordType

A C struct, or a Rust #[repr(C)] struct or enum with fields.

A struct value passes into C as a dictionary of its members and comes back out as one. A member left out of the dictionary is zero. A Rust enum with fields is a dictionary too, with a variant key naming the variant, and a variant without fields may be passed as just its name.

  • printable — has a @to_string(), so echo and print() show something useful

UnionType

class ffi.UnionType < RecordType

A C union.

A union value passes into C as a dictionary holding exactly one of its members, the one to store. Read back, it is a dictionary of every member, each read from the same bytes, since which one is meaningful is something only the program knows.

  • printable — has a @to_string(), so echo and print() show something useful

EnumType

class ffi.EnumType < Type

A C enum, or a Rust enum without fields.

A value of an enum type is a number, and anywhere one is passed in, the name of a constant can be passed instead:

var Color = ffi.enum('Color').add_constant('RED', 0).add_constant('GREEN', 1)
set_color('GREEN')
  • printable — has a @to_string(), so echo and print() show something useful

EnumType.add_constant()

ffi.EnumType.add_constant(name: string, value)

Adds a named constant, returning the enum for chaining.

Parameters

  • name (string)
  • value (number) — An integer that fits the enum’s storage type.

Returns — self

Raises ValueError when the enum already has a constant of that name.

Raises RangeError when the value does not fit.

EnumType.constants()

ffi.EnumType.constants() -> dict

Every constant, by name, in the order they were declared.

Returns dict

EnumType.value()

ffi.EnumType.value(name: string) -> number

The value of the constant called name.

Parameters

  • name (string)

Returns number

Raises ValueError when there is no such constant.

Typed

class ffi.Typed

A value marked with the C type it should travel as. Made by Type.of(), and only meaningful as an argument to a variadic function.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

ffi.Typed(type, value)

Parameters

  • type (Type)
  • value (any)

Typed.type()

ffi.Typed.type() -> Type

The type the value travels as.

Returns Type

Typed.value()

ffi.Typed.value() -> any

The value itself.

Returns any

Typed.to_string()

ffi.Typed.to_string()

2026, Richard Ore and Zuri contributors

types

import types

Provides type validation and conversion capabilities

This module is wrapper around the builtin functions where applicable and does and return the same thing as the builtin alternative.

The types API

Every public name in types, wherever it is declared. Each links to the page that documents it.

NameKindSummary
types.alphafunctionReturns true if the value is a character and alphabetic, otherwise returns false.
types.boolfunctionReturns true if the value is a boolean or false otherwise.
types.bytesfunctionReturns true if the value is a bytes or false otherwise.
types.callablefunctionReturns true if the value is a callable function or class and false otherwise.
types.charfunctionReturns true if the value is a single character or false otherwise.
types.dictfunctionReturns true if the value is a dictionary or false otherwise.
types.digitfunctionReturns true if the value is a character and digit, otherwise returns false.
types.filefunctionReturns true if the value is a file or false otherwise.
types.functionfunctionReturns true if the value is a function or false otherwise.
types.instancefunctionReturns true if the value is an instance the given class, false otherwise.
types.intfunctionReturns true if the value is an integer or false otherwise.
types.is_a_classfunctionReturns true if the value is a class or false otherwise.
types.iterablefunctionReturns true if the value is an iterable or false otherwise.
types.listfunctionReturns true if the value is a list or false otherwise.
types.numberfunctionReturns true if the value is a number or false otherwise.
types.objectfunctionReturns true if the value is an object or false otherwise.
types.offunctionReturns the name of the type of value
types.stringfunctionReturns true if the value is a string or false otherwise.

Functions

of()

types.of(value) -> string

Returns the name of the type of value

Parameters

  • value (any)

Returns string

Note: method implemented as part of core language features

digit()

types.digit(value: string) -> bool

Returns true if the value is a character and digit, otherwise returns false.

Parameters

  • value (string)

Returns bool

alpha()

types.alpha(value: string) -> bool

Returns true if the value is a character and alphabetic, otherwise returns false.

Parameters

  • value (string)

Returns bool

int()

types.int(value) -> bool

Returns true if the value is an integer or false otherwise.

Parameters

  • value (any)

Returns bool

bool()

types.bool(value) -> bool

Returns true if the value is a boolean or false otherwise.

Parameters

  • value (any)

Returns bool

number()

types.number(value) -> bool

Returns true if the value is a number or false otherwise.

Parameters

  • value (any)

Returns bool

Note: this method also returns true for integers.

char()

types.char(value) -> bool

Returns true if the value is a single character or false otherwise.

Parameters

  • value (any)

Returns bool

string()

types.string(value) -> bool

Returns true if the value is a string or false otherwise.

Parameters

  • value (any)

Returns bool

bytes()

types.bytes(value) -> bool

Returns true if the value is a bytes or false otherwise.

Parameters

  • value (any)

Returns bool

list()

types.list(value) -> bool

Returns true if the value is a list or false otherwise.

Parameters

  • value (any)

Returns bool

dict()

types.dict(value) -> bool

Returns true if the value is a dictionary or false otherwise.

Parameters

  • value (any)

Returns bool

object()

types.object(value) -> bool

Returns true if the value is an object or false otherwise.

Parameters

  • value (any)

Returns bool

function()

types.function(value) -> bool

Returns true if the value is a function or false otherwise.

Parameters

  • value (any)

Returns bool

is_a_class()

types.is_a_class(value) -> bool

Returns true if the value is a class or false otherwise.

Parameters

  • value (any)

Returns bool

file()

types.file(value) -> bool

Returns true if the value is a file or false otherwise.

Parameters

  • value (any)

Returns bool

iterable()

types.iterable(value) -> bool

Returns true if the value is an iterable or false otherwise.

Parameters

  • value (any)

Returns bool

callable()

types.callable(value) -> bool

Returns true if the value is a callable function or class and false otherwise.

Parameters

  • value (any)

Returns bool

instance()

types.instance(value, type: type) -> bool

Returns true if the value is an instance the given class, false otherwise.

Parameters

  • value (any)
  • type (type)

Returns bool


2021, Richard Ore and Zuri contributors

enum

import enum

This module provides Zuri’s implementation of enumerations as described in the language documentation: a set of unique values bound to symbolic names via an alias.

Creating an enumeration

Initialize from a list of symbolic names; each is automatically assigned an ordinal value starting from zero:

import enum

var Gender = enum(['Male', 'Female'])
echo Gender.Male     # 0
echo Gender.Female   # 1

Initialize from a dictionary to bind specific values (numbers or strings) instead of automatic ordinals:

var Color = enum({
  Red: 'r',
  Green: 'g',
  Blue: 'b',
})
echo enum.to_string(Color)   # <Enum Red=r Green=g Blue=b>

By default, duplicate values in a dictionary-initialized enumeration raise an error. Pass true as the second argument to allow them:

var Speed = enum({
  Slow: 1,
  Sluggish: 1,
  Fast: 2,
}, true)

The enum API

Every public name in enum, wherever it is declared. Each links to the page that documents it.

NameKindSummary
enum.ensurefunctionReturns value unchanged if it is a valid value for the enumeration, or raises an Error if it is not.
enum.enumfunctionCreates a new enumeration from either a list of symbolic names or a dictionary of symbolic-name to value…
enum.hasfunctionReturns true if the enumeration contains the given symbolic key, or false otherwise.
enum.keysfunctionReturns the symbolic keys of an enumeration, in the order they were declared.
enum.to_dictfunctionReturns the enumeration as a key/value dictionary.
enum.to_stringfunctionReturns a string representation of the enumeration.
enum.to_value_dictfunctionReturns the enumeration as a value/key dictionary.
enum.valuesfunctionReturns the possible values of an enumeration, in the order their corresponding keys were declared.

Functions

enum()

enum.enum(source, allow_duplicates) -> dict

Creates a new enumeration from either a list of symbolic names or a dictionary of symbolic-name to value pairs.

When source is a list, every item must be a string; each is bound, in order, to an automatically assigned ordinal value starting at zero.

When source is a dictionary, its keys must be strings and its values must be numbers or strings. Duplicate values are rejected unless allow_duplicates is true.

Enumeration keys are always unique; since the source is itself a dictionary or list, a duplicated symbolic name is never possible to express in the first place.

Parameters

  • source (list[string]|dict)
  • allow_duplicates (?bool) — Default value is false.

Returns dict

Raises TypeError: If source is not a list or dictionary, or its entries are of the wrong type.

Raises Error: If a duplicate value is found and allow_duplicates is not true.

keys()

enum.keys(e) -> list[string]

Returns the symbolic keys of an enumeration, in the order they were declared.

Parameters

  • e (dict)

Returns list[string]

Note: Equivalent to calling .keys() directly on the enum object, since an enumeration is itself a dictionary.

values()

enum.values(e) -> list[number|string]

Returns the possible values of an enumeration, in the order their corresponding keys were declared.

Parameters

  • e (dict)

Returns list[number|string]

Note: Equivalent to calling .values() directly on the enum object.

to_dict()

enum.to_dict(e) -> dict

Returns the enumeration as a key/value dictionary.

Parameters

  • e (dict)

Returns dict

to_value_dict()

enum.to_value_dict(e) -> dict

Returns the enumeration as a value/key dictionary.

Since dictionaries cannot contain duplicate keys, if multiple enumeration keys share the same value, only the LAST declared key for that value is represented in the result; matching the documented behavior.

Parameters

  • e (dict)

Returns dict

has()

enum.has(e, key) -> bool

Returns true if the enumeration contains the given symbolic key, or false otherwise.

Parameters

  • e (dict)
  • key (string)

Returns bool

Note: Equivalent to calling .contains(key) directly on the enum object.

ensure()

enum.ensure(e, value) -> any

Returns value unchanged if it is a valid value for the enumeration, or raises an Error if it is not.

Parameters

  • e (dict)
  • value (any)

Returns any

Raises Error: If value is not one of the enumeration’s values.

to_string()

enum.to_string(e) -> string

Returns a string representation of the enumeration.

Parameters

  • e (dict)

Returns string


2026, Richard Ore and The Zuri Contributors

convert

import convert

Data conversion between hexadecimal, arbitrary numeric bases, bytes, and decimal numbers.

The convert API

Every public name in convert, wherever it is declared. Each links to the page that documents it.

NameKindSummary
convert.binary_to_decimalfunctionConverts a binary (base 2) string to a decimal number.
convert.bytes_to_decimalfunctionConverts bytes (binary data) to a decimal number, treating the bytes as an unsigned integer.
convert.bytes_to_hexfunctionConverts binary data (bytes) of any length to its hexadecimal string representation.
convert.decimal_to_binaryfunctionConverts a decimal number to a binary (base 2) string.
convert.decimal_to_bytesfunctionConverts a decimal number to bytes, the reverse of bytes_to_decimal().
convert.decimal_to_hexfunctionConverts the given decimal number to a hexadecimal string.
convert.decimal_to_octalfunctionConverts a decimal number to an octal (base 8) string.
convert.from_basefunctionConverts a string in the given base back to a decimal number.
convert.hex_to_bytesfunctionConverts a hexadecimal string of any (even) length to bytes.
convert.hex_to_decimalfunctionConverts a hexadecimal string to a decimal (base 10) number.
convert.octal_to_decimalfunctionConverts an octal (base 8) string to a decimal number.
convert.to_basefunctionConverts a decimal number to a string in the given base, using 0-9 then a-z for digits beyond 9 (so…
convert.unicode_to_hexfunctionConverts a single unicode character to its hexadecimal code point.

Functions

hex_to_bytes()

convert.hex_to_bytes(str) -> bytes

Converts a hexadecimal string of any (even) length to bytes.

Parameters

  • str (string)

Returns bytes

Raises TypeError if str isn’t a string.

Raises ValueError if str has an odd number of characters: a hex string always encodes whole bytes, so an odd length is ambiguous rather than something this function guesses at.

bytes_to_hex()

convert.bytes_to_hex(data) -> string

Converts binary data (bytes) of any length to its hexadecimal string representation.

Parameters

  • data (bytes)

Returns string

Raises TypeError if data isn’t bytes.

decimal_to_hex()

convert.decimal_to_hex(n, digits) -> string

Converts the given decimal number to a hexadecimal string. If digits is given and the result is shorter, it’s padded with leading zeros; if the result is longer, it’s truncated to the least-significant digits characters.

Parameters

  • n (number)
  • digits (?number)

Returns string

Raises TypeError if n isn’t a number.

Raises ValueError if digits is given but isn’t a number.

hex_to_decimal()

convert.hex_to_decimal(str) -> number

Converts a hexadecimal string to a decimal (base 10) number.

Parameters

  • str (string)

Returns number

Raises TypeError if str isn’t a string.

Raises ValueError if str contains a character that isn’t a valid hex digit.

Note: str may either be a plain hex string or carry a leading 0x prefix.

unicode_to_hex()

convert.unicode_to_hex(chr) -> string

Converts a single unicode character to its hexadecimal code point.

Parameters

  • char — chr

Returns string

Raises ValueError if chr isn’t exactly one character.

decimal_to_binary()

convert.decimal_to_binary(n, digits) -> string

Converts a decimal number to a binary (base 2) string.

Parameters

  • n (number)
  • digits (?number) — Zero-pads (or truncates) to this many characters, same as decimal_to_hex()’s digits parameter.

Returns string

binary_to_decimal()

convert.binary_to_decimal(str) -> number

Converts a binary (base 2) string to a decimal number.

Parameters

  • str (string)

Returns number

Raises ValueError if str contains a character other than 0 or 1.

decimal_to_octal()

convert.decimal_to_octal(n, digits) -> string

Converts a decimal number to an octal (base 8) string.

Parameters

  • n (number)
  • digits (?number) — Same as decimal_to_hex()’s digits parameter.

Returns string

octal_to_decimal()

convert.octal_to_decimal(str) -> number

Converts an octal (base 8) string to a decimal number.

Parameters

  • str (string)

Returns number

Raises ValueError if str contains a character outside 0-7.

to_base()

convert.to_base(n, base, digits) -> string

Converts a decimal number to a string in the given base, using 0-9 then a-z for digits beyond 9 (so base 16 produces lowercase hex, matching decimal_to_hex()). If digits is given and the result is shorter, it’s padded with leading zeros; if longer, it’s truncated to the least-significant digits characters.

Parameters

  • n (number)
  • base (number) — Between 2 and 36.
  • digits (?number)

Returns string

Raises TypeError if n isn’t a number.

Raises ValueError if base is outside 2-36, or if digits is given but isn’t a number.

from_base()

convert.from_base(str, base) -> number

Converts a string in the given base back to a decimal number. Accepts an optional leading - for a negative value, and digit letters in either case.

Parameters

  • str (string)
  • base (number) — Between 2 and 36.

Returns number

Raises TypeError if str isn’t a string.

Raises ValueError if base is outside 2-36, or if str contains a character that isn’t a valid digit in that base.

bytes_to_decimal()

convert.bytes_to_decimal(data, little_endian) -> number

Converts bytes (binary data) to a decimal number, treating the bytes as an unsigned integer.

Parameters

  • data (bytes)
  • little_endian (?bool) — Default false: the most significant byte comes first. Set true to instead treat the least significant byte as first, as many binary file formats and network protocols do.

Returns number

Raises TypeError if data isn’t bytes.

decimal_to_bytes()

convert.decimal_to_bytes(n, length, little_endian) -> bytes

Converts a decimal number to bytes, the reverse of bytes_to_decimal().

Parameters

  • n (number) — Must be non-negative: there’s no signedness convention (two’s complement vs. sign-magnitude) implied here, so a negative value is rejected rather than guessed at.
  • length (number) — The exact number of bytes to produce. If n doesn’t fit in length bytes, the most significant (excess) bits are silently dropped, the same as fitting a number into a fixed-width integer type would.
  • little_endian (?bool) — Default false: the most significant byte is written first. Set true for the least significant byte first.

Returns bytes

Raises TypeError if n isn’t a number.

Raises ValueError if n is negative, or length isn’t a non-negative number.


2021, Richard Ore and Zuri contributors

zuri

import zuri

Exposes Zuri’s own compiler pipeline as a library: lexing, parsing, and compiling a Zuri source file, plus a reflect submodule for inspecting live functions, classes, and modules at runtime.

  • tokenize(source) returns every lexical token in source, in order; see zuri.token.
  • parse(source) returns source’s AST as a list of nodes, with every comment and doc block preserved in place; see zuri.ast.
  • compile(source) returns source’s compiled VM instructions; see zuri.compile.
  • parse_partial(source) reads as much of source as it can and returns the tree along with every syntax error, and check(source) returns every problem compiling source would report, both as zuri.Diagnostics and neither raising on bad source; see zuri.ast and zuri.compile.
  • zuri.doc reads documentation comments: the prose and @tags of a doc block, and which declaration each block documents; see zuri.doc.
  • zuri.reflect inspects live functions, classes and modules, reads or updates an instance’s own properties, and runs the garbage collector on demand; see zuri.reflect.

The ordinary starting point in practice is a real file on disk, not a source string already sitting in a variable, so each of the above also has a _file counterpart that reads path first: tokenize_file(path), parse_file(path), compile_file(path), parse_partial_file(path) and check_file(path). There’s one more, dump_file(path), with no bare-source equivalent of its own; it reads and parses path, then returns the dump() of every top-level node, which is usually the single fastest way to answer “what’s actually in this file”.

The mental model

tokenize()/parse()/compile() all return the same SHAPE of thing: a list of small, uniformly-tagged records. A token has a kind; an AST node has a kind; an instruction has an op. Whichever one it is, everything specific to that particular kind lives in one place: token.value, node.fields, instr.fields. There’s a Token class, a Node class, and an Instr class, not one class per token kind / grammar rule / opcode. Zuri’s grammar alone has around fifty expression/statement/declaration shapes, and the instruction set has roughly sixty opcodes; a dedicated class for each would be well over a hundred near-identical classes to keep in sync with the compiler forever after, for what would ultimately still be a name + a handful of fields each. This is the same tradeoff tools like Python’s ast module or Babel’s ESTree make, for the same reason.

The real cost of that choice: nothing points you at what fields a given kind/op carries the way a typed class would. You can’t autocomplete your way to a Binary node’s left/op/right. So this module leans hard on a different way of finding out: run something and look, rather than read a spec first.

import zuri

var nodes = zuri.parse('def add(a, b) { return a + b }')
echo nodes[0].dump()
Function@1:1
  name: 'add'
  name_span: 1:5-1:8
  parameters:
    Argument@1:9
      name: 'a'
      name_span: 1:9-1:10
      type_hint:
        TypeHint@1:10
          types:
            Any@1:10
              class_name: nil
          nullable: true
    ...
  body:
    Block@1:15
      statements:
        Return@1:17
          value:
            Binary@1:24
              left:
                Identifier@1:24
                  name: 'a'
              op: 'Plus'
              right:
                Identifier@1:28
                  name: 'b'
  is_variadic: false

That single dump() call answers “what does a Function node look like” far more completely than a paragraph of prose would, and it answers it for whatever kind you’re actually holding, not just the ones someone thought to write up. zuri.ast.Node.dump and zuri.compile.Instr.dump both work this way; zuri.token.Token doesn’t need the same treatment, since a token’s own fields (kind, line, column, start, end, text, value) are always the same seven, spelled out in zuri.token’s own docs.

Once you know the kind/op you’re after, walk_nodes()/ walk_instrs() (and their shorthand, find_nodes()/ find_instrs()) do the actual work. Almost nothing real gets done by manually indexing into fields node by node; the normal shape of a task is “for every node/instruction of kind X, do Y”, regardless of how deep in the tree it sits (inside a nested function, inside an if, wherever), which is exactly what these two functions are for, so reach for them first.

import zuri

var nodes = zuri.parse(source)

# every function name declared anywhere in the file, including
# nested/inner functions
var names = []
zuri.walk_nodes(nodes, {
  Function: @(node) { names.append(node.fields.name) },
})

# shorthand for the common "just collect them" case
var comments = zuri.find_nodes(nodes, 'Comment')

So, end to end, the actual workflow is:

  1. Call parse_file()/compile_file() on something real (or parse()/compile(), if you already have the source as a string). echo zuri.dump_file(path) first, if you just want to look before writing any real code against it. 2. dump() the specific node/instruction you care about, to see its actual shape. 3. walk_nodes()/walk_instrs() (or find_nodes()/ find_instrs()) to do something with every one of that kind, wherever it appears. 4. Reach for zuri.ast’s or zuri.compile’s own doc comments only for the parts exploration doesn’t answer on its own: what a node’s position covers, how a Closure instruction’s resolved value nests a whole other instruction list inside it, and so on.

zuri.reflect doesn’t need any of this: it’s ordinary named functions (function_info(f), has_method(object, name), …) with ordinary parameters, not a generic tagged-record system, so its own doc comments are the whole story; see zuri.reflect.

import zuri

for t in zuri.tokenize('var x = 1') {
  echo t.kind
}

for node in zuri.parse('var x = 1') {
  echo node.kind
}

for instr in zuri.compile('var x = 1') {
  echo instr.op
}

def add(a, b) {
  return a + b
}
echo zuri.reflect.function_info(add).arity

The zuri API

Every public name in zuri, wherever it is declared. Each links to the page that documents it.

NameKindSummary
zuri.DiagnosticclassOne problem in a piece of Zuri source: what is wrong and exactly where.
zuri.InstrclassOne VM instruction.
zuri.NodeclassOne AST node: an expression, statement, declaration, type hint, or a standalone comment/doc block sitting…
zuri.ParseResultclassWhat parse_partial() read from a piece of source: the tree it built and every syntax error it found on the…
zuri.TokenclassOne lexical token from a Zuri source file.
zuri.checkfunctionEvery problem compiling source would report, as a list of zuri.Diagnostic in the order they occur,…
zuri.check_filefunctionReads the file at path and checks it; exactly check(file(path).read()).
zuri.compilefunctionCompiles source as a standalone script and returns its instructions as a flat list of Instr.
zuri.compile_filefunctionReads the file at path and compiles its contents; exactly compile(file(path).read()), for the common case…
zuri.doc.AttachedclassOne node from a list of siblings, with the doc block written directly above it, as attach() pairs them.
zuri.doc.BlockclassA parsed doc block: body, its markdown prose as a list of lines, and tags, every @tag in the order it…
zuri.doc.TagclassOne @tag from a doc block, its wrapped lines already joined into one string.
zuri.doc.attachfunctionPairs every node in a list of siblings with the doc block written directly above it.
zuri.doc.emptyfunctionA block with neither prose nor tags, for a declaration that has no documentation.
zuri.doc.module_blockfunctionThe module’s own documentation among the top-level nodes of a file: the first doc block carrying an @module…
zuri.doc.parsefunctionParses the text of a doc block, the text a DocBlock node carries, into its prose and tags.
zuri.doc.parse_linesfunctionParses the lines of a doc block into its prose and tags; the same as parse() for text already split into…
zuri.dump_filefunctionReads, parses, and dumps the file at path in one call: the fastest way to answer “what does this file’s AST…
zuri.find_instrsfunctionEvery instruction reachable from instrs whose op is op, in the order they appear.
zuri.find_nodesfunctionEvery node reachable from nodes whose kind is kind, in the order they appear.
zuri.parsefunctionParses source in full and returns its top-level declarations as a list of Node.
zuri.parse_filefunctionReads the file at path and parses its contents; exactly parse(file(path).read()), for the common case of…
zuri.parse_partialfunctionParses source as far as it can and returns everything it read along with every syntax error it found,…
zuri.parse_partial_filefunctionReads the file at path and parses as much of it as it can; exactly parse_partial(file(path).read()).
zuri.reflect.bind_methodfunctionMethod name on object’s class, bound to object itself as the receiver; the result can be called…
zuri.reflect.class_infofunctionMetadata for a class: { name, superclass_name, methods, fields, statics }.
zuri.reflect.del_propfunctionResets object’s existing property name back to nil.
zuri.reflect.function_infofunctionMetadata for a function-like value: { name, arity, variadic, is_method, owning_class_name, source_path }.
zuri.reflect.gcfunctionRuns a full garbage collection now, instead of waiting for the heap to cross its threshold.
zuri.reflect.get_decoratorfunctionThe decorator function named name (excluding the leading @) on the class behind object, bound to…
zuri.reflect.get_methodfunctionThe raw (unbound) closure for method name on the class behind object, or nil if it declares no such…
zuri.reflect.get_propfunctionThe current value of object’s (an instance or a module) property/member named name, or nil if it has…
zuri.reflect.get_propsfunctionEvery property/member name object (an instance or a module) has, as a list of strings, or an empty list if…
zuri.reflect.has_decoratorfunctionDoes the class behind object implement the decorator named name? A decorator is just a method whose own…
zuri.reflect.has_methodfunctionDoes the class behind object (an instance, or a class used directly) declare or inherit a method named…
zuri.reflect.has_propfunctionDoes object (an instance or a module) have a property/member named name?
zuri.reflect.infofunctionDispatches to function_info/class_info/module_info based on kind(value).
zuri.reflect.kindfunctionThis value’s runtime type tag: 'nil', 'bool', 'number', 'string', 'bytes', 'bigint', 'list',…
zuri.reflect.module_infofunctionMetadata for a module: { name, path, loaded, members }.
zuri.reflect.pointer_typefunctionThe registered resource tag a pointer value was allocated with (e.g. a native socket handle’s own internal…
zuri.reflect.set_propfunctionOverwrites object’s existing property name with value.
zuri.tokenizefunctionLexes source in full and returns every token it contains, in source order, as a list of Token.
zuri.tokenize_filefunctionReads the file at path and lexes its contents; exactly tokenize(file(path).read()), for the common case…
zuri.walk_instrsfunctionRecursively visits every instruction reachable from instrs (typically zuri.compile()’s own result, or any…
zuri.walk_nodesfunctionRecursively visits every node reachable from nodes (typically zuri.parse()’s own result, or any single…

Submodules

ModuleReached asSummary
zuri.astzuri.*The Node type zuri.parse() returns: a generic representation of one AST node, tagged by kind…
zuri.compilezuri.*The Instr type zuri.compile() returns: a generic representation of one VM instruction, tagged by op…
zuri.diagnosticzuri.*The Diagnostic type: one problem found in a piece of Zuri source by reading it rather than running it, as…
zuri.doczuri.doc.*Reads Zuri’s documentation comments: the doc blocks a zuri.parse() tree keeps as DocBlock nodes, turned…
zuri.reflectzuri.reflect.*Runtime introspection: metadata about a live function, class, or module, and property/method access on an…
zuri.tokenzuri.*The Token type zuri.tokenize() returns: one entry per lexical token the lexer found in a source file, in…

2026, Richard Ore and Zuri contributors

zuri.ast

import zuri

Everything here is re-exported by zuri, so import zuri is enough and the names are called as zuri.*. Importing zuri.ast on its own works too and reaches the same definitions.

The Node type zuri.parse() returns: a generic representation of one AST node, tagged by kind ('Binary', 'If', 'Function', …) with a fields dict holding whatever that kind carries. There is one Node class rather than one class per grammar rule, since the grammar has roughly fifty expression/statement/declaration shapes, and a dedicated class per shape would mean fifty near-identical classes to maintain in lockstep with the parser forever after. A fields entry that’s itself node-shaped (or a list of node-shaped entries) is already wrapped into a Node recursively, so you never need to call Node.from_dict() yourself.

import zuri

for node in zuri.parse('def add(a, b) { return a + b }') {
  echo node.kind
}
# Function

The tree is the program as it was written. A for loop is a For node, x += 1 is a CompoundAssign, and an interpolated string is an Interpolation; none of them is rewritten into the simpler forms the compiler turns them into.

Positions

Every node records the whole of the source it was read from:

AttributeMeaning
startThe offset of its first character
endThe offset just past its last character
line, colWhere it starts, both counted from 1
end_line, end_colWhere it ends: the column just past its last character

Offsets count characters, not bytes, and index the source the way slicing does, so source[node.start, node.end] is exactly the node’s own text. A node covers its delimiters: a string literal’s quotes, a block’s braces, a call’s parentheses.

A statement or declaration starts at its keyword: var total = 1 starts at var and def add(a, b) at def. A class member starts at its first word, static included. In a list of declarations, var a = 1, b, each Var spans from its name to the end of its value.

A node that names something through a name of its own also says where that name is, in its fields:

KindFieldCovers
Function, Method, Classname_spanThe declared name
Property, Var, Argumentname_spanThe declared name
Get, Setname_spanThe property name after the .
Forkey_span, value_spanEach loop variable
ImportsegmentsEach part of the path

A span field is a plain dict with the same six keys a node has, and a For without a key has a key_span of nil. An import segment adds text, the part as written: '.', '..' or a name.

A node the source leaves implicit has an empty span, start == end, where it would have been written: the nil of a return with no value or of a var with no initializer sits just after the keyword or name, and so does the Any type of an untyped parameter and the List type of a ... parameter.

Comments and doc blocks

Every #-comment and doc block comes back as its own 'Comment'/'DocBlock'-kind node, sitting exactly where it appeared: as a sibling in the surrounding statement or declaration list, at the top level, inside a { } block, or between two class members.

A comment written in the middle of a statement, among a call’s arguments or between an if block’s } and its else, has no place among that statement’s own nodes. It comes back as a sibling placed immediately after the smallest statement, declaration or class member that contains it, at that statement’s depth: a comment inside an if body three blocks deep comes back three blocks deep, never hoisted to the top. Its position still says where it was written.

import zuri

var source = "def foo() {\n  echo bar(a, # x\n  b)\n  echo 2\n}\n"
var body = zuri.parse(source)[0].fields.body.fields.statements
echo body[0].kind        # Echo, the statement the comment sat inside
echo body[1].kind        # Comment, placed right after it
echo body[1].fields.text # ' x'
echo body[2].kind        # Echo, the "echo 2" line, unaffected

A comment above a declaration comes back right before it:

import zuri

var source = "# a helper function\ndef add(a, b) {\n  return a + b\n}\n"
var nodes = zuri.parse(source)
echo nodes[0].kind        # Comment
echo nodes[0].fields.text # " a helper function"
echo nodes[1].kind        # Function

A class body’s fields and methods are two separate lists, a Class node’s properties and methods, so a comment between a field and a method lands in methods, the list of the member right after it, and a comment between two fields lands in properties. A comment after the last member goes to the list of the member before it. To rebuild a class body in its written order, merge the two lists and sort them by start.

Node kinds

Expressions:

KindFields
Nil, Self, ParentNone
Bool, Integer, Float, BigNumbervalue
Literalvalue: a string’s text, or a dictionary key written as a name
Interpolationparts: a string for each run of text and a node for each ${...}
Identifiername
Unaryop, operand
Binaryleft, op, right, for the arithmetic, bitwise and shift operators
Logicalleft, op, right, for the comparisons
Circuitleft, op, right, for and and or
Conditioncondition, then, otherwise: a ? b : c
Groupinginner: an expression in parentheses
Rangelower, upper
Callcallee, arguments
Getobject, name, name_span
Setobject, name, name_span, value: object.name = value
Indexobject, index
Sliceobject, lower, upper; an omitted bound is an implicit Nil
Listitems
Dictkeys, values, in matching order
Shorthandname: the value of a { name } entry, which reads name
Assigntarget, value: target = value
CompoundAssigntarget, op, value: target += value and its kin
Updatetarget, op: target++ or target--
Anonymousdeclaration: a Function named @anon and a number
TypeHinttypes, nullable
Argumentname, name_span, type_hint

A Literal’s value leaves out the quotes, and an Interpolation’s text parts come with their escapes already applied. A CompoundAssign’s op and an Update’s name the operator written, such as 'PlusEq' or 'Increment'.

Each entry of a TypeHint’s types is a node named after the type: Any, Bool, Int, Number, BigInt, String, Bytes, List, Dict, Range, File, Function, Type, Callable, Iterable, or Instance for a class. Every one carries class_name, the class an Instance names and nil on the rest.

Statements:

KindFields
Expression, Echo, Raise, Returnvalue
Varname, name_span, value, type_hint, is_constant
VarListdeclarations: the Var nodes of var a = 1, b = 2
Blockstatements
Ifcondition, then, otherwise
Whilecondition, body
DoWhilebody, condition
Forkey, key_span, value, value_span, iterable, body
Iterinitializer, condition, steps, body
Usingsubject, arms, default_body
Whenlabels, body: one arm of a Using
Catchbody, catch_body, error_var
Assertcondition, message
Importpath, segments, name, aliased, elements, imports_all, exported
Continue, BreakNone
Decldeclaration: a function declared inside a block
Comment, DocBlocktext: the comment without its #, /* and */

A For written as for value in iterable has a key of nil. An Iter clause left empty is nil, and with no steps steps is an empty list.

Declarations:

KindFields
Stmtstatement: a statement at the top level
Functionname, name_span, parameters, body, is_variadic
Classname, name_span, superclass, properties, methods, is_extension
Propertyname, name_span, value, type_hint, is_static, is_constant
Methodname, name_span, parameters, body, is_variadic, is_static

An Import’s name is the Identifier it binds: the name after as when aliased is true, otherwise the last part of its path, placed on that part. path joins every part with the platform’s path separator. A decorated method’s name keeps its @, as in '@new'.

Functions

parse()

zuri.parse(source) -> list[Node]

Parses source in full and returns its top-level declarations as a list of Node. See this module’s own doc comment for the exact shape, and in particular for what is and isn’t preserved around comments.

Raises (rather than returning a partial result) on a syntax error, since there is no meaningful AST to hand back for source the parser itself couldn’t make sense of.

Parameters

  • source (string) — The Zuri source to parse

Returns list[Node]

Raises Error if source has a syntax error

parse_partial()

zuri.parse_partial(source: string) -> ParseResult

Parses source as far as it can and returns everything it read along with every syntax error it found, without raising.

At a statement it cannot read, the parser reports the problem, skips to where the next statement starts and carries on, so one mistake is reported once and the rest of the source still comes back. The statement that failed comes back as far as it was read, or as a None node when nothing of it could be kept. A name the source is missing, such as the variable name in var = 2, comes back as an empty string with an empty span where the name belongs.

import zuri

var result = zuri.parse_partial('var a = 1\nvar = 2\nvar c = 3')

echo result.errors[0]       # 2:5: Variable name expected.
echo result.nodes.length()  # 3

Parameters

  • source (string) — The Zuri source to parse

Returns ParseResult

parse_partial_file()

zuri.parse_partial_file(path: string) -> ParseResult

Reads the file at path and parses as much of it as it can; exactly parse_partial(file(path).read()).

Parameters

  • path (string) — Path to the Zuri source file to parse

Returns ParseResult

Raises Error if path can’t be opened or read

parse_file()

zuri.parse_file(path) -> list[Node]

Reads the file at path and parses its contents; exactly parse(file(path).read()), for the common case of parsing a real file rather than a source string you’ve already got in hand.

Parameters

  • path (string) — Path to the Zuri source file to parse

Returns list[Node]

Raises Error if path can’t be opened or read, or has a syntax error

dump_file()

zuri.dump_file(path) -> string

Reads, parses, and dumps the file at path in one call: the fastest way to answer “what does this file’s AST actually look like”. Equivalent to parsing path and joining every resulting top-level node’s own dump() with a blank line between them.

import zuri
echo zuri.dump_file('main.zu')

Parameters

  • path (string) — Path to the Zuri source file to parse and dump

Returns string

Raises Error if path can’t be opened or read, or has a syntax error

walk_nodes()

zuri.walk_nodes(nodes, visitor)

Recursively visits every node reachable from nodes (typically zuri.parse()’s own result, or any single Node plucked out of it), in the order they appear, calling visitor for each one. This is the tool for “do something with every X”: most real uses of an AST are a walk, not manual field-by-field navigation.

visitor is either a plain function, called with every node (visitor(node)), or a dict from kind name to function, where only a node of that kind gets called (visitor[node.kind](node)). Reach for the dict form when you only care about one or two kinds; it reads as a small dispatch table instead of a chain of if node.kind == ... checks inside a single function.

Always visits the whole tree; there’s no way for visitor to skip descending into a particular node’s children.

import zuri

var nodes = zuri.parse(source)

# every function name declared anywhere in the file, including
# nested/inner functions
var names = []
zuri.walk_nodes(nodes, {
  Function: @(node) { names.append(node.fields.name) },
})

Parameters

  • nodes (Node|list[Node])
  • visitor (function|dict)

find_nodes()

zuri.find_nodes(nodes, kind) -> list[Node]

Every node reachable from nodes whose kind is kind, in the order they appear. Shorthand for the common “just give me a flat list of these” case walk_nodes() itself is built on.

import zuri

var nodes = zuri.parse(source)
for comment in zuri.find_nodes(nodes, 'Comment') {
  echo comment.fields.text
}

Parameters

  • nodes (Node|list[Node])
  • kind (string)

Returns list[Node]

Classes

Node

class zuri.Node

One AST node: an expression, statement, declaration, type hint, or a standalone comment/doc block sitting between two of those.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

zuri.Node(kind: string, position: dict, fields: dict)

Returns a new instance of a Node directly, from already-wrapped field values. Ordinary Zuri code should use Node.from_dict() or zuri.parse() instead, since this constructor does no wrapping of its own.

Parameters

  • kind (string) — The node’s kind, e.g. 'Binary', 'If'
  • position (dict) — Where the node sits in its source, as { start, end, line, col, end_line, end_col }; see this module’s own docs for what each one counts
  • fields (dict) — This kind’s own fields, already wrapped

Node.from_dict()

zuri.Node.from_dict(data: dict) -> Node

Builds a Node from a dict holding a node’s parts ({ kind, start, end, line, col, end_line, end_col, fields }), recursively wrapping any node-shaped entry inside fields, a single nested node or a list of them, into its own Node. A fields entry that isn’t node-shaped (a string, a number, a bool, nil, or a plain dict such as a name_span) passes through unchanged.

Parameters

  • data (dict) — { kind, start, end, line, col, end_line, end_col, fields }

Returns Node

Node.is_trivia()

zuri.Node.is_trivia() -> bool

Is this node a standalone comment or doc block (as opposed to a real expression/statement/declaration)? See this module’s own doc comment for exactly where these can and can’t appear.

Returns bool

Node.to_string()

zuri.Node.to_string() -> string

This node as kind@line:col, e.g. 'Binary@3:5', naming the line and column it starts at. A one-line summary; use dump() to actually see what’s inside a node.

Returns string

Node.dump()

zuri.Node.dump() -> string

A readable, indented, multi-line dump of this node and everything inside it, recursively. This is the actual way to find out what a fields entry is called for a kind you haven’t looked up yet: echo node.dump() and read it, rather than guessing a key name and checking whether it happens to exist.

import zuri

var nodes = zuri.parse('def add(a, b) { return a + b }')
echo nodes[0].dump()

prints

Function@1:1
  name: 'add'
  name_span: 1:5-1:8
  parameters:
    Argument@1:9
      name: 'a'
      name_span: 1:9-1:10
      type_hint:
        TypeHint@1:10
          types:
            Any@1:10
              class_name: nil
          nullable: true
    Argument@1:12
      name: 'b'
      name_span: 1:12-1:13
      type_hint:
        TypeHint@1:13
          types:
            Any@1:13
              class_name: nil
          nullable: true
  body:
    Block@1:15
      statements:
        Return@1:17
          value:
            Binary@1:24
              left:
                Identifier@1:24
                  name: 'a'
              op: 'Plus'
              right:
                Identifier@1:28
                  name: 'b'
  is_variadic: false

Each node’s header is its kind and the line and column it starts at. A span field such as name_span prints as the range it covers, line:col-end_line:end_col. The untyped parameters carry an implicit Any type sitting just after their names, and class_name is nil on Any because only an Instance type names a class.

Returns string

ParseResult

class zuri.ParseResult

What parse_partial() read from a piece of source: the tree it built and every syntax error it found on the way.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

zuri.ParseResult(nodes: list, errors: list)

Returns a new ParseResult. Ordinary code receives these from parse_partial() rather than building them.

Parameters

  • nodes (list) — The top-level nodes read, as parse() returns them
  • errors (list) — Every syntax error, as {zuri.Diagnostic}s, in the order they occur in the source

ParseResult.is_clean()

zuri.ParseResult.is_clean() -> bool

True when the source parsed without a single error, in which case nodes is exactly what parse() returns.

Returns bool

ParseResult.to_string()

zuri.ParseResult.to_string() -> string

This result as ParseResult(n nodes, m errors).

Returns string


2026, Richard Ore and Zuri contributors

zuri.compile

import zuri

Everything here is re-exported by zuri, so import zuri is enough and the names are called as zuri.*. Importing zuri.compile on its own works too and reaches the same definitions.

The Instr type zuri.compile() returns: a generic representation of one VM instruction, tagged by op ('LoadConst', 'Add', 'Jmp', …) with a fields dict holding that opcode’s own operands. As with zuri.ast.Node, there’s one Instr class rather than one class per opcode. The instruction set has around sixty opcodes, and a dedicated class per opcode would be sixty near-identical classes to keep in sync with the compiler forever after.

import zuri

for instr in zuri.compile('var x = 1 + 2') {
  echo '${instr.op} ${instr.fields}'
}

Resolved operands

compile() returns a flat instruction list, matching the VM’s own Instr type, but several opcodes (LoadConst, GetGlobal, GetField, Invoke, …) only carry a raw index into the compiler’s own constant pool, which isn’t otherwise exposed. Rather than force every caller to also separately fetch and correlate a constants table, each such field gets its already-resolved value embedded right on the instruction too, alongside the raw index: e.g. a LoadConst node’s fields has both const_idx (the raw index) and value (what it actually points at). A resolved field is never literally named after the raw index’s own subject when that word is a reserved keyword (const, class, …), since a dict key can’t be read back with plain dot-access syntax then; class becomes class_reg (it’s a register index anyway, not a resolved class value), and similarly for a handful of others.

A Closure instruction’s resolved value is a nested function prototype ({ name, arity, variadic, instructions }), whose own instructions is recursively wrapped the exact same way, so a script with functions or methods in it exposes their bodies too, not just the code immediately around them.

Every instruction node carries the source line it was compiled from, but never a column. The compiler’s own line tracking is line-only (statement granularity), unlike a zuri.token.Token’s or zuri.ast.Node’s position, which both have real column info from the lexer/parser.

Functions

walk_instrs()

zuri.walk_instrs(instrs, visitor)

Recursively visits every instruction reachable from instrs (typically zuri.compile()’s own result, or any single Instr plucked out of it), including inside a Closure instruction’s own nested function body, in the order they appear, calling visitor for each one.

visitor is either a plain function, called with every instruction (visitor(instr)), or a dict from opcode name to function, where only a matching instruction gets called (visitor[instr.op](instr)). Reach for the dict form when you only care about one or two opcodes.

Always visits the whole tree; there’s no way for visitor to skip descending into a nested function body.

import zuri

var instrs = zuri.compile(source)

# every global name this script writes to, anywhere, including
# inside nested function bodies
var written = []
zuri.walk_instrs(instrs, {
  SetGlobal: @(instr) { written.append(instr.fields.name) },
})

Parameters

  • instrs (Instr|list[Instr])
  • visitor (function|dict)

find_instrs()

zuri.find_instrs(instrs, op) -> list[Instr]

Every instruction reachable from instrs whose op is op, in the order they appear. Shorthand for the common “just give me a flat list of these” case walk_instrs() itself is built on.

import zuri

var instrs = zuri.compile(source)
for jmp in zuri.find_instrs(instrs, 'Jmp') {
  echo jmp.fields.offset
}

Parameters

  • instrs (Instr|list[Instr])
  • op (string)

Returns list[Instr]

compile()

zuri.compile(source) -> list[Instr]

Compiles source as a standalone script and returns its instructions as a flat list of Instr. See this module’s own doc comment for exactly what each instruction carries.

Raises (rather than returning partial bytecode) on a lexer, parser, or compiler error.

Parameters

  • source (string) — The Zuri source to compile

Returns list[Instr]

Raises Error if source doesn’t compile

compile_file()

zuri.compile_file(path) -> list[Instr]

Reads the file at path and compiles its contents; exactly compile(file(path).read()), for the common case of compiling a real file rather than a source string you’ve already got in hand.

Parameters

  • path (string) — Path to the Zuri source file to compile

Returns list[Instr]

Raises Error if path can’t be opened or read, or doesn’t compile

check()

zuri.check(source: string) -> list[Diagnostic]

Every problem compiling source would report, as a list of zuri.Diagnostic in the order they occur, without running anything and without raising. An empty list means source compiles.

Source that does not parse reports its syntax errors, exactly as parse_partial() finds them. Source that parses goes on to the compiler, which reports a name declared twice in one scope, self or parent outside a method, parent in a class with no superclass, break or continue outside a loop, an assignment to a constant, a private member reached through anything other than self or parent, a class extension declaring a field or an instance method, and a function too large for its registers.

import zuri

for problem in zuri.check('if true {\n  break\n}') {
  echo problem  # 2:3: 'break' used outside of a loop
}

echo zuri.check('var x = 1').length()  # 0

Parameters

  • source (string) — The Zuri source to check

Returns list[Diagnostic]

check_file()

zuri.check_file(path: string) -> list[Diagnostic]

Reads the file at path and checks it; exactly check(file(path).read()).

Parameters

  • path (string) — Path to the Zuri source file to check

Returns list[Diagnostic]

Raises Error if path can’t be opened or read

Classes

Instr

class zuri.Instr

One VM instruction.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

zuri.Instr(op, line, fields)

Returns a new instance of an Instr directly, from already- wrapped field values. Ordinary Zuri code should use zuri.compile() instead; this constructor does no wrapping of its own.

Parameters

  • op (string) — The opcode name, e.g. 'LoadConst', 'Add'
  • line (?number) — The source line this instruction was compiled from, or nil if the compiler never stamped one
  • fields (dict) — This opcode’s own operands, already wrapped

Instr.to_string()

zuri.Instr.to_string() -> string

This instruction as op@line, e.g. 'LoadConst@3', or just op when it carries no line. A one-line summary; use dump() to actually see what’s inside an instruction.

Returns string

Instr.dump()

zuri.Instr.dump() -> string

A readable, indented, multi-line dump of this instruction and everything inside it, recursively (including a Closure instruction’s own nested function body). Same idea as zuri.ast.Node.dump: the actual way to find out what an opcode’s fields looks like is to run something and read the dump, not to guess a key name from the opcode’s name alone.

import zuri

var instrs = zuri.compile('var x = 1')
echo instrs[0].dump()

prints

LoadConst@0
  dst: 0
  const_idx: 1
  value: 1

Returns string


2026, Richard Ore and Zuri contributors

zuri.diagnostic

import zuri

Everything here is re-exported by zuri, so import zuri is enough and the names are called as zuri.*. Importing zuri.diagnostic on its own works too and reaches the same definitions.

The Diagnostic type: one problem found in a piece of Zuri source by reading it rather than running it, as zuri.parse_partial() and zuri.check() reports them.

Classes

Diagnostic

class zuri.Diagnostic

One problem in a piece of Zuri source: what is wrong and exactly where.

The position has the same six parts a zuri.ast.Node has. start and end are character offsets, the first character the problem is about and the one just past it, so source[d.start, d.end] is the text it is about. line and col are where that text starts and end_line and end_col where it ends, all counted from 1.

A syntax error covers the token it is about. When the source ends where more was expected, that is an empty span at the very end. A compiler error covers the token it is about, or the whole of its line when it belongs to the line rather than to one token.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

zuri.Diagnostic(message: string, position: dict)

Returns a new Diagnostic. Ordinary code receives these from zuri.parse_partial() and zuri.check() rather than building them.

Parameters

  • message (string) — What is wrong, without a position
  • position (dict) — { start, end, line, col, end_line, end_col }

Diagnostic.to_string()

zuri.Diagnostic.to_string() -> string

This diagnostic as line:col: message, e.g. '2:3: 'break' used outside of a loop'.

Returns string


2026, Richard Ore and Zuri contributors

zuri.doc

import zuri

zuri exposes this as zuri.doc, so import zuri is enough and the names are called as zuri.doc.*. import zuri.doc reaches the same definitions directly.

Reads Zuri’s documentation comments: the doc blocks a zuri.parse() tree keeps as DocBlock nodes, turned into prose and a list of @tag entries, and paired with the declarations they document.

A doc block is markdown prose followed by a run of tags. Each tag starts a line with @ and its name, and its text may wrap onto the lines below it, which are recognised by their indentation. A blank line ends the run of tags.

import zuri

# Spelled in pieces so this example can sit inside a doc block.
var open = '/' + '**'
var close = '*' + '/'

var source = '${open}
 * Adds two numbers.
 *
 * @param number a: The first number
 * @returns number
 ${close}
def add(a, b) {
  return a + b
}'

for attached in zuri.doc.attach(zuri.parse(source)) {
  echo attached.node.fields.name           # add
  echo attached.block.summary()            # Adds two numbers.
  echo attached.block.first('param').name  # a
}

Fenced code inside a block is kept as it is, with no line of it read as a tag, and since Zuri block comments nest, an example that opens a comment of its own does not end the block it sits in.

Several tags have a second spelling, read as the first: @return as @returns, @raises as @throws, @params as @param, and @number as @numeric.

Functions

parse_lines()

zuri.doc.parse_lines(lines: list) -> Block

Parses the lines of a doc block into its prose and tags; the same as parse() for text already split into lines.

Everything before the first tag is prose. Once tags start, a line beginning with whitespace continues the tag above it, a line beginning with @ starts a new one, and a blank line ends the run.

Each line is read from just after the * that starts a line of a doc block, dropping that * and the one space after it, so whatever indentation follows is what marks a continuation. A line with no * is read from its first character that is not whitespace, and so never continues a tag.

Parameters

  • lines (list) — The block’s text, one string per line

Returns Block

parse()

zuri.doc.parse(text: string) -> Block

Parses the text of a doc block, the text a DocBlock node carries, into its prose and tags.

import zuri

var block = zuri.doc.parse('Opens the file.\n\n@param string path\n@returns file')

echo block.summary()               # Opens the file.
echo block.first('returns').type   # file

Parameters

  • text (string) — The block’s text, as a DocBlock node carries it

Returns Block

empty()

zuri.doc.empty() -> Block

A block with neither prose nor tags, for a declaration that has no documentation.

Returns Block

module_block()

zuri.doc.module_block(nodes: list) -> ?Block

The module’s own documentation among the top-level nodes of a file: the first doc block carrying an @module tag, parsed, or nil when the file has none.

Parameters

  • nodes (list) — The file’s top-level nodes, as zuri.parse() returns them

Returns ?Block

attach()

zuri.doc.attach(nodes: list) -> list[Attached]

Pairs every node in a list of siblings with the doc block written directly above it.

A doc block documents the node that follows it and nothing else: anything in between, a plain # comment included, breaks the pairing. The module’s own doc block, the first carrying an @module tag, documents the module rather than the node after it, so it is paired with nothing. Every node other than a doc block comes back, in order, documented or not.

nodes is any list of siblings: a file’s top-level nodes, a block’s statements, or a class’s properties or methods. A top-level declaration that is a statement comes back as the Stmt node wrapping it, exactly as it sits in the list.

Parameters

  • nodes (list) — A list of sibling nodes from zuri.parse()

Returns list[Attached]

Classes

Tag

class zuri.doc.Tag

One @tag from a doc block, its wrapped lines already joined into one string.

kind is the tag’s name without its @, under its main spelling: an @return is a 'returns' tag. type is the type the tag names, written as {type} or as a type word leading its text, and name is the name an @param or @property tag names. Both are empty strings, never nil, when the tag carries none. text is what remains: the description.

import zuri

var block = zuri.doc.parse('@param {string} name: Who to greet')
var tag = block.tags[0]

echo tag.kind  # param
echo tag.type  # string
echo tag.name  # name
echo tag.text  # Who to greet
  • printable — has a @to_string(), so echo and print() show something useful

Constructor

zuri.doc.Tag(kind: string, type: string, name: string, text: string)

Returns a new Tag. Ordinary code receives these from parse() rather than building them.

Parameters

  • kind (string) — The tag’s name without its @
  • type (string) — The type it names, or ''
  • name (string) — The name it names, or ''
  • text (string) — Its description

Tag.to_string()

zuri.doc.Tag.to_string() -> string

This tag as Tag(@kind type name).

Returns string

Block

class zuri.doc.Block

A parsed doc block: body, its markdown prose as a list of lines, and tags, every @tag in the order it was written.

Blank lines at the start and end of the prose are dropped. Each line keeps the indentation it had after the * that starts it, so code blocks and nested lists read as they were written.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

zuri.doc.Block(body: list, tags: list)

Returns a new Block. Ordinary code receives these from parse() and attach() rather than building them.

Parameters

  • body (list) — The prose, one string per line
  • tags (list) — The block’s {Tag}s, in source order

Block.all()

zuri.doc.Block.all(kind: string) -> list[Tag]

Every tag of one kind, in source order; an empty list when the block has none. kind is a tag’s main spelling, such as 'returns'.

Parameters

  • kind (string)

Returns list[Tag]

Block.first()

zuri.doc.Block.first(kind: string) -> ?Tag

The first tag of one kind, or nil when the block has none.

Parameters

  • kind (string)

Returns ?Tag

Block.has()

zuri.doc.Block.has(kind: string) -> bool

True when the block carries at least one tag of this kind.

Parameters

  • kind (string)

Returns bool

Block.summary()

zuri.doc.Block.summary() -> string

The block’s first paragraph of prose as one line, for a one-line summary in an index. It stops at the first blank line, heading or code fence, and is an empty string for a block that opens with one of those.

Returns string

Block.is_empty()

zuri.doc.Block.is_empty() -> bool

True when the block has neither prose nor tags.

Returns bool

Block.to_string()

zuri.doc.Block.to_string() -> string

This block as Block(n lines, m tags).

Returns string

Attached

class zuri.doc.Attached

One node from a list of siblings, with the doc block written directly above it, as attach() pairs them.

block is an empty Block when there is no doc block above the node, and documented says which: a doc block that holds nothing still counts as documenting its node.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

zuri.doc.Attached(node, block: instance, documented: bool)

Returns a new Attached. Ordinary code receives these from attach() rather than building them.

Parameters

  • node (Node) — The node, exactly as it sits in the list
  • block (Block) — Its documentation
  • documented (bool) — Whether a doc block was written above it

Attached.to_string()

zuri.doc.Attached.to_string() -> string

This pairing as Attached(kind, documented).

Returns string


2026, Richard Ore and Zuri contributors

zuri.reflect

import zuri

zuri exposes this as zuri.reflect, so import zuri is enough and the names are called as zuri.reflect.*. import zuri.reflect reaches the same definitions directly.

Runtime introspection: metadata about a live function, class, or module, and property/method access on an instance or module by name, without needing to already know that name at compile time. It also runs the garbage collector on demand.

This is deliberately a RUNTIME-only view. It does not know a function/class’s declaration line or its doc block; those live in source, not in the running object, and are a zuri.parse()/zuri AST concern instead (walk the parsed source and read a Function/Class node’s own line/col, and any DocBlock node sitting right before it).

import zuri

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

echo zuri.reflect.function_info(add).arity
# 2

Functions

kind()

zuri.reflect.kind(value) -> string

This value’s runtime type tag: 'nil', 'bool', 'number', 'string', 'bytes', 'bigint', 'list', 'dict', 'function', 'class', 'instance', 'module', 'range', 'file', or a pointer’s own registered resource tag (see pointer_type). A closure, a native function, and a bound method are all reported as 'function', since from Zuri’s own point of view they’re interchangeably callable.

Parameters

  • value (any)

Returns string

pointer_type()

zuri.reflect.pointer_type(value) -> ?string

The registered resource tag a pointer value was allocated with (e.g. a native socket handle’s own internal type name), or nil if value isn’t a pointer at all. Pointers wrap an opaque native resource that Zuri code can’t otherwise inspect; this names what kind of resource one is without exposing its contents.

Parameters

  • value (any)

Returns ?string

function_info()

zuri.reflect.function_info(f) -> dict

Metadata for a function-like value: { name, arity, variadic, is_method, owning_class_name, source_path }. owning_class_name is the class it was declared a method of, or nil for an ordinary function/closure. source_path is nil for a native (built-in) function, which has no Zuri source file behind it.

Parameters

  • f (callable) — A closure, a native function, or a bound method

Returns dict

Raises Error if f isn’t callable as a function (a class, though itself callable to construct an instance, is a class_info subject instead)

class_info()

zuri.reflect.class_info(c) -> dict

Metadata for a class: { name, superclass_name, methods, fields, statics }. superclass_name is nil for a class with no superclass. methods is a dict from method name to that method’s own function_info-shaped entry, own and inherited pre-merged (an override shadows the inherited entry of the same name, matching how method dispatch itself works). fields and statics are plain lists of names, ordered by declaration; fields includes inherited fields, statics is own-only (statics are never inherited in this runtime).

Parameters

  • c (class)

Returns dict

Raises Error if c isn’t a class

module_info()

zuri.reflect.module_info(m) -> dict

Metadata for a module: { name, path, loaded, members }. members is a dict from every top-level name the module declares to that binding’s own kind (see kind above), e.g. { add: 'function', VERSION: 'string' }. Accepts either a raw module value or a promoted import PATH binding; both describe the same underlying module.

Parameters

  • m (module)

Returns dict

Raises Error if m isn’t a module

info()

zuri.reflect.info(value) -> dict

Dispatches to function_info/class_info/module_info based on kind(value).

Parameters

  • value (any)

Returns dict

Raises Error if value has no reflectable shape (a number, a list, an instance, …); reflect describes callables, classes, and modules only

has_prop()

zuri.reflect.has_prop(object, name) -> bool

Does object (an instance or a module) have a property/member named name?

Parameters

  • object (instance|module)
  • name (string)

Returns bool

get_prop()

zuri.reflect.get_prop(object, name) -> any

The current value of object’s (an instance or a module) property/member named name, or nil if it has none by that name.

Parameters

  • object (instance|module)
  • name (string)

Returns any

Raises TypeError if object is neither an instance nor a module

get_props()

zuri.reflect.get_props(object) -> list[string]

Every property/member name object (an instance or a module) has, as a list of strings, or an empty list if it has none. Instance fields are ordered by declaration; module members have no such inherent order.

Parameters

  • object (instance|module)

Returns list[string]

Raises TypeError if object is neither an instance nor a module

set_prop()

zuri.reflect.set_prop(object, name, value)

Overwrites object’s existing property name with value.

Parameters

  • object (instance)
  • name (string)

Returns — bool: true if the property was updated, false if name isn’t a field object’s class declares (nothing is changed in that case; this never creates a new field)

Raises TypeError if object isn’t an instance

Note: instances in this runtime have a fixed set of fields, sized once when their class is declared. Unlike a dict, a property can never be added to or removed from an instance at runtime. This only ever updates a field the instance’s class already declares; it can’t create a new one.

del_prop()

zuri.reflect.del_prop(object, name)

Resets object’s existing property name back to nil.

Parameters

  • object (instance)
  • name (string)

Returns — bool: true if the property was reset, false if name isn’t a field object’s class declares

Raises TypeError if object isn’t an instance

Note: as with set_prop, this can’t remove the field itself, only clear its value; see set_prop’s own note.

has_method()

zuri.reflect.has_method(object, name) -> bool

Does the class behind object (an instance, or a class used directly) declare or inherit a method named name?

Parameters

  • object (instance|class)
  • name (string)

Returns bool

has_decorator()

zuri.reflect.has_decorator(object, name) -> bool

Does the class behind object implement the decorator named name? A decorator is just a method whose own name starts with @ (@to_string, @new, @value, …); name here excludes that leading @.

Parameters

  • object (instance|class)
  • name (string) — The decorator’s name, without the leading @

Returns bool

get_method()

zuri.reflect.get_method(object, name) -> ?function

The raw (unbound) closure for method name on the class behind object, or nil if it declares no such method. Calling the result directly does NOT supply a receiver; see bind_method for a version that does.

Parameters

  • object (instance|class)
  • name (string)

Returns ?function

bind_method()

zuri.reflect.bind_method(object, name) -> ?function

Method name on object’s class, bound to object itself as the receiver; the result can be called directly with no receiver argument, unlike get_method’s raw closure.

Parameters

  • object (instance)
  • name (string)

Returns ?function

get_decorator()

zuri.reflect.get_decorator(object, name) -> function

The decorator function named name (excluding the leading @) on the class behind object, bound to object as its receiver and ready to call directly.

Parameters

  • object (instance)
  • name (string) — The decorator’s name, without the leading @

Returns function

Raises NotImplementedError if the class behind object implements no such decorator

gc()

zuri.reflect.gc() -> nil

Runs a full garbage collection now, instead of waiting for the heap to cross its threshold.

Every object nothing reaches is freed before this returns, and anything that releases a resource when collected releases it: an owned ffi pointer’s destructor runs, for one. The memory freed goes back to the system before this returns too.

A program never needs this to stay correct; the collector runs on its own as the program allocates. It is for the moments the timing matters: releasing native resources at a known point, or a test that checks what collection does. A full collection visits every live object, so calling it in a loop is slow.

import zuri

var scratch = [1, 2, 3]
scratch = nil
zuri.reflect.gc()

Returns nil


2026, Richard Ore and Zuri contributors

zuri.token

import zuri

Everything here is re-exported by zuri, so import zuri is enough and the names are called as zuri.*. Importing zuri.token on its own works too and reaches the same definitions.

The Token type zuri.tokenize() returns: one entry per lexical token the lexer found in a source file, in source order, with nothing filtered out. Comments, doc blocks, and newlines are all ordinary tokens here, exactly as the lexer itself produces them. The parser’s own grammar quietly skips those when it runs, but tokenize() reports everything the lexer actually saw.

import zuri

for t in zuri.tokenize('var x = 1 + 2') {
  echo '${t.kind} ${t.line}:${t.column} ${t.text}'
}

Functions

tokenize()

zuri.tokenize(source) -> list[Token]

Lexes source in full and returns every token it contains, in source order, as a list of Token.

Nothing is filtered: Comment, DocBlock, and Newline tokens are all included, and the list always ends with one Eof token.

A lexer-level problem in source, an unterminated string, an unbalanced doc block, an unexpected character, does not raise; it surfaces as an ordinary token with kind 'Error' instead, so this function is total over any input, including malformed source.

Parameters

  • source (string) — The Zuri source to lex

Returns list[Token]

tokenize_file()

zuri.tokenize_file(path) -> list[Token]

Reads the file at path and lexes its contents; exactly tokenize(file(path).read()), for the common case of lexing a real file rather than a source string you’ve already got in hand.

Parameters

  • path (string) — Path to the Zuri source file to lex

Returns list[Token]

Raises Error if path can’t be opened or read

Classes

Token

class zuri.Token

One lexical token from a Zuri source file.

Every instance has:

  • kind (string): the token’s kind, e.g. 'Identifier', 'Plus', 'Comment', 'Eof', the exact variant name from the lexer’s own token kind enum.

  • line / column (number): 1-indexed source position this token starts at; column counts characters, not bytes.

  • start / end (number): character offsets into the source string spanning this token’s exact text.

  • text (string): this token’s exact source text, source[start, end], delimiters included: a string literal’s quotes, a doc block’s opening and closing markers, and so on.

  • value (any): this token’s own payload, where it has one, a literal, identifier, comment, or doc block’s string content, a number, or a bigint. nil for every fixed symbol or keyword, and for Newline/Eof. An Error-kind token instead carries { message, line, offset } describing what went wrong.

  • printable — has a @to_string(), so echo and print() show something useful

Constructor

zuri.Token(data)

Returns a new instance of a Token. Ordinary Zuri code never needs to call this directly, since zuri.tokenize() builds these for you.

Parameters

  • data (dict) — { kind, line, column, start, end, text, value }, the token’s fields

Token.is_trivia()

zuri.Token.is_trivia() -> bool

Is this token a comment or a doc block? Neither ever reaches the parser’s own grammar, but both are real entries in zuri.tokenize()’s output.

Returns bool

Token.to_string()

zuri.Token.to_string() -> string

This token as kind@line:column, e.g. 'Identifier@1:5'.

Returns string


2026, Richard Ore and Zuri contributors