DKE Python — Language Specification

Version: 18.3.8, changed in rev 171 · alpha · API 1.5 · rev 171
Status: Published normative specification of DKE Python.
Audience: Programmers writing DKE Python; authors of third-party tooling (linters, formatters, syntax highlighters, language servers, IDE plugins, alternative compilers).
This document as Markdown: dke-python-spec.md — the same text this page is rendered from, for the tooling authors above who would rather read the source than the page.


1. Status and scope

This document is the normative specification of DKE Python — the language a DKE customer writes and submits to the DKE service at dke.langsyn.net. DKE Python is a Python-flavoured concrete surface over DKE’s knowledge-operation semantics. Source files use the extension .dpy.

The language and this specification are open under the MIT licence; the DKE service that runs DKE Python is LangSyn’s proprietary product and is not open. Third parties are free to implement compilers, linters, formatters, syntax highlighters, language servers, IDE plugins, and alternative tooling against this document.

A DKE client sends DKE Python source over the wire; DKE compiles, stores, and executes it. The published surface is the language; the implementation that runs it is LangSyn’s product.

1.1 Conformance

An implementation of DKE Python is conforming when, for every input program that this specification classifies as accepted, the implementation parses and accepts the program with the static-semantics outcomes this specification prescribes; and for every input program this specification classifies as rejected (parse_error, type_error), the implementation rejects it with the error class this specification names. Runtime outcomes (refuse, engine_error) are observable but depend on the target store’s state; conformance applies to the SHAPE of those outcomes (status discriminator, transcript shape), not to the data.

A third-party compiler that emits something other than what DKE emits internally is conforming as long as the observable behaviour matches: same input source plus same target store plus same arguments produces a transcript with the same shape and the same status discriminators across statements.

The conformance keywords MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY in this document carry their RFC 2119 meanings.

1.2 Relationship to DKE

DKE Python is the contract between a customer and the DKE service. Three documents divide that contract between them:

  • this specification — the language: lexical structure, grammar, type system, static and dynamic semantics, and the built-in verb surface;
  • the MCP Reference — the transport: the tool surface, request shapes, response envelope, error model and billing, and the wire-visible behaviour of the compile/store/run lifecycle;
  • the modules chapter of the Python Reference — the unit of submission: what a module is, how import resolves one, and what re-submitting one does to a store.

Internal storage shapes, code-generation strategies, and execution mechanisms are intentionally out of scope — they are the implementation’s choices, and a conforming client must not rely on them.

1.3 Spec versioning

This is the DKE Python — Language Specification, version V, changed in rev R, at alpha · API 1.5 · rev N, as the Version line above states it: this document’s own version, which moves only when its text does, and the revision it last changed in; then the release it belongs to, an alpha release describing API version 1.2, at revision N — one number for the whole published surface, which every document, the web pages and the service wire state and which moves whenever any of them changes. §11.1 says what each promises, and Appendix E records the version and revision at which this document changed. DKE Python is a Python-flavoured surface end to end:

  • Declarations use def. def audit(K: string, s: string): — the Python function-definition shape, with a :-headed indented body.
  • Verbs are function calls. The built-in verb surface (§7) is written as calls, e.g. remember(K.s.a, v, src), current(K.s.a), list_attributes(K), print(msg).
  • Bare assignment binds a read. cur = current(K.s.a).
  • match / case. Discrimination is spelled match <expr>: with case <discriminator>: arms. One arm may name ALTERNATIVES, separated by | — case int | string: — and may carry a guard, case int as n if n > 0: (§6.2).
  • try / except. Handled blocks use Python’s except <kind> [as <var>]: shape; the as <var> binding is optional (except refuse:).
  • Script invocation is a plain call. audit_subject(K, s) — a program reaches a stored script by importing its module and calling it qualified (MCP Reference §4.2, Python Reference §9.6).
  • Word-form logical operators. and / or / not.

1.4 What this specification does NOT define

  • The wire transport between the client and the target service.
  • The storage format used by the service to retain compiled scripts.
  • The internal representation of types at runtime.
  • DKE’s error message text (only the error category is normative; message strings are quality-of-implementation).
  • Operational characteristics: performance, latency, quotas, throttling.

1.5 Independence from a live service

This specification defines the conformance contract for any implementation of DKE Python and for the target service at dke.langsyn.net, independent of whether a given target service is reachable when this document is read.

Parser/checker conformance (§12.1) does not depend on a live target. Third parties can build conforming parsers, type-checkers, linters, formatters, syntax highlighters, language servers, and IDE plugins against this specification and test them in isolation per §12.2. Compiler/runtime conformance (§12.1) is claimed against whichever target service an implementation connects to.


2. Notation

Grammar productions use a small EBNF variant:

  • lowercase — non-terminal. Each one a right-hand side names has a production in this document.
  • 'literal' — terminal (matched verbatim, case-sensitive).
  • 'a'..'z' — any one character from the first to the last, inclusive.
  • UPPERCASE — token class (defined in §3).
  • A | B — alternative.
  • A B — sequence.
  • A? — zero or one occurrence.
  • A* — zero or more occurrences.
  • A+ — one or more occurrences.
  • A{n} — exactly n occurrences; A{m,n} — between m and n.
  • ( A ) — grouping.
  • <...> — a set described in words rather than spelled out, such as the characters a literal may hold or the names another section lists.
  • NEWLINE, INDENT, DEDENT — the layout tokens the lexer emits from the off-side rule (§3.2).
  • # ... — informal note.

Where lexical and syntactic productions both apply, lexical rules take precedence (tokens are recognised first; the parser consumes the token stream).


3. Lexical structure

3.1 Source encoding

DKE Python source MUST be valid UTF-8. A conforming implementation MUST reject ill-formed UTF-8 sequences with a parse_error that names the offending byte offset (and SHOULD name the invalid byte).

3.2 Whitespace, line terminators, and the off-side rule

DKE Python is indentation-significant. A line feed (U+000A), a carriage return (U+000D) and the carriage-return / line-feed pair each end a source line; the pair ends one line, not two. Every line ending reads as a line feed, including inside a triple-quoted string, so a program means the same whichever of the three its file uses. A block is introduced by a header line ending in : and is delimited by indentation:

def f(K: string):
    <statement>
    <statement>
  • Indentation is measured in spaces; a tab in indentation is a lexical error (parse_error).
  • A form feed (U+000C) is whitespace between tokens, as a space is. In a line’s indentation it is not counted: it sets the count back to zero, so only the spaces after it count. A line holding four spaces after a form feed is indented four, as CPython reads it, and a line holding a form feed after four spaces is not indented.
  • A deeper indent opens a nested block (the lexer emits INDENT); returning to a shallower level closes as many blocks as needed (DEDENT). An indent that matches no open level is a parse_error.
  • Only a header’s : opens a block, so a line indented deeper below a line that does not end in one is a parse_error, “unexpected indent”. It is answered where CPython answers it: at the line’s first token, with that token’s column counted from zero. The two lines x = 1 and y = 2 are a parse_error at line 2, column 4. A line holding only a \ after its indentation is answered on the line it continues onto.
  • Blank lines and comment-only lines do not affect block structure.
  • Inside (, [ or {, newlines are not significant: a bracketed expression may span lines.
  • A trailing comma may follow the last item of a comma-separated list written inside (, [ or {: the arguments of a call (to a script, a builtin, a method, a verb or a class), the parameters of a def or a method, the base of a class, the elements of a literal (§3.5.9, §3.5.11–§3.5.13) and the elements of a sequence or mapping pattern (§6.2). It means nothing: f(1, 2,) is f(1, 2), so a list written one item per line may end every line with a comma. It never stands for an item. f(,), f(1,,2), [,] and [1,,2] are parse_errors, and f(1,) passes one argument. A subscript’s comma makes a tuple, as in Python. In two bracketed forms the comma makes a tuple of one: in a subscript, where d[1,] is d[(1,)] (§4.4), so xs[0,] is a type_error on a list, which is indexed by an int; and in (e,), rather than a parenthesized e (§3.5.11). The target list of a for header or a comprehension clause is not bracketed and takes one too: for a, b, in xs: is for a, b in xs:, and in for a, in xs: the comma makes a target list of one (§8.11), as it does in a, = t.
  • A backslash immediately before a line ending is an explicit line continuation: the two physical lines join into one logical line (no NEWLINE is emitted and the continuation line’s leading spaces are not indentation). The \ MUST be the last character on the line; a \ followed by anything other than the line ending is a parse_error, answered one past the \, where CPython answers it: x = 1 \ y is a parse_error at column 8. So is a \ the source ends after, with or without its line ending, since there is no line for it to join: it is refused one past the \, as CPython refuses it. Inside a bracket the bracket left open is the answer instead (§10.1).

3.3 Comments

A # begins a comment that runs to the end of the line. Comments are equivalent to whitespace and do not affect block structure.

One comment is also read: a source’s first line, when it opens # dke:, is the source’s declaration of what it is written in (§3.9.1). It remains a comment, and changes nothing the program means.

3.4 Identifiers

DKE Python identifiers follow Unicode UAX #31 with the standard profile, as Python’s do:

identifier := XID_Start XID_Continue*     # read in NFKC

An identifier starts with any codepoint having the Unicode XID_Start property and continues with any codepoint having XID_Continue. Every script Unicode defines is available, including those outside the Basic Multilingual Plane — Latin, æ ø å, CJK ideographs, Arabic, Hebrew, Devanagari, Thai, Armenian, Georgian, Tamil, Hiragana, Katakana, Hangul, Cyrillic, Greek and the rest, together with the decimal digits and the marks a script needs to spell its words. The _ underscore is identifier-start by convention.

A name is read in Normalization Form KC, as Python reads it, so every spelling of one form is one name. x (U+FF58) is the name x, fi is fi, and e followed by U+0301 is é, so Cat and Cat are one class and one kind. Where the form does not compose, the marks are part of the name: بِسْم and बिल्ली carry their vowel marks, and U+0958 is read as क followed by the nukta U+093C. Whether a mark composes can depend on the letter before it: ا followed by U+0654 is أ, while the same U+0654 after a letter it does not compose with stays in the name.

A keyword is compared as it is written, before a name is read, as in Python. So if = 1 binds the name if, and print(if) reads it. A word this language reads only where its form stands is compared the same way, so match does not begin a match. The name of a built-in function is read like any other: print("p") calls print.

A name that keeps a combining mark from U+0300 to U+036F with no letter to compose into, as x followed by U+0301 does, may name a local, a parameter or an alias. It cannot name what a store holds. A class, a field, a method, a script, a rule or a constraint so named is a parse_error naming the mark, and a kind, a subject or an attribute written so in a path is a refuse naming it at run time. A module’s name is what import reads: m9 is refused, and the refusal says to write m9.

A string is not read, as Python’s getattr does not read one. A variable holding "x" addresses x where the path K.x.v addresses K.x.v, and a script sent to the service by name is found under the name it reads as: ok9.f, never ok9.f.

Two limits stand where Python answers otherwise. True, False and None are names a program may bind, and where it has not, they are names nothing binds, told name 'True' is not defined, where Python finds its constants. A name does not contain U+00B7, U+0387, U+013F or U+0140, the code points CPython continues a name with; each is refused wherever a name would hold it, as an unexpected character that the message names by its code point.

Identifiers are case-sensitive: Cat, cat, and CAT are distinct. As kinds in one store they cannot all be held: a store holds one kind of any names that differ only in the case of a letter A to Z, and a write that would make another is refused naming the kind held, with nothing written. Letters beyond A to Z are not compared this way, so Øy and øy are two kinds.

3.5 Literals

3.5.1 String literals

string-literal := '"' string-char* '"'
                | '"""' triple-char* '"""'   # triple-quoted (may span lines)
                | raw-prefix '"' raw-char* '"'             # raw
                | raw-prefix '"""' raw-triple-char* '"""'  # raw, triple-quoted
raw-prefix     := 'r' | 'R'
raw-char       := <any UTF-8 codepoint except '"', '\' and a line feed>
                | '\' <any UTF-8 codepoint>   # both kept: the backslash escapes nothing
raw-triple-char := <any UTF-8 codepoint except '\' and the closing '"""'>
                | '\' <any UTF-8 codepoint>
triple-char    := <any UTF-8 codepoint except '\' and the closing '"""'>
                | <a line feed, U+000A>      # written as itself, not escaped
                | string-escape              # the escapes of string-char
string-char    := <any UTF-8 codepoint except '"', '\' and a line feed>
                | string-escape
string-escape  := '\\'                   # backslash
                | '\"'                   # double quote
                | '\' <a single quote>   # single quote
                | '\n'                   # line feed        (U+000A)
                | '\t'                   # horizontal tab   (U+0009)
                | '\r'                   # carriage return  (U+000D)
                | '\a'                   # bell             (U+0007)
                | '\b'                   # backspace        (U+0008)
                | '\f'                   # form feed        (U+000C)
                | '\v'                   # vertical tab     (U+000B)
                | '\x' hex hex           # one codepoint <= U+00FF, two hex digits
                | '\u' hex{4}            # one codepoint ≤ U+FFFF, UTF-8 encoded
                | '\U' hex{8}            # one codepoint ≤ U+10FFFF, UTF-8 encoded
                | '\N{' name '}'         # a codepoint by its Unicode name
                | '\' octal octal? octal?   # one codepoint, up to three octal digits
                | '\' <a line feed>      # nothing: the string goes on at the next line
                | '\' <any other character>   # both kept: the backslash escapes nothing
octal          := '0'..'7'
hex            := '0'..'9' | 'a'..'f' | 'A'..'F'
name           := ( 'A'..'Z' | 'a'..'z' | '0'..'9' | ' ' | '-' )+   # a Unicode character name, matched case-insensitively

A conforming implementation MUST reject an unterminated string literal (newline or end-of-source before the closing quote) with a parse_error.

Either quote. The productions above are written in ", and each is also written in ': a string opens with either, ''' is the triple-quoted form as """ is, and it ends at the next of the quote it opened with. So 'a' and "a" are one string, and the other quote is ordinary content inside it: "it's", 'say "hi"'. A prefix u or U before either quote changes nothing, as in CPython. A string held inside a collection prints between ', as CPython’s repr does, so the printed form of a list, dict, set or tuple of strings, ['a', 'b'], reads back as written.

The \x escape denotes exactly two hex digits (one codepoint ≤ U+00FF); \u denotes exactly four hex digits (one codepoint ≤ U+FFFF); \U denotes exactly eight hex digits (one codepoint ≤ U+10FFFF, the only escape able to name a codepoint above the BMP). Each is encoded as UTF-8. A \u/\U value in the surrogate range U+D800..U+DFFF, or a \U value above U+10FFFF, is rejected.

The \N{name} escape denotes the codepoint whose Unicode character name is name, encoded as UTF-8 — e.g. \N{BULLET} is • (U+2022), \N{GREEK SMALL LETTER ALPHA} is α, and \N{CJK UNIFIED IDEOGRAPH-4E00} is 一. Every named character is recognized, across all scripts — Latin, Greek, Cyrillic, Arabic, Devanagari, the CJK ideographs, Hangul syllables, and the rest. The name is matched case-insensitively but is otherwise exact (interior spaces and hyphens are significant; no leading or trailing space). A name Unicode does not assign is a parse_error.

No NUL byte. Any escape that would produce a NUL byte — \0, \x00, or a \u escape denoting U+0000 — is a parse_error.

Any other escape. One to three octal digits name a codepoint, as in CPython: \101 is A, and \400 is U+0100. A backslash and a line feed are nothing, so a string goes on at the next line. A backslash before any other character keeps both: "\d" is the two characters \d, the same string as "\\d".

Adjacent literal concatenation. Two or more string literals separated only by whitespace are concatenated into a single string at compile time, with no + operator:

"long text "
"split over lines"     # same value as "long text split over lines"
"a" "b" "c"            # same value as "abc"

The merge is a compile-time fold — the result is indistinguishable from the spelled-out literal.

An f-string participates, in either order and in any mixture, and the whole adjacent run folds together: f"a" "b", "a" f"{x}", f"{x}" f"{y}" and "a" "b" f"{x}" "c" all concatenate. A run containing an interpolation is a single interpolated string, not a chain of joins, so it costs exactly what the one f-string it replaces costs. This is the idiomatic way to write a long interpolated message across lines:

message = (
    f"hello {name} "
    "and welcome"
)

A typed value — datetime("…"), duration("…"), blob("…"), blob_b64("…"), scientific("…") — does NOT participate, in either order: concatenation joins strings, and these are not strings. datetime("2026-01-01T00:00:00Z") "x" is a parse_error; render such a value with str(…) and join with +, or interpolate it in an f-string.

Triple-quoted strings. A string may be delimited by """ or ''' instead of one quote. A triple-quoted literal ends only at the next three of its quote, so it MAY span multiple lines — each embedded newline is a literal line feed (U+000A) — and a lone " or "" inside """ is ordinary content. The escape grammar is identical to a string in one quote, and the leading newline (if the literal opens with one) is part of the value; there is no indentation stripping. The result is an ordinary string — the delimiter is purely lexical. An unterminated triple- quoted literal (end-of-source before the closing three) is a parse_error. A triple-quoted f-string (f"""…""") is an f-string whose text may span lines, and so may its fields.

banner = """usage:
  run <name>"""            # a two-line string with an embedded newline
quoted = """he said "hi""" # value: he said "hi   — the literal ends at the
                           # first """, so the quote before it closes nothing
                           # and no quote survives at the end
tail = """he said "hi\""""  # value: he said "hi"  — escape the last quote to
                           # keep it

Raw strings. A string literal prefixed r or R is raw: a backslash escapes nothing. It stays in the value together with the character after it, so r"a\n" is the three characters a, \, n, and r"C:\path" is the path as written. A backslash still keeps the next " from closing the literal — r"say \"hi\"" holds both backslashes and both quotes — which is also why a raw literal cannot end in a single backslash: r"a\" is unterminated, a parse_error. A raw literal MAY be triple-quoted (r"""…"""), and a backslash before a line feed keeps both, as CPython does. The result is an ordinary string: the prefix is lexical, as the triple quote is, so a raw literal joins an adjacent run like any other string. A raw f-string is written rf"…" or fr"…", in any case (§3.5.10). There is no bytes literal; b"…" and rb"…" are refused naming the blob they would denote (§3.5.8).

3.5.2 Integer literals

int-literal := dec-int | hex-int | oct-int | bin-int
dec-int     := dec-digit ( '_'? dec-digit )*
hex-int     := '0' ( 'x' | 'X' ) '_'? hex-digit ( '_'? hex-digit )*
oct-int     := '0' ( 'o' | 'O' ) '_'? oct-digit ( '_'? oct-digit )*
bin-int     := '0' ( 'b' | 'B' ) '_'? bin-digit ( '_'? bin-digit )*
dec-digit   := '0'..'9'
oct-digit   := '0'..'7'
bin-digit   := '0' | '1'

Integer literals are decimal by default; the 0x / 0o / 0b prefixes (case-insensitive) select base 16 / 8 / 2. A single _ may separate digits (1_000_000, 0xDE_AD) and may also directly follow a base prefix (0x_FF); it may not otherwise lead, and it may not trail or double. A decimal literal with a leading zero and a nonzero digit is rejected (010, 007) — octal requires the explicit 0o prefix; an all-zero run (0, 00, 0_0) is the integer zero and is legal. A leading - is the prefix unary-minus operator (§4.4, §8.3), with one exception: -9223372036854775808, the smallest integer, is a literal. Its magnitude alone (9223372036854775808) is out of range and stays a parse_error, so the sign is part of this literal rather than an operator applied to it. The exception is exactly that one spelling — a binary minus is unaffected, so 1 - 9223372036854775808 remains a parse_error for its out-of-range operand. Integer values fit a signed 64-bit two’s-complement range, and any other literal outside it is a parse_error.

A number that breaks these rules, or those of a real (§3.5.7), is answered as CPython’s tokenizer answers it. The answer comes at the character where reading stops and names the kind of literal: an invalid decimal, hexadecimal, octal or binary literal. A _ with no digit after it is answered at the _, a base prefix with no digit after it at the prefix’s letter, and an exponent’s sign with no digit after it at the sign. A decimal digit outside an octal or binary literal’s base is answered at that digit, which the answer names, so 0b12 is a parse_error at its 2. Each is answered before a grammar error anywhere in the program (§10.1).

3.5.3 Boolean literals

bool-literal := 'True' | 'False'

True and False are reserved words (§3.7).

3.5.4 No None, and null in two positions

DKE Python has no None, because every name holds one type (§6.1) and no type means “a value or nothing” (§5.1). A None would need such a type, so the language has no value that stands for absence. Each use None has in Python is answered without one:

  • A stored value that is absent is asserted with the assert verb (§7.1), which carries no value argument. The absence is a claim with a source, not a value in the cell.
  • A parameter that may be omitted takes a default of its own type: def f(n: int = 0), not def f(n: int = None).
  • A script with no result returns no value and ends with a bare return. A script that returns a value returns one on every path; a result that may be missing has no spelling.

null is written in exactly two positions, and neither is a value a name can hold. It is an open bound of a validity window (§7.1), where it leaves that side of the window unbounded, and it is the pattern of a case null: arm (§5.6), which matches a window bound read back open. A program tests for an open bound by matching, not by comparing: c.valid_to == null is refused with a lesson that names match c.valid_to: with case null:.

None is written in exactly one position, and it is not a value a name can hold there either: a whole argument of slice(...) (§5.3), where it leaves that bound open, as CPython’s None does. slice(None, 3) is the slice CPython writes slice(None, 3, None), and prints as that. Its open bound reads back as the null class, the class an open window bound reads back as, so slice(None, 3).start is matched with case null: and not compared. x = None, f(None) and [None] stay refused, and so does slice(None if c else 1), where None is part of an argument rather than the whole of one.

Both words are reserved (§3.7). Neither can be bound as a name anywhere a name is bound, and a read of either where a value goes is refused with the lesson above. None is reserved although it names nothing, so that a reader writing Python meets the lesson rather than declaring a name nothing can read.

3.5.5 Datetime literals

datetime-literal := 'datetime' '(' '"' rfc3339-utc '"' ')'
rfc3339-utc      := date 'T' time frac? 'Z'
date             := dec-digit dec-digit dec-digit dec-digit '-'
                    dec-digit dec-digit '-' dec-digit dec-digit   # YYYY-MM-DD
time             := dec-digit dec-digit ':' dec-digit dec-digit
                    ':' dec-digit dec-digit                       # HH:MM:SS
frac             := '.' dec-digit{1,9}          # 1..9 fractional-second digits

A datetime literal denotes an instant in UTC, written as the factory call datetime("<rfc3339>") over an RFC 3339 timestamp — e.g. datetime("2026-01-02T03:04:05Z"). Its static type is datetime (§5.1); its value class is datetime on the read side (§5.6).

The argument is a single string literal. The trailing Z (UTC designator) is required. Fractional seconds are optional and carry 1 to 9 digits. A datetime whose text is not a well-formed RFC 3339 UTC timestamp — a missing Z, a malformed field, or more than 9 fractional digits — is a parse_error. (The field ranges — month 01..12, a valid day, and so on — are validated by the target service; a value that is lexically well-formed but denotes no real instant is a runtime refuse.)

Two datetime values that denote the same instant are equal; a value reads back with insignificant trailing fractional zeros removed, so datetime("2026-01-02T03:04:05.500Z") reads back as 2026-01-02T03:04:05.5Z and datetime("2026-01-02T03:04:05.000Z") as 2026-01-02T03:04:05Z.

3.5.6 Duration literals

duration-literal := 'duration' '(' '"' iso8601-duration '"' ')'
iso8601-duration := '-'? 'P' days? ( 'T' hours? minutes? seconds? )?  # >= one term
days             := dec-digit+ fraction? 'D'
hours            := dec-digit+ fraction? 'H'
minutes          := dec-digit+ fraction? 'M'
seconds          := dec-digit+ fraction? 'S'
fraction         := '.' dec-digit{1,9}   # lowest-order term only

A duration literal denotes a length of time, written as the factory call duration("<iso8601>") over an ISO 8601 duration — e.g. duration("PT1H30M") (one hour thirty minutes). Its static type is duration (§5.1); its value class is duration on the read side (§5.6).

The argument is a single string literal: a leading P, an optional day term <n>D, and an optional time part introduced by T carrying any of <n>H, <n>M, <n>S in that order; at least one term is required. Calendar years and months are not accepted (they have no fixed number of seconds). A duration whose text is not a well-formed ISO 8601 duration of this subset — a missing P, a stray unit, units out of order, or a bare T — is a parse_error.

Sub-second durations. The lowest-order term present may carry a decimal fraction of up to nine digits, so duration("PT0.5S"), duration("PT1.5H") and duration("PT0.000000123S") are all well formed. Only the last term may be fractional — duration("PT1.5H30M") is a parse_error, since a fraction on a term followed by smaller terms is ambiguous. Durations are exact to the nanosecond: duration("PT0.1S") is exactly one tenth of a second, not the nearest binary approximation of it. A span must fit within roughly ±292 years; a longer one is a parse_error rather than a silently wrapped value.

A duration reads back in a canonical form: equal durations render identically and smaller units roll up into larger ones, so duration("PT90M") reads back as PT1H30M and duration("PT1.5H") does too. A sub-second remainder reads back as a decimal fraction on the seconds term with trailing zeros removed, so duration("PT1.500S") reads back as PT1.5S and a whole number of seconds carries no fraction at all. Two durations that denote the same length are equal regardless of how they were written.

Negative durations. A leading - negates the whole span, so duration("-PT2H") is two hours backwards. A leading + is not accepted — positive is the default and the sign would say nothing. Negative spans arise naturally from arithmetic (§8.3): subtracting a later instant from an earlier one gives one, and it renders with the same leading -, so every value the language computes is a value the language can also write.

3.5.7 Real literals

real-literal := int-part '.' frac-part? exponent?  # 3.5, 5. (trailing dot), 1.5e-3
              | '.' frac-part exponent?             # .5 (leading dot), .5e2
              | int-part exponent                   # 1e5 (exponent, no point)
int-part     := dec-digit ('_'? dec-digit)*
frac-part    := dec-digit ('_'? dec-digit)*
exponent     := ('e' | 'E') ('+' | '-')? dec-digit ('_'? dec-digit)*

A real literal denotes a fractional number, written as a decimal with a point — 3.5, 0.25, a leading-dot form .5 (== 0.5), or a trailing-dot form 5. (== 5.0) — and/or an exponent (e/E, an optional sign, and one or more digits), e.g. 1e5 (100000.0), 1.5e-3 (0.0015), .5e2 (50.0), 6.022e23. A literal with an exponent is a real even without a decimal point (1e5). The literal’s static type is real (§5.1); its value class is real on the read side (§5.6). An exponent that leaves the finite real range upward is the real infinity, as in Python: 1e400 is inf. One that leaves it downward underflows to zero, so 1e-400 evaluates to 0.0. There is no literal for a NaN, as in Python; float("nan") and math.nan make one (§8.13, §8.16).

A real is distinguished from an integer literal (§3.5.2) by the decimal point or the exponent: 3 is an integer, while 3.5, .5, 5., and 3e0 are reals. A point is part of a real when it follows a digit (5.) or precedes one (.5); a point with a digit on neither side — a lone . — is the field/access operator (§4.4), not a real, so .name accesses a field. Because 5. is a complete real, 5.name is the real 5.0 followed by name (a parse error), not a field access on an integer. Likewise an e/E is part of a real only when followed by an optional sign and a digit — 1e5 is a real, but 1e is an integer 1 followed by the identifier e. An exponent is decimal only; a base prefix (0x/0o/0b) never takes an exponent (0xe5 is the hex integer 229). Underscores may separate digits for readability (1_000.5, 1_000e1) and are not significant; a _ with no digit after it is an invalid decimal literal, answered at the _ (§3.5.2), and a _ straight after the point, as in 1._5, begins a name the real 1. runs into.

A real renders as its shortest round-tripping decimal — the same text CPython’s repr(float) produces: fixed-point when the value’s decimal exponent is in [-4, 15] (insignificant trailing zeros are dropped, so 3.50 renders 3.5 and 5.0 renders 5.0), scientific (d[.ddd]e[+-]NN, a signed two-or-more-digit exponent) for larger or smaller magnitudes (1e20 renders 1e+20, 1.5e-8 renders 1.5e-08, 1e-5 renders 1e-05), -0.0 for negative zero, and inf, -inf and nan for the two infinities and a NaN. The identical text is used on the wire, on write-through, and on read-back — there is no store-vs-display difference: a cell holds every real exactly, -0.0, the infinities and a NaN included. A real is an expressible value: it renders in print and via str(…).

Reals are computable: +, -, *, /, //, %, and ** accept real operands, and the ordered comparisons <, >, <=, >= accept them too (§8.1, §8.3). The numeric operators promote: an operation with an int and a real operand produces a real (1 + 0.0 is 1.0), the exact value of the operation rounded once (§8.3). / (true division) always produces a real, even for int / int (7 / 2 is 3.5); the floored integer quotient is // (§8.3).

Only arithmetic promotes, and float(x) converts on request (§8.13). Nothing else changes a number’s class. A value handed to a declared real — assigned, passed, defaulted or returned — fills it and keeps its class (§6.4). A construct that selects one of two values — a conditional expression (§8.10), and / or (§8.2), min / max of separate values — joins their types. An int / real pair is typed real, a pair holding a bool is a value holding a number (§5.6), and the value is the one selected, unconverted, as in Python: 1 if True else 2.5 is 1. So a real can hold a value whose class is int. A real field written with one holds 1 where arithmetic would have written 1.0 (§13.3, Appendix C.9). The construct that produced the number decides which, not the declaration, and the two compare equal (§8.1).

A real arithmetic result whose magnitude leaves the finite range is inf or -inf, as Python answers it, and an operation with an infinite or NaN operand answers IEEE 754’s value, nan where there is none (§8.3). Real division by zero is a runtime refuse (§10.3), as in Python. An int result past 64 bits is a refuse too (§8.3); an int has no infinity.

3.5.8 Blob literals

blob-literal := hex-blob | b64-blob
hex-blob     := 'blob' '(' '"' ( hex-digit hex-digit )* '"' ')'
b64-blob     := 'blob_b64' '(' '"' base64-char* '"' ')'
hex-digit    := dec-digit | 'a'..'f' | 'A'..'F'
base64-char  := 'A'..'Z' | 'a'..'z' | dec-digit | '+' | '/' | '='

A blob literal denotes an opaque byte string. It has two input spellings that denote the same byte space; the static type is blob (§5.1) either way, and its value class is blob on the read side (§5.6).

  • Hex — an even number of hexadecimal digits, written blob("<hex>") — e.g. blob("48656C6C6F") (five bytes). An empty blob("") is a valid zero-byte blob. Each byte is exactly two hex digits; an odd digit count, or any non-hex character, is a parse_error.
  • Base64 — RFC 4648 base64, written blob_b64("<base64>") — e.g. blob_b64("SGVsbG8=") (the same five bytes as blob("48656C6C6F")). The content is a multiple of four characters drawn from A–Z, a–z, 0–9, +, /, with zero to two trailing = padding characters; an empty blob_b64("") is a valid zero-byte blob. A wrong length, a non-alphabet character, or misplaced padding is a parse_error. Base64 is a convenience input spelling for pasting base64-encoded payloads; it produces exactly the bytes it encodes.

Regardless of the input spelling, a blob reads back at the blob value class in a single canonical uppercase-hex form (so a value written blob_b64("SGVsbG8=") reads back 48656C6C6F). A blob is one value however it was written: the two spellings and a read of the same bytes are equal by their bytes wherever blobs compare — == and !=, in, .count, .index, a set’s elements and a dict’s keys — so blob_b64("AQI=") == blob("0102") is True and len({blob_b64("AQI="), blob("0102")}) is 1. A blob is an expressible value: it renders as that hex text in print, via str(…) and in a write’s transcript line, whichever spelling produced it (print(blob_b64("AQI=")) prints 0102). A write’s transcript line gives the hex whole up to 65,536 characters; past that it gives the length and the first 8 bytes, marked as shortened (MCP Reference §4.2). print and str(…) are never shortened. len(b) is its count of bytes (§5.2), and struct lays numbers out as one and reads them back (§8.18); it is cut, joined and read byte by byte as Python’s bytes are (below), and is not otherwise operated on.

Slice, join and byte. A blob is cut, joined and read byte by byte as Python’s bytes are, each form one operation. b[i:j] and b[i:j:k] answer the blob of the bytes selected, in order: a bound left out is open, a negative bound counts from the end, a bound past either end is the end, a range that is empty or inverted is the empty blob, and a negative step walks from the high end, so b[::-1] is the blob reversed and b[::2] is every other byte. A step of 0 is the catchable refusal slice step cannot be zero (slice_step_zero). A slice value s selects as its bounds do, b[s] being b[s.start:s.stop:s.step]. b1 + b2 is the blob that holds the bytes of b1 and then those of b2. b[i] is the byte at position i as an int from 0 to 255, a negative i counting from the end, and a bool position being the int it is. A position past either end is the catchable refusal index out of range (index_out_of_bounds), and an index of another type than int is a type_error at compile, in Python’s words, byte indices must be integers or slices, not str. The answers are those of this Python, over the bytes that hold the same bytes:

def blob_slice(b, start, stop, step):
    return b[start:stop:step]


def blob_join(b1, b2):
    return b1 + b2


def blob_byte(b, i):
    return b[i]

with None for a bound or a step left out. blob("000102030405")[1:4] is blob("010203"), blob("0001")[1] is 1, and blob("00FF") + blob("01") is blob("00FF01"). A blob is not a list, so none of the three builds cells, and each is one operation however long the blob is, as every built-in operation is; a join is held as any value is, and a run that holds more than its limit allows is refused as for a string.

No bytes literal. A Python bytes literal — b"…", or rb"…" / br"…" in any case — has no counterpart, because a byte string is a blob. It is a parse_error whose message names the blob it denotes, read with CPython’s bytes escapes: b"abc" is blob("616263"), and rb"a\n" is blob("615C6E"). A literal whose bytes cannot be read without guessing is told both spellings instead: one holding a character outside ASCII, which CPython refuses too, or a triple-quoted one.

3.5.9 List literals

A list literal builds a constructed list — a finite, ordered sequence of element values, evaluated as the program runs:

list-literal := '[' ( display-item ( ',' display-item )* ','? )? ']'
display-item := expr | '*' bitor-expr        # an element, or a spread (§8.15)

The elements are arbitrary expressions, evaluated left to right; a trailing comma is permitted ([1, 2,]); the empty list is []. Lists nest — a list may contain lists ([[1, 2], [3]]). The element type is inferred (§5.1): a list whose elements share a type is a list<T>; a list mixing scalar value classes is a list<value> — one mixing only ints and reals holds numbers, which the numeric operators and folds take while each element keeps its own class (§5.6: [1, 2.5][0] is 1, and sum([1, 2.5]) is 3.5), and one mixing a scientific with numbers holds magnitudes, which order (§5.6); a list mixing a complex with numbers is a list<complex> (§3.5.15); the empty list is list<unknown>, refined by context. Lists (or tuples, sets, dicts) whose element types meet (§8.1) are one element type, so [[1], [2.5]] is a list of lists holding numbers. A literal that mixes a scalar with a non-scalar ([1, [2]]), or collections whose elements do not meet ([[1], ["a"]]), has no common element type and is a type_error.

A constructed list exists only while the program runs: it holds a finite number of elements and is never stored. Unlike a string it is shared and changes in place — every name holding it sees a change made through any of them (§8.14). It is distinct from the store-derived finite collections of §5.1 (which are produced by read verbs); the two share the read surface (indexing, slicing, .length, for … in, membership, and the value-search methods xs.index(v[, start[, end]]) → the first index of v within an optional codepoint window and xs.count(v) → the number of occurrences — index of an absent value is a catchable refuse, §10.3) but a constructed list additionally renders in print and + (§8.4, §8.7), supports ==/!= (§8.1) and comprehensions (§8.9), and is copied with xs.copy() (§8.14).

3.5.10 Formatted string literals (f-strings)

A formatted string literal — an f-string — is a string literal carrying a lowercase or uppercase f prefix (f"…" / F"…") whose { … } sections are replacement fields: an expression written between braces is evaluated and its value is written into the string in place of the field.

fstring-literal := fstring-prefix '"' ( fstring-char | replacement-field )* '"'
fstring-prefix  := 'f' | 'F'
                 | raw-prefix ( 'f' | 'F' ) | ( 'f' | 'F' ) raw-prefix   # raw, §3.5.1
fstring-char    := string-char (§3.5.1)   #  '{{' and '}}' denote literal '{' and '}'
replacement-field := '{' expr '='? ( '!' ('r' | 's') )? ( ':' format-spec )? '}'
format-spec       := ( fill? align )? sign? 'z'? '#'? '0'? width? grouping? ( '.' precision )? format-type?  # §8.13
fill              := <any one character>
align             := '<' | '>' | '^' | '='
sign              := '+' | '-' | ' '
width             := dec-digit+
grouping          := ',' | '_'
precision         := dec-digit+
format-type       := 's' | 'd' | 'x' | 'X' | 'o' | 'b' | 'c' | 'n'
                   | 'e' | 'E' | 'f' | 'F' | 'g' | 'G' | '%'

The literal portions accept the same escapes as an ordinary string literal (§3.5.1). A doubled brace {{ or }} denotes a single literal { or }. Each replacement field holds a full expression (§4.4) — a bound variable, a literal, an arithmetic expression, a built-in method call, an index or slice, or a field read.

The value of an f-string is exactly the value of the equivalent + concatenation of its literal parts and its replacement-field expressions, in order: f"a{x}b" has the value "a" + str(x) + "b", and a field is written using the same rendering + gives that value (§8.7) — an int/real/bool/list is rendered as + renders it. A field whose value is a store read must first bind it to a name (§4.3), the same rule the + operator follows. An f-string is always of type string, including when it is a single replacement field (f"{n}" is a string, not the type of n).

A replacement field may carry a conversion and/or a format spec: {x!r} renders repr(x), {x!a} renders ascii(x) (and {x!s} renders str(x), the default); {x:spec} renders format(x, "spec") — the format mini-language of §8.13 (f"{x:.2f}", f"{n:>5}", f"{s:*^8}"), and the two combine as {x!r:>10}.

A replacement field may end its expression with a self-documenting = (f"{x=}"): the field writes the field’s source text from just after { up to and including the = verbatim (surrounding whitespace preserved), followed by the value. With no conversion or format spec the value defaults to repr (not str) — f"{x=}" is "x=5", f"{ x = }" is " x = 5", and f"{s=}" is "s='hi'" — while a :format-spec or !conversion overrides that default (f"{x=:.2f}" is "x=5.00", f"{x=!s}" is "x=5"). A trailing comparison operator (==, >=, <=, !=) is NOT the debug =.

A field may hold a string literal in the quote that does not enclose the f-string, as f"{d['k']}" does and f'{d["k"]}'. The quote that encloses it ends the f-string, so f"{\"a\"}" is a parse_error, as in CPython. An empty field {} is a parse_error, and so is a # in a field’s expression (f"{x # c}"), as in CPython. A format spec may hold fields of its own (f"{x:>{width}}"): each is replaced by its value, rendered as a field of the string is, before the spec is applied, so with width = 5, f"{1:>{width}}" is " 1". A field in a spec may carry a conversion and a format spec of its own, but one whose spec holds a field is a parse_error, nested too deeply, as in CPython. A conforming implementation MUST reject an unterminated f-string (newline or end-of-source before the closing ") with a parse_error.

A string’s .format and .format_map (§5.3) and % with a string on its left (§8.3) format as Python’s do. A template is a value, so what each asks of its arguments is asked as the program runs.

3.5.11 Tuple literals

A tuple literal builds a constructed tuple — a finite, ordered, immutable sequence whose positions may each hold a different type, evaluated on the client side:

tuple-literal := '(' ( display-item ( ',' display-item )* ','? )? ')'   # a `*` item alone needs its comma (§8.15)

A comma distinguishes a tuple from a parenthesized expression: (e) groups, (e,) is a one-element tuple, (a, b) a two-element tuple, and () the empty tuple. A group of one operand is that operand, and a call, an index or a .name after it reads it as it reads the operand alone, as in CPython: (c).foo() calls the method of the instance c holds, (f)(x) calls f, and (xs)[0] is xs[0]. Where a statement takes a value — after return and on the right of an assignment or an element write (expr-list, §4.3) — the parentheses may be left off, as in CPython: return 1, 2 returns (1, 2), x = 1, 2 and xs[0] = 1, 2 bind it, and x = 1, binds the one-element tuple (1,). In an argument list or a literal a comma separates items, so a tuple there keeps its parentheses. The element type is inferred per position (§5.1): (1, "a", True) is a tuple<int, string, bool>; tuples nest. Unlike a list, a tuple’s positions may differ in type, so it is indexed by a constant integer (§5.3) — t[0] has the type of the first position — and it is written after for … in when those positions all share one type, all hold numbers of the two classes ((1, 2.5), §5.6, §6.3), or all hold class instances, which the loop variable takes at their nearest common base, as a list display’s elements join (§13.8). A constructed tuple exists only while the program runs: bounded at construction, immutable, and never stored. It renders in print, supports ==/!= (§8.1), concatenates with another tuple via + (§8.4) and repeats by a literal int count via * (§8.3) — the result keeping each operand’s positions and their types, so a constant index into it reads the position it names. It is searched by the value-search methods t.index(v[, start[, end]]) → the first position holding v, within an optional window whose bounds resolve as a slice’s do, and t.count(v) → the number of positions holding it — wherever v in t is allowed. Each position is compared with v as == compares them (§8.1), and a position of another type is simply unequal, so a record is searched for any of its fields: (1, "a").index("a") is 1. index of an absent value is a catchable refuse (§10.3) whose reason is Python’s tuple.index(x): x not in tuple, discriminator value_not_in_tuple.

3.5.12 Dict literals

A dict literal builds a constructed dict — a finite mapping from hashable keys to values, kept in insertion order, evaluated on the client side:

dict-literal := '{' ( dict-item ( ',' dict-item )* ','? )? '}'
dict-item    := expr ':' expr | '**' bitor-expr

The empty dict is {}. A key must be hashable: a scalar value class (string / int / bool / datetime / duration / real / blob / scientific / complex), a value, or a tuple whose positions are recursively hashable (§5.1); a list, dict or set is unhashable. Keys are homogeneous (a mix of scalar value classes gives a value-typed key part); any other mix of keys — a tuple beside a scalar, as in {(1, 2): 1, "a": 2}, or tuples whose positions hold types that do not join — is a type_error naming the two types, since a value holds a scalar (§5.6), where CPython builds the dict, and a key written into a dict, d[k] = v, is held to the same rule. The values join as a list literal’s elements do (§3.5.9): a mix of scalar value classes gives a value-typed value part, holding numbers when every value is an int, a real or a bool (§5.6); collections whose elements meet are one type, so {"a": [1], "b": []} is a dict<string, list<int>>; and instances of several subclasses take their nearest common base (§13.8). The inferred type is dict<K, V> (§5.1): {"a": 1, "b": 2} is a dict<string, int>. When a key is written twice, the entry keeps its first position but takes the last value. A key is matched by value, as == compares (§8.1), so an int and a real of equal value are one key, as in Python: {1: "a", 1.0: "b"} is {1: 'b'}, and 1.0 in {1: "a"} is True. So are a bool and the int it is: {True: "a", 1: "b"} is {True: 'b'}. The keys of a dict whose keys are ints and reals hold numbers in the sense of §5.6, so sum, min, max and sorted fold them: sum({1: "a", 2.5: "b"}) is 3.5. A dict is read by key d[k] (a key absent at run time is a catchable refuse, §10.3), tested with k in d (§8.1), walked with for k in d over its keys (§6.3), measured with .length, viewed through .keys() / .values() / .items() (§5.1, §8.6), and copied with .copy() or dict(d) (§8.14). A constructed dict exists only while the program runs, is shared and changed in place as a list is (§8.14), and is never stored; it renders in print. + is not defined over dicts, the same as in Python.

A **d entry gives every entry of the dict d holds, in its order, at that place among the others, as CPython’s does, so a key written twice follows the rule above: {**d, "a": 0} keeps 0 for "a" and {"a": 0, **d} keeps what d holds for it. Its operand is read before the program runs, and one that is no dict is a type_error in CPython’s words, as in 'int' object is not a mapping. d | e (§8.3) is the same merge as {**d, **e}.

3.5.13 Set literals

A set literal builds a constructed set — a finite collection of unique values, evaluated as the program runs:

set-literal := '{' display-item ( ',' display-item )* ','? '}'

A { … } with no colon is a set (a { … } with a colon is a dict, §3.5.12); because the empty braces {} are the empty dict, the empty set has no literal form. It is written set(), as in Python, or set([]) (§8.13). An element must be hashable: a scalar value class, a value, or a tuple whose positions are recursively hashable (§5.1); a list, dict or set is unhashable. Elements are homogeneous (a mix of scalar value classes gives a set<value>, holding numbers when every element is an int, a real or a bool, §5.6); any other mix — a tuple beside a scalar, as in {1, (1, 2)}, or tuples whose positions hold types that do not join, as in {(1, "a"), (2, 3)} — is a type_error naming the two types, as a list literal’s is, since a value holds a scalar, where CPython builds the set. Duplicates are removed. An int and a real of equal value are duplicates, as in Python, and the one written first is kept: {1.0, 1} is {1.0}. So are a bool and the number it is: {True, 1, 1.0} is {True}. So are -0.0 and 0.0, which are equal although they render apart: {-0.0, 0.0} is {-0.0}. The inferred type is set<T> (§5.1). A set renders and is walked (for x in s) in a deterministic sorted order — this is a defined refinement of Python, whose set order is unspecified. It is the order sorted(s) gives (§8.6): numbers rank by value, an int, a real and a bool together, so {2.5, 1, 3} renders {1, 2.5, 3}, {0.5, 5, True} renders {0.5, True, 5}, and list({2, 1.5}) is [1.5, 2], as Python prints them. A scientific ranks among the numbers by its magnitude (§8.1). Where it ties a number, the number comes first, and two scientifics that tie come in the order of the values they write exactly, then with fewer significant figures first, so {scientific("6.0220e23"), scientific("6.022e23")} renders {6.022e23, 6.0220e23} whichever was written first. A complex has no order (§8.1), so a set lists its complexes by real part and then by imaginary part, which is a listing order only: {2 + 1j, 2j, 1 + 5j} renders {2j, (1+5j), (2+1j)}. A complex and a number that are equal are duplicates, like any two equal numbers: {1, 1 + 0j} is {1}. Python’s own order is not a sort and can differ elsewhere ({-1, 0} prints {0, -1} there). A set is tested with x in s (§8.1), measured with .length, compared by inclusion with <= / < / >= / > and .issubset / .issuperset / .isdisjoint, and combined with | / & / - / ^ and .union / .intersection / .difference / .symmetric_difference (§8.6), and copied with .copy() (§8.14). A constructed set exists only while the program runs, is shared and changed in place as a list is (§8.14), and is never stored.

3.5.14 Scientific literals

scientific-literal := 'scientific' '(' '"' scientific-numeral '"' ')'
scientific-numeral := '-'? mantissa ( 'e' | 'E' ) '-'? dec-digit+
mantissa           := dec-digit+ ( '.' dec-digit+ )?    # a nonzero magnitude

A scientific literal denotes a number carrying its significant figures, written as the factory call scientific("<numeral>") in scientific notation — e.g. scientific("6.022e23"). Its static type is scientific (§5.1); its value class is scientific on the read side (§5.6).

The argument is a single string literal: an optional leading -, a mantissa (digits with an optional decimal point) carrying at least one nonzero digit, then a required exponent — the letter e (or E), an optional sign, and one or more digits. The exponent is mandatory: it is what distinguishes a scientific value from a real (§3.5.7) and what makes the number of significant figures unambiguous. A numeral with no exponent or with an all-zero mantissa is a parse_error; a magnitude too large to represent is refused at write.

Significant figures are part of the value. scientific("6.022e23") (four significant figures) and scientific("6.0220e23") (five — the trailing zero is significant) denote the same magnitude but are distinct values: each reads back with its own figures, and writing one to a cell does not replace the other. This is the one property a real cannot carry — a real is a floating-point number that forgets how many figures were written.

A scientific reads back in a canonical form: the mantissa is normalized to a single nonzero leading digit with the significant figures preserved, so scientific("60.220e21") reads back as 6.0220e22. Numerals that normalize to the same figures are the same value regardless of how they were written.

3.5.15 Complex literals

imag-literal := ( dec-int | real-literal ) ( 'j' | 'J' )

An imaginary literal denotes a complex number whose real part is zero, written as a decimal number followed directly by j (or J), as in Python: 4j, 1.5j, .5j, 5.j, 1e3j. Its static type is complex (§5.1); its value class is complex on the read side (§5.6). There is no literal for a complex with a real part. 3+4j is the expression 3 + 4j, an int added to an imaginary literal (§8.3), and that is how such a number is written, here as in Python.

Only a decimal number takes the suffix. A base prefix never does: 0x1j is not a literal. The leading-zero rule of §3.5.2 does not reach an imaginary literal either, because its digits are never read as octal, so 07j is 7j, as in Python. Underscores separate digits as they do in a real (1_0j is 10j). An imaginary part that leaves the finite real range upward is infinite, as 1e400 is (§3.5.7): 1e400j is infj.

A complex holds two reals, its real part and its imaginary part, read with .real and .imag (§5.2). It renders as CPython’s repr of a complex does. When the real part is a positive zero the imaginary part stands alone: 4j, 1.5j, and 0 - 4j is -4j. Otherwise both parts are written, in parentheses, the imaginary part with its sign: (3+4j), (3-4j). Each part is written as a real is (§3.5.7), except that a whole number drops its .0: 3.0 + 4j is (3+4j), 1e16 + 1.5j is (1e+16+1.5j). A part may be infinite or a NaN, as a real may, and is written inf, -inf or nan, a NaN always with + before it: (inf+nanj), infj, (nan+1j). The same text is used by print, by str(…), in an f-string, on the wire and on read-back.

The sign of a zero part is kept through a computation and shown, as in Python: the prefix - negates both parts, so -4j renders (-0-4j) and -0j renders (-0-0j). A cell holds each part exactly (§3.5.7), so a -0j written to a cell reads back as (-0-0j). Two complexes that differ only in the sign of a zero part are one value to the store, as they are to ==, so writing the other one of them to a cell that holds one adds no claim.

Complex numbers are computable: +, -, *, / and ** take a complex beside a complex or a number and answer a complex (§8.3), abs(z) is its magnitude and z.conjugate() its conjugate (§8.6). They are equatable: == and != compare both parts, and a complex equals a number exactly when its imaginary part is zero and its real part equals that number, so 1 + 0j == 1 is True (§8.1). They have no order: <, >, <= and >= refuse one, as Python does, and so does everything that ranks its operands. A complex is hashable (§3.5.12), and is falsy exactly when both parts are zero (§8.2).

A value handed to a declared complex — assigned, passed, defaulted or returned — fills it and keeps its class, as a number handed to a declared real does (§6.4): after z: complex = 1, z prints 1, as in Python. A construct that selects one of two values (§3.5.7) joins a complex and a number to complex, and the value is the one selected, unconverted: 2j if c else 1 is typed complex and is 1 when c is false. Everything that takes a complex takes the number it may hold and answers as Python does for that number: its .real is the number, its .imag a zero of the number’s class, its .conjugate() the number. A collection joins them the same way. [1, 2j] is a list<complex> whose elements keep their classes, so [1, 2j][0] is 1, sum([1, 2j]) is (1+2j) and [1, 2j][1] * 2 is 4j, as in Python. So do a set, a dict’s keys and values, a tuple iterated over, + of two lists, set union and a nested display: [[1], [2j]] is a list of list<complex>. It still has no order, so sorted([1, 2j]) is a type_error.

A complex number reaches what the engine derives in three places. A computed field declared complex derives one by arithmetic over the class’s fields (§13.7). A standing rule tests a field against one with == and != and concludes one (§14.2, §14.3), and a hypothesis supposes one (§15). A rule and a hypothesis write it as a case label does (§6.2), the one form that is a literal where the rest of a program writes the expression.

3.6 Operators and punctuation

( ) [ ]   . , : =
== != < > <= >=
+ - * / // % **
| & ^ << >> ~

The tokens |, & and ^ are the bitwise operators on two integers and the set operators on two sets (§8.3, §8.6), and | also separates the alternatives of a case label (§6.2). << and >> are the shifts, and the prefix ~ is bitwise inversion (§8.3). A @ that begins its line begins the decorator of a rule or a constraint. After anything on its line it is Python’s matrix product, which reads and which no value here answers (§8.3). The token = appears in bare assignment (§4.3); a class field is written with a remember / update verb call (§13.3), not =. The token : ends a block header and separates a parameter or field name from its type. A newline or a ; ends a simple statement (§3.2, §4.3). The word forms and / or / not are the logical operators (§8.2); there are no && / || / ! tokens.

3.7 Reserved words

The following identifiers are reserved and MUST NOT be used as user identifiers (declaration, class, method and field names, parameter names, loop variables, handler bindings, assignment and unpack targets, and names a case pattern binds). They are Python 3.11’s keywords and the absence word null (§3.5.4). A conforming implementation MUST treat every one of them as reserved at the lexical level.

and        as         assert     async
await      break      class      continue
def        del        elif       else
except     False      finally    for
from       global     if         import
in         is         lambda     None
nonlocal   not        null       or
pass       raise      return     True
try        while      with       yield

This list is complete; no other word is reserved. Every keyword Python 3.11 reserves is in it. Compiling a module, forgetting a script and reading a script’s metadata are wire operations (MCP Reference §9.2), not statements of a program body.

This language’s own words are names. The head-words of the built-in verb surface (§7), match, case, branch, commit, rollback and script are ordinary names, as they are in Python, wherever the form each heads does not stand. A verb’s word is the verb immediately before its (. match heads a match statement at the head of a line that ends in :, and case an arm at the head of a line in that statement’s block. commit and rollback are the statement alone on a line or after a : or ;, and branch is the context manager before its (. So get = 1, def f(match: int):, case = 2, import list and xs.case(1) are all read as Python reads them. A def, a class or an import at the top level of a unit takes the word for the whole unit, so a call of it is a call of what the unit declares.

After a ., an identifier-shaped token is parsed as a field name; whether the field is meaningful for the base type is decided by the typechecker (§6). A word in the list above, which CPython reserves too — if, class, True, None and the rest of its keywords — names no field, method or path segment, as CPython’s grammar reads only a name after a .: xs.if, g(xs).class and remember(K.s.class, 1, "op") are parse_errors at the word (§10.1), and so is such a word beginning a path, current(class.s.a), bar lambda, which begins a lambda there as it does in Python and is answered where its header stops (§8.8). A kind, subject or attribute spelled as one is still addressed, through a string name that holds it (§6.7): k = "class", then remember(K.s.k, 1, "op"). The type names of §5.5 (string, int, claim_list, …) are contextual identifiers, not reserved words; programs SHOULD nonetheless avoid them as user identifiers.

3.8 Token classes

Class Examples
IDENT Cat, mittens, audit_subject, 猫
STRING "orange", "op1"
INT 42, 0, 9223372036854775807
REAL 3.5, .5, 1e5
IMAG 4j, 1.5j, 1e3j (§3.5.15)
KEYWORD any reserved word from §3.7
NEWLINE / INDENT / DEDENT layout tokens from the off-side rule (§3.2)
punctuation/operators as listed in §3.6
EOF end of source

The typed values of §3.5.5–§3.5.8 and §3.5.14 — datetime("…"), duration("…"), blob("…") / blob_b64("…") and scientific("…") — have no token class of their own: each lexes as an IDENT, punctuation and a STRING, and becomes a literal in the grammar (§4).

3.9 Natural language

The natural language a source is written in — which keyword vocabulary its statements use, and the language its responses are rendered in — is set by the language parameter supplied when a source is submitted. When the parameter is omitted, the language is the one the source declares (§3.9.1), and when it declares none, English (eng).

eng is the default. The parameter’s accepted values belong to the wire contract and are listed in the MCP Reference; a value outside that set is declined before the source is compiled.

This specification describes the English surface. Every keyword, verb and rendered response given here is the eng spelling.

The natural language is a property of how you are talking to the service, set per call, and the call decides it. A source may say what it was written in, so that a file read later carries that with it; the call is checked against what it says (§3.9.1), and it never decides in the call’s place.

3.9.1 The first-line declaration

A source MAY declare, on its first line, the edition and the natural language it is written in, each with an optional version:

# dke: edition=python.1 language=eng.1
def hello():
    print("written in English, at API 1")

The line is a comment (§3.3), so a declared source is valid DKE Python wherever nothing reads the declaration.

declaration   := '#' decl-blank* 'dke:' decl-blank* decl-setting decl-blank+ decl-setting decl-blank*
decl-setting  := decl-key '=' decl-name ('.' decl-version)?
decl-key      := 'edition' | 'language'
decl-name     := ('a'..'z')+
decl-version  := ('1'..'9') ('0'..'9')*
decl-blank    := ' ' | <a tab>

The two settings name different keys, in either order, and the line ends at its line terminator (§3.2).

  • The edition is python, DKE Python. Its version is the API’s MAJOR, the number before the dot of the API version this document’s Version line states (§11.1), so python.1 is DKE Python at API 1.0 and at every later 1.N: a MINOR adds and changes nothing a source written at 1.0 relies on.
  • The language is a tag the language parameter accepts. Its version is the version of that language’s vocabulary: the reserved words, soft keywords, type names and other words a source spells in that language. Each language counts its own. A language’s version moves when one of those words is removed or respelled, and never when one is added, so a source written at a version stays valid at it as words are added. eng.1 is the English vocabulary this specification describes.

A conforming service reads the declaration so:

  1. Only the first line is read. A first line that does not open with #, optional blanks and dke: declares nothing, and a declaration on any later line is an ordinary comment.
  2. A first line that opens so and does not match declaration is refused, and the refusal shows the form. latest is not a version.
  3. A declared name supplies the parameter a call omits, edition or language, and is checked as that parameter would be.
  4. A parameter that names another value than the source declares is refused. Neither overrides the other.
  5. A version that is written must be the one the service serves, or the source is refused, and the refusal names the version served. For the edition, that is the MAJOR of the API the service serves. A version left out is the one the service serves.

Each refusal is made before the source is compiled, as a parameter outside the accepted set is declined.

A file meant to be compiled later, by a service newer than the one it was written for, SHOULD write both versions: a version left out is read as whatever that later service serves.


4. Grammar

4.1 Programs

source       := option-verb NEWLINE | module-body
module-body  := docstring? top-item+
docstring    := STRING NEWLINE                           # documents what it opens; nothing runs it
top-item     := decl | class-decl | rule-decl            # decl §4.2, class-decl §13.1, rule-decl §14
              | constraint-decl                          # constraint-decl §14.10
              | import-stmt | stmt                       # import-stmt §4.3, stmt §4.3 (incl. the four writes) — see Modules
option-verb  := '--' IDENT IDENT?

A source is the module body (Python Reference §9.1) from its first line. Its body admits, at the top level, not only declarations but also import statements (Python Reference §9.3), data declarations (write statements at module scope, Python Reference §9.2), and the other statements legal in a def body (§4.3). The module body runs once, top to bottom, when the module is imported (Python Reference §9.1); a file whose body is exactly one def is the common single-script case. Because it runs again on every import, a module’s top level writes each cell once per source and period: a second write to one cell from one source over an overlapping window refuses (Python Reference §9.5).

The natural language the source is written in — the language of the knowledge it names and of its authored text — is set by the call’s language parameter, which the source’s first-line declaration may supply (§3.9.1); it also selects the language in which the script’s own structured output — its invocation transcript (MCP Reference §4.2) — is rendered. It does not select the response language of the surrounding service surface, which remains a property of the calling channel; the two may differ.

Option verbs. A ---prefixed word in declaration position is an option verb — a compiler-directed verb, addressed to the implementation rather than the store. An option verb’s spelling involves no language keyword, so --help means the same thing in every context. The supported set is --help (§4.2), which takes an optional topic word; an unknown option verb is a parse_error whose diagnostic names the supported set. An option verb never compiles and never stores anything, by construction.

A source file may declare one or more scripts — a compile unit. Every declaration compiles and stores under its own name. A declaration may call any other declaration of the same unit, above or below it, or an already-stored script, as long as no call leads back to its caller (§6.8, §9.7, compile-before-call). Two declarations in one unit MUST NOT share a name. A unit need not declare a script at all: a module whose body only installs a schema, defines a rule, or writes data is valid (Python Reference §9.1, §9.7).

A unit with an empty body — whitespace and comments only — is a parse_error.

A docstring is a string literal alone on the first line of a module, a class body (§13.1) or a def body (§4.2). It documents what it opens and is not a statement: nothing runs it, and a unit compiles exactly as it would with a comment in its place. A def whose body is only a docstring does nothing, as a body of pass does, and a module that holds only a docstring is still empty. A string standing alone anywhere else is an expression statement (§4.3).

Except where an example shows a complete compile unit, program examples elsewhere in this document are fragments: a fragment omits the surrounding program scaffolding for brevity.

4.2 Declarations

decl    := 'def' IDENT '(' params? ')' ( '->' return-type )? ':' NEWLINE INDENT ( docstring stmt* | stmt+ ) DEDENT
return-type := type | 'value' | 'None'              # `value`: a store fold's result, narrowed by the caller (§4.2); `None`: what no annotation says
params    := param-item ( ',' param-item )*             # which lists are read: §4.2
param-item := param | var-param
           | '/'                                        # what stands before it takes no keyword (§4.2)
           | '*'                                        # what follows it takes no position (§4.2)
param     := IDENT ':' param-type ( '=' const-expr )?   # a default value
var-param := '*' IDENT ':' binding-type                  # variadic: surplus positionals → tuple<type, …>
const-expr := expr                                      # a literal, or an operation over literals
param-type := binding-type
            | module-name '.' IDENT                     # a class an imported module declares (§13.2)
binding-type := type
type    := type-name
         | IDENT                                   # a class name (§13.1), in EVERY type position (§5.5)
         | 'list' '<' type '>' | 'set' '<' type '>'
         | 'dict' '<' type ',' type '>' | 'tuple' '<' type ( ',' type )* '>'
         | 'iterator' '<' type '>'                                          # §8.6
         | 'dict_keys' '<' type '>' | 'dict_values' '<' type '>'           # dict views (§5.1)
         | 'dict_items' '<' type ',' type '>'
         | collection-word '[' type ( ',' type )* ( ',' '...' )? ']'       # any of them in CPython's brackets, the same type (§5.5)
type-name := scalar-name
           | 'active_claim' | 'claim_list' | 'proof_tree' | 'diagnosis'   # read-result types (§5.1)
           | 'subject_set' | 'string_list' | 'verify_result' | 'result_type_set'
           | 'slice'                                                       # a slice value (§5.3)
           | 'range'                                                       # a range of ints (§5.3)
scalar-name := 'string' | 'int' | 'bool'                                 # primitives
             | 'datetime' | 'duration' | 'real' | 'blob' | 'scientific'  # value-class scalars (§5.5)
             | 'complex'
             | 'str' | 'float'                                           # CPython's names for `string` and `real` (§5.5)
collection-word := 'list' | 'set' | 'dict' | 'tuple' | 'iterator'
                 | 'dict_keys' | 'dict_values' | 'dict_items'

A declaration is the def keyword, a name, a parenthesised parameter list, an optional -> type return type, a :, and an indented suite. A parameter is name: type where type is one of the type names enumerated in §5.5 — a primitive (string, int, bool), a value-class scalar (datetime, duration, real, blob, scientific, complex), or a read-result type such as claim_list — or a parameterized collection type: list<T>, set<T>, dict<K, V>, or tuple<T, ...>, nestable (e.g. dict<string, list<int>>). So a collection crosses a function boundary both as an argument and as a return value. A class crosses it both ways, as a collection does. A parameter may name a class and receives an instance of it, and a return type may name one too (§13.2). (empty and null name absence and are never parameter types.) The declared objects are called scripts (the wire addresses them as such, see MCP Reference §9.2).

One name space. A compile unit declares four kinds of named thing — a script, a standing rule (§14.1), a standing constraint (§14.10) and a class (§13.1) — and they share one name space: no two declarations in a unit may have the same name, whatever their kinds. A second declaration of a name the unit already holds is a type_error (§10.2), reported at that declaration’s name, and its diagnostic names the kind of the declaration holding the name. Python also binds a class and a function in one module name space, and rebinds a name declared twice; DKE Python refuses the second declaration instead.

Void scripts and value scripts. A script with no -> type whose body holds no return <expr> is a void script: it produces no value and is called for its store effects and print output. A script written def name(...) -> type: is a value script: it produces a value of the declared type, and every path through its body MUST end in a return <expr> of that type (the exhaustive-return rule, §6.9). A value script may be called in expression position, its result bound or composed (§4.4, §6.8); a void script may not (using a void script’s call as a value is a type_error). The return type is any of the parameter types above, including a collection type (-> list<int>, -> dict<string, int>, …); an empty return [], return set([]) or return {} satisfies any concrete -> list<T>, -> set<T> or -> dict<K, V>. A script with no -> type whose body holds a return <expr> is a value script too, of the type its returns give, and behaves as one written with that type, wherever its call stands. The wider of two types one of which fills the other is that type, so a return of an int and one of a real give real. Two returns of types neither of which fills the other are a type_error, and so is a path that ends with no value, and a return no type is known for, such as an element of a list bound empty, which is told so, annotated or not, and the annotation it takes is on that name. So is a body whose type waits on the type of a body that may call it back: writing -> <type> on either settles what is no cycle (§9.7). The return statement itself is specified in §4.3 and §9.2.8.

-> value. A value script may also declare -> value, as a value method may (§13.4): Python has one construct for both. It is the return type of a script that hands on a store fold’s result, such as m = max(K.attr), whose class is whatever the cells hold. The caller narrows it with match (§6.2), and so does a script that would rather promise a narrower type: -> int, then match m: and case int as n: and return n. Only an expression whose type is value fills a -> value return, so return 1 is a type_error (§6.4), as it is in a method.

A parameter may carry a default value (def f(x: int, y: int = 1)). The default is a constant expression — a literal, or an operation over literals (so -1, 1 + 2, [1, 2] qualify) — and its type must match the parameter’s declared type or fill it (§6.4): a bool default is accepted for an int parameter, and either for a real one. A parameter with a default may not precede one without (a required parameter after a defaulted one is a compile-time error). A call that omits a defaulted argument uses the default; a required parameter with no supplied argument is an error (§4.3, §9.7).

Variadic parameter. A parameter may be written *name: T — a variadic parameter that collects the surplus positional arguments into a tuple<T, ...> (a tuple whose every element is T), possibly empty. There may be at most one, its element type T is required, and it takes no default. A call passes any number of trailing positional arguments of type T for it (f(a, 1, 2, 3)); it cannot be supplied by keyword. Inside the body the parameter is an ordinary tuple<T, ...> — indexed (name[0]), measured (len(name)), iterated (for x in name), and reduced (sum/max/comprehension). There is no **kwargs: a signature names every keyword a call may pass, which is what lets a misspelt keyword be refused where it is written rather than collected as data. Use explicit named parameters — with defaults where useful — or a dict parameter.

Positional-only and keyword-only parameters. A / makes the parameters before it positional-only, and a bare * or a variadic parameter makes the parameters after it keyword-only, as in Python: def f(a: int, /, b: int, *, c: int = 3) is called f(1, 2) or f(1, b=2, c=4). A keyword naming a positional-only parameter is told f() got some positional-only arguments passed as keyword arguments: 'a', and a positional argument that would reach a keyword-only one f() takes 2 positional arguments but 3 were given. A keyword-only parameter takes a default as any parameter does, and one with no default may follow one with a default. A spread (§8.15) fills only what a position may reach. Each marker stands at most once and in that order, as §10.1 says of a list Python would not read.

The listing of scripts writes each parameter list as it was written, markers included.

--help is the teaching option verb (§4.1) — the in-band way to learn the language from inside it. Bare --help yields the orientation lesson: the declaration shape, one complete example, and the topic index. --help <topic> yields one topic’s lesson; the topics are header, declarations, statements, expressions, types, verbs, and scripts. The lesson language follows the language parameter (§3.9). An unknown topic is a parse_error whose diagnostic names the topic index.

--help types

A unit whose body is a help form never compiles and stores nothing; on the wire it is classified parse_error, and its diagnostic message is the lesson. A conforming implementation whose surface faces a human directly (such as a CLI) SHOULD present the lesson without error framing and report success — asking for help is not a mistake.

4.3 Statements

A statement is either a simple statement, ending with a NEWLINE or a ;, or a block statement (if / for / match / try / branch / world), whose body is a :-headed indented suite. Where this grammar writes that suite, the simple statements of one line may stand after the header’s : instead, as in Python: if a: b = 1; c = 2, def f(): pass, class C: x: int. A block statement does not stand there, nor after a ;.

stmt         := simple-stmt ( ';' simple-stmt )* ';'? NEWLINE
              | block-stmt

simple-stmt  := assign-stmt
              | print-stmt
              | call-stmt
              | verb-call-stmt
              | commit-stmt
              | rollback-stmt
              | import-stmt
              | return-stmt                                  # §9.2.8
              | raise-stmt
              | assert-stmt
              | rebind-stmt                                  # changes a collection, §8.14
              | del-stmt                                     # §8.14
              | 'pass'                                       # a no-op
              | '...'                                        # a no-op, as `pass` is
              | expr-list                                    # an expression statement: it runs, and nothing keeps its value
              | 'break'                                      # loop control, §9.2.7
              | 'continue'

block-stmt   := match-stmt
              | class-match                                  # dispatch by class, §13.5
              | for-stmt
              | if-stmt
              | try-stmt
              | branch-block
              | world-block                                  # §9.2.9

assign-stmt      := IDENT '=' expr-list
                  | IDENT ':' binding-type                     # an annotation with no value; it binds nothing
                  | IDENT ':' binding-type '=' expr-list       # annotated assignment; the type must agree with the initializer, and only a name takes one (§6.4)
                  | chain-target ( '=' chain-target )+ '=' expr-list  # chained assignment; every target binds one value, left to right (§8.12)
                  | IDENT aug-op expr-list                     # augmented assignment on a bare name; `x op= e` is `x op e`, changed in place on a list or set (a field names a source, so it uses `remember(...)`, not `op=`)
                  | top-target ( ',' top-target )+ '=' expr-list                # unpacking a tuple or list; nested targets + one optional `*rest` (§8.11)
                  | target-group '=' expr-list                                  # a parenthesised or bracketed target list (§8.11)
expr-list        := display-item ( ',' display-item )* ','?     # a bare comma-list; holding a comma, it is a tuple (§3.5.11); a `*` item alone is none (§8.15)
top-target       := unpack-target | '*' unpack-target                           # a target, or THE single starred target (≤ one per list), which may be any target
unpack-target    := IDENT | element-target | target-group      # a name, an element or a slice, or a nested target list, which may hold its own star
target-group     := '(' top-target ( ',' top-target )* ','? ')'   # in parentheses; one target alone in them is that target
                  | '[' top-target ( ',' top-target )* ','? ']'   # in brackets; `[a]` is a list of one target (§8.11)
                  | '(' ')' | '[' ']'                           # a list of no targets; the value must hold nothing (§8.11)
chain-target     := top-target ( ',' top-target )* ','?        # a target or a target list, as an assignment takes one (§8.12)
element-target   := write-target subscript                      # an element or a slice, written as an element write writes it (§8.14)
                  | postfix-expr subscript                      # an element of what an expression answers, which no name holds (§8.14)
aug-op           := '+=' | '-=' | '*=' | '/=' | '%=' | '//=' | '**=' | '|=' | '&=' | '^=' | '@='
rebind-stmt      := write-target '.' rebind-method '(' arg-list? ')'  # changes the collection the target reaches; a statement, never an expression (§8.14)
                  | write-target '.' answer-method '(' arg-list? ')'  # changes the collection and discards what the call answers; the call is an expression too (§8.14)
                  | postfix-expr '.' ( rebind-method | answer-method ) '(' arg-list? ')'  # the same, on what an expression answers, which no name holds (§8.14)
                  | element-target '=' expr-list                # an element or slice write; changes the collection the target reaches (§8.14)
                  | element-target aug-op expr-list             # `c[k] op= e` is `c[k] = c[k] op e`, the target and `k` evaluated once
write-target     := IDENT ( subscript | '.' IDENT '(' arg-list? ')' )*  # a bound name, then any chain of subscripts and method calls; what it reaches is the collection changed (§8.14)
subscript        := '[' expr ( ',' expr )* ','? ']'             # an index or a key; a comma makes it a tuple, `d[1, 2]` is `d[(1, 2)]` (§4.4)
                  | '[' expr? ':' expr? ( ':' expr? )? ']'      # a slice; only a list's is written (§8.14)
rebind-method    := 'append' | 'extend' | 'insert' | 'remove' | 'sort' | 'reverse' | 'clear' | 'add' | 'discard' | 'update'
answer-method    := 'pop' | 'setdefault' | 'popitem'           # changes the collection and answers a value (§8.14)
del-stmt         := 'del' write-target subscript                # removes one element, entry or slice from the collection the target reaches (§8.14)
                  | 'del' postfix-expr subscript                # an element of what an expression answers, which no name holds (§8.14)
                  | 'del' '(' element-target ')'                # in parentheses, that element (§8.14)
print-stmt       := 'print' '(' ( print-arg ( ',' print-arg )* )? ')'
print-arg        := expr                                       # a value to render
                  | '*' expr                                   # each element of a collection, as an argument of its own (§8.15)
                  | ( 'sep' | 'end' | 'flush' ) '=' expr       # a keyword, read after the arguments
                  | 'file' '=' 'None'                          # the default, which is the line itself
                  | '**' dict-literal                          # its keys as keywords, each key a literal (§8.15)
raise-stmt       := 'raise' ( ( EXC-NAME ( '(' ( expr ( ',' expr )* ','? )? ')' )? | IDENT ) ( 'from' expr )? )?        # a catchable refuse; any other value after `raise` is read as a raise and is a parse_error at the value (§4.3)
assert-stmt      := 'assert' expr ( ',' expr )?                # Python assertion (the absence-assertion verb, §7.1, is separate)
call-stmt        := IDENT '(' arg-list? ')'
verb-call-stmt   := IDENT '(' arg-list? ')'                    # a built-in verb call (§7); the head IDENT is a verb head-word
commit-stmt      := 'commit'
rollback-stmt    := 'rollback'
import-stmt      := 'import' import-item ( ',' import-item )*   # load each module, Python Reference §9.3; `math` and `cmath` name the built-in namespaces, §8.16, `vector` one more, §8.17, and `struct` one more, §8.18
                  | 'from' IDENT 'import' import-names         # bind names a module declares
                  | 'from' IDENT 'import' '(' import-names ','? ')'
import-names     := import-name ( ',' import-name )*
import-name      := IDENT ( 'as' IDENT )?                      # bind the name, or bind it as the name after `as`
import-item      := module-name ( 'as' IDENT )?                # bind the module, or bind it as the name after `as`
module-name      := IDENT ( '.' IDENT )*
return-stmt      := 'return' expr-list?                        # early exit / value, §9.2.8

match-stmt   := 'match' expr-list ':' NEWLINE INDENT case-clause+ DEDENT   # two or more items, or one and a comma, are the tuple written without its parentheses (§3.5.11), as CPython reads a subject
case-clause  := 'case' class-label ( 'if' expr )? ':' arm-body            # §6.2
              | 'case' elem-pattern ( 'if' expr )? ':' arm-body           # structural: an alternative is a `pattern`, §6.2
              | 'case' value-pattern ( 'if' expr )? ':' arm-body          # value patterns, §6.2
              | 'case' 'default' ':' arm-body                             # never guarded, §6.2
class-label  := IDENT ( '|' IDENT )*                    # alternatives, §6.2
              | IDENT 'as' IDENT                        # `as` binds ONE class, never across `|`
value-pattern := value-alt ( '|' value-alt )* ( 'as' IDENT )?
value-alt    := case-literal | '_' | IDENT | class-pattern   # IDENT: a capture, a name that is no
                                                       #   class label, and alone; `_` last
class-pattern := IDENT ( '.' IDENT )? '(' ( elem-pattern ( ',' elem-pattern )* ','? )? ')'   # a class tested, §6.2
case-literal := '-'? ( INT | REAL ) | STRING+ | 'True' | 'False'
              | '-'? IMAG | '-'? ( INT | REAL ) ( '+' | '-' ) IMAG      # a complex, §3.5.15
arm-body     := simple-stmt NEWLINE | NEWLINE INDENT stmt+ DEDENT
pattern      := seq-pattern | map-pattern               # a SHAPE label, §6.2
seq-pattern  := '[' ( elem-pattern ( ',' elem-pattern )* ( ',' '*' IDENT )?
                    | '*' IDENT )? ']'                  # a `*` MUST be last
map-pattern  := '{' ( case-literal ':' elem-pattern ( ',' case-literal ':' elem-pattern )* )? '}'
                                                       # no key named twice, §6.2
elem-pattern := elem-alt ( '|' elem-alt )* ( 'as' IDENT )?   # the same names bound by
                                                       #   each; only the last may match every
                                                       #   value; IDENT is not `_`
elem-alt     := IDENT | '_' | case-literal | pattern | class-pattern   # one per §6.2 element row, plus
                                                       #   `'*' IDENT` above, whose position
                                                       #   is constrained, and the `|` and the
                                                       #   `as` above
for-stmt     := 'for' top-target ( ',' top-target )* ','? 'in' expr ':' NEWLINE INDENT stmt+ DEDENT   # a target list unpacks each element — the SAME list an assignment takes (§8.11)
#            (an element target among them is written each iteration; `top-target` is defined with `assign-stmt` above)
if-stmt      := 'if' expr ':' NEWLINE INDENT stmt+ DEDENT
               ( 'elif' expr ':' NEWLINE INDENT stmt+ DEDENT )*
               ( 'else' ':' NEWLINE INDENT stmt+ DEDENT )?
try-stmt     := 'try' ':' NEWLINE INDENT stmt+ DEDENT handler*
handler      := 'except' 'refuse' ( 'as' IDENT )? ':' NEWLINE INDENT stmt+ DEDENT
             | 'except' 'engine_error' ( 'as' IDENT )? ':' NEWLINE INDENT stmt+ DEDENT
branch-block := 'with' 'branch' '(' STRING ')' ':' NEWLINE INDENT stmt+ DEDENT
world-block  := 'with' 'world' '(' expr ')' ':' NEWLINE INDENT stmt+ DEDENT   # `world` is a soft keyword here

arg-list     := arg ( ',' arg )*
arg          := expr                             # positional
              | '*' expr                         # a spread: each element a positional argument of its own (§8.15)
              | IDENT '=' expr                   # a keyword argument
# several statements on a line, and a body on its header's line
def tally(a: int, b: int):
    c = a + b; d = a - b;
    if c > d: print(c); print(d)

In a call’s arg-list, a positional argument fills the next unfilled parameter left-to-right; a keyword argument name=value fills the parameter of that name. A positional argument may not follow a keyword argument or a ** argument, and a * argument may not follow a ** argument. Each is a grammar error, as it is in Python, and is answered where Python answers it (§10.1). So is a * that no expression follows, written after another argument. A parameter filled by neither uses its default (§4.2); binding the same parameter both positionally and by keyword, naming a parameter that does not exist, supplying more positional arguments than parameters, or leaving a required parameter unbound are each errors, told in Python’s words and in its order: each keyword as written, a keyword no parameter takes, too many positional arguments, a positional parameter left unbound, then a keyword-only one. A method’s count holds its receiver, and it is named by the class whose body holds its def. A positional argument written *xs is a spread: each element of xs is a positional argument of its own, in its place. Where a spread’s length is known only as the program runs, or the call has a ** argument, or no type is known for what the statement reads, those errors are found as it runs (§8.15), and a line that does not run is told nothing.

A bare assignment (assign-stmt) binds one name to an expression result; the right-hand side MUST NOT be a statement-only verb (§7.4). A call statement whose head identifier is not a verb head-word is a stored-script invocation — how a program calls compiled code, including from inside a run (MCP Reference §4.2). Resolution follows the compile-before-call rule (§6.8, §9.7). A call statement may name a void OR a value script; when it names a value script the returned value is discarded (§9.2.8). A builtin there, where no script of that name resolves, is an expression alone: it is evaluated and its value dropped, so len(xs) runs and next(e) reads an element and discards it, since reading changes the iterator.

An augmented assignment (IDENT aug-op expr) computes the plain form: x op= e is x op e, with x evaluated once, and binds it to x — except on a list or a set, below. It applies to a bare name only — a field names a source, so a field is written with remember(...) rather than an augmented operator. Because a variable keeps one type across a function (§6.9), the rewritten right-hand side must still have the variable’s type: / is true division and yields a real (§8.3), so /= applies to a real target and x /= 2 on an int is a type_error naming the type change, not a silent truncation. On a list, += and *= change the list the name holds in place to x + e or x * e (§8.4), and on a set |=, &=, -= and ^= change the set in place (§8.6), as Python’s do — so every other name holding that list or set sees the change (§8.14). On a number, a string or a tuple, which do not change, the name is bound to the new value and another name keeps the old one, as in Python.

# augmented-assign.dpy — `x op= e` is `x = x op e`, and `/=` needs a real target.
def augmented(start: int) -> int:
    total = start
    total += 10
    total -= 3
    total *= 2
    total //= 4
    total %= 7
    total **= 2
    rate = 1.0
    rate /= 8
    print("total=" + str(total) + " rate=" + str(rate))
    return total

Invoked with 5, that prints total=36 rate=0.125.

An augmented operator after what Python does not augment — anything but a name, an attribute or a subscript, in any parentheses around it whole — is a parse_error where that target begins, as in Python, naming what it is: f() += 1 is a parse_error at column 1, “‘function call’ is an illegal expression for augmented assignment”, and (a, b) += 1 one naming a ‘tuple’. A comma, a : or a ** in a lambda‘s header is no part of what its bracket holds, so (lambda x, y: k) += 1 names the ’lambda’, at column 2, and {lambda: 1} += 1 a ‘set display’. Python reads the value after the operator before it answers, so a grammar error there is answered first: f() += a if b is a parse_error at column 8, a conditional with no else. The value need only begin, as 1 + does; where none begins, as in f() +=, the answer is at the operator, at column 5. A lambda begins one only where Python reads its header, so f() += lambda x=1 y: 1 is answered at the operator too. True, False and None are refused as the reserved words they are.

A return statement (return-stmt) ends the enclosing script. A bare return (no expression) exits early; it is valid in any script, void or value. Outside a script return is refused, in Python’s words (§10.1). A return <expr> yields the expression’s value and is valid only in a value script (§4.2): the expression’s type MUST match the declared return type or fill it (§6.4) — a bool satisfies a -> int return and either satisfies a -> real one. A value script that does not return on every path is a type_error (§6.9). The return control-flow semantics are specified in §9.2.8.

The subject of a match statement (match-stmt) is an expr-list: two or more items, or one item followed by a comma, are the tuple written without its parentheses (§3.5.11), so match d, e: is match (d, e): and a * item spreads into the tuple as it does in a parenthesized one. A * item alone is no tuple and no value, and is a parse_error at the : after it, as it is in CPython.

Each case arm of a match may be an inline statement or an indented suite; over a type with a case set the arms must be exhaustive (§6.2). A case label is one discriminator, or two or more separated by | — an alternation, matching if the scrutinee’s discriminator is any one of them (§6.2) — and may be followed by if <expr>, a guard the arm must also satisfy to be taken (§6.2). The optional as IDENT on a case label binds the matched content and is well-typed only on a value-match arm (§5.6, §6.2), and only where that arm names a single class; elsewhere it is a type_error. The try handler clauses are optional and may appear in any order and any number, and where more than one handles the same category the first of them runs (§10). The as <name> binding on a handler is itself optional — except refuse: fires the handler without naming the caught exception (Python’s except ValueError:); omit it when the handler body does not use the refuse/engine_error info. commit and rollback are bare statements resolving to the enclosing branch block (§9.2.6). An import statement (import <module>) loads a stored module by name (Python Reference §9.3); import is a contextual keyword, recognised only as the statement head immediately before a module name (like the aggregate fold names, §7.1.1), and an ordinary identifier elsewhere. An import naming a module the store does not hold fails when control reaches it, with no module named <module>. A program that uses such a module, or has a type_error below a top-level import of it, is refused when it is compiled, at that import, in the same words — as CPython stops at the import before reaching either (Python Reference §9.3).

from <module> import <name>, … loads the module as import <module> does and binds each named script or class, so a later area(2, 3) means geo.area(2, 3) and compiles to the same program. from is contextual too. A name the module does not declare is refused at the import, used or not, as CPython refuses it. Like import, it runs the module’s body each time control reaches it (Python Reference §9.3), where CPython runs a module once. Only the names are bound: <module>.<name> needs an import <module> of its own, as in CPython. A bound name holds one thing, so assigning to it, taking it as a parameter or a for target, declaring a script or class under it, or importing it from another module with from is refused; Python would rebind it. Naming it in an annotation, as in item: Site = b, uses it and binds nothing. A diagnostic names a bound script or class as the unit binds it, Site rather than geo.Site, so a form it offers compiles as written. import a, b loads each module in turn. as binds another name, so after import geo as g a use of g means geo, and after from geo import area as ar a use of ar means geo.area, each exactly as if the module’s name had been written; geo itself is not bound by either, as in Python. A name an import bound is refused if it is bound again to something else, since every later use already reads as the first. A wildcard (from m import *), a dotted module and a relative import are refused, each naming the form to write.

from __future__ import <feature>, … loads nothing and binds nothing. Python’s features are all accepted and change nothing here. It stands where Python requires it, at the top of a unit, with only a docstring and other such imports above it. Anywhere else, and for a feature Python does not know, it is refused in Python’s words (§10.1).

A print statement (print-stmt) writes one output line. It renders each positional argument to its text form and joins them with sep (default a single space " "), then appends end (default a newline) — so print(a, b) writes one line <a> <b> and print() writes an empty line. A positional argument written with * spreads a collection: each of its elements is an argument of its own, so print(*xs) writes the elements of xs joined by sep (§8.15). sep and end are optional keyword arguments, each any expression that gives a string, and are read after the positional arguments, in the order written. None written as the whole of either is its default. One that gives anything else is a catchable refuse (§10) when the line is composed, “sep must be None or a string, not int”. flush is read in its turn, may hold anything, and changes nothing. file may be given None alone, which is its default, the line itself; given anything else it is a parse_error. A keyword print does not take is a type_error in Python’s words, the first one written, 'x' is an invalid keyword argument for print(), followed by the keywords it takes. A ** before a dict display gives these keywords as its keys (§8.15): print("a", "b", **{"sep": "-"}) is print("a", "b", sep="-"). A ** before any other dict, such as a name that holds one, gives them as the line is composed, and a name written beside it that print does not take is refused then, once its keys are merged (§8.15). Because output is line-oriented (each print writes exactly one line, as print does), a trailing newline in end is the line itself; end="" therefore reads the same as the default (there is no line continuation). print renders each value exactly as print and the str() conversion do — in particular a boolean renders as True / False. print is a contextual keyword, recognised only as a statement head immediately before (, and an ordinary identifier elsewhere.

A pass statement is a no-op: it has no effect and produces no output. It exists so a block that would otherwise be empty stays well-formed (e.g. an else branch that does nothing). pass is reserved (§3.7), as it is in Python: it is a statement and names nothing, so x = pass is a parse_error at column 5.

A raise statement (raise-stmt) raises a recoverable error — a refuse (§10) — that an enclosing try catches with except refuse as r:. It names one of the built-in exception names (ValueError, KeyError, TypeError, RuntimeError, and the like); that name is recorded on the caught handler’s r.discriminator, and its argument (rendered to its text form) on r.reason. Several arguments are held as CPython holds them, and the reason is the text of their tuple: raise ValueError("a", 1) gives ('a', 1). A bare raise ValueError gives an empty reason. Every exception name behaves identically — there are no custom exception types and no exception hierarchies. A raise never falls through, so a branch that ends in one counts as returning for the exhaustive-return rule of a value script (§4.2). raise is reserved (§3.7), as it is in Python: a statement head before any value.

raise alone raises again the exception being handled: in an except body, in a finally body while one is pending, and in a script either of them calls. It keeps its category, its r.reason and its r.discriminator. With nothing being handled it is a RuntimeError whose reason is No active exception to reraise, as the program runs, as in Python. A handler that ends restores the one around it.

raise e raises what the name e holds, which is an exception an except caught: the name it bound, or another name bound to that. A name holding anything else, or nothing, is a type_error teaching the exception names at the name.

from gives a cause, which is None or an exception an except caught. Any other value is a type_error in Python’s words, exception causes must derive from BaseException. Nothing here reads a cause back, so a cause changes nothing a program can observe; it is evaluated after the message, and one that fails fails in the raise’s place. An exception name is no value in this language (§10), so a cause written ValueError("a") is the same refusal it is anywhere else.

A value that is not a built-in exception name or a name alone, as raise 1 or raise (e), is a parse_error at the value, teaching the exception names, and a grammar error inside or after the value is answered where CPython answers it, as raise not c d is at column 13. It names nothing, so where it is assigned, read or written after a ., the parse_error is where Python answers it: raise = 1 is one at column 7, and x.raise = 1 one at column 3.

An assert statement (assert-stmt) is Python’s assertion. assert cond evaluates cond (which MUST be a bool) and, when it is false, raises an AssertionError — equivalent to if not cond: raise AssertionError, a recoverable refuse (§10) an enclosing try catches with except refuse as r:. The optional second operand is the message: assert cond, msg records msg (rendered to its text form) on r.reason, exactly as raise AssertionError(msg) would, and is evaluated only when the assertion fails; a bare assert cond gives an empty reason. A passing assertion produces no output and execution continues. This is the statement form of assert, and a condition may begin with a bracket: assert (x), assert (x), "m" and assert (x) == 1 are all this statement. The same word heads the absence-assertion verb (§7.1) in its call form assert(path, source), which records an explicit-absent claim. The verb is read where the bracket ends the statement and holds a first argument ending in a dotted name, and then a comma, and where it ends the statement holding one dotted path whose first name the program binds nowhere. So assert (x.y, "m"), which Python reads as a tuple and never fails, is the verb here.

A rebinding statement (rebind-stmt) and a del statement (del-stmt) change the list, set or dict a name holds, or one it reaches through subscripts and method calls, in place, so every name holding it sees the change: xs.append(v), d[k] = v, m[i][j] = v, xs[i:j] = ys, del d[k]. Each answers nothing and is a statement of its own, never an expression. The exceptions are pop, setdefault and popitem (answer-method), which change the collection and answer a value: a call of one is an expression, and as a statement its answer is discarded. Their semantics, typing and refusals are §8.14. del is a reserved word (§3.7), and heads a statement immediately before a name, or before the element or slice of a value no name holds that it removes, as in del (xs)[0] or del [xs][0][0] (§8.14). Parentheses around a name or an element at the head of a statement are that name or element, as in Python: (xs).append(v), (c).hi() and (f)() are the statements without them.

4.4 Expressions

expr         := cond-expr
cond-expr    := or-expr ( 'if' or-expr 'else' expr )?   # ternary; right-assoc; lower than `or`
or-expr      := and-expr ( 'or' and-expr )*
and-expr     := cmp-expr ( 'and' cmp-expr )*
cmp-expr     := bitor-expr ( cmp-op bitor-expr )*
cmp-op       := '==' | '!=' | '<' | '>' | '<=' | '>=' | 'in' | 'not' 'in' | 'is' | 'is' 'not'
bitor-expr   := bitxor-expr ( '|' bitxor-expr )*    # set union, §8.6
bitxor-expr  := bitand-expr ( '^' bitand-expr )*    # set symmetric difference
bitand-expr  := add-expr ( '&' add-expr )*          # set intersection
add-expr     := mul-expr ( ( '+' | '-' ) mul-expr )*
mul-expr     := unary-expr ( ( '*' | '/' | '//' | '%' | '@' ) unary-expr )*
unary-expr   := ( 'not' | '-' | '+' ) unary-expr
              | pow-expr
pow-expr     := postfix-expr ( '**' unary-expr )?   # right-assoc; tighter than unary `-`
postfix-expr := primary-expr postfix*
postfix      := '.' IDENT '(' arg-list? ')'   # method call (§13.4; string methods §5.3)
              | '.' IDENT                      # field access
              | '[' display-item ( ',' display-item )* ','? ']'   # indexing (string / collection, §5.3); a comma or a `*` makes the index a tuple (§8.15)
              | '[' expr? ':' expr? ( ':' expr? )? ']'   # slice (string / list / tuple, §5.3)
primary-expr := STRING | INT | 'True' | 'False'
              | fstring-literal                # formatted string literal (§3.5.10)
              | list-display                   # list literal / comprehension (§3.5.9, §8.9)
              | brace-display                  # dict/set literal or comprehension (§3.5.12, §3.5.13, §8.9)
              | tuple-display                  # tuple literal / grouping (§3.5.11)
              | builtin-call                   # zip / enumerate (§8.6), abs / round (§8.3)
              | namespace-member               # math / cmath (§8.16), vector (§8.17), struct (§8.18)
              | IDENT '(' expr ')'             # instance constructor (§13.2)
              | module-name '.' IDENT '(' expr ')'   # module-qualified construction (Python Reference §9.6)
              | value-call                     # value-script invocation (§6.8)
              | hypothesis                     # suppose (§15)
              | absence-hypothesis             # suppose_absent (§15.1)
              | IDENT
              | path-expr
              | IDENT '(' arg-list? ')'         # a built-in read verb call, §7
value-call   := IDENT '(' arg-list? ')'        # a value script's call, in expression position
list-display := '[' ( display-item ( ',' display-item )* ','? )? ']'          # list literal (§3.5.9); a spread, §8.15
              | '[' expr ( 'for' IDENT 'in' expr ( 'if' expr )* )+ ']'   # list comprehension (§8.9)
brace-display := '{' ( dict-item ( ',' dict-item )* ','? )? '}'      # dict literal (§3.5.12)
              | '{' display-item ( ',' display-item )* ','? '}'              # set literal (§3.5.13)
              | '{' expr ( 'for' IDENT 'in' expr ( 'if' expr )* )+ '}'           # set comprehension (§8.9)
              | '{' expr ':' expr ( 'for' IDENT 'in' expr ( 'if' expr )* )+ '}'  # dict comprehension (§8.9)
tuple-display := '(' expr ')'                  # grouping (no comma)
              | '(' ( display-item ( ',' display-item )* ','? )? ')'   # tuple literal (§3.5.11); a `*` item alone needs its comma
builtin-call := ( 'zip' | 'enumerate' | 'reversed' | 'range' | 'abs' | 'round' | 'divmod' | 'str' | 'int' | 'float' | 'list' | 'set' | 'dict' | 'isinstance' | 'issubclass' ) '(' ( ns-arg ( ',' ns-arg )* )? ')'   # zip/enumerate/reversed/range §8.6, abs/round/divmod §8.3, conversions §8.13, isinstance §13.9, issubclass §13.10; a spread §8.15
namespace-member := ( 'math' | 'cmath' | 'vector' | 'struct' ) '.' IDENT ( '(' ( ns-arg ( ',' ns-arg )* ( ',' IDENT '=' expr )* )? ')' )?   # §8.16–§8.18; a constant takes no call, a function takes one
ns-arg           := expr | '*' expr                           # a value, or a spread of its elements (§8.15)
path-expr    := IDENT ( '.' IDENT )+           # K.s.a slot-grammar dotted path

Prefix + takes what prefix - takes and changes no number, and any other operand is a type_error; a bool gives the int it is, so +True is 1. Prefix - (unary minus) negates an int, a real or a duration, and any other operand is a type_error. Negating an int has the runtime semantics of 0 - a (§8.3), so negating the most-negative integer overflows and refuses rather than wrapping; negating a real keeps the sign of zero, so -a is -0.0 when a is 0.0, as in Python. The boundary between path-expr (a multi-segment dotted reference to a store cell) and postfix-expr with .field chains (field access on a value) is resolved by the typechecker (§6) using the type of the head identifier. The comparison operators chain (§4.6): a < b < c is (a < b) and (b < c) — each adjacent pair compared, the results conjoined, with b evaluated only once.

A value-call f(args) is a value script’s invocation (§4.2) used as an expression: its value is what the callee returns, and its type is the callee’s declared return type (§6.8). It composes like any expression — assigned (y = f(x)), returned (return f(x)), passed as an argument (print(f(x)), g(f(x))), or combined with operators (f(a) + g(b)); nested value-calls evaluate inner-first, left-to-right (§9.1). The head identifier disambiguates IDENT(...) between the three call-shaped primaries: a class name is an instance constructor (§13.2), a built-in name the program has not bound is that built-in (zip/enumerate/len/ abs/str/int/float/list/set/dict/a fold/a verb), and any other identifier bound to a value script is a value-call. A name the program has bound comes first, as Python’s lookup finds it first. A def of the unit is what a call of its name finds in any body and in any statement below it, whatever built-in shares the name, print included, while a statement above it still reaches the built-in. A name holding a value is not called, whatever else carries the name: max = 3; max(1, 2) is a type_error at the call, 'int' object is not callable, as x = 3; x(1) is and as get = 3; get(1) is, and Python raises that TypeError as the program runs. A body that names a built-in which the statements above its def have bound to a value is refused too, since no body reads a name from outside it (§4.2), and so is one that names a built-in the statements below it bind. A class holds its name ahead of a built-in of that name in every body of the unit, and from its own line at the top of the unit, so a statement above it still reaches the built-in. A method’s name is its class’s and takes nothing from the built-in. A name a body binds anywhere is that body’s from its first line. An assignment binds one, and so do a for, a capture in a case, an import and an as. A call or a read of it above the binding is a type_error, cannot access local variable 'max' where it is not associated with a value, where Python raises that UnboundLocalError as the program runs. A comprehension’s variable takes the name through the comprehension but for its first source, which is read outside it, and holds it from there on: a later source or an if that reads a name a later clause binds is that type_error too, whatever an outer scope holds under the name. Read from inside a comprehension, a body’s name or an outer comprehension’s is a free variable, and the words are cannot access free variable 'q' where it is not associated with a value in enclosing scope, where Python raises that NameError. A module is no callable: a call of one is a type_error, 'module' object is not callable, where Python raises that TypeError as the program runs. An except’s name holds through its handler, and at the top of a unit the built-in is found again past it, since Python deletes the name there. __debug__ reads as True, as it does in Python, and nothing may bind it or delete it. A call to a void script (§4.2) in expression position is a type_error; call it as a statement (§4.3) instead.

A subscript that holds a comma indexes by a tuple, as Python reads it: d[1, 2] is d[(1, 2)], and d[1,] is d[(1,)]. That holds for a write and a del through one too (§8.14). A slice beside another index, as in xs[1:2, 0], is a parse_error at the slice’s :, since no value is indexed by a tuple that holds a slice. Every part of the subscript is read first, so a grammar error anywhere in it is the answer instead.

4.5 Verbs

The built-in verb surface (§7) is written as function calls — a verb name and a parenthesized, comma-separated argument list: remember(K.s.a, v, src), current(K.s.a), list_attributes(K). The first argument is the dotted K.s.a slot path (or a name literal or a claim, per the verb). Read verbs produce a value and stand in expression position as the whole right side of an assignment or the whole value of a return (§7.1.1), not inside a larger expression; write verbs have no value and stand only as statements.

Representative forms, as they read in a program:

remember(K.s.color, "orange", "op1")          # write: (path, value, source)
update(K.s.color, "black", "op1")
forget(K.s.color)                             # path-scoped
assert(K.s.nickname, "op1")                   # absence assertion: (path, source)

cur   = current(K.s.color)                    # reads
hist  = get(K.s.color)
proof = why(K.s.color)

attrs = list_attributes(K)                    # K or K.s
subs  = list_subjects(K.color)
vals  = list_values(K.color)
hits  = subjects(K.color, "orange")

cur     = current(K.s.color)                  # the current claim at this cell
deps    = dependents(cur)                     # claims derived from that claim
prem    = premises(cur)                       # claims that one was concluded from

v = verify(K.s.color, "orange")               # optional: verify(K.s.a, v, src)
n = agreement(K.s.color, "orange")            # how many sources recorded "orange"

See §7 for each verb’s slot shape and result type. A path whose segment count does not match the verb’s slot shape is a runtime refuse (§7.2).

4.6 Operator precedence and associativity

The conditional expression a if c else b (§8.10) binds lower than every operator in the table below — its then and else operands are full expressions, so x + 1 if c else y * 2 groups as (x + 1) if c else (y * 2), and it is right-associative (a if p else b if q else c is a if p else (b if q else c)).

Below the conditional expression, lowest to highest precedence; binary operators are left-associative except **, which is right-associative (2 ** 3 ** 2 is 2 ** (3 ** 2)).

Level Operators
1 or
2 and
3 not (boolean, prefix)
4 == != < > <= >= in not in is is not
5 \| (bitwise or, set union)
6 ^ (bitwise exclusive or, set symmetric difference)
7 & (bitwise and, set intersection)
8 << >> (shifts)
9 + - (binary)
10 * / // % @
11 - ~ (prefix, arithmetic and bitwise)
12 ** (power, right-associative)
13 . (field / method) , [] (index) , instance / verb call

Boolean not binds looser than comparison (Python), so not a == b is not (a == b); it is distinct from the arithmetic prefix -, which binds tightly (level 11). The bitwise operators and the shifts sit between comparison and + / -, in Python’s order, so c <= a | b compares c with the union, a | b & c is a | (b & c), a - b | c is (a - b) | c, and 1 + 2 << 3 is 24. ** binds more tightly than the prefix - and ~, so -2 ** 2 is -(2 ** 2) = -4; its right operand is a unary expression, so 2 ** -1 is well-formed.

Comparison operators chain (Python semantics): a op1 b op2 c … is (a op1 b) and (b op2 c) and … — each adjacent pair is compared under §8.1 and the results conjoined with and. An interior operand is compared in both of its pairs; because DKE operands are side-effect-free (a store read is bound to a name first, §9.1) the result is well-defined. A single comparison (a < b) is unchanged.


5. Type system

DKE Python has a closed vocabulary of built-in types. The one way a program adds a type is a class (§13.1): a class name is a type, and an instance of a subclass is accepted wherever its base is named (§13.8). There are no user-defined generics; the built-in parametric types are the constructed collections list<T>, tuple<…>, dict<K, V> and set<T> (§3.5.9, §3.5.11–§3.5.13, below). Programs compose by operating on the store through verb calls.

5.1 The base types

Type Origin Role
string user data; identifier-typed slots; values primitive
int counts (.length), positions, timestamps primitive
bool comparison + logical results primitive
value the stored content of a claim (active_claim.value, §5.6); result of suppose(…) (§15) tagged union
datetime a UTC instant, written datetime("…Z") (§3.5.5); the datetime class of value (§5.6) value class
duration a length of time, written duration("…") (§3.5.6); the duration class of value (§5.6) value class
real a fractional number, written 3.5 (§3.5.7); the real class of value (§5.6) value class
blob an opaque byte string, written blob("…") or blob_b64("…") (§3.5.8); the blob class of value (§5.6) value class
scientific a number carrying its significant figures, written scientific("…") (§3.5.14); the scientific class of value (§5.6) value class
complex a number with a real part and an imaginary part, written 3 + 4j (§3.5.15); the complex class of value (§5.6) value class
slice the result of slice(stop) or slice(start, stop[, step]): three bounds that select from a sequence as xs[a:b:c] does (§5.3) sequence selector
range the result of range(stop) or range(start, stop[, step]): the ints from start up to stop, step apart, held as its three bounds and never built (§8.6) integer sequence
active_claim result of current(K.s.a) and suppose_absent(…) (§15.1) (single live claim) discriminator
claim_list result of get(K.s.a), list_values(K.a), caveats(K.s), conflicts(), dependents(<claim>), premises(<claim>) finite collection
proof_tree result of why(K.s.a) finite collection (also .root, .truncated)
diagnosis result of why_not(K.s.a) and what_needs(K.s.a) finite collection
subject_set result of subjects(K.a, v) and list_subjects(K) / list_subjects(K.a) finite collection
string_list result of list_categories(), list_attributes(K), list_pinned(), list_scripts() finite collection
verify_result result of verify(K.s.a, v) discriminator
result_type_set result of list_result_types() finite collection
refuse_info the binding of except refuse as e (§10) handler binding
engine_error_info the binding of except engine_error as e (§10) handler binding
empty sentinel case of an active_claim match discriminator

All six finite collection types may appear as the iterand of a for … in, which also admits a constructed list / set / dict, a range, a string, and a tuple whose elements all have one type (§6.3). Four of them (subject_set, string_list, result_type_set, diagnosis) yield string elements; claim_list and proof_tree yield active_claim elements. A diagnosis is the explanation of an absence: printing it shows the whole answer, and iterating it takes that answer a line at a time. It carries no claims, because the question it answers is about a claim that is not there. A proof_tree is the collection of claims that participated in a why derivation. The proof_tree a why yields is the derivation alone, the conclusion and then the claims it rests on; the earlier, superseded values of the cell are no longer elements of it, and remain readable through get. Iterating it (or reading .length / [i], §5.3) yields those claims, each an active_claim carrying the same fields a claim_list element does (.value, .source, .trust, .created_at); a proof_tree additionally offers a .root accessor naming the queried claim and a .truncated accessor saying whether the derivation shown is the whole one (§5.2). match is meaningful over the discriminator types (active_claim with its empty case, and verify_result) and over the value tagged union, which a match destructures by the class of the stored content (§5.6, §6.2).

Listing order is part of this contract. Each of the six has a defined order. claim_list has two, because a read within one cell and a read spanning several answer different questions:

type order
subject_set ascending name order
string_list ascending name order
claim_list spanning more than one cell — list_values, conflicts, caveats, dependents, premises ascending name order
claim_list from one cell — get(K.s.a) the claim in force first, then the superseded ones in ascending created_at
proof_tree structural: the conclusion, then the claims it rests on
diagnosis structural: the explanation’s own line order
result_type_set ascending name order

Ascending name order is the order sorted() gives (§8.6) — codepoint order, not locale collation — the same deterministic sorted order a constructed set<T> is presented in, and, like that one, a defined refinement of Python rather than a promise Python makes. So xs == sorted(list(xs)) holds for every row that says ascending, and sorting such a read again is dead code.

This is stronger than the stability §9.6 already guarantees: it fixes not only that the order does not vary between runs but what the order is. The two structural rows are as binding as the sorted ones — a proof_tree leads with its conclusion, and a diagnosis reads top to bottom as written.

result_type_set sorts by type name, in the ascending name order above.

list_result_types() has a published membership — these nine, in this order:

active_claim, claim_list, diagnosis, empty, proof_tree,
result_type_set, string_list, subject_set, verify_result

They are the result types a program can hold. Eight of them are also type names you may write in an annotation; empty is the ninth member and is not one — it is the sentinel case of an active_claim (below), and it is listed because a program can hold it, which is the question this verb answers.

These names are canonical. They are the same strings .kind yields and the same case labels a match compares against.

empty is never produced standalone; it is the second case of an active_claim discriminator. The typechecker recognises empty as a valid case label on an active_claim scrutinee, not as a standalone expression.

value (§5.6) is not a user-composed type — it is the single built-in union, carrying whichever primitive a claim’s content happens to be. Classes (§13) remain the only user-defined types, and a class’s bases the only subtyping.

Constructed lists (list<T>). Separately from the store-derived finite collections above, a program may build a finite list with a list literal (§3.5.9) or a comprehension (§8.9): [1, 2, 3] is a list<int>, ["a", "b"] a list<string>, [[1], [2]] a list<list<int>>. list<T> is the one element-parametric type — T is the element type, itself any expressible type including another list. A literal whose elements share a type is a list<T>; a literal mixing scalar value classes is a list<value> (holding numbers when they are all ints and reals, §5.6); the empty list is list<unknown> until context fixes T. An empty list<unknown> is compatible with any list<T>: wherever two list types must agree — the two branches of a conditional expression ([1, 2] if c else []), a variable’s re-binding (xs = [] then xs = [1]), an annotated binding (xs: list<int> = []), the sibling elements of a nested list literal ([[1, 2], []] is list<list<int>>), a list-typed method argument (",".join([])), and list equality ([] == xs) — an empty list takes the concrete side’s element type. The empty set (set([]), a set<unknown>) and the empty dict ({}, a dict<unknown, unknown>) are compatible the same way with any set and any dict, at the same sites: seen = set([]) then seen |= {"a"} binds seen as a set<string>, and d = {} then d = {"a": 1} binds d as a dict<string, int>. An annotation supplies an unknown element type at any depth, and for the empty dict too: ys: list<list<int>> = [[]] and d: dict<string, int> = {} bind the annotated type. A constructed list shares the read surface of the finite collections — indexing xs[i] (including a negative from-end index), slicing xs[a:b:c], .length, for … in, and membership x in xs (§5.3) — and additionally supports equality (§8.1), concatenation with another list (xs + ys, §8.4), repetition by an int count (xs * n, §8.3), renders in print and + (§8.4, §8.7), and is produced by a comprehension (§8.9) and by s.split(...) (§8.6). Unlike the store-derived collections it exists only while the program runs and is never stored; it is shared and changed in place (§8.14). list<T> is not one of the base types — it is a type former over them.

Constructed tuples, dicts, and sets. Three further type formers build finite values alongside list<T> — a tuple immutable, a dict and a set shared and changed in place as a list is (§8.14):

  • tuple<T1, …, TN> — a fixed-length heterogeneous sequence built by a tuple literal (§3.5.11): (1, "a") is a tuple<int, string>. Each position keeps its own type, which a constant index reads; a tuple is sliced by constant bounds (t[1:] → a sub-tuple, §5.3), repeated by a literal int count into the repeated position list ((1, 2) * 2, §8.3), and compared with ==/!= or ordered with </>/<=/>= against a tuple of any length (§8.1). When every position holds the same type it also iterates (§6.3), converts (list/set/tuple, §8.13) and is indexed and sliced at a computed position, as a list of its elements is. So does a tuple whose positions are all scalars, whose element is a value (§5.6), and one whose positions are all class instances, whose element is their nearest common base (§13.8). A tuple holding a collection beside a value of another type does none of these, since there is no single element type to bind or collect.
  • tuple<T, ...> — a variadic homogeneous tuple: a tuple of runtime length whose every element is T, built by tuple(iterable) (§8.13). Because its length is not static it is indexed by a runtime int (t[i] → T) and unpacks as a list does, its length checked when the statement runs (§8.11); it is repeated by any int count, literal or not, since it has no arity in its type to name (§8.3); it compares (==) against any tuple whose elements agree with T. It is homogeneous by construction, so it always iterates (§6.3). A fixed tuple whose positions all agree with T is one, a tuple being immutable, so it fills a declared tuple<T, ...> wherever one is written: as a value, inside a collection literal written at that declaration (ts: list<tuple<int, ...>> = [(1, 2)]), by an element write (ds[k] = (1, 2)), and by append / insert / extend. A collection a name already holds is not widened into a second name: a list is shared (§8.14), so its element type stays the one that name has. Fixed tuples of different lengths whose elements all share a type T are held together as tuple<T, ...>, as a type checker joins them: the display [(1, 2), (3, 4, 5)] is a list<tuple<int, ...>>, so it fills that annotation as each of its tuples does on its own. Tuples of one length keep their fixed type, and tuples whose elements share no type are not held together.
  • dict<K, V> — a mapping from a hashable key type K (a scalar, or a tuple whose positions are recursively hashable) to a value type V, built by a dict literal (§3.5.12) or a dict comprehension (§8.9), kept in insertion order.
  • set<T> — a collection of unique hashable elements (a scalar, or a tuple whose positions are recursively hashable), built by a set literal (§3.5.13) or a set comprehension (§8.9), presented in sorted order.

All three, like list<T>, exist only while the program runs and are never stored, and none is one of the base types. A key or set element is hashable: a scalar value class (every class a cell value has, §5.5), a value, or a tuple whose positions are recursively hashable. A list, dict or set is unhashable and is not itself a key or set element. Writing one where a key or an element is held is told in Python’s words, unhashable type: 'list', and a tuple is asked of what it holds. Looking one up is told the same as the program runs, whatever the dict or the set holds, since Python hashes first: [1] in d, d.get([1], 0), d.pop([1], 0), d[[1]], del d[[1]], s.discard([1]), s.remove([1]). A set asked after in a set is read as the frozen set it spells, so {1} in {1} is False. A range is hashed by the integers it denotes, as CPython’s is, so it may be a key or an element.

Iterators. iterator<T> is one further type former: a one-pass reader over a collection, returned by zip, enumerate, reversed and iter (§8.6), which states what reads one. It gives each element once, has no length and no position, and is equal only to itself. Like the three above it exists only while the program runs, and is not a base type. It is not a key or set element either, where CPython hashes an iterator by its identity. A key or set element here is held by its value, and an iterator has none to hold it by, since it is equal only to itself: a set is walked in sorted order (§3.5.13), where an iterator has no place, and CPython prints a set or a dict of iterators with each one’s address, which no other run repeats.

Dict views. dict_keys<K>, dict_values<V> and dict_items<K, V> are the views a dict<K, V> answers from .keys(), .values() and .items() (§8.6), as CPython’s are. A view reads its dict when it is read, not when it was made: after ks = d.keys() and d["b"] = 2, ks holds "b". It walks the dict’s keys, its values or its tuple<K, V> pairs in insertion order — for for (§6.3), a comprehension, a conversion (§8.13) and every builtin that reads a collection — has a length (len, .length) and a truth value (§8.2), and tests membership (§8.1). It has no position: d.keys()[0] is a type_error, and list(d.keys())[0] returns the first key. A keys view and an items view are set-like: each equals a set holding the same elements, takes the set operators beside any collection, on either side, and takes the inclusion order beside a set or another such view (§8.1, §8.3). A values view is equal only to itself. A view prints as CPython’s does, dict_keys(['a', 'b']). Like an iterator it exists only while the program runs, and is not a base type, and it is not a key or set element, where CPython hashes a values view by its identity, for the reason an iterator is not one.

The base types do not include an absence-of-value type. Absence is asserted through the assert verb (§7.1), which carries no value argument — so the absence semantic stays implicit in the verb head-word rather than being represented as a typed value. Since a name holds one type (§6.1), no name can hold “a value or nothing” either, which is why DKE Python has no None (§3.5.4).

5.2 Field access

The expression <value>.<field> is well-typed when the table below has a row for the value’s type and field; otherwise it is a type_error. <value> is any expression (a postfix in the grammar, §4.4), so a field accessor composes on a computed value — a method result, an index, or a slice — not only on a bare name: text.upper().length, xs[0].value, and history[-1].source are all well-typed reads. (A store read remains bound to a name first, §9.1: a verb call is not itself a value expression, so current(K.s.a).value is written as two lines — c = current(K.s.a) then c.value.)

Type Field Field type
active_claim value value (§5.6)
active_claim source string
active_claim path string
active_claim claim_id string
active_claim created_at string
active_claim trust string
active_claim preferred bool
active_claim maintained bool
active_claim written_by string
active_claim derived_by string
active_claim rule_kind string
active_claim valid_from value (§5.6)
active_claim valid_to value (§5.6)
active_claim qualifiers dict<string, value> (§3.5.12)
proof_tree root active_claim
proof_tree truncated bool
verify_result match bool
verify_result actual string
verify_result expected string
verify_result conflicted bool
refuse_info reason string
refuse_info teaching_hint string
refuse_info discriminator string
engine_error_info reason string
engine_error_info code string
complex real real
complex imag real
int real int
int imag int
int numerator int
int denominator int
bool real int
bool imag int
bool numerator int
bool denominator int
real real real
real imag real
slice start value (§5.6)
slice stop value (§5.6)
slice step value (§5.6)
range start int
range stop int
range step int

Every number has the two parts CPython gives it. A number is its own real part, a bool read as the int it is, so True.real is 1; its imaginary part is a zero of its class, 0 for an int or a bool and 0.0 for a real. A claim’s value (§5.6) has neither until it is narrowed: it may hold a string, and what takes only a number takes a value only once a match or a test has said it holds one.

Two types in the table are ones a program cannot write: refuse_info and engine_error_info arise only as the binding of a try handler (§5.4), and are listed here under the names .kind reports for them. Field access on them is governed by this table like any other type.

claim_id names the claim: its cell path followed by # and a code of twelve lowercase hexadecimal digits (Cat.felix.color#3f09c1a27b44). The code is made from what the claim shows — its value and its type, where it came from (source, or the rule that concluded it), its validity window, its qualifiers and its created_at — so a claim carries the same claim_id on every read that returns it, and writing, revising or removing another claim leaves it unchanged. A claim recorded anew has a claim_id of its own: the new claim of a revision (the claim it supersedes keeps its id), a conclusion a rule draws again, and a value in force the restore tool brings back — an older version it puts back into the history keeps its claim_id, since it keeps its created_at. Two claims at one cell that agree on all of those fields share a claim_id. dependents, premises, forget and would_forget (§7.1) take a claim_id in place of the claim it names.

written_by names the store key that wrote the claim. It is derived from the credential the call was authenticated with — never from anything the program or the caller states — so unlike source it cannot be asserted, only observed. A store key identifier is never empty, so a claim with a writer is always distinguishable from a claim without one.

written_by is empty when no key wrote the claim, which happens two different ways: the write was not authenticated, or nothing wrote the claim at all because a rule concluded it (§14). trust is what tells those two apart, and the distinction matters — a derived claim has no writer because there was no write, not because a write went unattributed.

valid_from and valid_to report the validity window the claim was written with (§7.1) — the stretch of world-time the claim says something about, as distinct from when it was recorded. A conclusion reports the window it inherits from its premises (§14.4): the overlap of the windows of every premise it was drawn from.

Both are typed value rather than datetime because a bound may be open. A claim valid from a date with no stated end has a valid_to of the null class (§5.6), and a claim written with no window at all has null for both. So a program reads these fields by matching, not by comparing: there is no sentinel instant standing for “no bound”, and a program that compared against one would be reading a date the store does not hold. Only the datetime and null classes ever arise here, but a match still needs a case default: arm, since exhaustiveness (§6.2) is checked against the whole union.

# claim-window.dpy — a claim reports the window it is valid in, and a
# conclusion reports the overlap of the windows it was drawn from.
@rule
def flagged():
    for e in Emp:
        if e.status == "active" and e.dept == "eng":
            e.flagged = True

def show(label: string, c: active_claim):
    match c.valid_to:
        case datetime as d: print(label + " until " + str(d))
        case null:          print(label + " has no end")
        case default:       pass

def window_report():
    remember(Emp.e1.status, "active", "hr",
             datetime("2020-01-01T00:00:00Z"), datetime("2022-12-31T00:00:00Z"))
    remember(Emp.e1.dept, "eng", "hr",
             datetime("2021-01-01T00:00:00Z"), datetime("2023-12-31T00:00:00Z"))
    remember(Emp.e2.ongoing, "yes", "hr", datetime("2021-01-01T00:00:00Z"), null)
    a = current(Emp.e1.dept, datetime("2021-06-01T00:00:00Z"))
    show("dept", a)
    d = current(Emp.e1.flagged, datetime("2021-06-01T00:00:00Z"))
    show("flagged", d)
    o = current(Emp.e2.ongoing)
    show("ongoing", o)
OK script window_report(): 3 claims written, 3 prints
  remember Emp.e1.status = "active" valid_from=datetime("2020-01-01T00:00:00Z") valid_to=datetime("2022-12-31T00:00:00Z")
  remember Emp.e1.dept = "eng" valid_from=datetime("2021-01-01T00:00:00Z") valid_to=datetime("2023-12-31T00:00:00Z")
  remember Emp.e2.ongoing = "yes" valid_from=datetime("2021-01-01T00:00:00Z")
  print dept until 2023-12-31T00:00:00Z
  print flagged until 2022-12-31T00:00:00Z
  print ongoing has no end

dept is valid until 2023-12-31 and the conclusion drawn from it only until 2022-12-31, because the other premise stops there. That difference is the whole of what these two fields add: an as-of read (§7.1) tells you whether a claim is in force at an instant you name, one instant at a time, and these say where the window ends without your having to find it by asking.

qualifiers reports the facts the writer stated about this claim when it wrote it — how the value was obtained, how sure the writer is of it, what it was measured with: whatever the writer chose to say, as a dict from name to value. They are written with the qualifiers= argument of remember and update (§7.1), and they belong to the one claim they were written with. Another source’s claim at the same attribute carries its own, and a claim written with none reports an empty dict. The store keeps them with the claim, returns them on every read that returns the claim, removes them with it, and never acts on one. A qualifier named confidence is what its writer said about its own value, not a judgement the store has made: the store’s account of how a claim arrived is trust, and no qualifier changes it.

Each value keeps its class (§5.6), exactly as value does — a qualifier written as 0.8 reads back as a real and one written as datetime("…") as a datetime — and the names come back in name order.

# claim-qualifiers.dpy — a writer states facts about its own claim, and every
# read that returns the claim gives them back.
def qualifier_report():
    remember(Reading.r1.temp, 21, "lab",
             qualifiers={"method": "observed", "confidence": 0.8})
    remember(Reading.r1.temp, 23, "forecast", qualifiers={"method": "predicted"})
    remember(Reading.r2.temp, 19, "lab")
    readings = get(Reading.r1.temp)
    for r in readings:
        q = r.qualifiers
        print(r.source + " says " + str(r.value) + ", " + str(q["method"]))
    plain = current(Reading.r2.temp)
    none = plain.qualifiers
    print("r2 carries " + str(len(none)) + " qualifiers")
OK script qualifier_report(): 3 claims written, 3 prints
  remember Reading.r1.temp = 21 qualifiers={"confidence", "method"}
  remember Reading.r1.temp = 23 qualifiers={"method"}
  remember Reading.r2.temp = 19
  print lab says 21, observed
  print forecast says 23, predicted
  print r2 carries 0 qualifiers

The two sources disagree about r1, and each claim still says how it was reached. That is the point of putting the account on the claim rather than beside it: a program weighing the two reads both values and both accounts from one place, and nothing about either was lost when the other was written.

maintained is False when the claim holds a derived value its rule can no longer keep current — the value is the last one that rule produced, and the data behind it has moved on since. It is True for every asserted claim and for every derivation whose rule is still keeping its value current. Nothing about an unmaintained claim is in disagreement with anything: it is neither a conflict nor a matter of preference (§7.6), and it continues to be served by every read. The store says the value is not being kept up rather than letting it pass as current, and a reader guards on it directly:

c = current(Totals.all.sum)
match c:
    case active_claim:
        if not c.maintained:
            print("this total is not being kept current")
    case empty:
        pass

The mark is established when the claim is read, not recorded on it, so it appears as soon as the rule stops keeping the value current and clears on its own once that is no longer so. A program must therefore read it alongside the value it qualifies rather than carrying it forward: a maintained kept from an earlier read describes the earlier read.

truncated is True when a proof_tree shows less than the whole derivation — the tree is cut off rather than complete. A proof that ended and a proof that was cut short are otherwise indistinguishable, and a cut one reads as whole, so a program that treats a derivation as exhaustive checks this first.

trust says how the claim came to hold its value. It is one of two words: asserted — someone recorded this value — or derived — a rule concluded it. Like written_by and unlike source, it is established by the engine and cannot be written: a caller may cite the literal string "derived" as a source, and that claim still reads trust as asserted, because citing a word is not the same as being concluded by a rule.

Together with derived_by the two fields separate the three cases a reader meets:

trust written_by derived_by what happened
asserted a key empty someone recorded this value, and this key did it
asserted empty empty someone recorded this value, without an authenticated key
derived empty a rule no one recorded it — the named rule concluded it

derived_by names what concluded a derived claim. A rule (§14) reads as the name list(target = rules) lists it under (§14.7) — heat.comfy for the rule comfy in the module heat, warm for a rule compiled on its own — and a computed field (§13.7) as its class and its field, qualified by its module the same way — heat.Box.area. It is the empty string on a claim someone recorded. premises(c) (§7.1) returns the claims the conclusion was drawn from. rule_kind is the kind of relation the concluding rule states, when its author said (§14.1) — "causal" for a rule written @rule(kind="causal") — and the empty string otherwise: on a claim someone recorded, on a computed field’s conclusion, and on a conclusion of a rule written without one.

Asking who wrote a derived claim has no answer. Asking why it holds does: why_not(K.s.a) and what_needs(K.s.a) answer about a conclusion that is not there. why requires a claim to explain; when a read comes back empty the question is a different one, and these two answer it. why_not names the condition that blocked the conclusion and distinguishes eight cases that are otherwise one observable: a condition never recorded, a condition recorded whose validity window does not cover the time asked about, a condition recorded that fails its test, a condition recorded as absent (assert), a condition recorded with a value that has no order under an ordered test (a blob, or a recorded absence), a condition recorded with a value that has an order and compared under an ordered test with one that has none, a condition required to be absent that is present, and a condition whose sources disagree. A recorded absence holds no value, so a rule that only reads the field is told it is absent, not that it failed a comparison. A condition comparing two fields (o.qty > o.limit) is stated with both, as it was written. A value with no order is not a value that failed, on either side of the test: no value of its type passes <, <=, > or >=, so what it needs is a different value, or ==. The value compared with is named when the rule read one value for it. what_needs names the facts that would make the conclusion hold, walking through conditions the standing rules can themselves derive and stopping at the ones only the caller can supply. When a rule has every condition it needs and still concludes nothing — a fold of the rule cannot take the values it reads (§14.9, declined), the comparison it concludes cannot rank the values it reads, or the arithmetic it computes has no answer for them (a divisor of zero, say) — both verbs say so of that rule, the last with the operation over the values it read and the fields they came from, and what_needs says it before the conditions it lists as recorded. A condition whose cell already holds a value is named as recorded, with the case why_not would give it (out of its window, failing its test, recorded as absent, without an order, compared with a value without one, or disagreed on), rather than as a fact to supply. A condition required to be absent whose field holds a value is named as present, never as a fact to supply. The move is removing the value or narrowing its window where the caller recorded it, and changing what the concluding rule reads where a rule concludes it, since a conclusion forget removes is drawn again while its premises hold (§14.4). The walk does not go through the rules that would conclude such a field, since what they need would make it hold, and a condition required to be absent that is absent is listed as held. Both are reads and write nothing; both answer no standing rule concludes this when no rule produces the cell at all, which is a different finding from an unmet condition and usually a more useful one. Where the cell asked about itself holds a claim whose validity window does not cover the time asked about, that is reported first, before anything the standing rules have to say: the fact is recorded and out of force, so what it needs is a wider window or an as-of the window covers rather than another write. Both take no as_of: a diagnosis is about the store as it stands.

why(K.s.a) returns the derivation (§7.1), which is the account of a derived claim in the same way a key is the account of a written one. Anchored, why(K.s.a, datetime("…")) returns the derivation as it stood at that instant, built from the premises whose validity windows covered it. This is what explains a changed answer: a conclusion can hold at one instant and not at another because a premise moved, and asking the same cell at two instants gives two derivations whose difference names the premise that moved. A proof_tree is a finite collection (§5.1) whose elements are active_claims, so a program can hold both and compare them.

An anchored derivation may be absent where the unanchored one is not, and the reverse. Absence here is the empty discriminator (§5.1) rather than a zero-length proof_tree, so the collection surface is for an answer already established to be a proof — by match, whose case set for a proof_tree is { proof_tree, empty } (§6.2). It means no conclusion was derivable at that instant, which is a different finding from a conclusion that is derivable and false; when there is no derivation, why_not explains the absence.

source and written_by answer different questions and are not interchangeable. source is the origin the writer cited for the content — a claim about the data, which the engine records and cannot check. written_by is who performed the write — a fact about the act, established by authentication. A claim may cite a trusted registry and have been written by anyone holding a key to the store.

path is the claim’s cell path (Cat.felix.color) — the claim_id without its # and code. It is most useful for the reads whose claims live at cells the call site did not name: dependents and premises (the cell each claim they return lives at) and conflicts (each conflicting cell). path and claim_id are spelled the way a program addresses the cell, so a kind a module declares reads back qualified by its module — inventory.Item.i1.qty — as §7.1’s listings name it.

created_at is the claim’s creation moment: the UTC wall-clock instant at which the claim was recorded, rendered as an RFC 3339 timestamp with microsecond precision and a literal Z suffix — e.g. 2026-07-04T09:15:42.123456Z. The value is fixed when the claim is created and is the same string no matter how the claim was obtained (current and get agree). A claim with no recorded creation moment carries the empty string. Lexicographic comparison of two non-empty created_at values orders them chronologically (RFC 3339 UTC is sortable). Two claims recorded within the same microsecond carry the same created_at, so equal values mean recorded in the same microsecond, not recorded at the same moment, and the field does not say which of the two came first. Sorting by created_at is a correct chronological sort in which such claims tie.

created_at orders claims; it does not measure elapsed time. Ordering is the whole of what it guarantees: given two claims with different stamps, it tells you which was recorded first. The difference between two created_at values is not the time that passed between them, and no reading of it as a duration is supported — when two claims are recorded in quick succession their stamps may differ by an amount unrelated to the interval between them. This is why created_at is a string and not a datetime: temporal arithmetic on it is rejected at compile time rather than producing a plausible wrong answer. To record how long something took, measure it with your own clock and store the result as a duration (§3.5) — that value means what it says, composes with datetime arithmetic, and is what the aggregate folds are defined over.

Universal accessors:

  • <expr>.kind returns string for any discriminator value (active_claim, proof_tree, empty, verify_result, and the try-handler discriminators of §5.4). The returned string is the type name as written in §5.1.
  • <expr>.length returns int for any string, any blob or any finite collection. For a string the result is the number of characters (Unicode characters, not bytes); for a blob it is the number of bytes; for a collection it is the number of elements. The Python builtin len(x) is accepted as an alias — len(x) is exactly x.length (and a type_error when x is none of these).

5.3 Indexing and slicing

Indexing. The expression <value>[<int>] selects one element. It is well-typed when the value has one of the types in this table, and the result type is the one the table gives:

Value type Element type
claim_list active_claim
proof_tree active_claim
diagnosis string
string_list string
subject_set string
result_type_set string
string string
list<T> T
tuple<T1, …, TN> the indexed position’s type
dict<K, V> V (indexed by a key of type K, not an int)

A set does not index. A set has no positional index, in Python or here, so s[i] and s[a:b] are refused before the run, in Python’s words: 'set' object is not subscriptable. A set’s elements are read with a bounded for, which walks them in sorted order (§3.5.13), and one is tested with in (§8.1).

Tuple indexing. A tuple’s positions may differ in type, so t[i] with a compile-time constant integer i (a literal, an optional leading minus, or a constant +/- of such) has the type of that position: t[0] the first, t[-1] the last. A computed index reads what a for over the tuple binds (§5.1): the positions’ one type, value over scalars of several (§5.6), or the nearest common base of class instances. Over a tuple holding a collection beside a value of another type a computed index is a type_error, which costs no function, every position being reachable by a literal index. An index past either end compiles, and refuses when it runs, catchably, as Python raises its IndexError there (§10.3), so t[9] on a pair is a catchable refusal at run time, saying tuple index out of range. A constant one is typed as an element of the tuple read as a list, the positions’ one type or value over scalars of several (§5.1), so what reads it is checked as it would be against such an element. This is one application of a general rule (§8.3, Value-dependent typing), not a rule of its own.

Dict indexing. d[k] looks a value up by key, comparing k with the keys as == does, and the result is V. A key absent at run time is a catchable refuse (§10.3), and so is a key of another type, which no key equals ({1: 2}["a"] refuses as 'a'). Because dict keys are scalars (not necessarily int), dict indexing is resolved by the key type rather than the <int> rule below.

Indexing a string returns the one-character string at that position; positions count characters (Unicode characters, not bytes), starting at 0. A negative index counts from the end: s[-1] is the last character, s[-2] the one before it, down to s[-len] (the first). Negative from-end indexing is defined for a string and for a constructed list<T> (xs[-1] is the last element); a store-derived collection (§5.1) takes a non-negative index only. An index still out of range after the from-end shift — of a string or a constructed list, or a non-negative index of a store-derived collection — is a runtime refuse (§10.3), not a static error. The index expression MUST be of type int. An index of another type is a type_error, told in Python’s words over a string, a list or a tuple, naming the index’s class: list indices must be integers or slices, not str for [1]["a"], and string indices must be integers, not 'float' for "ab"[1.5]. A del or a write through such an index says the same, and a list’s pop given one says 'str' object cannot be interpreted as an integer. An index of a type only this language has keeps this language’s words.

Slicing. The expression <string>[<start>:<end>] returns the substring from character position <start> up to (but not including) position <end>. Both bounds are optional: s[<start>:] runs to the end of the string, s[:<end>] starts at the beginning, and s[:] is the whole string. Each bound that is present MUST be of type int. Slicing applies to a string (result string) and to a constructed list<T> (result list<T> — a slice yields a new list of the same element type, which a change to the source does not reach, §8.14); it does not apply to the store-derived collections.

Step. A slice may carry an optional third component — the step — <string>[<start>:<end>:<step>]. The step (an int, default 1) selects every <step>-th character. A negative step walks the string in reverse: s[::-1] is s reversed, and s[::-2] takes every other character from the end. When a step is present the bound defaults follow the step’s direction: a positive step defaults <start> to the beginning and <end> to the end, while a negative step defaults <start> to the last character and <end> to before the first. A step of 0 is a catchable runtime refuse (slice_step_zero); it is the only non-total slice.

A negative bound counts from the end, like indexing: s[-3:] is the last three characters, s[:-1] all but the last. A negative bound past the start of the string resolves to 0.

Slicing is otherwise total — it never refuses. A bound beyond the ends of the string is clamped to it, and a start at or past the end (or at or past the end bound) yields the empty string "". Positions count characters, consistent with indexing and .length. A negative step is the reverse direction; a negative bound is a from-end position — the two are independent (s[-1:-4:-1] walks the last three characters backwards).

A constructed list<T> slices by the identical rules — optional bounds, an optional step (with xs[::-1] reversing), clamped out-of-range bounds, negative from-end bounds, and the step-0 refuse — producing a new list<T>. xs[a:b] selects the elements at positions a up to (but not including) b; positions count elements, consistent with list indexing and .length.

A tuple slices by the same rules and yields a tuple (Python’s t[a:b] → tuple). With constant integer bounds (t[1:], t[:2], t[1:-1], t[::-1]) the result type is the sub-tuple of the selected positions. With a computed bound it is a variadic tuple<T, ...> of what a computed index reads, and over a tuple holding a collection beside a value of another type a computed bound is a type_error, as a computed index is (§5.3). list(t[a:b]) (§8.13) then converts the result to a list<T> — the idiom behind starred unpacking.

A slice as a value. slice(stop), slice(start, stop) and slice(start, stop, step) build a value of type slice (§5.1) that holds three bounds. Each argument is an int, a bool (kept as written, as CPython keeps it) or None (§3.5.4), which leaves that bound open, and a bound not given is open too, so slice(5) is CPython’s slice(None, 5, None). A slice indexes as its bounds written in brackets do: xs[s] answers what xs[a:b:c] answers on a string, a list<T> and a variadic tuple. A zero step is refused there as the bracketed form refuses it. On a fixed-length tuple a slice is a bound the compiler does not see, so t[s] reads as a computed bound does: a variadic tuple of what a computed index reads, and a type_error over a tuple holding a collection beside a value of another type. On a list, xs[s] = c, xs[s] op= c and del xs[s] change what the bracketed slice would (§8.14).

A slice carries:

Member Signature Result
s.start, s.stop, s.step value (§5.6) the bound as it was given; an open bound reads back as the null class
s.indices(n) (int) → tuple<int, int, int> the (start, stop, step) that xs[s] selects from a sequence of length n, as CPython’s slice.indices answers it: bounds counted from the end when negative and clamped to the sequence, and a step of 1 when open. A negative n is refused length should not be negative (value_error), and that is checked before the step; a zero step is refused slice step cannot be zero (slice_step_zero)

A slice prints as CPython prints it, slice(1, 5, None), through print, str, repr, an f-string hole and inside a collection. Two slices are == when their three bounds are, so slice(1, 2) == slice(True, 2) holds as it does in CPython. A slice is not ordered, since CPython orders slices by their bounds and fails wherever one is None. It is always true and has no .length, and it is not hashable, as in CPython 3.11, so it is neither a set element nor a dict key. It is a value a program builds rather than a fact: no field and no verb stores one.

One difference from CPython: an open bound read back prints null, as an open window bound does (§3.5.4), where CPython prints None. print(slice(3).start) writes null, while the slice itself prints slice(None, 3, None).

String methods. A string value carries these built-in methods:

Method Signature Result
s.find(sub[, start[, end]]) (string[, int[, int]]) → int the character position of the first occurrence of sub in s, or -1 if absent; the optional start[, end] window searches s[start:end] and the result is an index into the whole s (negative bounds count from the end)
s.rfind(sub[, start[, end]]) (string[, int[, int]]) → int the character position of the last occurrence of sub in the start:end window, or -1 if absent
s.index(sub[, start[, end]]) (string[, int[, int]]) → int like find, but refuses (§10.3, substring_not_found) instead of returning -1 when sub is absent from the window
s.rindex(sub[, start[, end]]) (string[, int[, int]]) → int like rfind, but refuses (substring_not_found) when sub is absent from the window
s.count(sub[, start[, end]]) (string[, int[, int]]) → int the number of non-overlapping occurrences of sub in the start:end window; an empty sub yields the window’s length + 1
s.startswith(prefix[, start[, end]]) ((string \| tuple<string, …>)[, int[, int]]) → bool True if the start:end window of s begins with prefix (an empty prefix yields True). prefix may instead be a tuple of strings — True if the window begins with any of them (an empty tuple yields False)
s.endswith(suffix[, start[, end]]) ((string \| tuple<string, …>)[, int[, int]]) → bool True if the start:end window of s ends with suffix (an empty suffix yields True). suffix may instead be a tuple of strings — True if the window ends with any of them (an empty tuple yields False)
s.replace(old, new[, count]) (string, string[, int]) → string a copy of s with each non-overlapping occurrence of old replaced by new, at most count times when given (default: all); an empty old inserts new in each gap
s.upper() () → string s with each character replaced by its full Unicode uppercase — a character may expand to several (ß→SS, fi→FI), so the result can be longer than s
s.lower() () → string s with each character replaced by its full Unicode lowercase (a character may expand, İ→i̇); a Greek Σ becomes word-final ς when it ends a word (preceded by a cased character, not followed by one), else σ
s.capitalize() () → string s with its first character titlecased and the rest lowercased
s.title() () → string s with the first character of each word titlecased and the rest lowercased — a word is a maximal run of cased characters, so any non-letter (space, digit, apostrophe) starts a new word
s.swapcase() () → string s with each uppercase character lowercased and each lowercase character uppercased (the full mapping above; the lowercasing applies the word-final Σ→ς rule)
s.casefold() () → string s case-folded for caseless matching — the full Unicode case-folding mapping, more aggressive than lower (ß→ss) and context-free (both Σ and ς fold to σ; NO word-final rule, so "ΟΔΟΣ".casefold() is "οδοσ", unlike .lower()’s "οδος")
s.strip([chars]) ([string]) → string s with leading and trailing characters removed — whitespace when chars is omitted, otherwise any character that occurs in chars
s.lstrip([chars]) ([string]) → string like strip, but removes only leading characters
s.rstrip([chars]) ([string]) → string like strip, but removes only trailing characters
s.removeprefix(prefix) (string) → string s with a leading prefix removed if s begins with it, otherwise s unchanged
s.removesuffix(suffix) (string) → string s with a trailing suffix removed if s ends with it (and suffix is non-empty), otherwise s unchanged
s.expandtabs([tabsize]) ([int]) → string s with each tab replaced by spaces to advance to the next tab stop (a multiple of tabsize, default 8); the column counts characters and resets to zero after each \n or \r. A tabsize ≤ 0 removes tabs
s.zfill(width) (int) → string s left-padded with '0' to width characters; a leading sign (+/-) stays first and the zeros follow it. width ≤ s.length returns s unchanged
s.ljust(width[, fill]) (int[, string]) → string s left-justified in a field of width characters, padded on the right with fill (a single character, default ' ')
s.rjust(width[, fill]) (int[, string]) → string s right-justified — padded on the left with fill
s.center(width[, fill]) (int[, string]) → string s centred in a field of width characters, padded with fill; when the padding cannot be split evenly the extra character goes on the left iff both the margin and width are odd (CPython’s rule). width ≤ s.length returns s
s.split([sep[, maxsplit]]) ([string[, int]]) → list<string> the substrings of s split at each non-overlapping occurrence of sep (empty pieces kept); an optional maxsplit caps the number of splits (≤ maxsplit + 1 pieces); with no argument, s is split on runs of whitespace with empty pieces dropped and leading/trailing whitespace ignored. Naming maxsplit and not sep splits on whitespace too, at most maxsplit times, and keeps the rest as one piece (Arguments by name, below)
s.rsplit([sep[, maxsplit]]) ([string[, int]]) → list<string> as split, but a bounded maxsplit takes its splits from the right ("a,b,c".rsplit(",", 1) is ['a,b', 'c']); with no maxsplit the result equals split. Naming maxsplit and not sep takes whitespace pieces from the right
sep.join(xs) (iterable of string) → string the strings xs yields concatenated with the separator sep between each adjacent pair: a string’s characters, the elements of a list, tuple, set or iterator of strings, or a dict’s string keys (an empty one yields "")
s.partition(sep) (string) → tuple<string, string, string> the part of s before the first occurrence of sep, sep itself, and the part after ("a@b@c".partition("@") is ('a', '@', 'b@c')); when sep is absent from s the whole string lands in the first position and the other two are ""; an empty sep refuses (§10.3, empty_separator)
s.rpartition(sep) (string) → tuple<string, string, string> as partition, but split at the last occurrence ("a@b@c".rpartition("@") is ('a@b', '@', 'c')); when sep is absent the whole string lands in the third position — not the first — and an empty sep refuses alike
s.translate(table) (table) → string s with each character whose code point table holds replaced by what it holds for it: an int or bool is a code point, a string stands as written, and any other character stays. A dict finds a code point as any dict does, so 97.0 is the key 97 and a string key finds nothing, as in Python. A list, a tuple or a string is indexed by the code point, and a character past its end stays. A table of any other type, reached by a character, is a catchable refuse (§10), and so is a value of any other type, a code point outside range(0x110000) or a surrogate, when a character reaches it
s.format(*args, **kwargs) (any…) → string s read as a template, as CPython reads it (String templates, below), each field replaced by what it names; a * spreads positions and a ** names. An argument no field reaches is no fault
s.format_map(m) (any) → string as format, every name looked up in m and no field a position
s.splitlines([keepends]) ([bool]) → list<string> the lines of s with their terminators removed, split at each of CPython’s line boundaries — \n, \r, \r\n (one boundary), \v, \f, \x1c, \x1d, \x1e, U+0085, U+2028 and U+2029; a trailing line boundary yields no final empty piece ("a\nb\n".splitlines() and "a\nb".splitlines() are both ['a', 'b']), a blank line yields an empty piece ("a\n\nb" gives ['a', '', 'b']), and "" yields []. With a true keepends each line keeps the boundary that ends it, "a\r\nb".splitlines(True) being ['a\r\n', 'b']; an int there is read for its truth value, as in Python
s.isspace() () → bool True if s is non-empty and every character is whitespace
s.isdigit() () → bool True if s is non-empty and every character is a digit
s.isdecimal() () → bool True if s is non-empty and every character is a decimal digit
s.isnumeric() () → bool True if s is non-empty and every character is numeric (includes decimals, fractions, and numeral characters)
s.isalpha() () → bool True if s is non-empty and every character is a letter
s.isalnum() () → bool True if s is non-empty and every character is a letter or a numeric character
s.isupper() () → bool True if s has at least one cased character and none of them is lowercase or titlecase
s.islower() () → bool True if s has at least one cased character and none of them is uppercase or titlecase
s.istitle() () → bool True if s is titlecased — every uppercase/titlecase character follows an uncased one and every lowercase character follows a cased one, with at least one cased character
s.isascii() () → bool True if every character is in the ASCII range (U+0000–U+007F); the empty string is ASCII ("".isascii() is True)
s.isprintable() () → bool True if every character is printable: not a control, a format character, a private-use or unassigned codepoint, or any separator but the space. The empty string is printable ("".isprintable() is True). Past U+007F these are exactly the characters repr writes as themselves (§8.13)
s.isidentifier() () → bool True if s is non-empty, its first character is _ or has the Unicode property XID_Start, and every later one has XID_Continue, as Python decides. A keyword is an identifier too: "class".isidentifier() is True.

Arguments by name. A string method takes by name exactly the arguments CPython lets a caller name, and places each in the position it names, as a built-in function does (§8.13): sep and maxsplit on split and rsplit, keepends on splitlines and tabsize on expandtabs, so s.split(sep=",", maxsplit=1) splits as s.split(",", 1) does. A separator left out is Python’s None, which has no spelling here, so naming maxsplit alone is how a whitespace split is bounded: " a b c ".split(maxsplit=1) is ['a', 'b c '], the rest kept whole after the whitespace that follows the last piece, and " a b c ".rsplit(maxsplit=1) is [' a b', 'c'], its pieces taken from the right. A dict’s update takes any name as a key and sort takes key and reverse (§8.14). Every other method of a string, a list, a tuple, a set, a dict, a dict view or a number takes no argument by name, as in Python, and a name given to one is a compile-time error in Python’s words, naming the receiver’s Python class, such as str.center() takes no keyword arguments. A receiver without the method is told that instead, since Python finds the method before it reads an argument.

Arguments of a type a method does not take. An argument of a type a string’s method does not take is a compile-time error in Python’s words, which follow how the method is written in C, and so differ by method for one fault: "abc".find(1) is refused with must be str, not int, "abc".replace("a", 1) with replace() argument 2 must be str, not int, "abc".startswith(1) with startswith first arg must be str or a tuple of str, not int, "abc".split(1) with must be str or None, not int, "abc".strip(1) with strip arg must be None or str, "abc".center(5, 1) with The fill character must be a unicode character, not int, and "abc".zfill("a") with 'str' object cannot be interpreted as an integer. A search’s bounds are read as a slice’s: "abc".find("a", 2.5) is refused with slice indices must be integers or None or have an __index__ method, and [1, 2].index(1, "a"), over a list or a tuple, with slice indices must be integers or have an __index__ method. A list’s insert position and a slice’s indices argument are integers, so xs.insert("a", 2) is refused with 'str' object cannot be interpreted as an integer. An argument of a type Python has no class for, such as a claim’s value, is told instead which type the method takes.

All character positions and widths count characters (Unicode codepoints), not bytes (§5.2). The case transforms (upper/lower/capitalize/title/ swapcase/casefold) apply the full Unicode case mapping — a character may map to more than one (so the result, and its .length, can grow) — and the forms that lowercase apply the Greek word-final Σ→ς rule; casefold is the sole exception (context-free, no word-final rule). The character-class predicates (is…) classify each character by its Unicode properties. Every method is total except split, ljust/rjust/center, and index/rindex: split refuses (§10.3, empty_separator) only when an explicit separator argument is the empty string (the argument-free whitespace form never refuses), ljust/rjust /center refuse (fill_not_one_char) when an explicit fill argument is not exactly one character, and index/rindex refuse (substring_not_found) when the substring is absent. s.split and sep.join bridge the string and constructed-list surfaces: split produces a list<string> (§5.1) and join consumes one. join reads any iterable of strings as for reads it, as Python does — a string, a list, a tuple, a set (in its order, §3.5.12), a dict’s keys, a dict view or an iterator, which it reads to the end: "-".join("abc") is "a-b-c". An iterable whose elements are not all strings is a type_error, in Python’s words where the first that is not is of a class Python has: "a".join([1]) is refused with sequence item 0: expected str instance, int found, and "a".join(1), over what is no iterable, with can only join an iterable. These are the only method calls the language defines on a built-in value.

upper() and lower() apply the full case mapping stated above: a character whose case change needs more than one character maps to all of them, so "straße".upper() is "STRASSE" and len("ß".upper()) is 2. strip(), lstrip(), and rstrip() remove leading and/or trailing characters; with no argument they remove whitespace, and with a chars argument they remove any character that occurs in chars (the chars argument is a set of characters, not a prefix or suffix). The interior is untouched, and stripping a string down to nothing yields "".

5.4 Try-handler discriminator types

A try statement’s except handlers may each introduce one bound identifier (via the optional as <name>, §9.2), carrying a discriminator type the user cannot write as a type annotation. When a handler omits as <name> it binds nothing and the types below simply do not arise for that handler:

  • except refuse as r — r has fields reason, teaching_hint, discriminator, all of type string. r.kind returns the literal string "refuse_info". reason states what was refused, in the script’s natural language. teaching_hint carries a suggestion for correcting the call, and is empty when there is none — it is an aid to a human reader, never a second reason. discriminator is a short tag naming which refusal this is, for a program that must branch rather than report; a refusal raised by raise <ExcType>(…) (§4.3) carries the exception name as its tag.
  • except engine_error as e — e has fields reason, code, both of type string. e.kind returns the literal string "engine_error_info". reason states what failed; code is a short tag naming which failure this is, in the same role discriminator plays for a refusal.

Every one of the five fields is a string and is always present; the ones that may carry nothing carry "" (§5.1: the language has no absent value).

The set of tags discriminator and code can take is deliberately not specified. Both are readable, comparable, and safe to branch on within a release, but the vocabulary is not a stable contract and this document does not enumerate it: a program that must behave differently for different failures should branch on the tags it has observed and keep a fallback arm. Programs that report rather than branch should use reason, which is written for a reader.

These types are produced only by their corresponding handlers; they cannot be passed as parameters, returned by verbs, or written in a type annotation. Their fields are in the §5.2 table under the names .kind reports.

5.5 Type names in source

The type names valid as type-name tokens are the primitives (string, int, bool), the value-class scalars (datetime, duration, real, blob, scientific, complex), and the read-result types from §5.1 (active_claim, claim_list, proof_tree, diagnosis, subject_set, string_list, verify_result, result_type_set). A parameter or return annotation (§4.2) additionally admits a parameterized collection type — list<T>, set<T>, dict<K, V>, tuple<T, ...> — nestable to any depth, and an iterator type iterator<T> (§8.6), which typing.Iterator[T] is taught to write, and the three dict view types dict_keys<K>, dict_values<V> and dict_items<K, V> (§5.1). A collection type is built from these names or from a class name, so list<Animal> is a type.

CPython’s spellings are read beside these. str and float are string and real, on the surface whose words are CPython’s, and a class the program declares by either word is that class; another surface has its own word and teaches it. A collection type takes CPython’s brackets as well as angle brackets, so list[int] is list<int>, dict[str, int] is dict<string, int> and tuple[int, ...] is tuple<int, ...>. They are one type, and a diagnostic renders it with angle brackets. A collection word with nothing after it, as in x: list = [], says nothing of what it holds and is refused, teaching list[int], and so are typing’s capitalised names, List[int] among them, which have no module here to come from. bytes is refused, teaching blob, since a blob prints otherwise. A return annotation of None alone, as in def f() -> None:, says what no annotation says; None as any other annotation is refused.

A class name the program declares (§13.1) is a type too, and is valid in every position a type is written: a parameter, a binding annotation, a collection’s element type, a script’s return and a value method’s return. It is not a type-name token — it enters the grammar at type, as an IDENT (§4.2) — so the list above remains the list of names the language itself reserves for types, and is not the list of everything writable where a type goes. What a class name means in those positions is §13.8: a subclass instance stands where its base class is named.

A parameter (§4.2) may be typed by any of them. A value-class-scalar parameter supplied over the wire is given as its canonical text — a datetime as RFC 3339 UTC (§3.5.5), a duration as ISO 8601 (§3.5.6), a real as a decimal with a point (§3.5.7), a blob as hex (§3.5.8), a scientific as its canonical numeral (§3.5.14), a complex as the text it renders as, with or without its parentheses (§3.5.15) — and refuses if the text is malformed. A complex is given as 3+4j, (3+4j) or -2.5j.

A class field (§13.1) must be a primitive or a value-class scalar — a scalar-name, a type the engine stores as a cell value (string, int, bool, datetime, duration, real, blob, scientific, complex) — because a field holds one stored value; the read-result types are not stored values and are not valid field types.

The name empty is valid as a match case label (§6.2) but NOT as a type-name token. The names refuse_info and engine_error_info name the try-handler discriminator types (§5.4) but are NOT writable as type-name tokens. The name value (§5.6) names the claim-content union; it arises only from .value field access and is likewise NOT writable as a type-name token. The name null (§5.6) is the absence value class — it arises only from a case null: match arm and, having no payload to pass or store, is NOT a type-name token (a parameter or field is never null-typed). Type names are contextual identifiers, not reserved words (§3.7).

5.6 The value union

The .value field of an active_claim (§5.2) has type value: the stored content of a claim, carrying its own type. A value written as an integer reads back as an int; a value written as a boolean reads back as a bool; a value written as text reads back as a string; a value written as a datetime("…Z") datetime literal (§3.5.5) reads back as a datetime; a value written as a duration("…") duration literal (§3.5.6) reads back as a duration; a value written as a 3.5 real literal (§3.5.7) reads back as a real; a value written as an blob("…") blob literal (§3.5.8) reads back as a blob; a value written as a complex number (§3.5.15) reads back as a complex. The type is a property of the claim, fixed when the claim is recorded, and is the same however the claim is obtained (current and get agree, as for every other field).

value is a tagged union over its value classes:

value  =  int | bool | string | datetime | duration | real | null | blob | scientific | complex

Every class but one is write-producible from DKE Python — a program writes an int, bool, string, datetime, duration, real, blob, scientific or complex and reads it back at that class. The remaining class, null (an explicit absent value), is read-only: it is asserted through the assert verb (§7.1), which carries no value argument, so no value literal produces it — but a claim carrying one is discriminable on read (see below).

A program consumes a value in one of four ways:

  • Destructure it with a match (§6.2) to obtain the typed content:

    c = current(Sensor.room.count)
    match c.value:
        case bool as b:     print("flag = " + str(b))           # b : bool
        case int as n:      print("next = " + str((n + 1)))     # n : int — arithmetic is well-typed
        case string as s:   print("text = " + str(s))           # s : string
        case datetime as d: print("time = " + str(d))           # d : datetime — renders RFC 3339
        case duration as u: print("span = " + str(u))           # u : duration — renders ISO 8601
        case real as r:     print("value = " + str(r))          # r : real — renders decimal
        case blob as b:     print("bytes = " + str(b))          # b : blob — renders hex
        case scientific as s: print("sci = " + str(s))          # s : scientific — renders the canonical numeral
        case complex as z:  print("size = " + str(abs(z)))      # z : complex — arithmetic is well-typed
        case null:          print("an absent value")       # detect-only — no `as` binding

    A write-producible class (int, bool, string, datetime, duration, real, blob, scientific, complex) supports a named case <class> as <var>: arm that binds <var> to the content at the arm’s class type, scoped to that arm, so arithmetic and other type-specific operators apply without a cast. This and a test (below) recover an int (for arithmetic), a bool (for logical operators), or a datetime / duration / real / blob / scientific / complex (to render, compute with or re-store, §8.4) from a claim’s content.

    The one read-only class, null, is written as a bare case null: arm — it discriminates the class but binds no payload; attaching as <var> to it is a type_error (an absent value has no content to bind).

  • Narrow it with a test. Where a test has shown what a name holding a value holds, the name is read at that class: in the branch an isinstance test takes (§13.9), after the test in an and, after a comprehension’s if making it, in a conditional expression’s first branch, in an arm of a match naming its class (§6.2), and in the body of an arm whose guard tests it, as CPython runs each. isinstance(v, int) reads v as an int, and so does a test of int beside bool, since a bool is one; a test of either beside real reads it as a value holding a number, and an or of two tests reads it as what the two join to. Only a name is narrowed, not a field read such as c.value.

    A value built from scalars holds their classes and no other: [1, "a"] holds an int or a string, and so does an element read from it, a loop variable over it and a join of two such values. Where a test fails, a name holding one is read at the classes left to it: in the else of an isinstance test, in the branch a negated test takes, in a conditional expression’s second branch, after the test in an or, and in an arm of a match below arms naming classes (§6.2). A value whose classes are not known, such as a claim’s, may be any class, so a failed test leaves it a value; so does the else of an and, whose left test may have held. A branch no value can take is never run and is not read: a test of types no value has (isinstance(v, list)), two tests no class passes both, or the else of a test of object. An operator meeting a value of known classes runs where it takes each class beside the other operand, and answers what their answers join to, so x * 2 over a value holding an int, a real or a string is a value of the three; a name read on both sides meets only a class with itself.

    for e in (1, 2.5, "a"):
        if isinstance(e, int):
            print(e + 1)                       # e : int
        elif isinstance(e, str):
            print(e.upper())                   # e : string
        else:
            print(e * 2)                       # e : real
  • Render it directly. A value is expressible: print(<value>) appends its textual rendering (§8.7), and str(…) renders it ("count=" + str(c.value) → string, §8.4). Rendering never requires knowing the class.

  • Compare it with == / != against any value — a literal of any of its classes, another value, a collection (§8.1). The result is always bool; a class mismatch compares unequal rather than coercing (c.value == 41 is False when the content is the text "41").

A value match covers every class named above, or any subset of them plus a case default: catch-all (§6.2). case default: keeps a match well-defined without enumerating every class, and also covers any class the service might add beyond this set in a later revision. A value cannot be written in a type annotation (§5.5). Reading a claim is not the only way to get one: the element of a collection mixing scalar value classes is a value ([1, "a"], §3.5.9), and so is whatever is read from it — an index, a loop or comprehension variable — and so is an or or and over two value classes (1 or "yes", §8.2).

A value that holds a number. When every element a mixed collection can hold is an int, a real or a bool — [1, 2.5], {1, 2.5}, [True, 2.5], the values of {"a": 1, "b": 2.5}, the positions of (1, 2.5) — its value is known to hold a number, and the numeric operators and built-ins take it as one: arithmetic (§8.3), ordering, == and membership against numbers (§8.1), sum / min / max (§8.3), sorted (§8.6), and abs, round, divmod, int and float (§8.3, §8.13). Each element keeps its own class, so [1, 2.5][0] is 1, [1, 2.5][1] + 1 is 3.5 and min([1, 2.5]) is 1, as in Python. Its type is still written value — in a diagnostic, and wherever it meets a declared type — so it does not satisfy an int or real parameter, return or annotation; convert it with int(…) or float(…) (§8.13).

An operation over it is typed by the classes that can reach the result. Where the rules of §8.3 give a real whichever class the value holds — a real other operand, /, a negative literal exponent — the result is a real; otherwise it is again a value holding a number, because its class is the element’s. So is sum over such a collection: sum([1, 2.5]) is 3.5, and sum([x for x in [1, 2.5] if x < 2]) is 1.

The property composes, and is lost wherever a value that is not a number could arrive:

  • it is kept through reading (xs[i], for x in xs, a comprehension, a generator expression, list / set / tuple of the collection), through arithmetic, through + of two collections that each hold numbers ([1, 2.5] + [3]), and into the keys of a dict, which are matched by value (§3.5.12): sum({1: "a", 2.5: "b"}) is 3.5;
  • where one expression may produce either of two values — a conditional expression, and / or, the re-binding of a variable, a loop variable left over from a loop, min / max of separate values or with default= — the result holds a number only when both sides do, so ys = [1, 2.5] if c else ["a", 1] is a list<value> that sum refuses;
  • the inner lists of a nested literal unify the same way, so the lists [[1, 2.5], ["a", 1]] holds are not lists of numbers;
  • a bool is the int it is, as in Python (§8.1), so [True, 2] is a list<int> and [True, 2.5] a list<value> holding numbers: sum([True, 2.5]) is 3.5, and True in [1, 2.5] is True. It keeps its class, so max([True, 0]) is True, the element itself.

A value that holds a magnitude. When every element a mixed collection can hold is a number or a scientific — [scientific("2.0e0"), 1], the keys of {scientific("2.0e0"): "a", 1: "b"} — its value is known to hold a magnitude, and it orders as a scientific does, by magnitude (§8.1): against a number, a scientific or another such value, in sorted, min and max, and inside two lists or tuples being ordered. It is not a number, so arithmetic, sum and the numeric built-ins refuse it, as they refuse a scientific. It composes as a value holding a number does, and a number has a magnitude too, so [1, 2.5] + [scientific("3.0e0")] holds magnitudes, and min of a scientific and a number holds one.


6. Static semantics

6.1 Scoping

DKE Python is function-scoped for variables, as Python is. An assignment x = <expr> binds x in the enclosing script body, not in the :-headed suite that contains it: an assignment inside an if, for, match, try, except, or branch block binds the script’s variable, and its value persists past the block. A for loop body may therefore accumulate into an outer variable, and an if branch may conditionally reassign one. A re-assignment MUST keep the variable’s type — assigning a value of a different type to a name already in scope is a type_error (§6.4); a same-type re-assignment is permitted. Script parameters are in scope for the entire body.

A name is read only where every path that reaches the read has bound it — the definite-assignment rule, which the compiler checks, so that no run reads a name unbound. A name first bound inside a block is readable within that block and its nested blocks, and after the statement holding the block when every path through that statement that completes binds it:

  • both arms of an if/else, and every arm of an if/elif/…/else chain;
  • every arm of a match with an unguarded catch-all (case _:, a capture, or case default:), and every arm of a match over an instance of a class, whose arms must cover every class the instance can hold;
  • a try body, which runs on into its else, so the else reads what the body bound, and each except handler present; a name the finally binds is bound on every path;
  • a branch block’s body, which runs in full (§9.2.6).

A path ending in return, raise, break, or continue reaches nothing after the statement, so it need not bind the name. Every path must bind the name to one type: paths binding it to two types compile, and a read after them is a type_error naming both. A name bound on some paths only — after an if with no else, a match with no catch-all, or a try with a handler that does not bind it — or in a for body, which may run zero times, or in the for’s else, which a break skips, is readable within its block only; a read after is a type_error saying it is not bound on every path. Bind it before the statement to read it after. Where CPython raises NameError on a run that reaches such a read with the name unbound, DKE Python refuses the read before any run. A finally reads none of its try’s bindings, since it also runs when the body did not complete.

The except handler variable is block-local and does not leak past its block: it is visible only within its own block, and when the block ends the name is restored to whatever it held beforehand, or becomes unbound if it was not previously bound. The for loop variable is not: the loop assigns each element to it in the enclosing scope, so after the loop, and in its else body, it holds the last element bound, as in Python, and a loop that runs zero times binds nothing and leaves the name as it was. Being an assignment, a loop over a name that is already a variable keeps that variable’s type (§6.3). A comprehension’s variable, by contrast, is local to the comprehension (§8.9). match case clauses bind no name of their own; the scrutinee’s binding (if any) stays visible inside every case body. A class arm’s as binding is local to its arm (§6.2), while a value pattern’s capture and as bind as assignments do (§6.2).

A branch block rolls back the store on abort (§9.2.5), never variable bindings: a variable assigned inside a branch keeps its value whether the branch commits or rolls back — variables are not transactional.

A path segment that names an in-scope variable (current(k.s.a) with k, s parameters) is resolved to that variable’s value; a segment that is not an in-scope name at the point of the call is a literal kind/subject/attribute name. Because this is decided from lexical scope, a variable never captures a same-named literal segment introduced later.

A resolved segment MUST be a name — the identifier shape of §3.4. A value that is not (it holds a space, a ., leads with a digit, or is empty) raises a refuse (§10) naming the value, the variable it arrived in, and which of the kind / subject / attribute positions it filled. Naming the value is what makes the refusal usable where the segments come from data: a loop writing one claim per row runs on a single line, so the line identifies the loop and only the value identifies the row. Being a refuse rather than an error is what lets a handler take it per row (§9.3) — see the tutorial’s Loading many facts at once for the skip-the-bad-rows-and-keep-the-rest shape. A value whose one fault is a combining mark — "cafe" followed by U+0301, which renders as café — is refused naming the mark by its codepoint, since the mark draws onto the letter before it and nothing on screen shows it. A held value is not read as a written name is (§3.4), so the mark stays and does not compose. Reading the cell and writing it are refused alike.

Where the path’s leading segment is a declared class, a segment that names BOTH an in-scope variable and a declared field of that class is rejected at compile time — the two readings write to different places and the compiler will not guess between them. Rename the variable (or address a field the variable is not named after):

class Box:
    label: string

def put(id: string, label: string):
    remember(Box.id.label, label, "op")   # rejected: `label` is both
def put(id: string, text: string):
    remember(Box.id.label, text, "op")    # writes Box.<id>.label

This restricts only the ambiguous spelling, not the capability: a segment holding a variable that is not a declared field of the class still resolves to that variable’s value, so a computed attribute name remains expressible. A kind with no class declaration has no declared fields, so every segment there keeps the plain in-scope-variable-else-literal reading.

The class may be declared by an imported module as well as by this one — M.Class.<s>.<field> and, where the bare name resolves to it (Python Reference §9.6), Class.<s>.<field> are checked against that module’s declared fields. Where a name is declared both here and by an import, this unit’s own declaration is the one checked, matching how the bare name resolves.

6.2 Match exhaustiveness

A match over a value Python has is Python’s: a number, a text, a collection, a slice, a view, an iterator or a class instance. Every arm is a test, tried in the order written, and when none holds none runs. What only this language has dispatches by CLASS, and such a match MUST cover the case set of the scrutinee’s type:

Scrutinee type Required case set
active_claim { active_claim, empty }
proof_tree { proof_tree, empty }
verify_result { verify_result }
value (§5.6) { int, bool, string, datetime, duration, real, null, blob, scientific, complex }, or any subset plus case default:; none when no arm names a class, or when every arm naming one writes Python’s parentheses
a value Python has none — no arm is required to take every value, and when none matches, none runs

Any other scrutinee type is a type_error. Over a type with a case set, a missing case is a type_error; a case label not in the set is a type_error; a repeated case label is a type_error, and the order of class labels is not significant. Value patterns are tried in the order written. A class instance the compiler follows to its class also takes the class match (§13.5).

Value match. When the scrutinee has type value (§5.6), the case labels are the value classes int, bool, string, datetime, duration, real, null, blob, scientific and complex. Like a class match (§13.5), the arms MUST either cover every class or provide a case default: catch-all; a value match that is neither exhaustive nor defaulted is a type_error. That holds where an arm writes a label of this language’s own, a class named with no parentheses (case int:, case int as n:, case null:). Where every arm naming a class writes Python’s parentheses instead, the match is Python’s, as over a value Python has: every arm is a test, tried in the order written, and none is required. In an arm naming classes, a scrutinee written as a name is read at the class they join to (§5.6), in the arm’s guard and body, and in an arm below them at what their tests failing leave it. A bool is an int (§8.1), so an arm naming int takes a bool too, as Python’s class patterns do, and covers it: the classes are all covered without an arm naming bool. An arm naming bool below an unguarded arm naming int could never run and is a type_error; to tell the two apart, write the bool arm first. A write-producible class (int, bool, string, datetime, duration, real, blob, scientific, complex) MAY bind the content with a named case <class> as <var>: arm — n : int inside case int as n: — scoped to the arm; the binding is optional (case int: with no as is well-formed). case int():, Python’s class-pattern spelling, tests what case int: tests for every write-producible class, as case <Class>(): does for a declared class (§13.5), and case str(): and case float(): are the string and real arms, the names isinstance reads for them. In a match that is Python’s, a type Python has holding one pattern binds the value to it, as Python’s does: case int(n): binds n, and a capture binds it too. Beside a label of this language’s own, a type with a pattern inside its parentheses is a type_error, as is a type only this language has holding one; bind the content with as. A label that is no class, null, default or empty, takes no parentheses: case null(): is a parse_error. The one read-only class, null, is a bare case null: arm only: attaching as <var> to it is a type_error, because an absent value has no content to bind. A case default: arm likewise MUST NOT carry an as binding (it spans more than one class, so no single payload type exists). The bound identifier obeys the reserved-word rule (§6.6) and is visible only within its arm.

Alternation. A case label MAY name two or more discriminators separated by | — case int | string: — and the arm is selected when the scrutinee’s discriminator is any one of them. An alternation is a spelling, not a widening: it covers exactly the classes it names, and the arms of the match must still be exhaustive or defaulted by §6.2’s table. case int | string: and the two arms case int: / case string: with the same body are equivalent; which to write is a matter of style.

# match-arms.dpy — one arm may name ALTERNATIVES, and a guarded arm may be
# followed by a second arm naming the same class.
def describe_reading() -> string:
    remember(Sensor.roomA.count, 42, "demo")
    c = current(Sensor.roomA.count)
    match c.value:
        case int as n if n > 100:
            return "high"
        case int as n:
            return "count " + str(n)
        case string | datetime:
            return "not a number"
        case default:
            return "other"

Four rules constrain it, each a compile-time error:

Written Refused because
case int \| int: a discriminator appears twice in one arm; an alternation with a repeat narrows nothing and is a typo
case default \| int: default already covers everything not named above, so OR-ing into it cannot narrow it. Write case default: alone
case int \| string as n: as binds the matched content at one class’s type, and the alternatives have different types — there is no single type for n. Bind in separate arms, or discriminate without binding
case int & string: there is no AND-pattern. A value has exactly one class, so a conjunction matches nothing

The third rule is the case default: restriction above, generalised: a label spanning more than one class has no single payload type, and default is that same condition reached by a different spelling.

| is the alternation separator between the labels of a case. In an expression, a guard’s included, it is the set union (§8.6), so case int as n if {n} <= allowed | extra: reads it there, and on two integers it is the bitwise or (§8.3).

Guards. A case label MAY carry a condition: case <label> [as IDENT] if <expr>:. The arm is taken when the scrutinee’s discriminator matches AND the condition holds; when the condition is false the arm is not taken and the next arm is tried. The condition is any expression with a truth value (§9.2.2’s rule for if, unchanged), and the arm’s as binding is in scope in it — case int as n if n > 0: is the form guards exist for. The body reads a name holding a value at the class its guard has shown, as a branch reads its test’s (§5.6).

A guarded arm does not satisfy exhaustiveness, and this is the rule the rest of the construct rests on. Its guard may be false, so the arm cannot be counted as covering its class: a match with no case default: must name every class of §6.2’s table in an arm with NO guard. One consequence is worth stating plainly, because it is what the rule buys: an exhaustive match always runs exactly one arm, and no program can make it fall through. A match with no class set — over a value Python has, or over a value naming no class — is not exhaustive, and runs no arm when no pattern matches, as Python’s does (value patterns, below).

Two rules follow from it, both compile-time errors:

Written Refused because
case default if <expr>: default is what runs when nothing else does; a guard on it could leave the match with no arm at all, and exhaustiveness would be unsatisfiable by any program
an arm naming a class an EARLIER UNGUARDED arm already names the earlier arm runs for every value of that class, so this one cannot be reached. Several arms may name one class provided every earlier one is guarded

Because a class may be named more than once, arm order is significant for guarded arms: the first arm whose label matches and whose guard holds runs. Where no guards are involved no class can repeat, so order remains immaterial.

Sequence and mapping patterns. A case label MAY be a SHAPE rather than a class: case [a, b]: matches a sequence of exactly two elements, and case {"id": i}: a mapping containing the key id. A sequence pattern matches a list or a tuple and a mapping pattern a dict; over any other value Python has, it compiles and never matches, at any depth. As one alternative of several it is left out of its arm, unread, and the arm stands on the alternatives left, taking its names from the one that matched: over x = 1, case 1 | [2]: is taken and case [1] | 2: is not. Over what only this language has, a shape label is a type_error.

A sequence pattern is a bracketed list of element patterns:

Element Matches Binds
IDENT, other than _ anything at that position that name
_ anything at that position nothing
a literal, as a value pattern writes one (below): an int or real (negative included), a complex, a text (adjacent texts joined), True, False a value equal to it — True and False by identity, so each takes only itself (below) nothing
a nested [...] or {...} a value of that shape whatever its own elements bind
*IDENT — MUST be last every remaining element, however many that name, as a sequence; nothing for *_
element patterns other than *IDENT separated by \| a value any one of them matches, tried left to right the names the one taken binds
an element pattern other than *IDENT followed by as IDENT what the pattern matches that name, besides what the pattern binds
a class pattern (below), as int(), C() or int(n) a value of that class what its parentheses bind

Without a * the pattern matches a sequence of EXACTLY that length; with one, of AT LEAST the fixed length. case []: is the empty sequence. A * that is not the last element is a compile-time error: matching one requires counting the fixed elements from the end, which this surface does not do. The elements after it are still read as Python reads them, so a syntax error among them is the answer over this refusal (§10.1): case [*y, 1 +]: is a parse_error at the ].

Alternatives separated by | are an element pattern too, in a sequence and as a mapping’s value: case [1 | 2, b]:, case {"k": "a" | "b"}:. Each alternative binds the same names, and a name is bound from the one taken, so case [[1, a] | [2, a, 0]]: binds a from either. Two compile-time errors, each answered below a syntax error after it (§10.1): an alternative that matches every value — _ or a capture — before the last, as case [a | 1]:, since the alternatives after it could never match; and alternatives binding different names, as case [1 | a]:, since a name could be left unbound. An element pattern MAY end in as IDENT, which binds the value it matched, alternatives included: case [1 | 2 as k, w]: binds k to the 1 or the 2.

A case label MAY be an element pattern itself when one of its alternatives is a sequence or a mapping pattern, and it is then a shape label: case [1] | [2]:, case 1 | [b, 2]:, case {"k": 1} | {"j": 2}:, case [1, _] as z:. The rules for alternatives and as above are its rules. When its last alternative is _ or a capture and it carries no guard, it matches every value, and it is a catch-all with a catch-all’s rules: case [1] | _: must be the last arm, and it covers the match.

A mapping pattern is a braced list of <literal>: <element pattern> entries. It matches when every named key is PRESENT, and says nothing about keys it does not name — a mapping pattern is a subset test, never an exact one. The key MUST be a literal as a value pattern writes one — a text, a number, a complex, True or False — and is found as in finds a key, so case {1.0: v}: matches {1: "a"}. A key that were itself a pattern would be a lookup by an unknown value, which is a search rather than a test, so a name or a bracket where a key goes is a parse_error, as in Python, and a dotted name, Python’s value pattern, is refused as it is wherever a pattern holds one. A pattern MUST NOT name one key twice: case {1: a, 1.0: b}: is a compile-time error, as in Python. Keys that are equal are one key, as 1, 1.0 and True are.

A match over a value Python has takes any expression, evaluated once, whatever its arms, and needs no catch-all. Its arms are tried in source order. Shape labels, value patterns and class patterns may stand in one match, and each MAY carry a guard — case [a, b] if a > b: — with the pattern’s own bindings in scope in it.

Class patterns. case <Class>():, case <module>.<Class>(): and case <type>(): test the class, as isinstance does (§13.9): an instance is of its class and of each base, a bool is an int, and object takes every value. A class pattern over what can never be of that class compiles and never matches. Python’s types that read as themselves, int, bool, float, str, list, tuple, dict and set, MAY hold one pattern, over the same value: case int(n): binds n, case int(1): takes only 1, and case list([a, b]): is case [a, b]: over a list. Anything past that count, or anything inside the parentheses of a declared class, complex, slice or object, raises a catchable type_error in Python’s words once the class has held: int() accepts 1 positional sub-pattern (2 given), C() accepts 0 positional sub-patterns (1 given). A keyword, as case C(n=1):, reads a field while the match dispatches, and a read stands on its own line (§7.1.1), so it is a parse_error, as it is on a type: read the field inside the arm. A class pattern MAY stand inside a sequence or a mapping pattern, or beside a literal as an alternative: case [int(), C() as c]:, case 1 | str():. A guard and a body read the name the match holds, and the name after as, as the one class an arm tests.

A class named with no (), as case C: or case int:, is a label of this language, and Python reads it as a capture. Over an instance the compiler follows to its class it is the class match of §13.5; over any other value Python has it is a compile-time error, naming case C():, as null and empty are, which label what a value or a read holds.

Value patterns. A case label MAY be a VALUE PATTERN, Python’s own:

Pattern Matches Binds
a literal — an int or real (negative included), a complex (2j, -2j, 1+2j, 1-2j, §3.5.15), a text (adjacent texts joined, "a" "b"), True, False a scrutinee == to it (§8.1); True and False only themselves, by identity (PEP 634) nothing
literals separated by \| a scrutinee equal to any one of them nothing
_ anything nothing
IDENT, when the name is not a class label anything that name
any of the above followed by as IDENT what the pattern matches that name

A name is a class label when it is a type name (§5.5), empty, or a class the unit declares; every other name after case is a CAPTURE. _ MAY end an alternation of literals (case 1 | _:), which then matches anything. Value patterns take guards like any label. The name after as is never _, which binds nothing: case 1 as _: is a parse_error at the _, and so is an as _ inside a sequence or a mapping pattern. A pattern binds each name once: case a as a: is a compile-time error at the pattern’s start, and a name bound twice inside a sequence or a mapping pattern is one at the pattern binding it the second time, as case [a, a]: at the second a. An as binds once the pattern before it has matched, so a name it binds twice is answered at the last pattern read before it: case [a] as a: at the inner a. A pattern in parentheses is the pattern it holds, here and inside a sequence or a mapping pattern, as Python’s group pattern is: case (1 | 2) as z:, case [(1 | 2), b]:. Parentheses that hold a comma, or hold nothing, are a sequence pattern instead, as square brackets are: case (1, b): is case [1, b]:, case (1,): is case [1]: and case (): is case []:, here and inside another pattern, as case [(1, 2)]:. A sequence pattern MAY also be written with no brackets at all, as Python’s open sequence pattern, when commas outside every bracket separate its elements: case a, b: is case [a, b]:, case a,: is case [a]: and case 1, *rest: is case [1, *rest]:, and it ends at the : or the guard’s if. The case-clause grammar above does not write these forms; this paragraph defines them.

A literal is read as Python reads one, here and inside a sequence or a mapping pattern. A sign after a number begins a complex literal, so an imaginary number follows the sign: case 1 +: is a parse_error at the :, case 1 + 2: at the 2, where an imaginary number is required, and case 1j + 2j: at the 1j, where a real one is. A - begins a negative number, and what follows it when that is no number is answered there: case -x: is a parse_error at the x.

Where each is admitted:

Scrutinee Value patterns
a value Python has all, beside case default:
value (§5.6) a literal of any of its classes a pattern can write (int, real, string, bool, complex), and _, among the class arms. A match naming no class is not exhaustive
active_claim, verify_result _

Arms are tried IN THE ORDER WRITTEN, class arms and value patterns alike, and the first whose label matches and whose guard holds runs; no other arm runs. A literal matches exactly when == would say the scrutinee equals it, so case 1: matches 1.0, case 1.0: matches 1 and case 1: matches True, compared exactly at every magnitude (§8.1). True and False are the exception, as in Python (PEP 634): they match by IDENTITY, so case True: takes True and not 1, and case False: takes False and not 0. A literal of a type the scrutinee can never equal compiles and never matches, and neither does one over an instance. A literal of one class does not match a value of another, whatever the two render as: over a value holding the integer 7, case "7": does not match and case 7: does. A capture and an as binding over value patterns are ASSIGNMENTS: they are made once the pattern has matched and before the guard is evaluated, and the name keeps the value when the guard is false, as in Python. They follow §6.1 like any assignment, so a name first bound in an arm is not readable after the match. _ and an unguarded capture satisfy exhaustiveness as case default: does, where a match requires it.

Over an int, a literal, an alternation, a guarded capture and _:

# value-match.dpy — a value pattern compares a scalar, as Python's does.
def sizes():
    said = []
    for n in [0, 2, 7, 40]:
        match n:
            case 0:
                said.append("none")
            case 1 | 2:
                said.append("a couple")
            case m if m < 10:
                said.append(str(m) + " is a few")
            case _:
                said.append("many")
    print(said)                             # ['none', 'a couple', '7 is a few', 'many']

A CATCH-ALL MUST BE THE LAST ARM. _, an unguarded capture, and case default: in a match with any value pattern each take every value that reaches them, so an arm after one could never run. One before another arm is refused at the _, the capture or the default itself, a pattern’s last alternative included, which is where Python refuses the first two. Each rule below is a compile-time error:

Written Refused because
case _:, case n: or case default: before another arm the arm after it could never run. Python refuses the first two for the same reason
case _ \| 1: the alternative after _ could never match. Python refuses it too
case 1 \| n: a capture that is one alternative would be unbound when another is taken. Python refuses it too; capture the whole pattern with case 1 \| 2 as n:
case 1 \| int: a class label selects by class and a value pattern by value, and one arm does not alternate the two. Give each its own arm
case bool: below an unguarded case int:, over a value a bool is an int (§8.1), so the int arm takes every bool first and this arm could never run. Python compiles it and never runs it; DKE Python names it. Write the bool arm first
case datetime("…"):, and any typed value (§3.5) a typed value is not a pattern: the call reads as a class pattern, and a value class takes no sub-patterns. Match the class with case datetime() as d: and compare d in a guard
case p.x:, a dotted name, at any depth a dotted name is Python’s value pattern, compared with what the name reads, and a pattern here reads nothing while it dispatches. Compare it in a guard: case v if v == p.x:
case {p.x: v}:, a dotted key, at any depth a dotted name is Python’s value pattern, and a key here is a literal, which a pattern finds without reading anything. Test the key in a guard: case m if p.x in m:
case {1: a, 1.0: b}: a mapping pattern names each key once, and keys that are equal are one key, as 1, 1.0 and True are. Python refuses it too
case int: over an int, and case C: over what the compiler does not follow to its class a class named with no () is a label here and Python reads it as a capture, so it is refused rather than answered differently. Write case int(): to test the class, or another name to capture the value
case n: over a value, an active_claim or a verify_result the words a class match takes are its class labels, and a misspelt label must not quietly take every value. Write case _: or case default:

A match over a datetime, a duration, a blob or a scientific remains a type_error: only this language has them, and an if compares them.

6.3 Bounded iteration

A for x in <expr>: statement MUST have <expr> of a finite collection type (claim_list, proof_tree, string_list, subject_set, result_type_set, diagnosis, or a constructed list<T>, set<T>, or dict<K, V>), a string, a tuple whose elements all have one type, a dict view (§5.1), or an iterator (§8.6), which the loop reads one element per pass. Iterating a diagnosis binds its report lines, each a string (§5.1). Iterating a dict binds its keys; iterating a dict view binds what it views, the keys, the values or the (key, value) pairs, reading a value when the loop reaches it; iterating a set binds its elements in sorted order; iterating a string binds each character — a one-character string, by Unicode character (not by byte), matching Python. A tuple iterates exactly when its elements all have one type: always so for a variadic tuple<T, ...>, and true of a fixed tuple<T1, …, TN> when T1 … TN are the same type, or are all int and real, when the loop variable is a value holding a number (for x in (1, 2.5), §5.6). A fixed tuple whose positions differ in type otherwise is not iterable — one loop variable cannot hold values of two types — so index it by a constant instead (§5.3), and the type_error says which property is missing rather than citing this rule. That refusal is about ELEMENT TYPES and never about termination: a fixed tuple’s length is a compile-time constant, which makes it the most obviously bounded iterand in the language. The tuple may be written without its brackets, as in Python: for x in 1, 2: iterates (1, 2), and for x in 1,: the one-element tuple, a trailing comma included. It is held to the same rules, and an element written with * spreads a collection into it, as it does in a bracketed tuple (§8.15), so for x in *a, 3: walks the elements of a and then 3. Any other iterand type is a type_error. The loop variable x carries the collection’s element type (§5.3) within the loop body, and stays bound after it (§6.1). So a for over a name that is already a variable assigns every element to that variable and is held to the rule for a re-assignment (§6.1): the element type must be the variable’s type, or give an empty list its element type (§5.1). After x = 1, the loop for x in ["a"]: would leave a string in an int variable. Python allows that and fails at the variable’s first use as an int; DKE Python refuses the loop itself, with a type_error that names it. A comprehension (§8.9) is bounded iteration in expression position, subject to the same finite-iterand rule. This rule, together with the absence of any recursion construct (§9.7), gives DKE Python its termination guarantee (§9.5). A counting loop is for i in range(n): — range (§8.6) returns a range, which the loop walks without building it, so a count costs no more to hold than its bounds.

An iterand whose element type nothing has supplied is iterable too, and the loop runs zero times, as in Python. That is [] or {} which no context refined (§5.1), a name bound only to one, and a collection or an iterator derived from one whose element is a tuple with such a position (enumerate([]), {}.items()). The loop variable then has no type, so what reads it cannot be checked, and is not: a statement that reads the loop variable, or a name bound from it, is not type-checked. An if whose condition reads it and a for whose source reads it lose only that expression, and their bodies are checked; a match on it is skipped with its arms. In a comprehension the same holds for a filter, the result, and a dict’s key and value (§8.9). Everything else in the body is checked as usual, and a target list over a tuple element must still match its arity (§8.11). No element is ever bound, so the loop leaves its variable as it found it.

The checker relies on those zero iterations, so a program where they may not hold is a type_error. They may not hold when the source reads a name and a loop around the iteration also gives an empty list, set or dict binding its element type, by a re-binding such as xs = xs + [1] or seen = seen | {1} (§5.1): on that loop’s next pass the source could hold elements. Declare the element type where the binding starts instead, as in xs: list<int> = [], seen: set<int> = set([]) or d: dict<string, int> = {}.

6.4 Assignment type agreement

A bare assignment x = <expr> binds x to the initializer’s static type. Binding a statement-only verb (§7.4) is a type_error — the right-hand side carries no result value.

A declared type. An annotated assignment (x: T = e), an argument for a declared parameter, a parameter’s default and a return agree with the type they declare when the value’s type IS that type or fills it. A bool fills an int, as in Python, where bool is a subclass of int, and an int or a bool fills a real. The value keeps its class: x: int = True binds True, and print(x) prints True. A tuple fills position by position, since nothing can change one, so (True, 2) fills a tuple<int, int>. A list, set or dict does NOT: a list<bool> does not fill a list<int>, because a collection is shared (§8.14), and a name holding it as list<int> could write a 5 where another name reads a bool. That is the variance mypy gives each. A written collection is held to the same rule where mypy would type it from the name it fills, so ys: list<int> = [True, False] is a type_error. A literal that holds an int beside its bools is a list<int> itself (§5.6). Where a program names an int without declaring one — an index, a slice bound or step, a repetition count, a string method’s count, width or bound, and the int arguments of range, enumerate, chr, round and int — a bool is taken as the int it is.

An annotation with no value binds nothing. x: int on its own line does nothing, in CPython and here, and x is still to be bound. ... alone on its line does nothing either, as pass does.

An expression alone is a statement. It runs, and nothing keeps its value, as in CPython, so what it would raise is raised and a store it would read is read and billed: alone, f() + 1 calls f, and an index past a list’s end stops the program. A construction alone, K("a"), reads its subject and keeps nothing.

Only a name is annotated. An annotation declares the type of the name it binds, so one written on any other target Python’s grammar reads, an attribute or an element, is refused at its :, with a value or without one: xs[0]: int = 1 is written xs[0] = 1. A name in parentheses is the name, so (x): int = 1 binds x. A target list, x, y: int or [x]: int = 1, is not annotated in Python either, and is a parse_error at its first target.

An annotation names a type. CPython reads any expression as an annotation and evaluates it. Here one that begins with a value no type begins with — x: 5 = 1, x: -int = 1, x: (int) = 1, def f(x: [int]): or def f() -> 5: — is a parse_error at its first token, in each place an annotation stands. It is a refusal, read past as CPython reads the expression, so a grammar error later on its line or below it is the answer.

A type may be written as a string. def f(x: "int") -> "K":, x: "int" = 1, n: "int" as a field and list["K"] read as the type the string spells, in names, dots, brackets and commas, and a run of strings reads as one. CPython keeps any string in that place and never reads it, so x: "a b" runs there. Here a name keeps the type it declares, so a string that spells no type is a parse_error at the string, in words saying so, and one naming no type this unit knows is told it as x: Nope is.

6.5 Name arguments

The name argument of a wire tool addresses what it names with identifiers, each identifier-shaped per §3.4: first codepoint XID_Start, remaining codepoints XID_Continue. A module is addressed by its bare name — by compile (MCP Reference §4.1) and the module tools (MCP Reference §9.4). A script, a rule or a constraint is addressed qualified, as module.name: its module’s name and its own, joined by a dot (MCP Reference §9.2, §9.3). The name is a name, not a string value; it denotes what it names, verbatim. A name of the wrong shape is refused, and nothing is compiled or removed.

6.6 Reserved-word constraint

A user identifier (declaration, class, method or field name, parameter name, loop variable, handler binding, assignment or unpack target, or a name a case pattern binds) MUST NOT be any of the reserved words in §3.7. DKE raises this at the lexer or parser; a conforming implementation MAY raise it at the typechecker.

6.7 Scope resolution for plain identifiers and paths

A bare identifier in expression position MUST resolve to a parameter, an assignment binding, a for binding, or an except handler binding visible in the current scope. An unresolved identifier is a type_error.

A dotted path H.<rest> is classified by the type of its head identifier H:

  • If H is in scope with any non-string type, the path is a field-access chain on H’s value; each segment after H MUST be a field of the previous segment’s type (§5.2).
  • If H is in scope with type string, the path is a slot-grammar reference addressing the store cell named by kind H and the following slot segments; slot-grammar references are meaningful only as verb-call arguments. A string-typed segment resolves to that binding’s value at invocation; otherwise the segment is taken verbatim as a slot name.
  • If H is not in scope, it is a literal kind name and the path is a slot-grammar reference addressing the cell named by that kind and the following slot segments. This is the ordinary form — remember(K.s.a, 1, src), count(Order.customer), avg(Reading.value) — and it is why the kind position does not require a binding: a kind is a name in the store, and a program that names one need not have declared it.

An out-of-scope head is therefore not an error in the kind position, and IS one in expression position, where the first rule of this section applies and an unresolved identifier is a type_error. The classification is keyed on POSITION, and one consequence follows that is worth stating rather than discovering: a typo in a kind position is indistinguishable from a deliberate literal and cannot be diagnosed, so it writes to a cell nobody reads instead of being refused.

6.8 Calls between scripts

A call <name>(<args>) whose head is not a verb head-word resolves as a stored-script invocation under the compile-before-call rule: the callee MUST be a declaration of the same compile unit, above or below the caller, or an already-stored script, and the unit compiles a callee before its caller, whichever is written first. A call that leads back to its caller is a type_error naming the cycle, so the same-unit call graph is acyclic (§9.7). A module’s own statement runs where it stands, so one that reaches a def below it, directly or through what it calls, is told name 'g' is not defined when the unit compiles, whether or not the program would reach that statement. Argument types are checked against the callee’s declared parameters at compile of the unit, and so is the argument count, unless a spread whose length is known only as the program runs stands among the arguments. The count is then checked as the program runs (§8.15).

This holds for both call positions. A call statement (§4.3) may name a void or a value script and discards any result. A value-call in expression position (§4.4) MUST name a value script (§4.2); its result type is the callee’s return type, written or given by its returns, and naming a void script — or a script neither declared nor stored — where a value is expected is a type_error. The compile-before-call rule makes a value-call’s return type known at the call site, and a body whose returns give its type is read before a body that calls it.

6.9 Exhaustive return

A value script (§4.2) MUST return a value on every path through its body. A conforming implementation checks this conservatively: a suite definitely returns iff one of its statements definitely returns, what follows that statement never running, where

  • a return <expr> definitely returns, and so does a raise;
  • an if/else with a non-empty else definitely returns iff both its then and else suites do;
  • a match with a case set (§6.2) definitely returns iff every arm does, and any other match iff, besides, one of its arms takes every value;
  • a try definitely returns iff its finally does, or iff its body or its else does and every handler it has does;
  • every other statement (including a for, which may run zero times) does not.

A value script whose body does not definitely return is a type_error. The check is sound (it never accepts a value script that could fall off its end with no value) and conservative (it may reject a value script that happens to return on every dynamic path but not by the structural rule above — add a trailing return). A void script (§4.2) is never subject to this rule; a bare return in it is a plain early exit (§9.2.8).


7. Verb surface

DKE Python uses DKE’s published canonical verb surface. Each verb has a fixed call form, a fixed position (statement, expression, or both), and a fixed result type when used in expression position. The path argument uses the slot symbols K (kind), K.s (kind + subject), K.s.a (kind + subject + attribute), K.a (kind + attribute). A K.s.a path may also be written as an instance’s field, s.f, and the K.s subject caveats and list_attributes take as the instance itself, s (§13.3).

7.1 The in-language verbs

Call form (canonical) Position Result type
remember(K.s.a, v, src) / remember(K.s.a, v, src, datetime("…"), datetime("…"))  /  either with , qualifiers={…} stmt (void)
update(K.s.a, v, src) / update(K.s.a, v, src, datetime("…"), datetime("…"))  /  either with , qualifiers={…} stmt (void)
forget(K.s.a) / forget(K.s) / forget(K.s.a, v, src) / forget(K.s.a, datetime("…"))  /  forget(<address>, recorded_before=datetime("…"))  /  recorded_at_or_before= / valid_before= / valid_at_or_before=  /  forget(<claim>) stmt (void)
assert(K.s.a, src) stmt (void)
prefer(K.s.a, src) stmt (void)
unprefer(K.s.a) stmt (void)
pin(K.s.a) stmt (void)
unpin(K.s.a) stmt (void)
current(K.s.a) / current(K.s.a, datetime("…"))  /  either with , known_at=datetime("…") expr active_claim
get(K.s.a) / get(K.s.a, datetime("…"))  /  either with , known_at=datetime("…") expr claim_list
why(K.s.a) / why(K.s.a, datetime("…"))  /  either with , known_at=datetime("…") expr proof_tree
why_not(K.s.a) expr diagnosis
what_needs(K.s.a) expr diagnosis
caveats(K.s) expr claim_list
dependents(<claim>)  /  dependents(<claim>, transitive=True) expr claim_list
premises(<claim>) expr claim_list
conflicts() expr claim_list
verify(K.s.a, v)  /  verify(K.s.a, v, src) expr verify_result
agreement(K.s.a, v) expr int
writers(K.s.a, v) expr int
would_forget(<address>, recorded_before=datetime("…"))  /  recorded_at_or_before= / valid_before= / valid_at_or_before=  /  would_forget(<claim>) expr int
subjects(K.a, v) expr subject_set
list_subjects(K) / list_subjects(K.a)  /  either with , datetime("…"), , known_at=datetime("…") or both expr subject_set
list_attributes(K)  /  list_attributes(K.s) / list_attributes(K.s, datetime("…"))  /  the two-part forms with , known_at=datetime("…") expr string_list
list_values(K.a) / list_values(K.a, datetime("…"))  /  either with , known_at=datetime("…") expr claim_list
list_categories() expr string_list
list_pinned() expr string_list
create_world(name) stmt (void)
merge_world(name) stmt (void)
discard_world(name) stmt (void)
list_worlds() expr string_list
compare_worlds(name) / compare_worlds(name, name) expr claim_list
list_result_types() expr result_type_set
list_scripts() expr string_list

The call form’s compound heads (list_attributes, list_values, list_subjects, list_categories, list_result_types, list_pinned, list_scripts, create_world, merge_world, discard_world, list_worlds, compare_worlds) are single snake_case tokens. They are soft keywords, as every verb’s word is (§3.7): recognized as a verb only immediately before (, so the same spelling remains a usable ordinary identifier elsewhere.

Keyword arguments. A verb takes the keyword arguments its row above writes, and no other. A keyword argument it does not take is a parse_error at the keyword that names the keywords the verb takes, or says it takes its arguments by position: current(K.s.a, a=1) names known_at=, and pin(K.s.a, a=1) says pin takes its arguments by position. A keyword argument the verb takes comes after the arguments it takes by position, and a call writes one. Written before them, it is a parse_error at the keyword that says they come first: remember(K.s.a, 1, qualifiers={}), which has no source, is one at column 20. Written after another keyword argument, it is a parse_error at the keyword that says the verb takes one keyword argument in a call: forget(K.s.a, recorded_before=t, valid_before=u) is one at column 34. Python reads such a call, so a grammar error later in it, or below it, is the answer instead (§10.1). A positional argument after a keyword argument is a grammar error, as it is in any call (§7): y = current(K.s.a, known_at=t, b) is a parse_error at column 33, the ).

Arguments by position. A verb takes as many arguments by position as its row above writes, and a fold (§7.1.1) and the literal factories (§3.5) as many as their forms write. An argument after them is a parse_error at that argument that says how many the call takes: y = current(K.s.a, t, u) is one at column 23, “current takes 2 positional argument(s) here, and this is one more”. A * or ** argument there may spread nothing, and is refused (§10.1). A call short of an argument it takes is a parse_error at its ) that says how many the call takes: remember(K.s.a) is one at column 15, “remember takes at least 3 positional argument(s), and this call gives 1”. Python reads such a call and leaves its arity to run time, so a grammar error later in it, or below it, is the answer instead (§10.1). len, isinstance and issubclass are built-in functions, and a count one does not take is answered as any built-in function’s is (§8.13).

A path argument. Where a verb’s row above writes a path (K.s.a, K.s, K.a or K), the path is the whole argument. An argument that goes on past a path as an expression is a parse_error at the argument that says so: y = current(K.s.a + 1) is one at column 13, “current takes a path here, and this argument goes on past the path as an expression — the path is the whole argument”. So is a call in the path’s place: y = current(f(x)) is one at column 13. An argument there that is no path at all is a parse_error at it that says what a path there wants: y = current("s") is one at column 13, “expected a path such as K.s.a”. An instance written where a path names one of its fields, without the field, is one where the field was wanted. Python reads such a call, so a grammar error in the argument, later in the call, or below it, is the answer instead (§10.1).

branch, commit, and rollback are language constructs (§4.3, §9.2.5, §9.2.6), not verbs, and are spelled directly in DKE Python syntax. So is the world block (§9.2.9), where world is a soft keyword: recognised only immediately after with, and an ordinary identifier everywhere else.

Walking a derivation. dependents(c) returns the claims concluded from c, one step down, and premises(c) the claims c was concluded from, one step up: the claims the rule read, or the fields a computed field reads. The operand is a claim bound to a name — read with current or get, or an element of a proof_tree or of dependents — or a claim’s claim_id (§5.2), which names the claim whatever has been written since it was read; an operand that is neither is a type error. A claim_id that names no claim the store holds is refused (refuse, catchable), as is one that two claims share; a string that is not a claim_id is an error. An element of dependents is the dependent claim itself, so a chain can be walked a step at a time in either direction. dependents(c, transitive=True) goes the whole way down in one read: the claims concluded from c, the claims concluded from those, and so on, each listed once, in name order. It answers in full on every store, one where two conclusions each rest on the other included. transitive= is a keyword, takes a bool and is given at most once; transitive=False is the one-step read. Both verbs answer for the store as it stands. A claim someone recorded has no premises. A claim the store no longer holds is refused (refuse, catchable) by both verbs, whether it is passed as the claim or as its claim_id, as forget and would_forget refuse it (§10.3). That covers a conclusion withdrawn since it was read, and one a known_at= read worked out again, which the store never recorded. An empty answer there could not be told apart from a recorded claim’s true lack of premises. A conclusion whose validity window does not cover the present still names its premises: they are read at a time inside its window. Where two rules conclude different values at one cell, each claim answers its own premises.

As-of reads. current, get, why, list_subjects, list_values and list_attributes(K.s) accept an optional datetime (§3.5.5) final argument naming the time the read is answered as of. current(K.s.a, datetime("…")) yields the single value active then, get(K.s.a, datetime("…")) the claims valid then, and why(K.s.a, datetime("…")) the derivation as it stood then — the premises that were in force at that instant, which is not always the derivation that holds now and is sometimes a derivation where none holds now at all; the three enumerations list what a traversal would find at that time — list_subjects(K, datetime("…")) the subjects of the kind, list_values(K.a, datetime("…")) the values at the attribute, list_attributes(K.s, datetime("…")) the attributes the subject carries. Omitting the argument reads the present, so a bare call and its anchored form differ only where a validity window (below) is in play. A fact carried without a window is valid at all times, so its as-of value is its present value.

The time may be future as well as past. A scheduled commitment written with a window that has not yet opened is not part of the present, so nothing at the present reports it — the anchor is how it is read back, and on the enumerations it is how it is found at all.

Reads as of what the store knew. The same six reads accept one keyword argument, known_at=, a datetime naming a moment on the other time axis: not when a fact was true of the world, but what the store had been told by then. get(K.s.a, known_at=datetime("…")) returns the claims the store held at that moment. A claim written later is not among them, and a claim later revised or replaced is, with the standing it had then, so a disagreement the store held at that moment reads as one, and current refuses over it as it does over a disagreement now. why and the three enumerations answer the same way. The positional time and known_at= are independent and may be given together: current(K.s.a, datetime("T1"), known_at=datetime("T2")) is the value the store, as of T2, held to be in force at T1. A read without known_at= answers over everything the store holds now, and one whose known_at= is later than every write answers the same.

forget is retroactive: what was forgotten was never known, as far as any read can tell. A conclusion is answered as it stood at that moment: what the rules, as they read then, concluded from what the store had been told by then, including a conclusion withdrawn since and leaving out any that only a rule written later concluded. A conclusion in such an answer may carry an empty .created_at, since when it was recorded is not kept once what it rests on has changed. The read refuses, catchably, in three cases (§10.3). The answer rests on a rule that was changed after that moment, or on one whose text at that moment the store holds no record of, so its text then is not known; the reason is derived_field, and the remedy is to read without known_at= or as of a moment after the change. A rule whose earlier text the store holds no record of takes part once it is compiled again, unchanged, for a read as of any moment from then on. Or a value the answer rests on, at the address or at one a rule reads to reach it, was replaced by one that has since been forgotten, so when it stopped being in force is no longer recorded; the reason is not_recorded. Or the answer rests on more of the store than a read as of an earlier moment may work over; the reason is limit_exceeded, and the same read over the same data refuses the same way every time.

A read that finds nothing says why, when the reason is an import. A read naming a kind the store does not know refuses (§10.3). Where a missing import is the reason, the refusal says so instead, in the two cases one produces. This holds for current, get, subjects and the list_* reads, and for forget, which a missing import would otherwise turn into a removal of nothing that reports success:

  • the name is itself a stored module (list_attributes(catalog.Widget) with no import catalog reads catalog as a kind and Widget as a subject);
  • some stored module declares a class of the name asked for (list_attributes(Widget) reads a global kind Widget, which does not exist, while catalog declares one).

Both name the module and the import that reaches it. Both are gated on the kind being absent, so a read of a kind that exists still answers empty, or a collection with no elements, where that kind holds nothing; and a name no module declares refuses as a kind the store does not know (§10.3), naming no import.

Kind names in a listing are addressable. list_categories() names a kind a module declares in the qualified form <module>.<Kind> — the form §7.1’s verbs take, given an import of that module (Python Reference §9.6). A kind no module declares carries no qualifier and is named as written. A name a listing returns is always a name the language accepts. The same holds for an address: list_pinned() names each pinned cell in the form pin and unpin take, so an address it returns, written back into unpin, lifts that pin.

list_attributes(K) and list_categories() take no as-of argument, because neither answers about an instant. The first gives the attribute names recorded for a kind and the second the kinds recorded in the store; both describe what the store holds rather than what is true at a moment, so each already covers every time and an anchor could only narrow a complete answer. Note that list_attributes therefore takes one in its K.s form and not in its K form: the first reads a subject’s claims, the second a kind’s vocabulary. Offering one where it is not accepted is a compile error naming the form that takes it.

Validity windows. remember and update accept an optional pair of trailing datetime (§3.5.5) bounds — a from time and a to time — naming the window during which the written fact is valid: remember(K.s.a, v, src, datetime("2019-01-01T00:00:00Z"), datetime("2021-01-01T00:00:00Z")). Either bound may be the literal null, leaving that side open — datetime("2024-…"), null is valid from 2024 onward with no end, and null, datetime("2024-…") up to 2024 with no start; at least one bound must be given. Omitting the pair records an always-valid fact (the default). Both bounds are inclusive. A windowed fact is selected by an as-of read whose time falls inside its window (above), so two writes at one cell with non-overlapping windows model a value that changed over time, whether one source writes both or two sources write one each. Where a window overlaps one the same source already gave at the cell, remember refuses a second value, and refuses the same value too unless the window is the same: moving a window is a revision, which is update’s. update supersedes only that source’s claims whose windows overlap the new one (§7.5).

Qualifiers. remember and update accept one keyword argument, qualifiers=: a dict (§3.5.12) from name to value stating facts the writer wants the claim to carry about itself — remember(K.s.a, v, src, qualifiers={"method": "observed", "confidence": 0.8}). It follows every positional argument, the window included, and is keyword-only because the positions after the source are the window’s. Each name is a name in the sense a path segment is — it starts with a letter or _ and continues with letters, digits, _ or - — and each value is one value a claim could hold: a string, int, bool, real, datetime, duration, blob, scientific or complex. A claim carries at most 16. An empty dict writes none. They are read back through the claim’s qualifiers field (§5.2).

A claim’s qualifiers are written with it and stay with it. The same source restating the same value with the same qualifiers is an ordinary no-op restatement; restating it with different qualifiers is refused as a conflicting write (§10.3), exactly as restating a different value is, and update is how a source revises its claim — value, qualifiers or both.

Forget is hard, and every form names what it takes. forget takes a path — K.s.a (one cell) or K.s (a whole subject) — or that path plus the value and source that name one claim, forget(K.s.a, v, src). Both v and src are required in the claim-scoped form, because a value alone does not name a claim: two sources may record the same value at one cell.

v MAY be of any type a value can be written with — string, int, bool, real, datetime, duration, blob, scientific or complex — and it is matched by value and by type together. A cell may hold one text in two types, so 5 and "5" name different claims and a literal of the wrong type names none: the call is a refuse, and the refusal states the type recorded there. Type equality in this form is EXACT. §8.1 makes int and real one numeric family for comparison, and that widening does NOT extend here: a removal names a specific claim and takes it permanently, so an int literal never reaches a stored real.

A claim can also be named by the claim itself: forget(c) removes the one claim c and leaves every other claim at its cell standing. The operand is the one dependents takes — a claim bound to a name, read with current or get or bound as an element of what they return, or a claim’s claim_id (§5.2). It tells apart the two claims the value-and-source form cannot: one source’s value recorded for two periods is two claims with one value and one source, and forget(K.s.a, v, src) removes the first of them, while get(K.s.a, datetime("…")) at an instant inside the second period returns the claim that forget then removes alone. The claim is looked for in the store as it stands. A claim removed since it was read, or written inside a branch that rolled back, is refused (refuse, catchable), as is a claim_id two claims share, and nothing is removed; a superseded claim is still recorded and is removed like any other. The claim is the whole address, so nothing follows it, and a claim’s field in the place of a path, forget(c.claim_id), is a compile error naming forget(c).

A fourth form is time-scoped: forget(K.s.a, datetime("…")) removes the values at that cell whose validity window (§7.1) covers the instant named, and leaves every other value there standing. It is the removal counterpart of an as-of read — the same instant that would tell you what was in force selects what to remove. The time-scoped form takes a K.s.a path only: an instant narrows which values AT A CELL are named, and a subject holds many cells with many windows rather than one window to narrow. Offering a time on a K.s path is a compile error naming the two forms that would have worked.

The two-argument forms are told apart by the type of the second argument, not by counting: a datetime is the time-scoped form, anything else is the start of the claim-scoped one and its source is still required.

A fifth form is range-scoped and is spelled with a keyword argument: forget(K.s.a, recorded_before=datetime("…")) removes the older versions before the instant, and never the value in force. It is the only removal form that cannot empty what it names, and that guarantee is the reason it is a separate address rather than an option on the others.

It is also the only form that takes more than a cell. Three widths — K.s.a (one cell), K.s (every cell of one subject), K (every cell of the kind) — and the safety property is the same predicate at each, so a kind-wide range cannot empty anything. K is an address ONLY with a range: a bare forget(K) is refused at compile time. Four keywords choose an axis and a boundary: recorded_before= / recorded_at_or_before= select by when a version was written, valid_before= / valid_at_or_before= by when it stopped being true. A version carrying no closed validity window is not named by any valid_* range, so the two axes may report different counts over the same cell; that is the axes disagreeing, not an error. There is no after spelling: it would name the versions newer than the instant, reaching the value in force, and removing a current value is what the first four forms are for.

The range form is keyword-only. A positional instant already means the time-scoped form above, the two select disjoint sets of versions, and a call that could be read as either would have no diagnostic available to catch it.

would_forget(K.s.a, recorded_before=datetime("…")) returns, as an int, how many versions the matching range would remove — removing none of them, recording nothing, and requiring only a read grant. It takes the same three widths as the range removal, so every removal that can be written has a preview that can be written, and both read one selection rather than agreeing by convention. With an address the bound is required; there is no bare form. would_forget(c) previews forget(c): it returns 1 while the store holds the claim, and refuses as forget(c) would when it does not.

Removal is irreversible. forget(K.s.a) permanently removes every value ever recorded at the cell, not just the current one; forget(K.s) removes the whole subject, after which its kind is gone; forget(K.s.a, v, src) removes exactly the claim it names and leaves every other claim at that cell standing, as forget(c) removes the claim c; forget(K.s.a, datetime("…")) removes the values in force at that instant and leaves the rest. Nothing is retained in history that a read can reach, and nothing in this language reinstates what it removed. Removal means what it says.

One qualifier, and it is not part of this language: while a checkpoint stands, a removed value stays recoverable outside every read, so the restore tool of the MCP Reference can bring it back: a value that was in force as a new claim at the current instant, and an older version into the history where it stood. That is a re-assertion and not a rewind — the removal still happened and the claim was still reclaimed. With no checkpoint standing at the moment of removal, nothing is kept.

Removal cascades; it does not refuse. Forgetting a fact that other facts were derived from succeeds, and those derived facts go with it. Derived facts are conclusions the engine can rebuild, not data you wrote — so a conclusion that some other standing rule still supports is re-derived immediately and stays. The practical guarantee is that a removal always completes: “removed” never means “removed unless something pointed at it.”

Removal needs the delete grant. A key scoped read-write can add and revise but cannot remove; removal requires a read-write-delete key. A key’s scope is fixed when the key is issued and is not something a program can change, so a forget that exceeds it is refused before any statement runs. Compiling a module the store holds removes what an earlier text installed and this one no longer declares — a def, a method, a rule, a computed field or a constraint (Python Reference §9.5) — and that is a removal too. Under a read-write key such a compile is refused with scope_forbidden before any statement runs: the answer names what would go, and the store is left as it was. A compile that removes nothing, changes a declaration in place or drops only the module’s body is an ordinary write.

Adding without revising takes the read-add grant. A key scoped read-add reads, and writes only what adds to the store: a value where none is in force, a restatement of a value its source holds, a value from a source that holds none there, and an assert where nothing is recorded. An update over a value its own source holds is a revision, whether it changes the value, the window or the qualifiers, and it is refused when it runs, with scope_forbidden. The program ends at that statement, try does not catch the refusal, and what the program wrote before it is kept. Committing a branch is refused in the same way when the commit would replace a value the store holds, and the branch stays open. What a standing rule concludes from an added fact changes as it does under any key. A read-add key cannot remove, cannot pin, unpin, prefer or unprefer, cannot restore a checkpoint or drop one, and cannot compile over a module the store holds; each of these is refused before any statement runs.

Absence-assertion verb carries no value. assert(K.s.a, src) asserts that the value at K.s.a is known to be absent per src — an assertion observably distinct from there being no claim at all.

Script data in a program. list_scripts() executes from inside a program and returns the stored script names as a string_list. Forgetting a script and reading its metadata are the wire’s forget and info tools (MCP Reference §9.2), not statements of a program body. The service stores no source text, so there is no source-readback verb.

7.1.1 Aggregate query functions

An aggregate query computes a summary value over a whole attribute of stored data — the values of one attribute across every subject of a kind. It is a read-only expression: it never changes the store.

Three call shapes, by the function’s arity:

Shape Call form Meaning
single-attribute fn(K.attr) fold K.attr over all subjects of K
ranked percentile(K.attr, <int>) the <int>-th percentile (rank 0–100) of K.attr
two-attribute fn(K.x, K.y) fold the paired (K.x, K.y) values (same kind K)

The available folds:

Family Functions
totals count, sum, product
central tendency avg, median, midpoint, mode, geomean, harmmean, rms
spread min, max, span, variance, stdev, iqr, mad, cv
shape skewness, kurtosis, entropy
identity / positional distinct, argmin, argmax, first, last
ranked percentile
two-attribute (association / regression) covariance, correlation, slope, intercept, rsquared

The identity / positional folds answer “which” and “when-ordered”, where the others answer “how much”: distinct(K.attr) is the number of distinct values in the attribute (where count counts the values THEMSELVES, so a location holding two values in force contributes two — §7.6); argmin(K.attr) / argmax(K.attr) return the subject whose value is smallest / largest (the identity, where min/max return the value); first(K.attr) / last(K.attr) return the earliest- / latest-recorded current value (by when it was recorded, not by magnitude). When several subjects tie for the extreme, argmin/argmax return the first subject in name order.

Equal values are one value. distinct, mode and entropy count values the way a Python set holds them: numbers equal in value are one value whatever their type, so 1, 1.0 and True count once, while the string "42" and the int 42 are two values. Over an attribute holding 1, 1.0, 2 and 2, distinct yields 2, as len({1, 1.0, 2, 2}) does. mode yields the most frequent value, and among equally frequent values the smallest (Python’s statistics.mode takes the one seen first). The value it yields is the one recorded first of those equal to it, so over 1.0 then 1 mode yields 1.0.

The two-attribute folds take two attributes of one kind (slope(Point.x, Point.y)); both attributes are read over the same subjects. K, attr, x, y follow the ordinary slot-grammar rules (§7.2) — each may be a literal name or a string parameter/variable resolved at runtime. A second operand that goes on past an attribute or a name, or is a string or a list, is a parse_error at the fold’s name that says it must be a K.attr path: y = covariance(K.x, x + 1) is one at column 5. Python reads such a call, so a grammar error inside the operand, later in the call, or below it, is the answer instead (§10.1).

The rank of percentile is an int from 0 to 100, written as an integer literal or as the name of an int parameter or variable, which is resolved at runtime as the names beside it are. A literal outside the range is a compile error, and a name holding one is a runtime refuse (§10.3). An expression is bound to a name first: r = p * 10, then percentile(K.attr, r).

A single-attribute fold takes nothing after its attribute. Whatever is written there is a parse_error at the fold’s name that gives the fold’s form, with one attribute: y = avg(K.c, 1) and y = avg(K.c, x + 1) are each one at column 5. A third argument to a ranked or two-attribute fold is one more than it takes (§7.1).

An aggregate query takes no keyword argument. One written is a parse_error at the keyword: y = avg(K.c, key=1) is one at column 14. key= and default= on min and max, and start= on sum, are each refused as what the same name takes over a list (§8.3). Python reads such a call, so a grammar error later in it, or below it, is the answer instead (§10.1).

Result type. An aggregate query returns a value (§5.1). Its runtime class follows the attribute(s): a numeric attribute yields a number (int or real); over a datetime or duration attribute, a fold yields a datetime or duration as appropriate. A bool attribute is numeric, since a bool is the int it is (§8.1). A fold that computes a value yields a number, so over True, True and False sum yields 2 and avg 0.6666666666666666. min, max, mode, first and last select a stored value and yield it as it was stored, so max over the same attribute yields True. Among equal values a selection yields the one recorded first, and last the one recorded last: over an int attribute holding True and then 1, max yields True and last yields 1, as max and [-1] answer over the list [True, 1] in Python. Over values holding a real, a fold that computes a value yields a real, whole or not: over 0.5 and 1.5 sum yields 2.0 and median 1.0, and over 1 and 1.0 sum yields 2.0, as Python’s sum and statistics.median do. Over int and bool values alone, sum, avg, span, midpoint, variance and product yield an int when the result is whole and a real otherwise, so over 1 and 3 avg yields 2, as statistics.mean does. stdev, iqr, percentile, mad, cv, geomean, harmmean, rms, skewness, kurtosis, the two-attribute folds, and median over an even count yield a real, whole or not: over 1 and 3 stdev yields 1.0, as statistics.pstdev does, and median yields 2.0, as statistics.median does. Over an odd count of int values median yields the middle value, as statistics.median does. It is computed, not selected, so over an odd count of bool values it yields 1 or 0, and over 1.0, 2 and 3 it yields 2.0, where Python’s statistics.median yields the middle element. Three folds do not follow the attribute: distinct always yields an int (a count), and argmin/argmax always yield a string (the subject), regardless of the attribute’s class; first/last follow the attribute like the value folds. Because the class is not fixed at compile time, bind the result to a name and destructure it with match (§9.2.3) to consume a specific class, or use it directly wherever a value is expressible (print, +, ==).

Units, for a fold over a datetime or duration attribute. Most folds return a duration (or datetime), which carries its own units — stdev over durations reads back as PT1H30M, and the question does not arise. Four folds instead return a plain number whose magnitude depends on a unit that has to be chosen: variance (a squared span), covariance (a span times the other attribute’s unit), slope where the first attribute is temporal (a rate per unit of time), and product. For these, the unit is the SECOND. So a variance over durations of one and two minutes is 900 (seconds squared), and a slope against a datetime is a change per second. This is worth stating because nothing about the value itself reveals which unit was chosen, and a statistic read in the wrong unit is wrong by a large factor rather than visibly broken. A fold that returns a duration or a datetime yields its exact value rounded once to a nanosecond, half away from zero, a datetime as the group’s first instant plus its exact offset: over PT0S and PT15638400.000000001S, avg, median, midpoint, stdev, iqr and mad each yield P90DT12H0.000000001S, and over -PT0.000000003S and PT0S, avg, median and midpoint yield -PT0.000000002S.

Definitions, where a fold name admits more than one. Some of these names denote a different number in different systems, and for the same reason the paragraph above gives — the value does not reveal which was chosen, and the alternative is a plausible number rather than a visibly broken one — this section states which:

Fold Definition
variance, stdev the population figures (divide by n), not the sample ones (n-1)
skewness the population moment coefficient g1, not the sample-adjusted G1
kurtosis excess kurtosis: 0 for a normal distribution, not the raw fourth moment
mad the median absolute deviation about the median, not the mean absolute deviation
entropy Shannon entropy in nats (natural log), not bits
percentile, iqr linear interpolation between the two nearest ranks, not a nearest-rank or exclusive method, computed exactly and rounded once; iqr is that method’s Q3 − Q1, one exact difference rounded once
covariance the population covariance (divide by n), where Python’s statistics.covariance is the sample one (n-1)

stdev is the one a Python reader should read twice. In CPython’s statistics module stdev is the SAMPLE standard deviation and pstdev is the population one; here stdev is the population figure. Over [2, 4, 4, 4, 5, 5, 7, 9] this language answers 2.0 and statistics.stdev answers 2.138090. Both are correct for their own definition, and only this sentence tells you which you have.

Formulas. A single-attribute fold reads the values x₁ … xₙ of the attribute: every value in force, a subject holding two values contributing both, so n is what count yields. x̄ is their mean, and mₖ, for k from 2 to 4, is the central moment Σ (xᵢ − x̄)ᵏ / n. The values in order are x₍₀₎ ≤ … ≤ x₍ₙ₋₁₎. A fold whose last column names a condition yields no value under it, and refuses as §10.3 and the paragraph on position below say.

Fold Value No value where
count n
sum Σ xᵢ
product Π xᵢ
avg Σ xᵢ / n
median x₍ₖ₎ for n = 2k + 1; (x₍ₖ₋₁₎ + x₍ₖ₎) / 2 for n = 2k
midpoint (x₍₀₎ + x₍ₙ₋₁₎) / 2, from the least and the greatest value alone
geomean (Π xᵢ)^(1/n) a value is zero or negative
harmmean n / Σ (1 / xᵢ) a value is zero or negative
rms √(Σ xᵢ² / n)
span x₍ₙ₋₁₎ − x₍₀₎
variance m₂
stdev √m₂
cv √m₂ / x̄, negative where the mean is x̄ = 0
skewness m₃ / m₂^(3/2), and 0 where m₂ = 0
kurtosis m₄ / m₂² − 3, and 0 where m₂ = 0
percentile at rank p with h = (n − 1) · p / 100 and j = ⌊h⌋: x₍ⱼ₎ + (h − j) · (x₍ⱼ₊₁₎ − x₍ⱼ₎), and x₍ⱼ₎ where h = j
iqr the percentile at 75 less the one at 25
mad the median of the distances xᵢ lie from the median, with no scale factor
entropy −Σ (c / n) · ln(c / n) over the distinct values, c the number of the values equal to each

entropy yields the int 0 where every value is equal, and a real otherwise. A two-attribute fold reads the pairs (xᵢ, yᵢ) of the n subjects that hold both attributes, x from the first and y from the second, so slope(K.x, K.y) is the slope of y against x. vₓ and vᵧ are the variances, m₂ of each:

Fold Value No value where
covariance Σ (xᵢ − x̄) · (yᵢ − ȳ) / n
slope covariance / vₓ, the least-squares slope vₓ = 0, as over one pair
intercept ȳ − slope · x̄ vₓ = 0
correlation covariance / √(vₓ · vᵧ) vₓ = 0 or vᵧ = 0
rsquared correlation² vₓ = 0 or vᵧ = 0

Over 1, 2, 3 and 10, median yields 2.5, midpoint 5.5 and harmmean 2.0689655172413794; over 10, 20, 30, 40 and 50, percentile at 10 yields 14.0, mad 10.0 and kurtosis -1.3; over -1.0, -2.0 and -6.0, cv yields -0.7200822998230956; and over 0.0, 2.0 and 8.0, geomean and harmmean refuse.

Rounding, over real values. A fold has no order to add its values in: not the order they were recorded in, and not any order a program can see. So it does not add them the way a loop over a list does. sum yields the exactly rounded sum: the exact sum rounded once, which is the value Python’s math.fsum gives over the same reals, and an int enters as itself, where math.fsum converts it to a real first. avg yields the exact mean rounded once, and rms the exact root mean square rounded once, every square exact. Python’s statistics.fmean rounds the sum before it divides, and math.sqrt(statistics.fmean([x * x for x in xs])) rounds each square, the mean and the root, so either can differ in the last place: over -6.875469, 88.671343, 29.794911, 80.180098, -77.358807, -6.18619 and -50.685433, avg yields 8.220064714285712, where statistics.fmean gives 8.220064714285714. product, geomean and harmmean yield their exact values rounded once too, in no order, every int entering as itself: over 1e-160, 1e-160, 1e160 and 1e160, product yields 1.0, where math.prod over them in that order gives 0.9999888671826831, and over ten values of 0.1, geomean yields 0.1, where statistics.geometric_mean gives 0.10000000000000002. So over ten values of 0.1, sum yields 1.0 and avg yields 0.1, and sum over the list [0.1] * 10 yields 1.0 too (§8.3). min and max select a member, and median, midpoint and span select one or combine two, exactly and rounded once, every int entering as itself. percentile, iqr and mad yield their exact values rounded once too: neither the rank nor the interpolation between two members rounds, iqr is one difference of two exact quantiles, and every deviation mad takes is exact, where the same formulas computed in reals round at each step, and over a column near 10⁸ an iqr near 1 can be wrong from its eighth digit. variance, stdev, cv, skewness, kurtosis and the two-attribute folds yield their exact values rounded once as well, so variance and stdev yield what statistics.pvariance and statistics.pstdev give, and over three values of 0.1 each of variance, stdev, skewness and kurtosis yields 0.0. entropy yields its exact value rounded once too, where a Python computation over a list rounds each logarithm, quotient and addition: over six values held once, once, twice, five, three and five times, entropy yields 1.6110656763229676, where -sum(c / n * math.log(c / n) for c in counts) gives 1.6110656763229678. Where a fold’s formula, computed as Python computes it, passes the largest real on the way though the fold’s value does not, the fold answers its value: over 1e308 and 1.5e308, avg yields 1.25e+308, where statistics.fmean raises OverflowError, and over 1e300, 1e300 and 3e300, rms yields 1.9148542155126762e+300, where Python’s formula for it answers inf. A fold whose value is itself past the largest real yields inf or -inf, as variance over those three values does.

Position. An aggregate query reads the service, so — like current and the other reads — it is used in expression position bound to a name on its own line (m = avg(Reading.value)), then referenced, or as the whole value of a return (return avg(Reading.value)), in a value script and a value method alike; it may not be written as a bare argument to another call. An aggregate over an attribute with no data is a runtime refuse (§10.3), with the aggregate_empty discriminator, and so is one that yields no value under a condition the formulas above name. A fold that compares by value — sum, avg, min, max, stdev, argmin, argmax, and the rest — over an attribute whose values are not numbers (a text attribute, or one mixing numbers with non-numeric values) is likewise a runtime refuse: the fold declines rather than treating a non-number as zero. So is one over an attribute holding values of two kinds among the numbers, duration, datetime and scientific — a duration beside an int, say — which Python’s sum and max refuse with a TypeError. The folds that do not compare values numerically — count, mode, entropy, distinct, first, last — read how the values are distributed, or return a stored value unchanged, so a string, bool, datetime, duration, scientific or complex attribute folds under all six without refusing on this ground. (A complex has no order, so the folds that compare by value refuse an attribute holding one, sum among them.) An attribute mixing those classes folds under them too, and mode, first and last return the value they chose in its own class. A blob value is the exception: count folds an attribute holding one, and the other five refuse over it — a blob is opaque, so there is nothing in it to group by, to tell apart, or to hand back as a result.

The fold names are soft keywords: recognized as an aggregate only immediately before (, so every name remains a usable ordinary identifier elsewhere.

7.1.2 Worlds

A world is a named alternative to the store that is kept until it is merged or discarded. It holds everything the store holds, and what is written in it is seen only there. A program writes into a world and reads it inside a world block (§9.2.9). A later run can enter the same world again, so one world can be built up, read and compared over as many runs as it takes.

Call form Position What it does
create_world(name) stmt creates the world name, holding nothing of its own yet
list_worlds() expr → string_list the worlds that exist, in name order
compare_worlds(a) expr → claim_list the claims world a holds that the store does not hold
compare_worlds(a, b) expr → claim_list the claims world a holds that world b does not hold
merge_world(name) stmt makes the world’s writes the store’s own, and removes the world
discard_world(name) stmt removes the world and everything written in it

Each name is a string expression. A world’s name follows the rule a branch’s name does (§9.2.5), and a world never shares a name with an open branch.

Comparing. A claim of a is left out of compare_worlds(a, b) when b holds an equal one: the same field, value and type, source, validity window and qualifiers. A claim since revised or replaced is history and is not compared. A validity window is compared, not used as a filter, so a claim valid only in the past or only in the future is compared like any other. A conclusion is compared like any other claim, so where two worlds’ facts differ, the conclusions drawn from them differ too, and both are listed. Everything the store holds is held in every world, so compare_worlds(a) is what a adds to the store. Asking in both directions answers every difference, and a field named on both sides is one where the two worlds disagree. The claims come in name order, and each is an active_claim carrying the fields every claim does (§5.2).

Keeping a world current. What the store gains reaches a world when the world is entered, compared or merged: the world’s rules then draw their conclusions over the store’s newer facts. What the store loses reaches every world at once. When a fact the store holds is forgotten, revised or replaced, or a rule is removed, a conclusion a world drew from it is withdrawn in that world too.

Merging. merge_world(a) makes a’s writes the store’s own. Where the store and a each hold a claim of one source at one field, the one written later stands, as a revision by that source would. The store’s rules then run over the merged facts. The store holds the merged writes from the moment of the merge: a read as of an earlier time (known_at=) answers without them, and each keeps, as its .created_at, the time it was written in the world. After a merge or a discard the world no longer exists. A merge that would put a value written in a at a field one of the store’s rules concludes, where the rule does not yield to a recorded value, is a refuse (asserted_field) naming the value and the rule; nothing is merged, and a keeps everything written in it.

What a world cannot change. A world adds to the store and never changes what the store held before it. Inside a world, forgetting a claim or a rule the store holds is refused, and so is prefer or unprefer at a field the store has judged, exactly as inside a branch. A conclusion the store drew from a claim the world revises with update does not hold inside the world, as it would not outside once revised (§7.5); the store keeps it, and reads it again outside the world. The same holds of a conclusion the store drew from the absence of a fact the world writes: outside, that fact would withdraw it. To see the store with one of its facts taken away, use suppose_absent (§15.1), which answers inside a world as it does outside.

Where the verbs are reached. create_world, merge_world, discard_world and compare_worlds are refused inside a world block or a branch block; list_worlds answers anywhere. Each of these is a refuse, catchable, as are: creating a world that exists, or one whose name an open branch holds; entering, merging, discarding or comparing a world that does not exist; and a name that is not one identifier (§9.2.5).

Which keys may. Creating or merging a world, and writing inside one, need a key that can write. Discarding a world removes what was written in it, so it needs the delete grant, as forget does. Reading inside a world block, compare_worlds and list_worlds need only a key that can read.

A world keeps what is written in it until it is merged or discarded.

7.2 Slot validity

Slot identifiers (kind, subject, attribute) are strings under §3.4 (UAX #31); a source is a label rather than an identifier (§7.3). At runtime, an implementation MAY refuse a slot containing characters not in the target service’s identifier alphabet; this is a refuse, not a type_error.

Values (the v in remember, update, verify, and subjects) are scalar literals — a string, an int, or a bool — and are NOT identifier-shaped; the two write verbs (remember, update) additionally accept a datetime literal (§3.5.5), a duration literal (§3.5.6), a real literal (§3.5.7), a blob literal (§3.5.8), a scientific literal (§3.5.14), or a complex (§3.5.15). The value slot is type-checked against these types; a compound value (a claim_list, a path, and so on) is a type_error.

  • A write verb (remember, update) records the value with its literal type: remember(K.s.a, 42, src) stores the int 42, remember(K.s.a, True, src) stores the bool True, remember(K.s.a, "42", src) stores the text "42", remember(K.s.a, datetime("2026-01-02T03:04:05Z"), src) stores a datetime, remember(K.s.a, duration("PT1H30M"), src) stores a duration, remember(K.s.a, 3.5, src) stores a real, remember(K.s.a, blob("48656C6C6F"), src) stores a blob, and remember(K.s.a, scientific("1.5e3"), src) stores a scientific, and remember(K.s.a, 3 + 4j, src) stores a complex. The recorded type is a property of the claim and reads back through .value (§5.6).
  • The query verbs (verify, agreement, writers, subjects) compare type-discriminatingly: the query literal’s type is part of the match, so subjects(K.a, 42) selects the subjects whose value at K.a is the int 42 and does not select a subject carrying the text "42"; subjects(K.a, "42") selects text "42" only. verify(K.s.a, v) reports a match only when the stored value and its type agree with v. This is the same equality == follows (§8.1), applied at the query boundary: a class mismatch compares unequal rather than coercing, and no value is widened or narrowed to fit — with §8.1’s one numeric exception, which applies here too. int, real and bool are one numeric family, so subjects(K.a, 5) selects a subject whose value is the real 5.0 as well as one whose value is the int 5, verify(K.s.a, 5) matches a stored 5.0, and subjects(K.a, 1) selects a subject holding True, as True == 1. It is the number that is compared, never its spelling — so a stored 5.0 matches the literal 5 while a stored 5.5 does not, and a matched claim still reads back at the type it was written (.value on that claim is 5.0, not 5). Everything else stays discriminating — the text "5" is a different class and does not match. This is what makes a query verb and a rule premise answer “which subjects have K.a == 5” the same way (§14.2). datetime, duration, real, blob, scientific and complex are not query-slot types: such a literal in verify / agreement / writers / subjects is a type_error. A real-valued claim is therefore reached with the equal int literal where one exists (subjects(K.a, 5) finds 5.0); where none does (5.5), read the claim back and match its value (§5.6).
  • agreement(K.s.a, v) returns, as an int, how many distinct sources recorded v at K.s.a (type-aware, as above). It is a count of corroboration, not a judgment of truth: a value recorded by many sources is not thereby correct, and the language does not assert those sources are independent — it reports only how many recorded the value and never ranks or certifies them. Which sources to trust is the caller’s judgment; read them by name with get (§7.1). agreement counts every source, so it takes no source argument (one is a type_error). A cell that never held v reports 0.
  • writers(K.s.a, v) returns, as an int, how many distinct store keys wrote v at K.s.a (type-aware, as above). It is the companion of agreement on the other provenance axis, and the two answer genuinely different questions: agreement counts what was cited, writers counts who wrote. Three sources cited by one key is corroboration a single party assembled; one source written by three keys is three parties independently recording the same thing. Reading either as the other is the mistake this pair exists to prevent. Like agreement it is a count, never a judgment, and it takes no source argument. A claim written outside an authenticated call has no key and is not counted — so writers may be lower than agreement at the same cell, and a cell whose claims were all written unauthenticated reports 0.

A path whose segment count does not match the verb’s slot shape is a runtime refuse with a teaching message.

7.3 Source argument

The source attribution of the write verbs (remember, update), of assert, and of the optional third argument of verify is the last call argument and MUST be a string.

A source is a label, not an identifier: any non-empty text naming where the content came from — a citation ("2026 finance act"), a register, a URL. It is stored and read back exactly as written, with no normalization, so two texts that differ in any character are two sources: "2026 finance act" and "2026-finance-act" are not one, and agreement counts them twice.

A source MUST NOT be empty, and MUST NOT hold one of the few characters the implementation reserves. A literal source that breaks either rule is a type_error. One that arrives at run time — from a parameter, a row, a computed expression — is a refuse (§10.3), so a loader can catch it per row and keep the rest. Either refusal names its fault; a reserved character is named by its code point (U+XXXX) and byte, never echoed back.

7.4 Statement-position vs expression-position

A verb listed as stmt in §7.1 MUST appear only as a statement (not bound by an assignment, not embedded in a larger expression). Binding a stmt-position verb is a type_error (§6.4). A verb listed as expr produces a value of the named result type and may bind an assignment, be a match scrutinee (if a discriminator), be iterated (if a finite collection), or be composed with operators per §8.

7.5 Write-verb semantics

  • Writes are permissive. update(K.s.a, v, src) on a location with no active claim succeeds and establishes the claim — it does not require a prior remember.
  • update revises the writing source’s own claim. update(K.s.a, v, src) supersedes the claims src holds at K.s.a whose windows overlap the new one, and no other claim. A value another source holds there stands beside the new one, so a revision never settles a disagreement (§7.6); to replace another source’s value rather than stand beside it, remove that claim with forget(K.s.a, v, src). A superseded claim stays readable through get and why. Restating exactly what src holds there — the same value, the same qualifiers and the same window — writes nothing. The same value over a different window that overlaps the old one is a revision, and the new window is the one in force. A claim src holds in a window the new one does not overlap stands, so one source records a value that changed over time by writing each period with update.
  • update inside a branch or world block revises a claim src recorded before the block as it would outside one. Inside the block that claim no longer reads, at any instant, and a later remember there is judged against the new claim. The earlier claim itself is not changed: a rollback, or leaving a world, leaves the store as it was (§9.2.5, §9.2.9), and a commit or a merge leaves it as the same update written outside the block would. Of two such claims the later stands, in the block as after the commit or the merge: where the store is revised after a world wrote, the store’s revision reads in the world in place of the world’s own. What the store’s rules concluded from the earlier claim, and anything concluded from that in turn, no longer holds inside the block either, as after the commit or the merge; the store keeps it. A conclusion the rules still reach from what the block reads holds there as it would outside.
  • A fact written inside a branch or world block where the store’s rules concluded something from that fact’s absence: the conclusion, and anything concluded from it in turn, no longer holds inside the block, as after the commit or the merge. The store keeps it, so a rollback, or leaving a world, reads it again (§9.2.5, §9.2.9).
  • pin protects. At runtime, forget(K.s.a) at a pinned location refuses, and so does forget(K.s) for the subject above it and every removal of one claim there, forget(c) included, until unpin(K.s.a) lifts the pin (§10.3).
  • pin, unpin, prefer and unprefer never change nothing. Each refuses a call that names something the store does not hold — pin or prefer at a location with no value, prefer naming a source that recorded no value there, unpin or unprefer where there is neither the mark it lifts nor a value — with the reason not_recorded. Each refuses a call asking for a state that already holds — a location already pinned, or not pinned, a source already preferred, no preference standing there — with the reason already_in_effect. A program making sure of a state catches that one and carries on. unpin and unprefer lift any pin or preference that stands, whatever the location holds now.
  • Writes are durable as executed. A later statement’s failure does not undo earlier writes; transactional grouping is the branch construct’s job (§9.2.5).
  • forget removes what you address. forget(K.s.a) removes the whole cell and forget(K.s) the whole subject, as above. forget(K.s.a, v, src) removes one claim — the one recording value v from source src — and leaves every other claim at that cell untouched. The value and the source are both required: a value alone does not name a claim, since two sources may record the same value at one cell. Naming a claim the cell does not hold is a refuse (§10.3), not a silent no-op; get(K.s.a) lists what is actually there. forget(c) removes the claim c a read returned, which is how one of two claims with one value from one source is chosen. forget(K.s.a, datetime("…")) addresses by time instead of by value: it removes the values at that cell whose window covers the instant. An instant no value covers removes nothing and is not an error — unlike the claim-scoped form, it names a moment rather than a claim, and a moment nothing was true at is an ordinary answer.

7.6 Disagreement between sources

Two claims may stand at one location with different values from different sources. That is a state the store holds, not an error.

  • Both claims remain. Neither write fails and neither claim is discarded, whether the second is written with remember or with update: update revises only its own source’s claim (§7.5). Reads at the location keep working.
  • Disagreement is between VALUES at a location. It is a property of what the claims say. Which caller recorded a claim does not enter into it, and neither does which source each one cites: a rule that concludes on both sides of a disagreement is a single producer, and the location it concludes at is in disagreement like any other. conflicts() and caveats() report it.
  • Nothing resolves a disagreement for you. The service never chooses between the claims and never marks a disagreement settled. No verb takes a disagreement as its argument. What it will do is record which source you act on: prefer(K.s.a, src) states that at this location you go by src. That is your judgment held beside the claims — it changes no claim, discards nothing, and leaves the location in disagreement.
  • The two sides are never one recorded value and one derived value. A disagreement is between two recorded values, or between two values rules derived. A field a rule or computed field derives cannot also be written (§13.7, §14.3), so a recorded value is never added beside a derivation at one location.
  • A disagreement ends when the claims change. forget one of them, or have one source update its claim to the value the other holds, and the location is no longer in disagreement. It clears as a consequence of ordinary claim writes — that is the only way it clears.
  • A standing rule reads every value at a location in disagreement (§14) and derives from each one that satisfies its condition. Two sources disagreeing m against n therefore yield two conclusions, one drawn from each, and the disagreement travels into the conclusion rather than stopping at the premise. Deriving from both sides is not a resolution: nothing chooses between the claims, and the location stays in disagreement.
  • A preference names a source that is there. prefer(K.s.a, src) refuses at a location holding no value, and when src recorded no value at that location — a preference over nothing is a mistyped address or source, not a judgment (§7.5). A location with one value is not in disagreement, and you may still prefer its source there.
  • A preference covers the sources it weighed. It holds until you withdraw it with unprefer(K.s.a). If a further source later disagrees at that location, that is a disagreement you have not spoken to, and .preferred reads False on every claim there until you state a preference over the sources now present — which may be the same source again: stating it over the new set is a new judgment, and is refused as already in effect only while the preference still covers every source there. Deriving is unaffected either way — a preference records a judgment and marks a claim; it does not steer a rule.

conflicts() (§7.1) answers which locations are in disagreement at the moment you call it; caveats(K.s) asks the same question about one subject.

A read of a location where you have stated a preference returns every claim standing there, exactly as it did before — a preference marks a claim, and changes neither what a read gives back nor what a rule derives. Each claim carries .preferred (§5.2): True on the one you act on, False on the rest. It reports whether your judgment is in force, so it reads False on every claim at a location where a source you have not weighed is disagreeing. current(K.s.a) still refuses a location holding competing values; use get(K.s.a). It refuses on the CLAIMS standing there, not on how many sources wrote them — a rule reading a location in disagreement concludes once per side (§14.4), and all of its conclusions cite the same derived provenance, so a derived location can hold competing values from one source.

verify(K.s.a, v) answers at such a location rather than refusing, and it answers about every claim standing there: .match is True when v is one of the competing values, so at a location holding both "20" and "99", verifying either one matches. Both are recorded there, and the store reports what it holds rather than choosing between them.

Because there is no single value at such a location, .actual reports none — it is the empty string — and .conflicted (§5.2) is True to say why. That distinction is the point of the field: an empty .actual also arises at a location where nothing has been written, and the two are not the same observation. .conflicted is False there, and False at any ordinary location, where .actual reports the value that stands.

A source-scoped verify(K.s.a, v, src) is never marked conflicted. It asks what one source recorded, which has a single answer however loudly the others disagree — so .actual carries that source’s value even at a location in disagreement.


8. Built-in operators and functions

An operator given operands it does not take is a type_error. Where CPython raises a TypeError over the same operands, each of a class CPython has — a number, a string, a list, a tuple, a set, a dict, a view or an iterator — the refusal is CPython’s sentence, naming the classes as CPython does (a real is its float and a string its str): unsupported operand type(s) for -: 'str' and 'str', '<' not supported between instances of 'str' and 'int', can only concatenate list (not "int") to list, can't multiply sequence by non-int of type 'float', bad operand type for unary -: 'str', argument of type 'int' is not iterable, and an augmented operator named with its =, as in unsupported operand type(s) for +=: 'int' and 'str'. Where an operand is of a type CPython has no class for — a value read from a claim, a datetime, a duration — or is an instance, or where CPython answers and this language refuses, the refusal names the operand types the operator takes. A range is a list<int> (§5.3), so a refusal names it list.

8.1 Comparison

Operator Signature
==, != any two values → bool. Values of different types are unequal, as in Python: "7" == 7 is False, and so are [1] == ["a"] and [1] == (1,). Numbers compare exactly by value across int, real, bool and a value holding a number (§5.6): 1 == 1.0 is True, and a bool is the int it is, True == 1 is True. A complex (§3.5.15) equals another when both parts are equal, and equals a number exactly when its imaginary part is zero and its real part equals that number: 1 + 0j == 1 is True, 1j == 1 is False. A value compares with any value as the class it holds. Two lists, tuples (of any two lengths), sets or dicts compare element by element, at any depth, and two discriminators D compare by .kind. An iterator (§8.6) is equal only to itself, as in CPython. A keys or an items view (§5.1) equals a set, or another keys or items view, holding the same elements, and a values view is equal only to itself, as in CPython. Two operands are refused: a class instance (below), and an answer read from the store — a claim, a claim list, a proof and the other shapes of §7 — against anything but its own type
<, >, <=, >= (N, N) → bool for numeric operands N ∈ {int, real, bool, scientific} or a value holding a number (mixed classes compare exactly by value, and a scientific by its magnitude), (string, string) → bool lexicographically by character (Unicode codepoint order), (datetime, datetime) → bool chronologically, (duration, duration) → bool by length, (list<T>, list<T>) → bool for an orderable T (two element types that meet count as one T, as for ==, and so do a scientific and a number), (tuple, tuple) → bool for two tuples of any lengths whose positions are orderable and meet where both have one — lists and tuples compare element-wise lexicographically — or (set, set) → bool by inclusion, for any two sets, and so do (dict_keys, dict_keys) and (dict_items, dict_items) and any pairing of a set with either view, each view compared as the set of its elements (§5.1)
in, not in (string, string) → bool — substring membership (sub in s is True when sub occurs anywhere in s; the empty string is in every string) — or element membership → bool: x in xs over a constructed list<T>, x in s over a set<T>, x in t over a tuple, or k in d over a dict<K, V> (testing its keys), k in d.keys() the same way, x in v over a values or an items view (§5.1), or x in e over an iterator (§8.6), which reads it up to and including the first element found — each element is compared with x as == compares them, so a number is found among numbers of either class by value (1 in [1.0] is True) and an item of another type is not found ("a" in [1] is False); not in is the negation of any
is, is not (C, D) → bool for two collections C, D ∈ {list, set, dict} — identity: a is b is True when the two are one collection (§8.14), so a name bound to another’s list is it and a copy is not; is not is the negation. A number, a string or any other value is not an operand (below)

String ordering (<, >, <=, >= on two strings) compares lexicographically — character by character in Unicode codepoint order, the shorter string ordering first when it is a prefix of the longer ("a" < "ab"). So an uppercase letter orders before a lowercase one ("Z" < "a"), matching Python. Each ordered comparison takes two operands of the same family — two numbers, two strings, two datetimes, two durations, or two collections; a string compared against a number is a type_error (no coercion). A complex has no order: <, >, <= and >= with one on either side is a type_error, stated as Python states it ('<' not supported between instances of 'complex' and 'int'), and so is everything that ranks its operands — sorted, min, max, a list or tuple ordered against another. Compare one with == and !=, or order by a part or the magnitude: sorted(zs, key=abs). Comparisons chain (a < b < c is (a < b) and (b < c) with b evaluated once, §4.6).

Infinity and NaN. inf and -inf order beyond every other number, as in Python. A NaN compares as IEEE 754 has it: math.nan == math.nan is False, math.nan != math.nan is True, and every ordered comparison with one is False. A collection is where DKE Python departs, because it has one NaN (§3.5.7) and Python finds a NaN only when it is the very object:

  • Membership, .index, .count and a dict’s keys find the NaN by value, and a set or a dict holds it once. float("nan") in [math.nan] is True, {math.nan: 1}[float("nan")] is 1, len({math.nan, -math.nan}) is 1, and [float("nan")] == [float("nan")] is True; Python answers False, KeyError, 2 and False.
  • Sorting puts a NaN after inf, as IEEE 754’s total order does: sorted([3.0, math.nan, 1.0]) is [1.0, 3.0, nan], where Python’s leaves the list in an order that depends on where the NaN stood, [3.0, nan, 1.0].
  • min and max answer nan when any operand is one, as IEEE 754’s minimum and maximum do: max(1, x) and min(x, 1) are nan for a NaN x, where Python answers 1 and nan by position. With a NaN present, min(xs) can therefore differ from sorted(xs)[0].

Temporal ordering (<, >, <=, >=) compares two datetime values chronologically — the earlier instant orders first — and two duration values by length, the shorter first. Both compare by value, not by spelling: duration("PT90M") < duration("PT1H30M") is False and so is >, because the two denote the same length (§3.5.6), while duration("PT90M") <= duration("PT1H30M") is True. Likewise a datetime written with fractional seconds orders against one written without them by the instants they denote, not by their text (datetime("2026-01-02T03:04:05Z") < datetime("2026-01-02T03:04:05.5Z")). A datetime and a duration are different things — an instant and a length — so comparing one against the other, or against a number or string, is a type_error. Temporal values are orderable elements, so they may also be sorted (sorted(timestamps)) and compared inside lists and tuples.

List and tuple ordering (<, >, <=, >=) is element-wise lexicographic (Python): the first pair of unequal elements decides, and if one sequence is a prefix of the other the shorter orders first ([1, 2] < [1, 2, 3], (1, 2) < (1, 3)). Two lists must have orderable element types that meet, as for list equality, or that are a scientific and a number; two tuples must have orderable positions, and the positions both have must meet — their lengths may differ, as two lists’ may (a homogeneous or heterogeneous fixed tuple, or a variadic tuple). So [1, 2.5] <= [1.0, 2.5] and (1, 2.5) < (1.0, 3) are True, (1, 2) < (1,) is False, and () < (1,) is True. A shared position of another kind is a type_error: in (1, "a") < (1, 2) the second positions are a string and an int. An orderable element type is a numeric type, scientific, string, datetime, duration, or a nested list/tuple whose own element types are (recursively) orderable — so the comparison recurses all the way down ([[1], [2]] < [[1], [3]]; [[2]] < [[10]] is True because element 2 < 10, not a comparison of rendered text). A value holding a number (§5.6) is a numeric element, and at every depth an int and a real compare by value, so an equal pair is passed over as Python passes it: [1, 2.5] < [1.0, 3] is True. A non-orderable element (e.g. [1] < ["a"], mixing int and string) is a type_error. An empty list orders before any non-empty list ([] < [1]).

Set ordering (<, >, <=, >= on two sets) is inclusion, as in Python: a <= b is True when every element of a is in b, a < b when also b has an element a does not, and >= and > are the same with the operands swapped. Elements are found as == compares them, so numbers are found by value exactly ({1} <= {1.0} is True, {2**53 + 1} <= {2.0**53} is False) and an element of another type is not found ({1} <= {"a"} is False). Inclusion is a partial order: {1} < {2} and {2} < {1} are both False, and so is {1} == {2}. That is why a set is not an orderable element (the list above): sorted, min, max and the ordering of two lists or tuples of sets are type_errors, where Python answers them in an order that depends on the input’s.

Numeric equality (== / !=) compares an int and a real exactly, by the values they hold: 1 == 1.0 is True, 2.0 == 2 is True, 1 == 2.0 is False, and 2**53 + 1 == 2.0**53 is False although that int becomes 2.0**53 when it is converted to a real. The ordered operators, membership, set identity, sorted, min and max compare the two the same exact way, at every magnitude, matching Python. A bool is the int it is, as in Python: True == 1 is True, False == 0.0 is True, True < 2 is True, and it compares, orders, dedups and is found the same exact way.

A scientific orders by its magnitude. Against an int, a real, a bool or another scientific, the ordered operators read a scientific as the real nearest the number it writes, and compare that exactly, as two numbers compare above. So scientific("2.0e4") > 100 and 1.5 < scientific("2.0e0") are True, and a list of scientifics sorts from the smallest magnitude up. Its significant figures take no part in the order: scientific("6.022e23") and scientific("6.0220e23") are neither < nor > each other, and <= and >= both hold. Equality keeps them, and keeps the class: those two are unequal (§3.5.14), and scientific("1.0e0") == 1 is False, though <= and >= both hold there too. A rule premise orders a stored scientific the same way (§14.2), so a program and a rule give one answer to one comparison. sorted, min and max take a collection of scientifics, and a list or a tuple of them orders against one of numbers; a collection that holds a scientific together with another class of number orders the same way, each element keeping its class (§5.6): sorted([scientific("2.0e0"), 1, 2.5]) is [1, scientific("2.0e0"), 2.5], min(scientific("1.0e0"), 2) is scientific("1.0e0"), and [scientific("1.0e0"), 0] < [1, 5] is True. Its elements still compute nothing, so sum over it is a type_error.

Values of different types are unequal. == takes any two values and != is its negation, so "7" == 7 is False and "7" != 7 is True, as in Python; no value is converted to another’s type to be compared. The rule holds at every depth and in every comparison built on ==: membership, .index, .count, .remove, a dict key read, set inclusion and difference, a literal case over a value, and is. A search that finds nothing answers as it would for any absent value, with the catchable refuse Python raises ([1].index("a") refuses as 'a' is not in list, {1: 2}["a"] as 'a'). Ordering is a different question: <, >, <= and >= still require two values of one family, since Python raises there too ("a" < 1 and [1] < ["a"] are type_errors).

A bool is an int, by value and by class. Wherever a value is compared or computed with — an operator, a fold, membership, a set or a dict key, a literal case, a rule premise and a query verb (§14.2, §7) — a bool is the number it is. And wherever a program names an int, a bool fills it, as in Python, where bool is a subclass of int: a parameter, annotation or return declared int, an index, a bound and a repetition count (§6.4), and a class arm case int over a value, which takes a True (§6.2). It keeps its class throughout, so it prints as True. What does not widen is a collection’s element type: a list<bool> is not a list<int> (§6.4).

Class instances are not compared by == / !=. The table above lists every operand pair the two operators accept, and a class instance is not among them, so comparing two instances is rejected whether or not they share a class. Compare a field of each instead, or use isinstance(x, C) to test an instance’s class. Membership is deliberately different: x in xs over a list<C> IS defined, and compares by an instance’s identity — its subject together with its concrete class — so a C built on one subject is not found in a list holding a C built on another. An instance therefore HAS an identity in this language; what it does not have is an equality operator.

Identity (is / is not) asks whether two lists, sets or dicts are one collection. Each is shared (§8.14): every name, parameter, element and loop variable holding it holds the same one, and is answers as Python does. After b = a, a is b is True; a.copy(), list(a), a comprehension and a literal each build a new collection, so a is a.copy() and [1] is [1] are False, and an element read is the collection its container holds, so m[0] is m[1] is True after m = [row, row]. is not is the negation, and both chain as every comparison does. Two collections of different kinds or element types are never one, so [1] is {1} and [1] is ["a"] are False, as in Python. Over a number or a string there is no identity to ask. Whether two equal numbers or strings are the same object in Python depends on how the implementation interns them, which the language leaves undefined, so there is no answer a DKE Python program could match, and is there is a compile-time error naming == or !=. Over any other value — a tuple, a class instance, a datetime — the error names the three kinds is takes. A rule premise compares a field’s value, so is there is refused as it is over a number. x is None and x is not None have no == form, because DKE Python has no None: a rule asks that a field has no value with absent(…) (§14.2). Writing either is a compile-time error naming no comparison — and the absence premise, in a rule premise that can take one. is is reserved (Appendix B), as it is in Python, and names nothing: is = 1 is a parse_error at column 1.

List equality (== / !=) is element-wise: the two lists must have element types that meet — one type, numbers of either class, a complex and a number (§3.5.15), or collections whose own elements meet, at any depth — and each pair of elements compares as == compares them, so [1] == [1.0] and [[1], [2.5]] == [[1.0], [2.5]] are True, as in Python. Elements of different types are unequal, so [1] == ["a"] is False. An empty list<unknown> compares with any list ([] == [1] is False), and lists compare unequal when their lengths differ or any pair of elements differs (recursively, for nested lists). List membership (x in xs) tests a constructed list<T>, comparing x with each element as == does. A number is found among numbers of either class by value, as 1 == 1.0 compares: 1 in [1.0] and 2.5 in [1, 2.5] are True, and so are [1.0].index(1) and [1, 1.0].count(1) counting 2. An item of another type is not found: "a" in [1, 2] is False and [1, 2].count("a") is 0. An element that is a collection is found by list equality, so [1] in [[1.0]] is True. The in/not in operator does not test a store-derived set; to walk one, use the for <var> in <set>: statement (§6.3, §9.2.2), whose in is the loop keyword, not this operator.

Tuple equality is position by position, so (1, "a") == (1.0, "a") is True. Their lengths may differ, and two tuples of different lengths are unequal: (1, 2) == (1,) is False, and so is (1,) == ("a",), whose one position holds another type. Set equality is element-wise and order-independent — two sets are equal when they hold the same elements, and numbers are equal by value as a set’s members are: {1, 2.0} == {1.0, 2} and {(1,)} == {(1.0,)} are True, while {1} == {"a"} is False, and so is a set compared with a list or a tuple. Two dicts are equal when they map equal keys to equal values: {"a": 1} == {"a": 1.0} and {1: "a"} == {1.0: "a"} are True. Set membership (x in s), dict membership (k in d, over keys), and tuple membership (x in t, over every position) compare the item as == does. A set and a tuple find a number among numbers by value, as a list does (1.0 in {1, 2.5} is True), and so does a dict among its keys, as d[k] and d.get do (§3.5.12). An item of another type is not found, and a record tuple is searched for any of its fields: "alice" in ("alice", 30) is True and "a" in {1: 2} is False. Dict equality (== / !=) is order-independent (Python dict equality): two dicts of a matching key and value type are equal when they hold the same key→value mapping, regardless of insertion order ({"a": 1, "b": 2} == {"b": 2, "a": 1} is True). Keys and values compare as list elements do, so numbers of either class compare by value, and a key or value of another type is unequal: {"a": 1} == {"a": "x"} is False. An empty dict compares with any dict, so {} == {"a": 1} is False.

A value (§5.6) may be compared with == / != against any value without first destructuring it — a literal of any of its classes, another value, or a collection; the result is bool regardless of the runtime class, and a class mismatch compares unequal rather than coercing (c.value == 41 is False when the content is text, even the text "41", and c.value == "True" is False when it holds True). This mirrors Python’s cross-type ==. Ordering (<, >, …) requires operands of the same family, so a value must be destructured through match (§5.6) into an orderable arm before it can be ordered — unless it holds a number (§5.6), when it orders and compares against any number as the number it holds ([1, 2.5][0] < 2 and [1, 2.5][0] == 1.0 are True).

8.2 Logical

Operator Signature
and (T, U) → T ⊔ U — short-circuit, operand-return
or (T, U) → T ⊔ U — short-circuit, operand-return
not any → bool

Truthiness. The eight read-result types have no truth value. Every other value has a truth value and may be used where a condition is expected — an if / elif condition (§9.2.1), a comprehension if filter (§8.9), the condition of a conditional expression a if c else b (§8.10), and the operands of and / or / not. Falsy values are False, 0, 0.0 (and -0.0), a complex whose parts are both zero (0j), a zero scientific, a zero duration, the empty string "", an empty list / set / dict / tuple, an empty blob, and None; every other value — every non-zero number, every non-empty container, every datetime, every class instance, every iterator (§8.6), used up or not — is truthy. This matches CPython for every type that has a truth value, and the exclusion below is a deliberate divergence rather than a gap.

The eight read-result types have no truth value: active_claim, claim_list, proof_tree, diagnosis, subject_set, string_list, verify_result, result_type_set. Each of the four positions above refuses one, and the refusal names that position.

This diverges from CPython on purpose, where an empty container is falsy. A store-derived collection that came back empty and a read that found nothing to tell you are different answers, and a truth value collapses them into one False — so if claims: would read as “there are claims” while also meaning “the read had nothing to say”. Test what you mean instead: .length or len() for emptiness (§8.6), and for … in to walk (§5.1).

and / or return an operand, not a coerced bool. a and b evaluates a; when a is falsy it returns a (leaving b unevaluated), otherwise it returns b. a or b evaluates a; when a is truthy it returns a (leaving b unevaluated), otherwise it returns b. So name or "anonymous" is name when it is a non-empty string and "anonymous" when it is empty; count or 0 is the first non-zero of the two; x > 0 and label is label when x > 0, else False. Both are short-circuit — the right operand is evaluated only when the left does not already decide the result, so a right operand that would refuse is not reached when the left short-circuits.

The result type is the join T ⊔ U of the two operand types: identical types collapse to that type; an int / real pair joins to real without converting the operand returned (1 or 2.5 is 1, §3.5.7); an empty-list operand unifies to the concrete list type; and two different value scalars (x > 0 and "yes" → bool or string) yield a value (§5.6 — expressible and equatable). Two operands with no common result type — e.g. a list and a dict — are a type_error (and / or must be able to name the type of whichever operand it returns). For two bool operands the result is a bool, identical to a plain boolean and / or.

not always yields a bool. not x is True when x is falsy and False when x is truthy, for any value x.

DKE Python uses the word forms and / or / not; there are no && / || / ! tokens. In precedence (§4.6) or binds loosest, then and, then not, then the comparison operators — so not a == b is not (a == b) and a or b and c is a or (b and c).

# operand-return.dpy — `or` and `and` return one of their operands, not a bool.
def operands():
    name = ""
    print(name or "anonymous")              # anonymous
    count = 3
    print(count > 0 and "some")             # some
    zero = 0
    print(zero and 10 // zero)              # 0

The last line never divides: zero is falsy, so and returns it and 10 // zero is not evaluated.

8.3 Arithmetic

Operator Signature
+ (int, int) → int; (N, N) → real when at least one operand is real; (complex, complex) → complex, and a complex with an N on either side
-, * (int, int) → int; (N, N) → real when at least one operand is real; (complex, complex) → complex, and a complex with an N on either side
/ (N, N) → real — true division (always a real, including int / int: 7 / 2 is 3.5); (complex, complex) → complex, and a complex with an N on either side
// (int, int) → int; (N, N) → real when a real operand — floor division (rounds toward −∞: -7 // 2 is -4)
% (int, int) → int; (N, N) → real when a real operand — floor modulo (result takes the sign of the divisor: -7 % 2 is 1, -7.5 % 2 is 0.5)
% (string) (string, any) → string — percent formatting, as CPython’s: each conversion takes the next element of a tuple on the right, or the one value that is not. %(key)d looks its key up in a dict or a list. The conversions are s r a d i u o x X e E f F g G c, with the flags - + # 0, a width, a precision and * for either; %% is a %. Every fault is a catchable refuse as the program runs, in CPython’s words: "%s %s" % 1 refuses with not enough arguments for format string
@ CPython’s matrix product, of %’s level, and @= beside it. No value here answers it, so every one is a type_error naming its operands: 1 @ 2 refuses with unsupported operand type(s) for @: 'int' and 'int'
** (int, int) → int when the exponent ≥ 0; (N, N) → real when a real operand, or when a negative exponent is written as a literal (§8.3, Value-dependent typing) — power (right-associative)
+ (sequence) (list<T>, list<U>) → list<…> — the element type unifies as a list literal’s does, so [1] + ["a"] is list<value> and [] + xs takes xs’s type; (tuple<…>, tuple<…>) → tuple<…> — the POSITION lists concatenate, so tuple<int, string> + tuple<bool> is tuple<int, string, bool>. Two strings concatenate too (§8.4)
* (repetition) (string, int) → string; (list<T>, int) → list<T>; (tuple<T1, …, TN>, int) → tuple<T1, …, TN, T1, …> — either order ("ab" * 3 and 3 * "ab"). The count is an int; a real count is a type_error, and a count ≤ 0 gives the empty sequence. A tuple’s type IS its position list, so its count must be written as a literal (§8.3, Value-dependent typing) and the result may name at most 65536 positions
- (prefix) unary negation: int → int, equivalent to 0 - x; bool → int (-True is -1); real → real, keeping the sign of zero; duration → duration; complex → complex, negating both parts
~ (prefix) bitwise inversion: int → int, equivalent to -x - 1 (~5 is -6); bool → int (~True is -2)
+ (temporal) (datetime, duration) → datetime (either order); (duration, duration) → duration
- (temporal) (datetime, duration) → datetime; (datetime, datetime) → duration; (duration, duration) → duration
* (temporal) (duration, N) → duration (either order) — scale a length
/ (temporal) (duration, N) → duration — split a length; (duration, duration) → real — how many times one fits into the other
% (temporal) (duration, duration) → duration — the length left over
- (set) (set<T>, set<U>) → set<T> — difference, a.difference(b) (§8.6). A keys or an items view (§5.1) on either side is read as the set of its elements, and beside one any collection is read as set(...) reads it (§8.13): ["a", "b"] - {"a": 1}.keys() is {'b'}
\|, &, ^ (int, int) → int, a bool counting as the int it is, and (bool, bool) → bool — bitwise or, and and exclusive or (Bitwise operators, below): 12 & 10 is 8, True ^ True is False. (set<T>, set<U>) → set<…> — union, intersection and symmetric difference, a.union(b), a.intersection(b) and a.symmetric_difference(b) (§8.6). They take two integers, two of a set, a keys view and an items view (§5.1) in any pairing, or a keys or an items view beside any collection on either side, a view read as the set of its elements and a collection as set(...) reads it (§8.13). \| also takes two dicts, (dict<K, V>, dict<K2, V2>) → dict<…> — a merge, the same as {**d, **e} (§3.5.12), and d \|= e changes d in place, e being anything update reads (§8.14), as in d \|= [("b", 2)]. They take nothing else
<<, >> (int, int) → int, a bool counting as the int it is — left and right shift by a count of bits: 1 << 4 is 16, -5 >> 1 is -3 (Bitwise operators, below)

The numeric operand set is N ∈ {int, real, bool}. A bool computes as the int it is, as in Python — True + True is 2, True / 2 is 0.5, sum([True, True]) is 2 — so wherever the rules below say int, a bool counts as one. A value holding a number (§5.6) is a numeric operand too: its result is a real wherever the rules below make it one for either class, and otherwise a value holding a number. +, -, *, //, %, ** promote: when both operands are int the result is int; when at least one is real the result is real. / is the exception — it is true division and always produces a real, so int / int promotes (7 / 2 is 3.5, 8 / 2 is 4.0); a floored integer quotient is // (7 // 2 is 3). // and % are the matched floor pair: both round toward −∞ / take the sign of the divisor, so the identity a == (a // b) * b + (a % b) holds — and it holds at both operand classes, so -7.5 % 2 is 0.5 just as -7 % 2 is 1. A remainder of zero takes the divisor’s sign as well, which is visible only on a real: 6.0 % -2 is -0.0.

Complex arithmetic. +, -, *, / and ** take a complex (§3.5.15) beside another complex or beside a number, which computes as the complex with that real part, and answer a complex: (3 + 4j) * 2 is (6+8j), (3 + 4j) / 2 is (1.5+2j), 1 / (3 + 4j) is (0.12-0.16j), True + 1j is (1+1j). Each answers what CPython answers for the same operands, the signs of zero parts included. An int operand computes as the real nearest it, as in Python, though == compares it exactly (§8.1): (2 ** 53 + 1) + 0j is (9007199254740992+0j), and 2 ** 53 + 1 == 9007199254740992 + 0j is False. The prefix - negates both parts. // and % do not take a complex, and neither do divmod, round, int and float: each is undefined for one in Python as well. A division by a zero of any class refuses at runtime with the division_by_zero discriminator, in Python’s words, complex division by zero. A part may be infinite or a NaN, in an operand or in the result, and computes as Infinity and NaN (below) says.

** raises a complex to a number or a complex, or a number to a complex, and answers a complex computed as Python computes it, each step correctly rounded (below): 1j ** 2 is (-1+0j), (3 + 4j) ** 0.5 is (2+1j), and 2 ** 1j is (0.7692389013639721+0.6389612763136348j). An exponent that is a whole number no larger than 100 in magnitude is computed by repeated multiplication, so (1 + 1j) ** 100 is (-1125899906842624+0j) exactly. A zero base under a negative or a complex exponent refuses with the division_by_zero discriminator, in Python’s words, 0.0 to a negative or complex power; a base that is not zero never does. A part past the range is inf or -inf, where Python raises OverflowError: complex exponentiation, and Infinity and NaN (below) gives the rest.

Bitwise operators. &, |, ^ and the prefix ~ compute on an int’s two’s-complement bits, as Python’s do, so -12 & 10 is 0, -12 | 10 is -2 and ~5 is -6; each answers an int in range. Two bools given &, | or ^ answer a bool, as in Python, and ~ and the shifts answer an int for a bool too: ~True is -2, True << 2 is 4. >> rounds toward −∞, as // by a power of two does, so -5 >> 1 is -3, and a count of 64 or more answers 0 or -1 by the sign. << multiplies by a power of two: -1 << 63 is -9223372036854775808, and a result outside the int range refuses with arithmetic_overflow, where Python answers a larger integer. A negative count refuses either shift with the value_error discriminator, in Python’s words, negative shift count. An operand that is not an int or a bool is a type_error, a set included for the shifts and ~.

Temporal arithmetic. datetime and duration compute together, with the operand types deciding what an operation means. Adding a duration to a datetime shifts the instant (t + duration("PT1H") is an hour later, and subtracting shifts it earlier); subtracting one datetime from another gives the length between them; and two durations add and subtract to a duration.

A duration also scales: multiplying by a number stretches it (duration("PT30M") * 3 is PT1H30M, duration("PT1H") * 2.5 is PT2H30M) and dividing by a number splits it (duration("PT1H") / 4 is PT15M). Dividing a duration by another duration asks how many times one fits into the other and yields a plain real — the answer has left the temporal domain, so duration("PT3H") / duration("PT45M") is 4.0, not a length. The matching % gives the length left over: duration("PT3H10M") % duration("PT45M") is PT10M.

Shifts respect the calendar, so datetime("2024-02-28T12:00:00Z") + duration("P1D") is 2024-02-29T12:00:00Z in a leap year and 2023-03-01T12:00:00Z in a common one.

Addition, subtraction, %, and scaling by an int are exact to the nanosecond — no result is rounded. Scaling by a real (* or /) rounds to the nearest nanosecond, halves away from zero, since a fractional nanosecond is not a duration.

Combinations that name nothing are type errors: two datetimes have no sum (only a difference), a duration minus a datetime is meaningless (the operand order carries the meaning), a duration times a duration is an area rather than a time, a number divided by a duration is a frequency rather than a time (division is not commutative here), and // and ** are not defined over temporal values. Subtracting a later instant from an earlier one yields a negative duration (§3.5.6), which renders and reads back with a leading -. A result whose span exceeds what a duration holds (about ±292 years), or whose instant falls outside years 0001 through 9999, is a catchable refuse rather than a wrapped value; so is dividing or taking a remainder by a zero-length duration.

Sequence repetition. * is additionally overloaded so a string, a constructed list or a constructed tuple may be repeated by an int count (either order): "ab" * 3 is "ababab", "-" * 5 is "-----", [0] * 4 is [0, 0, 0, 0], [1, 2] * 3 is [1, 2, 1, 2, 1, 2], and (1, 2) * 2 is (1, 2, 1, 2). A string * int yields a string; a list<T> * int yields a list<T>. A non-positive count yields the empty sequence ("x" * 0, [9] * -1 and (1, 2) * 0 are "", [] and ()), matching Python. The count MUST be an int — a real or non-int repeat count, and a string, list or tuple multiplied by anything but an int, is a type_error. The result is bounded (the count is a finite int), so termination (§9.5) is preserved.

A tuple repeats under one further condition, and the reason is its type. A string and a list<T> keep their own type however often they are repeated — length is in neither — while a tuple’s type IS its position list (§3.5.11, §5.3), so (1, 2) * 2 is a tuple<int, int, int, int> and a constant index into it still reads the position it names. Naming that type needs the count, so the count must be written as a literal: the value-dependent typing rule below. (1, 2) * n for a count the run supplies is a type_error. Its message says so, and names what to write when the count really is decided as the program runs: repeat a list, whose length is outside its type, and convert — tuple([1, 2] * n) gives a tuple<int, …>. A tuple built that way is a variadic tuple (§5.1), and it repeats by any count at all, literal or not, because it has no arity in its type to name. A repeated tuple type may name at most 65536 positions; a literal count whose product exceeds that is a type_error rather than a very long type, and the list remedy has no such ceiling.

Value-dependent typing. A few results are typed by an operand’s value and not by its type alone. One rule governs every one of them, and it is stated here once rather than at each: a static type may depend on an operand’s value only where the program writes that value as a literal. An integer expression over literals folds to a literal before the type is decided (§8.3, above), so (1, 2) * (1 + 1) counts as written; a name, a parameter, a field read or a call result does not. The same test decides the type and what the expression does when it runs, so the two never disagree about which case a program is in.

Three rules use it, and they are applications of one rule rather than three exceptions:

  • ** gives a real for a negative int exponent when the exponent is a literal — 2 ** -1 is 0.5 (§8.3, above);
  • * repeats a tuple when the count is a literal, giving the repeated position list — (1, 2) * 2 is a tuple<int, int, int, int> (§8.3, above);
  • t[i] on a tuple reads the indexed position’s own type when i is a compile-time constant (§5.3).

Where the value is not written the expression is refused — or, for **, is typed as an int and refuses when it runs — and the message says why: a type is fixed before the program runs, and it cannot be two things at once. The rule costs no function in any of the three, because every negative exponent, every repeat count and every tuple position a program can name, it names with something it wrote.

abs(x). The built-in abs returns the absolute value of a number: abs(x) takes exactly one operand and preserves its numeric type — abs(-5) is the int 5, abs(-3.5) is the real 3.5. The abs of a complex is its magnitude, a real: abs(3 + 4j) is 5.0. A magnitude past the largest real is inf, where Python raises OverflowError: absolute value too large (abs(complex(1.7e308, 1.7e308))), and a part that is infinite makes it inf and one that is a NaN, beside no infinite part, makes it nan, as in Python. An argument that is neither a number nor a complex, or an argument count other than one, is a type_error. abs of the most-negative int refuses at runtime (its negation is out of range), the same as unary minus (§8.3 above).

ord(s), chr(i). The Unicode codepoint round-trip. ord(s) takes a one-character string and returns its integer codepoint (ord("A") is 65, ord("æ") is 230); chr(i) takes an int codepoint in range(0x110000) and returns the one-character string for it (chr(97) is "a", chr(937) is "Ω"). A non-string ord argument or a non-int chr argument (or an argument count other than one) is a type_error. At runtime, ord of a string whose length is not one, and chr of a codepoint outside range(0x110000) — or of a lone surrogate (U+D800..U+DFFF), which has no UTF-8 encoding — are catchable refuses (§10.3).

round(x[, n]). The built-in round rounds a number half to even (“banker’s rounding”): a value exactly halfway between two candidates goes to the even one, so round(0.5) and round(2.5) are both 0 and 2, and round(0.125, 2) is 0.12. The optional second operand n is an int digit count (it may be negative). The result type follows the operands: round(int, …) is an int; round(real) without a digit count is an int (rounded to the nearest whole number); round(real, n) is a real — a real even at n = 0, so round(2.5, 0) is 2.0. A negative n rounds to a power of ten (round(35, -1) is 40). Because a real holds an IEEE-754 double, rounding follows the stored value, not the written decimal — round(2.675, 2) is 2.67 (the nearest double to 2.675 is slightly below it), matching CPython. A non-numeric first argument, a non-int digit count, or an argument count other than one or two is a type_error; a result whose magnitude leaves the int range refuses at runtime. Without a digit count, an infinity or a NaN refuses at runtime too, an infinity with arithmetic_overflow and a NaN with value_error, as int(x) does (§8.13); with one, round(x, n) of an infinity or a NaN is x, and a result past the largest real is inf or -inf (§8.3, Infinity and NaN).

divmod(a, b). Returns the pair (a // b, a % b) — the floored quotient and the remainder, computed in one call. Both operands must be numeric, and the pair promotes together: tuple<int, int> when both are int, tuple<real, real> when either is real — never a mixed pair. An operand that is a value holding a number (§5.6) with no real beside it gives a tuple<value, value>, whose two positions still share one class when it runs. divmod is defined in terms of // and %, so it takes exactly the operands those two share (§8.3 above); a non-numeric operand, or an argument count other than two, is a type_error. The quotient rounds toward negative infinity and the remainder takes the sign of the divisor, exactly as the two operators do, so divmod(7, 2) is (3, 1), divmod(-7, 2) is (-4, 1), divmod(7.5, 2) is (3.0, 1.5), and q * b + r == a always holds. A zero divisor is a runtime refuse (§10.3), and divmod of the most-negative int by -1 refuses on overflow, the same as // and %.

pow(base, exp) and pow(base, exp, mod). With two arguments pow is **: pow(a, b) has the type, the value and any refusal that a ** b has, so pow(2, 10) is 1024, pow(2, -1) is 0.5, and a refusal names **, as Python’s own message for either spelling does. With three it is modular exponentiation, which no operator spells. Every argument must be an int or a bool, and the answer is the int Python gives for base ** exp % mod, computed below the modulus so that an exponent of any size finishes: pow(2, 10, 1000) is 24 and pow(2, 9223372036854775807, 1000000007) is 529367677. The answer takes the modulus’s sign, as % does, so pow(2, 3, -5) is -2, and a modulus of 1 or -1 gives 0. A negative exponent raises the base’s inverse modulo mod, so pow(3, -1, 7) is 5. A base with no inverse, and a modulus of 0, are runtime refuses (§10.3) with the value_error discriminator, in Python’s words. A count other than two or three is a type_error. So is a non-integer in the three-argument form, in Python’s words for the first class whose power Python asks, the base’s, then the exponent’s, then the modulus’s: a real is refused, pow() 3rd argument not allowed unless all arguments are integers; a complex, once the base and the exponent are numbers, complex modulo; and where neither speaks the three classes are named, so pow(2, "a", 5) is refused, unsupported operand type(s) for ** or pow(): 'int', 'str', 'int'. A value is told to narrow, as it is beside any operator (§5.6).

sum(iterable), min(…), max(…). These reduce an in-memory iterable (a list / set / dict / string / store-derived collection), distinct from the same-named store-aggregate folds sum(K.attr) / min(K.attr) / max(K.attr) (§7.1.1), which take a bare K.attr path — the argument shape selects the form. sum(iterable) adds numeric elements: all-int → int (an empty iterable is 0), any real element → real; complex elements → complex (sum([1j, 2j]) is 3j), and a complex start beside numeric elements, or a number start beside complex ones, gives a complex too; any other element is a type_error. Over elements that are values holding numbers ([1, 2.5], §5.6) the sum is such a value, of the class the additions give when it runs (sum([1, 2.5]) is 3.5). Where any element or the start is a real and none is a complex, the sum is the exact sum of the start and every element, rounded once to the nearest real, ties to even, so the order the elements come in cannot change it: sum([0.1] * 10) is 1.0 and sum([1e16, 1.0, -1e16]) is 1.0, where CPython 3.11’s sum, which rounds after each addition, answers 0.9999999999999999 and 0.0. An int enters as the integer it is, so sum([9007199254740993, 0.5]) is 9007199254740994.0. A sum whose rounded value is past the largest real is inf or -inf, as sum([1e308, 1e308]) is in Python, but sum([1e308, 1e308, -1e308]) is 1e+308, where Python’s, which overflows on the way, answers inf. An infinite element adds as IEEE 754 adds, so sum([math.inf, -math.inf]) is nan, and a NaN element makes the sum nan, as in Python. A zero sum is -0.0 only when the start and every element are -0.0, and the default start is the int 0: sum([-0.0]) is 0.0 and sum([-0.0], -0.0) is -0.0, as in Python. An all-int sum is the exact integer, refused as arithmetic_overflow only when that total is outside the signed 64-bit range. A sum holding a complex takes each element as Python widens it, a number as the complex with that real part, and each of its two parts is then that part’s exact sum rounded once, as above: sum([0.1j] * 10) is 1j and sum([1e16j, 1j, -1e16j]) is 1j, where CPython 3.11, which adds one element at a time, answers 0.9999999999999999j and 0j. The store fold sum(K.attr) is exactly rounded too, 1.0 over ten stored 0.1s (§7.1.1). A second argument, written sum(iterable, start) or sum(iterable, start=…), is the value the elements are added to, first, as in Python, and over numbers it is an operand of the exact sum above: sum([1, 2], 10) is 13, an empty iterable answers the start (sum([], 2.5) is 2.5), and a real start makes the sum real. A start that is not a number takes what + adds to it. A list or a tuple start concatenates the elements: sum([[1], [2]], []) is [1, 2], and sum([(1,), (2,)], ()) is (1, 2). A duration start adds lengths: sum([duration("PT1H"), duration("PT2H")], duration("PT0S")) is PT3H. A datetime start is shifted by each duration in turn. The sum has the type + gives the start and one element, and then that and a second, since the run decides how many elements there are, so the tuple sum above is a tuple<int, ...>, of runtime length. Over an empty iterable a list start answers a new list equal to it, not the start itself. A string start is a type_error in Python’s words, sum() can't sum strings [use ''.join(seq) instead]. A start that + cannot keep adding the elements to is a type_error too: one that takes no element, like a list start over ints, and one that takes a single element but not a second, like a duration start over datetimes. A tuple element whose positions share no type is refused for the same reason. min and max take either one iterable or two-or-more scalar values, returning the smallest / largest in the order sorted puts them in (§8.1): numbers by value, a scientific by its magnitude, strings lexicographically, a datetime chronologically and a duration by length. The elements or values must share an orderable type — numeric, scientific, string, datetime, duration, or a nested list/tuple of those, in which case the comparison recurses lexicographically (§8.1). Of two equal elements the first is returned. An int and a real compare by value and the one returned keeps its class (min([1, 2.5]) is 1, max([1, 2.5]) is 2.5). min/max of a runtime-empty iterable is a catchable refuse (Python’s ValueError); min([]) on a literal empty list is instead a compile-time type_error (its element type is unknowable) — unless a keyword-only default=<value> is given, which is returned for an empty iterable in place of the refuse (min([], default=0) is 0). The default= is valid only with a single iterable argument (not with multiple positional values), and its type must match the element type.

A keyword-only key=<name> compares the elements by what the named function returns for each, in place of the elements themselves: max(words, key=len) is the longest word. The name is that of a builtin taking one argument (len, abs, ord, str, …) or of a script — declared in the same unit, imported by from, or qualified by its module (key=m.f) — and it resolves as a call to that name does. A method of a built-in type may be named as well, as in key=str.lower, which calls lower on each element, and an element of another type is a type_error, descriptor 'lower' for 'str' objects doesn't apply to a 'int' object. The key is called once per element, in order, after the call’s other arguments are evaluated. Only the keys are compared, so the keys must be orderable and the elements need not be (max([{"a": 1}, {}], key=len) is {'a': 1}); of two elements with equal keys the first is returned. min and max take a key over one iterable or over several values (min("bb", "a", key=len) is 'a'), and default= answers an empty iterable beside a key as it does without one, in either order. key= takes a name and nothing else: a lambda (§8.8), a method of a value (d.get), a call, a class or an expression is a parse_error, and so is key=None, CPython’s spelling of no key, which DKE Python writes by leaving key= out. sum takes no key=, nor does the store fold min(K.attr); a key whose values do not order, or which cannot take the elements, is a type_error, and so is a key over what is no collection, sorted(1, key=abs), in Python’s words, 'int' object is not iterable.

** is right-associative and binds more tightly than the multiplicative and unary operators (§4.6): 2 ** 3 ** 2 is 2 ** (3 ** 2) = 512, and -2 ** 2 is -(2 ** 2) = -4. An int base raised to a non-negative int exponent yields an int (2 ** 10 is 1024); a real operand yields a real. An int base with a negative int exponent yields a real when the exponent is a literal — 2 ** -1 is 0.5 and (-2) ** -1 is -0.5, as in Python. When the exponent is not a literal the expression is a runtime refuse instead, and the message says why: a value’s type is fixed before the program runs, so 2 ** n would have to be an int and a real at once. Write the base as a real (2.0 ** n) when the exponent can be negative. This is one application of a general rule (§8.3, Value-dependent typing), not a rule of its own.

Division by zero (/ or //, integer or real) and modulo by zero are runtime refuses (§10.3). Integer overflow — any integer +, -, *, //, %, **, <<, or prefix - whose mathematically correct result falls outside the signed 64-bit range — is a runtime refuse with a teaching message rather than wrapping. A real result whose magnitude leaves the finite range is inf or -inf, and so is such a part of a complex result (Infinity and NaN, below). Reals compute to the precision of an IEEE-754 double and render in the canonical decimal form of §3.5.7.

Infinity and NaN. A real may be an infinity or a NaN (§3.5.7), and so may a part of a complex (§3.5.15). Every operation takes one and answers IEEE 754’s value, as Python’s operators do: math.inf + 1 is inf, math.inf - math.inf is nan, 1 / math.inf is 0.0, and an operation with a NaN operand is nan, save where IEEE 754 answers a number (math.nan ** 0 is 1.0). A result past the largest real from finite operands is inf or -inf, as 1e308 * 10 is in Python. A zero divisor refuses at runtime when the operands are finite (1 / 0.0), as in Python. Python answers most of these and raises for some of the same kind; DKE Python answers each one, so it departs from Python where Python raises, and at three corners where Python’s answer breaks its operator’s own rule:

Expression DKE Python Python 3.11
a ** with a real operand and a result past the range: 10.0 ** 400 inf OverflowError
round(x, n) past the range: round(1.7976931348623157e308, -308) inf OverflowError
an infinite or NaN dividend over a zero divisor: math.inf / 0.0, math.inf // 0.0, math.inf % 0.0, divmod(math.inf, 0.0) inf, inf, nan, (inf, nan) ZeroDivisionError
math.inf // 1 inf, the floor of the quotient nan, derived from %
-1 % math.inf nan: no remainder in [0, inf) leaves -1 inf
-1 // math.inf -0.0, the floor of -0.0 -1.0

A complex adds and subtracts as Python does, part by part, a number beside it as the complex with that real part, so complex(math.inf, 1) + 1 is (inf+1j). A product, a quotient and a power of operands whose parts are all finite, and a power’s base not zero, answer each part as its exact value rounded once to the nearest real, ties to even: inf or -inf only where that value is past the largest real, and a zero of the value’s sign where it is below the least subnormal one. Python’s algorithms round at every step, and can carry a part past the range on the way though the part itself is not, so their answers can differ from the exact ones, in the last places or entirely:

Expression DKE Python Python 3.11
(0.1+0.2j) * (3-4j) (1.1+0.2j) (1.1+0.20000000000000007j)
complex(1e200, 1e200) * complex(1e200, 1e200) infj (nan+infj)
complex(1e308, 1e308) / complex(2, 2) (5e+307+0j) (inf+0j)
(1 + 1j) ** 101 (-1125899906842624-1125899906842624j) (-1125899906842634.5-1125899906842629.5j)
(1+2j) ** (3+4j) (0.12900959407446688+0.03392409290517013j) (0.129009594074467+0.03392409290517014j)
complex(1e200, 1e200) ** 2 infj (nan+nanj)
complex(1e300, 0) ** 2 (inf+0j) OverflowError
complex(-1, 0) ** 1e308 (1+0j) ZeroDivisionError

A part whose exact value is zero is a signed zero: of the sign Python’s algorithm gives it where Python answers that part a zero, and +0 where Python answers it otherwise or raises. So (-1+0j) ** 2 is (1+0j), as in Python, and (-8) ** 0.5 is 2.8284271247461903j, its real part exactly zero, where Python’s is 1.7319121124709868e-16. With an infinite or NaN part, or a zero base, an operation computes by Python’s algorithms, as IEEE 754 gives each step: complex(math.inf, 0) * 2.0 is (inf+nanj), as in Python, and complex(math.inf, 0) / 0j is (inf+nanj), C99 Annex G’s quotient by zero, where Python raises ZeroDivisionError.

The departures of the math and cmath functions are §8.16’s.

Powers, logarithms and moduli are correctly rounded. A ** with a real operand, and a complex’s abs, answer the real nearest the exact result. A ** over a complex answers each part’s exact value rounded once (above); one with an infinite or NaN part computes through powers, logarithms, exponentials, trigonometric functions and moduli, and each of those steps is rounded the same way. So each such answer depends on its operands alone, and is the same wherever it is computed. Python takes these functions from the platform it runs on. They are correctly rounded there almost always; where one is not, Python’s last digit differs from DKE’s. abs(0.00023571114292694718-0.00029192858677257096j) is 0.0003752093318068705, the exact modulus rounded, where Python on a platform that rounds this one wrongly prints 0.00037520933180687044.

A quotient of two ints is exact. / over two ints answers their exact quotient rounded once to the nearest real, as Python’s does: 9007199254740993 / 3 is 3002399751580331.0, where dividing the two as reals, each already rounded, gives 3002399751580330.5.

Floor division is exact. A // with a real operand, and the quotient of divmod, answer the floor of the exact quotient, rounded once to the nearest real. Python floors a quotient it has already rounded, which is off in the last place where that rounding crosses an integer: 4.064465006478144e+17 // 27.2389457125187 is 1.4921521006630456e+16, where Python answers 1.4921521006630458e+16. The floor is a real at any size, so -1.1036980736650453e+20 // -3.778510741988643e-30 is 2.920986994691367e+49, and a zero dividend keeps the quotient’s sign: 0.0 // -3.0 is -0.0.

An int beside a real is exact. +, -, *, /, //, % and divmod with one int operand and one real answer the exact value of the operation, rounded once to the nearest real. Python converts the int to a real first, which rounds an int past 2**53, and then rounds the result again: 9007199254740993 + 0.5 is 9007199254740994.0, where Python answers 9007199254740992.0, and 9007199254740993 % 1000.0 is 993.0, where Python answers 992.0. A remainder is the dividend less the divisor times the floor of their exact quotient, so a zero one takes the divisor’s sign: 9007199254740993 % -3.0 is -0.0. An int a real holds exactly, and a real that is zero, infinite or NaN, answer as they did. ** is exact the same way, with such an int as its base or its exponent, and so is such an int to a negative int literal power: (-1.0000000000000002) ** 9007199254740993 is -7.38905609893065, where Python rounds the odd exponent to an even real and answers 7.389056098930649, and 9007199254740993 ** -1 is 1.1102230246251564e-16, where Python answers 1.1102230246251565e-16. The math functions take such an int exactly too (§8.16).

An integer expression whose operands are all literals is evaluated at compile time, in a range wider than int, and replaced by its result — but only when that result is itself in range. So 2 ** 63 - 1 is the int 9223372036854775807, even though the intermediate 2 ** 63 is not representable: the range rule above governs the expression’s value, not each step of a calculation the compiler did on the program’s behalf. When the folded result is out of range the expression is left exactly as written and refuses at runtime, so an overflow stays catchable (§10.3). A division or modulo by zero is never folded, for the same reason: its refusal belongs to the run.

A folded expression is also free at the meter — it states a value rather than asking for work, as a prefix - on a literal does by folding into it. Charging for 2 ** 63 - 1 while the same number written out in full costs nothing would price the legible spelling above the opaque one.

** refuses in two further cases at runtime, and neither of them is an overflow. Both forms compile: the fault is in the values, not the shapes. A zero base under a negative exponent (0 ** -1, 0.0 ** -2) is a division by zero written differently, and refuses at runtime as one — discriminator division_by_zero, carrying the same wording Python gives that route. A negative base under a fractional exponent (x ** 0.5 with x a negative real) has no real value; Python answers it with a complex number, which the real this expression is typed as cannot hold — so it too refuses at runtime, with the discriminator complex_result. The computation is performed wherever the expression is typed complex (§3.5.15), which is two places. Written with a negative literal base and a fractional literal exponent, the power is typed complex and computes as a complex power does, each part its exact value rounded once: (-8) ** 0.5 is 2.8284271247461903j. And a complex operand makes any power a complex, so complex(x) ** 0.5 computes what x ** 0.5 refuses at runtime for a negative x. What is declined is the power of two operands typed as numbers and not both literals, since its type is fixed before its values are known. Neither restriction reaches the neighbouring shapes: a negative base under an integral exponent is ordinary ((-8.0) ** 3.0 is -512.0), and so is a zero base under a non-negative one (0 ** 0 is 1, 0 ** 1 is 0).

8.4 String concatenation

The + operator concatenates two strings:

Form Signature
(string, string) → string string concatenation

A + with one string operand and one non-string operand is a type error — as in Python, which raises TypeError, DKE Python does not implicitly render the other side. Convert it with str(…), which renders any expressible value to its text form: "count = " + str(xs.length), "flag = " + str(b), "at = " + str(t.value). An f-string applies str(…) to each field automatically, so f"count = {xs.length}" is the idiomatic form (§3.5.10).

List concatenation. With two list operands (and no string), + concatenates: [1, 2] + [3, 4] is [1, 2, 3, 4], a new list. xs += ys, xs.append(v) and xs.extend(ys) change xs in place to the same concatenation (§8.14), and xs += c takes any finite collection c, as extend does, where xs + c takes a list alone: xs = [1] then xs += (2, 3) leaves [1, 2, 3], and xs + (2, 3) is refused in Python’s words, can only concatenate list (not "tuple") to list. The result element type follows the list-literal rule (§5.1): two lists of the same element type give that type (list<int> + list<int> → list<int>), two lists of differing scalar element types box into list<value> ([1] + [3.5] → list<value>, holding numbers when both sides hold numbers, §5.6), and an empty operand takes the other side ([] + [1] → list<int>); a non-scalar element mismatch is a type_error, the same kind-strictness list literals apply. A list combined with a non-list, non-string operand ([1] + 5) is a type_error.

Tuple concatenation. With two tuple operands, + concatenates: (1, 2) + (3,) is (1, 2, 3), a new immutable tuple. The result type is the two operands’ position lists joined, not an element type unified — each position of a tuple keeps its own type, which a constant index reads (§3.5.11, §5.3), so its type is the sequence of its positions, and tuple<int, string> + tuple<bool> is tuple<int, string, bool>. A constant index into the result therefore reads that position’s own type: after t = (1, "a") + (5,), t[2] is an int and t[1] a string. Both arities are known at compile time, so this is exact. The empty tuple takes the other side (() + (1,) → tuple<int>). A tuple combined with a non-tuple operand ((1,) + [2], (1,) + 5) is a type_error.

A concatenation involving a variadic tuple — one whose length is not known until run time — has no static arity, so its result is a variadic tuple and needs one element type covering both sides. That type is unified by the rule list concatenation uses above: equal element types give that type, and differing scalar element types box into value. Element types that do not unify (a collection element beside a scalar one) are a type_error, because no static type describes an unknown-length run of one type followed by positions of another. Two fixed tuples never meet this restriction.

8.5 Field access

See §5.2 and §5.4 for the full table of well-typed field accesses. Accessing a field not in the table is a type_error.

8.6 Indexing, slicing, and string methods

See §5.3. Indexing selects one element; the index expression MUST be int, and an out-of-range index refuses at runtime (§10.3). A string may additionally be indexed to read a single character (a negative index counts from the end — s[-1] is the last character), sliced (s[start:end:step], all three components optional; negative bounds count from the end and a negative step reverses, e.g. s[::-1]) to read a substring, and transformed by the string methods s.find, s.rfind, s.index/s.rindex (like find/rfind but refusing when the substring is absent), s.count, s.startswith, s.endswith, s.replace, s.upper, s.lower, the strip family s.strip/s.lstrip/s.rstrip, the character-class predicates s.isspace/s.isdigit/s.isdecimal/s.isnumeric/s.isalpha/s.isalnum/ s.isupper/s.islower/s.istitle/s.isascii/s.isprintable/s.isidentifier (each () → bool), the list-bridge methods s.split (→ list<string>) and sep.join (an iterable of strings → string, §5.3), and the collection-returning methods s.partition / s.rpartition (→ tuple<string, string, string>, splitting once at the first / last occurrence of the separator) and s.splitlines (→ list<string>, splitting at line boundaries) (§5.3). A constructed list<T> (§5.1) is indexed (xs[i], negative from-end permitted) and sliced (xs[a:b:c], yielding a new list<T>) by the same rules. Slicing and every string method except split/partition/rpartition and index/rindex and translate are total — they never refuse: an out-of-range slice bound is clamped, an inverted range yields "" (or the empty list), find returns -1 when the substring is absent, startswith/endswith and the is… predicates return a bool, and case mapping applies the full mapping, which can lengthen a string (§5.3). split/partition/rpartition refuse only on an explicit empty separator (empty_separator, §10.3), index/rindex refuse when the substring is absent (substring_not_found), and a slice step of 0 is likewise a catchable refuse (§5.3). All positions count characters (a string) or elements (a list) (§5.3).

The number methods. Every number and a complex take x.conjugate(), with no argument. A complex’s conjugate is the value with its imaginary part’s sign changed, (3+4j).conjugate() is (3-4j), and a number is its own: an int or a bool answers an int, so True.conjugate() is 1, and a real answers itself. Their parts are fields, .real and .imag (§5.2). A receiver that is not a number is refused, and a claim’s value is told to narrow first, as an operator tells it (§5.6).

The rest take no argument either, each on the numbers Python gives it to, a bool as the int it is, and each is refused elsewhere as .conjugate() is:

Method Taken by Answers
x.is_integer() real bool: whether it has no fraction, (2.0).is_integer() is True
x.hex() real string: its exact value in hexadecimal, (1.5).hex() is 0x1.8000000000000p+0
x.as_integer_ratio() int, bool, real tuple<int, int>: its value in lowest terms, (1.5).as_integer_ratio() is (3, 2)
x.bit_length() int, bool int: the bits its magnitude takes, (255).bit_length() is 8
x.bit_count() int, bool int: the one bits in its magnitude, (7).bit_count() is 3

An int and a bool have two more fields: .numerator, the int itself, and .denominator, which is 1. A ratio past the signed 64-bit range refuses with arithmetic_overflow, where Python answers a larger integer, and the denominator of a real as small as 0.00001 is already past it. An infinity refuses with arithmetic_overflow and a NaN with value_error, in Python’s words, cannot convert Infinity to integer ratio and cannot convert NaN to integer ratio. math.inf.is_integer() is False, and math.inf.hex(), (-math.inf).hex() and math.nan.hex() are inf, -inf and nan, as in Python.

What a type itself answers. Three methods are a type’s own, called on its name as in Python. A name the program binds is no longer that type, as with any built-in.

Call Answers
str.maketrans(x, y) dict<int, int>: each character of the string x to the character at its place in the string y, as code points, the last mapping of a character given twice kept. A value_error where the two differ in length
str.maketrans(d) dict<int, V>: d with each string key as its code point, its int and bool keys and every value as they are. A value_error where a string key is not one character long, and a type_error for a key of any other type
dict.fromkeys(xs, v) dict<K, V>: each element of xs to the one value v, in the order of their first appearance
float.fromhex(s) real: what s spells in hexadecimal, float.fromhex("0x1p3") is 8.0, and inf, infinity and nan as float reads them. A value past the largest real is inf or -inf, where Python raises OverflowError (§8.3, Infinity and NaN), and a value_error where s spells nothing

Python answers str.maketrans(x, y, z) and dict.fromkeys(xs) with None among the values, which has no spelling here, so each is a type_error that says what to write instead. Neither takes a keyword, as in Python, and a * among their arguments spreads as at any built-in method’s call (§8.15): str.maketrans(*pair). A value of the type reaches each of them too, as in Python: "a".maketrans("b", "c"), d.fromkeys(xs, 0) and x.fromhex("0x1p3") where x is a real.

A method through its type’s name. T.m(x, …) is x.m(…), its first argument the receiver, as in Python: str.upper("a") is "a".upper(), list.count([1, 1], 1) is 2 and int.bit_length(255) is 8. T is str, int, bool, float, complex, list, dict, set or tuple, and m any method that type has here. One that changes its receiver changes what is given first: list.append(xs[0], 2) is xs[0].append(2), and as a value it is refused as that is, since the None it answers has no spelling here. A call with no receiver is a type_error in Python’s words, unbound method str.upper() needs an argument; so is a receiver of another type, descriptor 'upper' for 'str' objects doesn't apply to a 'int' object, and a name the type does not hold, type object 'str' has no attribute 'nope'. A name the program binds, in a comprehension’s for as well, is no longer that type, as with any built-in.

String templates. A template holds text and fields, read in order as CPython’s str.format reads them. {{ and }} are a brace. A field is {name!conversion:spec}, each part after the name optional. {} takes the next positional argument, {0} the one at that position and {a} the keyword, and one template may not use both of the first two. After its first part, .x reads a field and [w] an item: a w all digits is a position in a string, a list or a tuple and a key of a dict, and any other w a string key. !r, !s and !a convert first, by repr, str and ascii. :spec is what format(value, spec) takes (§3.5.10), and it may hold fields of its own, one level deep. .x on an instance names a field the store holds, which is not read in the middle of an expression (§13.3), so it refuses as the program runs, and a name bound to the field reads it. Every fault in a template is a catchable refuse in CPython’s words (§10.3): a key_not_found for a name no argument holds, an index_out_of_bounds for a position past the last, a type_error for a value that does not take what a field asks of it, and a value_error for the rest, as with Single '{' encountered in format string.

Dict and set methods, and the collection builtins. A dict<K, V> (§5.1) is viewed with d.keys() (→ dict_keys<K>), d.values() (→ dict_values<V>), and d.items() (→ dict_items<K, V>), each walking the dict in insertion order as it stands when the view is read (§5.1), and read defensively with d.get(key, default) — the value for key, or default when the key is absent. A default is required (DKE Python has no None to return for the 1-argument form); the result type is the join of V and the default’s type (the same unification and / or use, §8.2). d.pop(k, default), d.setdefault(k, default) and d.popitem() read the dict and change it at once (§8.14). A set<T> is combined with s.union(t), s.intersection(t), s.difference(t), and s.symmetric_difference(t), each returning a new sorted set, and compared by inclusion with s.issubset(t), s.issuperset(t) and s.isdisjoint(t), each returning a bool. The argument of all seven is any collection set(t) takes (§8.13) — a set, a list, a tuple, a string, a dict’s keys, or a generator expression (§8.9). union, intersection and difference take any number of them, each read over what the ones before it left, and with none answer a copy of s, as in Python: {1, 2, 3}.difference([1], [2]) is {3} and {1}.union() is {1}. For union and symmetric_difference, whose result holds elements of both, an element type other than T joins T as a list literal’s elements join (§3.5.9): two scalar value classes give a set<value> (§5.6), so {1}.union(["a"]) is {1, 'a'}, and a tuple beside a scalar is a type_error, since no value holds one. The five that keep only what is in s, or answer a bool, take any element type, since an element of another type equals nothing in s: {1}.issubset(["a"]) is False, {1}.isdisjoint(["a"]) is True and {1}.intersection(["a"]) is an empty set<int>. The operators on two sets are the same relations: s | t, s & t, s - t and s ^ t are the four combinations, and s <= t, s < t, s >= t and s > t are subset, proper subset, superset and proper superset (§8.1). An operator takes two sets where the method takes any collection, as in Python. The augmented forms s |= t, s &= t, s -= t and s ^= t change s in place to the new set (§4.3), and so do the statements s.add(x), s.discard(x), s.remove(x), s.update(t), s.difference_update(t), s.intersection_update(t), s.symmetric_difference_update(t) and s.clear(), while s.pop() removes an element and answers it (§8.14). One that would hold an element type s cannot is a type_error, as a name keeps one type (§6.4): after s = {1}, s.add("a") and s |= {"a"} are refused, as xs.append("a") is on a list<int>, and a set<value> takes any scalar. The operators follow the methods: s - t and s & t take a t of any element type, and s | t and s ^ t one whose elements join s‘s as the methods’ do. A difference holds T, and so does an intersection with a set of another element type; a union, intersection or symmetric difference of an int and a real element type holds a value holding a number, and a union or symmetric difference of two other scalar element types a value (§5.6).

xs.copy(), s.copy() and d.copy() each answer a new collection of the receiver’s kind holding its elements — what list(xs), set(s) and dict(d) build (§8.13) — and take no argument. It is a list, a set and a dict that take it; on any other type it is a type_error naming the three.

An int, a real and a bool of equal value are one element to every relation, compared exactly (§8.1). Where both sides hold such a pair, the class that survives is Python’s: a union keeps the left operand’s element, a difference the left’s, and an intersection the element it walks — the argument’s when that is not a set, otherwise the smaller set’s, and the right operand’s when the two are the same size ({1, 2.5} & {1.0} is {1.0}).

# set-relations.dpy — compare two sets by inclusion, and combine them four ways.
def set_relations():
    staff = {"ana", "bo", "cy"}
    on_call = {"bo", "dee"}
    print(on_call <= staff, {"bo"} < on_call, staff.isdisjoint(["eve"]))
    print(staff | on_call, staff & on_call, staff - on_call, staff ^ on_call)
    seen = set([])
    seen |= on_call
    seen -= {"dee"}
    print(seen, staff.issuperset(seen), {1, 2.5} == {1.0, 2.5})

That prints False True True, then {'ana', 'bo', 'cy', 'dee'} {'bo'} {'ana', 'cy'} {'ana', 'cy', 'dee'}, then {'bo'} True True.

Two builtin functions pair collections into tuples, and a third reads one backwards. Each returns an iterator, as CPython’s do. zip(a, b, …) → iterator<tuple<…>> pairs elements position by position up to the shortest input, and zip() with no input gives nothing. With strict=True it pairs them the same way, and inputs that are not all the same length end in a catchable refusal, as CPython’s zip raises its ValueError. The refusal names the input that ended early or late against those before it: zip() argument 2 is shorter than argument 1, zip() argument 3 is longer than arguments 1-2. strict is read for its truth value, as Python reads it, and the pairs read before the refusal have been given. enumerate(xs [, start]) → iterator<tuple<int, T>> numbers each element, from 0 by default or from an optional int start (given positionally or as the start= keyword; start may be negative). Each argument to zip/enumerate is any finite collection (a dict contributes its keys, a set its sorted elements, a dict view what it views) or an iterator. reversed(xs) → iterator<T> reads xs in the reverse of the order for walks it, so a dict contributes its keys, a set its sorted elements and a dict view what it views, backwards. It accepts a set, which CPython refuses, because this language orders every set (§3.5.13). An argument that is not a finite collection — an iterator included, which CPython refuses too, since an iterator is read only forwards — or an argument count other than one is a type_error.

An iterator is read once. What reads it — a for, a comprehension, list, tuple, set, dict, sorted, sum, min, max, any, all, a string’s join, a list’s extend, a set’s or a dict’s update, another zip or enumerate, next, which reads one, or a target list it is unpacked into (§8.11) — takes each element it gives, and that element is gone. So a second read finds only what the first left: after e = enumerate([5]), list(e) is [(0, 5)] and a second list(e) is []. A for reads one element per pass, so a break leaves the rest for the next reader, and x in e reads up to and including the first element equal to x. any and all read up to the element that decides, s.intersection(e) until it has found as many elements as s holds, s.issuperset(e) up to one s lacks, and s.isdisjoint(e) up to one s holds, as CPython’s do. Every name holding an iterator holds the same one, so reading it through one name uses it up for all of them.

An iterator reads its source as the source stands when each element is read, as CPython’s does, and never past the length the source had when the iterator was made, which is the bound a for keeps (§6.3). A list that shrinks is seen shrinking: reversed over a list whose last element has since been removed ends at once, as CPython’s does. A dict or a set whose size changes refuses the next read with CPython’s RuntimeError text, dictionary changed size during iteration or Set changed size during iteration, which except refuse catches, and refuses every read after it the same way. A set’s update and difference_update and a dict’s update read an iterator one element at a time, changing the collection by each before the next read (§8.14), so one over the collection they change is refused there. A source that has ended stays ended.

An iterator has no length and no position. len(e), e[0] and e[1:] are type_errors in CPython’s words — object of type 'zip' has no len(), 'zip' object is not subscriptable — each naming list(...), which reads the iterator into a list that has both. An iterator is always truthy (§8.2), used up or not, and is equal only to itself (§8.1). It prints as its class — <zip object>, <enumerate object>, and for reversed the class CPython gives its argument: <list_reverseiterator object> over a list, <dict_reversekeyiterator object> over a dict or a keys view, <dict_reversevalueiterator object> over a values view, <dict_reverseitemiterator object> over an items view, and <reversed object> over anything else. CPython prints the object’s address as well, which differs from run to run; a DKE Python render is deterministic and leaves it out, as an instance’s does (§13.2). Printing an iterator reads nothing from it. An annotation writes its type iterator<T> (§5.5).

names = ["ana", "bo"]
pairs = zip(names, [90, 85])
print(pairs)                     # <zip object>
print(list(pairs))               # [('ana', 90), ('bo', 85)]
print(list(pairs))               # []
for i, name in enumerate(names, 1):
    print(i, name)               # 1 ana, then 2 bo
print("".join(reversed("abc")))  # cba
scores = dict(zip(names, [90, 85]))

iter(x) → iterator<T> makes an iterator over x, which is any finite collection or an iterator. A collection is read in the order for walks it, so a dict gives its keys, a set its sorted elements, a dict view what it views and a string its characters, by the rules above. An iterator is answered with itself, as CPython answers it: after e = iter(xs), iter(e) is e, and reads on from where e stands. The iterator prints as the class CPython gives it: <list_iterator object> over a list, <tuple_iterator object> over a tuple, <set_iterator object> over a set, <dict_keyiterator object> over a dict or a keys view, <dict_valueiterator object> over a values view, <dict_itemiterator object> over an items view, and over a string <str_ascii_iterator object> when each of its characters is ASCII and <str_iterator object> when one is not, over a range <range_iterator object>, as CPython’s, and a collection read from the store prints <iterator object>, the class CPython gives an iterator over a sequence that names none of its own. A compile-time message names a string’s iterator iterator, since its class depends on characters a program holds only when it runs. iter takes exactly one argument. CPython’s two-argument form calls a function until it answers a sentinel, and a function is not a value here (§8.8), so iter(f, s) is a type_error. So are iter() and an argument that is not a collection: iter(5) names the collections iter takes.

next(e [, default]) reads the next element of the iterator e, which is then gone from e, as it is for every reader above. When e has none left, next(e, default) answers default, and next(e) refuses with discriminator stop_iteration, which except refuse catches. CPython raises StopIteration there, and this language catches by category rather than by exception class (§9.2). The default is evaluated before e is read, whether or not it is answered, as CPython evaluates a call’s arguments. The default and the elements join as two elements of one list display do (§5.1), so a default of another scalar class gives a value (§5.6): next(iter([1, 2]), "done") is a value. A default that cannot join, such as a list beside an int, is a type_error. An e that is not an iterator is a type_error in CPython’s words: next(xs) over a list is 'list' object is not an iterator, and the message names iter(...), which makes one. When the source refuses the read, because a dict or a set changed size or a strict zip’s sources differ, next refuses as every reader does, and the default does not answer in its place. next takes one or two arguments and no keyword argument, as CPython’s does. It may stand alone as a statement, because reading changes e: next(rows) skips an element (§4.3). Each call reads at most one element, so next adds no loop to the language (§9.5).

e = iter([1, 2])
print(e)                     # <list_iterator object>
print(next(e), next(e))      # 1 2
print(next(e, "done"))       # done
try:
    next(e)
except refuse as r:
    print(r.discriminator)   # stop_iteration

Dict views. A dict’s .keys(), .values() and .items() answer a view (§5.1), which differs from an iterator in two ways: it is read as often as it is read, each time from the dict as it then stands, and it has a length. So len(d.keys()) is the dict’s size and list(d.items()) can be called twice. A keys or an items view takes the set operators beside any collection, on either side, and the inclusion order beside a set or another such view, and each combination answers a set: {"a": 1, "b": 2}.keys() & {"a", "z"} and {"a": 1, "b": 2}.keys() & ["a", "z"] are {'a'}, and {"a": 1, "b": 2}.keys() <= {"a", "b", "c"} is True. The collection is read as set(...) reads it (§8.13), so a list among its elements is a type_error, and a number beside a view is one in CPython’s words, 'int' object is not iterable. Of two equal numbers of two classes, an intersection keeps the one CPython’s keeps: the collection’s, unless it is a set no smaller than the view, so {1: 0}.keys() & [1.0] is {1.0} and {1: 0}.keys() & {1.0} is {1}. Of the seven set methods a keys or an items view has isdisjoint alone, which takes any collection, as CPython’s does, and a values view has none. A view is read into a list, a tuple or a set by the conversions (§8.13), and dict(d.items()) is a copy of d. A view has no .mapping, the attribute CPython’s has for the dict behind it: the dict is the one the program read the view from.

range(stop), range(start, stop) and range(start, stop, step) return a range, as CPython’s do: the integers from start (default 0) up to but not including stop, step apart (default 1). A negative step counts down, toward a stop below start. A range whose stop is not past start in the direction of step is empty: list(range(0)), list(range(5, 2)) and list(range(0, 5, -1)) are all []. Every argument is an int; a bool, a real or any other type is a type_error, and so is a call with no argument or with more than three. A zero step refuses at run time with CPython’s ValueError text, range() arg 3 must not be zero, and discriminator value_error, which except refuse catches. A range holds its three bounds and never its integers. A for or a comprehension walks it, and its length, an index, in, .index(x) and .count(x) are answered from the bounds, so for i in range(1000000): builds nothing and len(range(10 ** 12)) is 1000000000000. It prints as CPython’s does, range(0, 3), with the step written when it is not 1; it equals a range that denotes the same integers, range(0) == range(1, 1), and nothing else, a list of the same integers included, and two equal ranges hash alike. A slice of it is a range: range(10)[2:8:2] is range(2, 8, 2). Its .start, .stop and .step are its bounds. +, * and an ordering are refused in CPython’s words, and iter and reversed give a range_iterator. list(range(n)), and every other read that holds its integers, builds them, and each counts toward any limit a service sets on how much one run builds, exactly as the elements of [0] * n do; so does each element a run adds to a list, a set or a dict it holds, which a loop over a range can fill. Past such a limit the run is refused, and except refuse does not catch it. sum, min and max read a range from its bounds, and a generator of ints one element at a time, so neither builds anything: sum(range(10 ** 6)) is 499999500000, and max(x % 1000 for x in range(10 ** 6)) is 999. From the first element of a generator that is not an int, the rest is held, so a sum that holds a real is still rounded once (§8.3). any and all given a generator hold none of it, and stop at the element that decides.

Three more read a whole iterable. sorted(iterable [, key=<name>] [, reverse=<bool>]) returns a new list<T> of the elements in ascending order — descending when reverse is True (a stable sort; key= and reverse= are keyword-only, in either order) — ordered by the elements themselves or, given a key=, by their keys, as min and max are (§8.3), so that only the keys need be orderable and elements of equal key keep their order in both directions: sorted(words, key=len, reverse=True) is longest first, equal lengths in the order they came. Without a key the elements must be orderable: numeric, scientific, string, datetime, duration, or a nested list/tuple whose own elements are (recursively) orderable, in which case the sort recurses lexicographically (§8.1). An int and a real sort by value and keep their classes (sorted([3, 2.5]) is [2.5, 3]); two of equal value keep their order, the sort being stable. A keyword sorted does not take is a type_error in Python’s words, the first one written, and is named for sort(), which Python’s sorted passes it to: sorted(xs, x=1) is 'x' is an invalid keyword argument for sort(). So is one a list’s sort does not take, after its positional arguments are counted (§8.14). any(iterable) and all(iterable) fold truthiness (§8.2) over the elements to a bool: any is True when at least one element is truthy (False for an empty iterable), all is True when every element is truthy (True for an empty iterable). Each of the three reads an iterator as readily as a collection.

8.7 The print statement

print(<expr>)

Appends output to the script’s transcript. print is written as a call — print("temp=" + str(t.value)). Evaluates <expr> and appends its rendering. An argument written with * is a spread, and each of its elements is rendered as an argument of its own (§8.15). The argument type MUST be a type for which a textual rendering is defined: string, int, bool, a value (§5.6, rendered by its class), a constructed list<T>, a class instance (§13.2), or any discriminator type. A constructed list<T> renders as [e1, e2, …] — the elements comma-separated in brackets, each rendered recursively (a nested list renders in full, e.g. [[1, 2], [3]]; an empty list is []). A string element inside any collection is rendered as its repr — single-quoted, switching to " only when the value contains a ' and no ", with \, the quote, and \n/\r/\t escaped (e.g. ['a', 'b'], byte-matching CPython str([...])) — so a nested string is unambiguous, while a bare top-level string printed on its own stays unquoted (print("a") → a). A constructed tuple renders (e1, e2, …) (a one-element tuple (e,)); a dict renders {k1: v1, k2: v2} in insertion order; a set renders {e1, e2, …} in sorted order (the empty set, only reachable as a computed result, renders set()) — each element rendered recursively. A class instance renders as its class and its subject, <Sensor 'hall'> (§13.2).

Written without its parentheses, as Python 2 wrote it, print x is a parse_error wherever an expression is read, as CPython reads it: a call missing its parentheses, answered at the word print (§10.1). The same holds for exec, a builtin this language leaves out. A call of the same shape among the values after the word is answered first, as CPython reads them first. A print followed by a ( or not in begins an expression, and is not this error. A [ after the word begins an index of it, as CPython reads print [a]; where a grammar error fails the line past the word and the [ opens a list that reads, CPython’s rule for a Python 2 statement reads that list as the call’s first argument, so print [a], b + and print [] + are this error, answered at the word. A grammar error that rule raises among the values it reads is the answer instead, as print [b for x in y], (c=d) = 1 is a parse_error at column 24, and inside a bracket a value straight after the list is a forgotten comma, answered at the list, as [print [] b] is at column 8. A list among the targets before an = is read as a target, and is not this error. An await and the value after it are the word’s first argument, as CPython reads them, so print await x is this error, answered at the word. A ~ stands only before a value, so print ~x is this error, answered at print. A - or + after the word begins an expression CPython compiles, so print -a subtracts and print -a, b is a tuple; where a value stands side by side after that expression, outside a bracket, CPython’s first parse fails and its rule for a Python 2 statement then reads -a as the call’s first argument, so print -a b and print -a, b c are this error, answered at the word. A * after the word is read as a sign is, so print *a b is this error too, answered at the word. Inside a bracket the two values are the answer, as a forgotten comma is, from the word that begins the first of them. Inside a bracket an = after the word’s values makes no target of them, a keyword argument’s name included, so f(print [a] = 1), f(print -a = 1) and [print [a] = 1] are this error, answered at the word. The grammar error that fails the source past the word may stand on a later line: CPython’s first reading of the source fails there, and its second, with its error rules on, meets the word first, so the two lines print [a] and b + are this error at line 1, and so are print -a and b +. A decorator’s expression is read as any other, so the three lines @print [a], def f(): pass and b + are this error at line 1, column 2, at the word. An error CPython raises on its first reading is the answer instead, at its own line: a bracket never closed, a string not terminated, a number its tokenizer refuses, an error in an f-string’s field, and a dict’s own error at a key that reads, so the two lines print [a] and x = {1: 2, a} are a parse_error at line 2, column 12. A character no token begins with is not one of them: CPython’s parser meets it as a token and raises nothing there, so the two lines print [a] and x = $ are this error at line 1. So is the later error where the word stands among the targets a del, a for or an as names, outside a bracket of their own, which CPython reads as no expression: the two lines del print [a] and b + are a parse_error at line 2.

The arguments are read as CPython reads them. The values after the word are a comma-separated list, and after each one the value side by side with it, where one stands there, is read whole, so a bracket that reading opens and the source never closes is the answer (the bracket rule, below): each of print 's', y (, print y, z d ( and print x, 't' 'u' ( is a parse_error, the bracket never closed, while print y, z d, whose reading stops short of the end, is answered at the word. A starred or awaited value among them is read as any other, so print x, *a(1 is a parse_error at column 12, the bracket never closed. After a list that is the word’s first argument, the values that follow it are read as the call’s arguments, so print [a], c(1 is a parse_error at column 13, never closed, where print [a] c, d(1, whose reading stops at the c, is answered at the word. Inside a bracket the two values side by side are the answer, a forgotten comma at the first, as CPython answers them before it weighs the word: f(print e < b d e is a parse_error at column 9, where print e < b d e alone is one at column 1. After a first argument that is a conditional or a lambda nothing is read on, as CPython’s rule for a forgotten comma has read that argument once already with its error rules off and keeps the reading: print e if b else a d ( is answered at the word, where print x, e if b else a d (, with the conditional second, is never closed. That rule is skipped where a string follows the word, and reads the word as an operand where a sign does, so print 's' if b else a d ( and print -a if b else c d ( are never closed, at columns 25 and 24. A first argument’s call whose arguments stand in an order CPython’s grammar does not read is read to the first out of place, for the same reason, so print a(c=1, 1 is answered at the word, where print x, a(c=1, 1, with the call second, is never closed, and so is print -a(c=1, 1, whose word that rule reads as an operand. Where that rule read no first argument, after a string, a list, a sign or a *, what Python raises inside a call or a subscript that trails the first argument stands: print [a](a b, 1 is a parse_error at column 11, two values side by side, and so are print -x(a b, 1 at column 10 and print [a][a b, 1 at column 11. print x(a b, 1, whose first argument that rule read, is answered at the word, and so is print [a](, 1, whose call fails where no rule raises. In a statement that begins with an element and a comma, the elements are read on, with or without a comma between them, to a call of this shape, which is answered at its word with this error: y, a b print 's' d 't' ( is a parse_error at column 8.

The store-derived finite collections and proof_tree each have a rendering — str(x) is total over “any expressible value” (§8.13). print accepts any expressible value, and the renderings of the store-derived types are stated in §8.13. Reaching for a string-shaped field instead of printing the value is correct too, and the better habit for anything you intend to parse.

8.8 No I/O, no closures, no first-class functions

DKE Python has no I/O surface other than (i) verb calls against the store and (ii) print. It has no closures, no anonymous functions, no first-class function values, and no captured environments. A script is reached by its name: through a plain call (§4.3, §9.7), or named as the key= of sorted, min or max (§8.3), where the name resolves as a call’s does. lambda is a parse_error: a lambda is a function held as a value, and no type (§5) holds a function. Where Python reads no lambda, as an operator’s operand, the parse_error is at the word, with a header or without one, so y, not lambda: e is one at column 8. The word is reserved (§3.7), as it is in Python, so it names nothing either: where it begins a lambda whose header reads no parameter list, as an assigned name, a target, an argument or a path’s head does, the parse_error is where Python’s reading of that header stops, so lambda = 5 is one at column 8 and current(lambda.s.a) one at column 15; as a del’s target or a with’s name it is one at the word. A call names what it calls, so (…) after any other value — a literal, or what an index, a call or a .name answers, as in xs[0](1) or f()(1) — calls that value, which CPython reads and fails on as it runs, and is a type_error here in its words, 'int' object is not callable, or as the program runs where no type is known for it. What only this language has is told that no value holds a function, and so is a change through such a call, f()(1).pop(), as a parse_error, and the name of a def, a method or a module read with no call, as a type_error. Its arguments, and what follows them, are read as CPython reads a call’s, so a grammar error among them is the answer, as it is in a call of a method no value has: x = {a}(, 1 and x = a.b(, 1 are parse_errors at column 9. None, which this language does not have, is read there as the value Python reads, so x = None(, 1 is a parse_error at column 10. Where that reading reaches the end of the source inside a bracket never closed, the bracket is the answer (§10.1), two values side by side among the arguments read to the token after the second, so x = [a](a b is a parse_error at column 8, never closed, and so is the value of an = that CPython reads as a named expression, as if a = b(1)( is at column 12. It has no generators and no yield; a generator expression is read only where it cannot be told apart from a comprehension (§8.9). Nor has it coroutines: an await with a value straight after it, which Python reads as its keyword, is a parse_error at the await, as y = await b is at column 5, and one before a not, a ~ or a lambda, none of which begins the primary Python reads after it, is a parse_error at that word. After a print written without its parentheses, the call is the answer instead (§8.7). Nor has it async: an async before def, for or with, or before a comprehension’s for, is a parse_error at the async, after what follows it has been read as CPython reads the statement or the clause, so a grammar error inside is the answer — async def f() d is a parse_error at column 15, and x = [e async for 1 in xs] one at column 18. Both words are reserved (§3.7), as they are in Python, so they name nothing either: await = 1 and async = 1 are parse_errors at column 7, where Python’s reading of them stops, and x = await is one at column 10.

8.9 Comprehensions

A comprehension builds a constructed collection by bounded iteration in expression position. There are three forms — a list comprehension ([…]), a set comprehension ({…}, no colon), and a dict comprehension ({… : …}):

comp-clause        := 'for' comp-target ( ',' comp-target )* ','? 'in' iter-expr ( 'if' cond-expr )*
comp-target        := name-target | '*' name-target     # THE single starred target (<= one per list), a name or a nesting
name-target        := IDENT | '(' comp-target ( ',' comp-target )* ','? ')'   # a name, or a nesting of names, which may hold its own star; an element target needs a `for` statement (§8.11)
                    | '[' comp-target ( ',' comp-target )* ','? ']'   # a nesting in brackets
                    | '(' ')' | '[' ']'                               # a nesting of no names; each element must hold nothing
iter-expr          := or-expr   # a conditional expression needs parentheses here
list-comprehension := '[' expr comp-clause+ ']'
set-comprehension  := '{' expr comp-clause+ '}'
dict-comprehension := '{' expr ':' expr comp-clause+ '}'
generator-argument := expr comp-clause+           # bare: the only argument of a call that reads it whole
                    | '(' expr comp-clause+ ')'   # parenthesized: an argument of such a call

Each form has one or more for clauses. A single-clause comprehension iterates the finite <source> (any finite collection type, a constructed list/set/dict, a string — by character, §6.3 — or an iterator, §8.6), binds each element to <var> — or, when the clause names a target list (for a, b in …), destructures each element into those names (§8.11) — evaluates the optional <cond> (tested for truthiness, §8.2 — any value), and, for each element the condition admits, contributes a value to a new collection. A list comprehension appends <result> → list<T> (T the type of <result>); a set comprehension collects <result> → set<T>, removing duplicates and sorting (the <result> must be hashable — a scalar or a tuple of hashables); a dict comprehension maps <key> : <value> → dict<K, V> in insertion order, last value winning on a repeated key (<key> must be hashable the same way). <result> (and <value>) MUST produce a value; a statement-position verb is a type_error. Over a source whose element type nothing has supplied, a comprehension produces nothing, and a filter, <result>, <key> or <value> that reads its loop variable is not checked (§6.3). A part skipped this way leaves its place in the type unknown, so [x.upper() for x in []] is list<unknown>.

A list or set comprehension’s <result> is one expression, so a tuple there MUST be written in its parentheses: [(a, b) for b in xs]. Without them, [a, b for b in xs] is a parse_error, as it is in CPython, where the element begins, and its message names the parentheses the element needs. Where such a comprehension holds another, among its elements or in what its clauses read, the inner one is answered. CPython reads such a comprehension wherever its reading of the statement reaches it, and answers it there before it weighs any target, so f([a, b for b in xs]) = 1 and f() = [a, b for b in xs] are answered at the element rather than at the call. A comprehension whose clause names a target that cannot be assigned to is answered at that target in the same way: del [x for 1 in xs] is a parse_error at the 1.

A comprehension with more than one for clause is the flat cartesian product: the first clause is the outermost loop, each following clause nests inside the previous, and <result> (and a dict’s <value>) is evaluated only in the innermost scope. A later clause’s <source> may reference the loop variables bound by earlier clauses ([y for row in grid for y in row] flattens grid). A clause may carry multiple if filters (for x in xs if p if q), which apply conjunctively. Each is asked only for its truth value, so their types need not join as the operands of an and must (§8.2): [1 for b in [1] if 1 if [1]] is [1].

The loop variable <var> of each clause is comprehension-local: it is in scope within the clauses to its right, the <cond>s, <result> (and a dict comprehension’s <value>), may shadow an outer or earlier binding of the same name, and does not leak to the enclosing scope. A list comprehension whose <result> is itself a comprehension nests naturally ([[y for y in row] for row in grid] is a list<list<T>>).

A comprehension is bounded iteration over a finite source, so it terminates (§9.5) and, like a list literal, is evaluated as the program runs and stores nothing.

evens   = [n for n in nums if n % 2 == 0]      # list  → list<int>
uniq    = {n % 2 for n in nums}                 # set   → set<int> (deduped, sorted)
squares = {n: n * n for n in nums}              # dict  → dict<int, int>
pairs   = [a + b for a in xs for b in ys]       # cartesian product (multi-clause)
flat    = [y for row in grid for y in row]      # inner source sees the outer var
tags    = get(K.s.tags)                         # a read is bound first (§9.1)…
labels  = [c.value for c in tags]               # …then iterated → list<value>
shouted = [w.upper() for w in text.split(" ")]  # compose with split → list<string>

The <source> is any expression of a finite type; a store read (a verb call) is bound to a name first (as tags above) rather than written inline, consistent with §9.1.

A generator expression is a comprehension’s result and clauses written in a call’s parentheses instead of brackets: sum(x * x for x in xs). DKE Python has no generators, so it reads one only where nothing can tell it apart from the comprehension it spells, which is as the whole argument of a call that reads every element of that argument before it returns. Those calls are sum, min, max, any, all, sorted, list, tuple, set, dict (over pairs), a string’s join, a set’s union, intersection, difference, symmetric_difference, issubset, issuperset and isdisjoint, and the statements that change a collection by what they read (§8.14): a list’s extend, a set’s update, difference_update, intersection_update and symmetric_difference_update, and a dict’s update (over pairs). There a generator expression is the list comprehension a reader would write in brackets, with the same element type, order, duplicates and value: sum(x for x in xs) is sum([x for x in xs]).

A * reads every element of what it spreads before anything else goes on, so a generator expression in its own parentheses right after one, in a call or a display, is read the same way: print(*(x for x in xs)) and [*(x for x in xs)]. A set’s method taking several arguments reads each of them so: s.union([1], (x for x in xs)).

Written bare, a generator expression MUST be its call’s only argument, with no trailing comma. Beside another argument it MUST be parenthesized: sorted((x for x in xs), reverse=True), max((x for x in xs), default=0). A bare one beside another argument or before a trailing comma (sum(x for x in xs, 1), sum(x for x in xs,)) is a parse_error, as it is in CPython. That holds in a call of any kind, not only one that reads its argument whole, and it is a grammar error, answered ahead of a refusal this language makes: print(1, x for x in xs) is a parse_error at column 10, where the generator expression begins. Where CPython’s rule for it reads the arguments after the generator to the end of the source, inside the call’s bracket never closed, the bracket is the answer (§10.1), as x = a(a for a in b, 1 is a parse_error at column 6. A comma straight after another begins no argument, so an empty place after a keyword argument is no argument out of order, and the second comma is answered: x = a(c=1,, 1 is a parse_error at column 11.

Two of those calls read differently from a list, and they follow CPython. any stops at the first element that is truthy and all at the first that is not, so no element after it is evaluated: any(1 // x > 0 for x in [1, 0]) is True, while any([1 // x > 0 for x in [1, 0]]) refuses at runtime on the division by zero. And sorted’s reverse= and the default= of min or max are evaluated after the generator expression’s first source and before its elements, which a program can observe only when both call a script that prints.

Three more read it one element at a time, as CPython’s do: a set’s update and difference_update and a dict’s update change the collection by each element before they read the next (§8.14). A generator expression that reads the collection being changed can tell this from its list comprehension: one over it finds it changed, so s.update(x + 1 for x in s) is refused with Set changed size during iteration, where s.update([x + 1 for x in s]) adds every element, and one reading its length reads it as each element lands, so s = {1}, s.update(len(s) + x for x in [10, 20]) leaves {1, 11, 22}.

Anywhere else a generator expression would be a generator object, a one-pass iterator that CPython prints as <generator object …>, so it is a parse_error whose message names the list comprehension. That covers a generator expression bound to a name (g = (x for x in xs)), returned, held in a collection, used as an operand or a subscript base, iterated by for, or passed to any other call: print, which in CPython prints the object; str, len, reversed and bool, which would show it, refuse it or only test it; enumerate and zip, which read it one element at a time between other work; a method or verb not listed above; and a script, whose parameter would receive the object. Write the list comprehension there: print([x for x in xs]).

nums = [3, 1, 4, 1, 5]
print(sum(n * n for n in nums))                     # 52
print(any(n > 3 for n in nums))                     # True
print(sorted((n % 3 for n in nums), reverse=True))  # [2, 1, 1, 1, 0]
print(", ".join(str(n) for n in nums))              # 3, 1, 4, 1, 5

8.10 Conditional expression

A conditional expression a if c else b yields a when the condition c is truthy and b otherwise. The condition c may be any value (tested for truthiness, §8.2). The two branches must have the same type — which is the type of the whole expression — be two numbers, which join (§3.5.7), or be two class instances, which join under their common base as a list display’s elements do (§13.8), or be two scalars that are not both numbers, or a scalar beside a value, which join to a value holding their classes as a list display’s elements do (§5.6); any other mismatched pair (e.g. a list and an int) is a type_error. The first branch reads a name the condition narrows at its narrower class, and the second reads it at what the condition failing leaves it (§5.6). An int / real pair is typed real, and the expression yields the branch it selects, unconverted, as Python’s does: 2.5 if False else 1 is 1. That is a join of the types, not a promotion.

grade = "pass" if score >= 60 else "fail"
step  = n // 2 if n > 0 else 0

A conditional expression with no else is a parse_error, answered where the expression begins, as CPython answers it: y = x + 1 if a at the x. One whose condition a : follows is answered at the :. A := is no :, so y = a if b := 1 is answered at the a. For the rule of §10.1 that a bracket never closed is the answer over a grammar error past its line, this one stands at the token that follows its condition. A condition refused part way, after a value it begins with, is answered the same way, since CPython reads the longest condition it can and then finds no else: y = a if b + and y = a if b + else 1 are answered at the a. A refusal CPython words itself, one inside an f-string, and a bracket never closed are the answer in its place, and a string the condition holds is read before it: y = a if b + "\x4" is answered at the string (§10.1). A condition that ends in a call whose arguments do not parse is read to the value before the call, as Python backs off a trailer that fails, and finds no else after it: x = a if f(, 1 is answered at the a, column 5. Where Python’s reading of the arguments reaches the end of the source inside the bracket, the bracket is the answer, as x = a if f(1 is at column 11.

Evaluation is short-circuit: c is evaluated first, then only the selected branch — the unselected branch never runs, so a branch that would refuse (a division by zero, an out-of-range index) is not reached when it is not chosen. The conditional expression is right-associative and binds lower than every operator (§4.6). It is terminating by construction (a single selection, no iteration).

8.11 Unpacking

A target list — two or more targets separated by commas, or one target followed by a comma, on the left of an = or between for and in — destructures a tuple, a list or an iterator into its targets. A target is a name, or an element or a slice of a collection (below):

a, b = (1, "two")                    # a = 1, b = "two"
a, b = b, a                          # swap — the RHS may be a bare comma-list
x, y, z = 10, 20, 30                 # (an implicit tuple), parentheses optional
k, v = "port=80".split("=")          # a LIST — k = "port", v = "80"
(a, b), c = ((1, 2), 3)              # NESTED — a = 1, b = 2, c = 3
[a, b] = [1, 2]                      # in BRACKETS — the same target list
p, (q, r) = 1, (2, 3)                # nested target anywhere in the list
a, *rest = (1, 2, 3, 4)              # STARRED — a = 1, rest = [2, 3, 4]
first, *mid, last = [1, 2, 3, 4]     # star absorbs the middle: mid = [2, 3]
p, (q, *r) = 1, (2, 3, 4)            # a nested group holds its own star: r = [3, 4]
xs[0], xs[1] = xs[1], xs[0]          # ELEMENT targets — swap two elements
i, xs[i] = 1, 5                      # left to right: xs[1] = 5
for i, x in enumerate(xs):           # i = index, x = element
    print(str(i) + ": " + str(x))
for k, v in inventory.items():       # k = key, v = value
    print(k + " = " + str(v))
  • Assignment unpacking (a, b = <expr>) takes a right-hand side that is a tuple whose arity exactly matches the number of targets, or a list (below). The RHS may be written as a parenthesized tuple ((1, 2)) or as a bare, unparenthesized comma-list (b, a) — the two are equivalent, so the swap idiom a, b = b, a works. The whole right-hand side is evaluated before any target is bound, so a swap reads each name’s prior value. Each target binds the tuple’s value at its position (with that position’s type). A tuple of a different arity (too few OR too many values) is a type_error, because a tuple’s arity is in its type.

  • A list, a variadic tuple<T, ...> (§8.13), a string, a set, a dict, a dict’s view or an iterator (§8.6) unpacks the same way, and each target takes the element type: k, v = line.split("="), x, y = args for a *args parameter, i, j = zip(xs, ys), a, b = "xy", and k, j = d, which takes the dict’s keys. No length of these is in the type, so it is checked when the statement runs, before any target binds. An iterator is read as far as the check needs, as in CPython: one element past the targets, or to its end when a target is starred. One of a different length is a catchable refusal (§10.3) with discriminator value_error, in CPython’s words — not enough values to unpack (expected 2, got 1), too many values to unpack (expected 2), and, with a starred target, not enough values to unpack (expected at least 2, got 1). A list the checker can see is empty — the literal [] — has a length known before the run, and is refused then, in the same words, as a type_error. Any other right-hand side is a type_error, where CPython raises cannot unpack non-iterable int object as the program runs. That covers a value joined from two types, which CPython unpacks when it holds a collection, and the set of subjects a store read answers with, which a list made from it unpacks. In a comprehension clause a nested group below a list is typed with the values beside it, so a set, a dict, a view or an iterator there stays a type_error, and a string there unpacks: [a for a, (b,) in [[{1}, {2}]]] is refused.

  • Nested targets — a target may itself be a parenthesized target list, so (a, b), c = <expr> (or a, (b, c) = <expr>, and deeper) destructures a tuple or list of tuples or lists in one statement. Each nesting level is checked for exact arity independently, a tuple’s before the run and a list’s as it runs: (a, b), c = ((1, 2, 3), 4) is a type_error because the inner tuple has three values, not two, and (a, b), c = [[1], [2]] refuses not enough values to unpack (expected 2, got 1). A fully-parenthesized top-level list is the same as the unparenthesized one — (a, b) = t is a, b = t — while a single trailing comma makes a one-element tuple target: (a,) = t binds a to t’s sole element (and (a) = t, a bare parenthesized name, is the plain assignment a = t). A target list may be written in square brackets as well, at the top level or nested and in each of the three positions below, and means what the parenthesised one means: [a, b] = t is a, b = t, and for [k, v] in d.items(): is for k, v in d.items():. One target in brackets is a list of one, so [a] = t is (a,) = t and [*rest] = t takes every element of t. No target in parentheses or brackets, () or [], is a list of none, as in Python, and the value must hold nothing. [] = xs binds no name, and when xs holds an element it is the catchable refusal too many values to unpack (expected 0) as it runs. for () in xs: asks the same of each element. Parentheses around a single target only group it, as in Python, so (xs)[0] = v writes xs[0] and (x) += 1 is x += 1.

  • Starred target — one target in the list may be starred (*rest); it absorbs the elements the fixed targets do not, as a list (Python’s rule: a starred target is always a list, even from a tuple). a, *rest = (1, 2, 3) gives a = 1, rest = [2, 3]; the star may lead or sit in the middle (*init, z = t, a, *mid, b = t), and it absorbs zero elements when the tuple is exactly the fixed count (a, *mid, b = (1, 2) → mid = []). The RHS must be a tuple with at least as many elements as the fixed targets (too few is a type_error), or any of the values above, whose length is checked as it runs. Because the absorbed elements become one list<T>, they must convert as list(...) converts a tuple (§8.13) — a run holding a collection beside a value of another type is a type_error (index the tuple by position instead). A list may hold at most one star, and a lone *rest = t (no sibling, no trailing comma) is rejected. A sibling or a trailing comma makes it a list. A parenthesized group is a target list of its own, at any depth, so it may hold its own star on the same two terms: a, (b, *c, d) = 1, (2, 3, 4, 5) gives c = [3, 4], its width a minimum checked as the group’s exact width is, and (*c) alone is refused where (*c,) takes the whole group as a list. (a, *b) = t is a, *b = t. A second star and a lone one are refused as Python’s compiler refuses them, where the list begins, so a, *b, *c = t is a parse_error at column 1 and for *b in xs: one at column 5. Each is a decision (§10.1), so a grammar error later in the statement is the answer instead: *b = 1 + is a parse_error at column 9. A star alone in parentheses is Python’s parser’s to refuse, a grammar error where the star stands: (*b) = t is a parse_error at column 2, “cannot use starred expression here”, as Python names it, where a star alone with no parentheses keeps its own lesson. The star may stand before any target the list takes, as in CPython. Before a group it unpacks the list it absorbs, so the group’s members bind what they would bind standing in its place: a, *(b, c) = 1, 2, 3 gives b = 2 and c = 3, and a, *[b, *c] = t is a, b, *c = t. Before an element it writes the list there: with xs = [[0]], a, *xs[0] = 1, 2 makes xs[0] the list [2]. *(b) is *b. A comprehension clause checks a starred group’s width over what its star absorbs, as a statement does: running [b for a, *(b, c) in [[1, 2]]] says not enough values to unpack (expected 2, got 1). An element whose width is in its type, a tuple, is refused at compile in those words.

  • Element targets — a target may be an element or a slice of a collection a name reaches, xs[i], d[k], m[i][j] or xs[i:j], and it is written as the element write xs[i] = v writes it (§8.14), changing the collection in place and keeping its type. The right-hand side is evaluated first, then the targets are assigned left to right, each target’s receiver and index read when its turn comes, as in CPython: i, xs[i] = 1, 5 binds i and then writes xs[1]. So the swap idiom xs[i], xs[j] = xs[j], xs[i] exchanges two elements. An element target may sit in a nested target list, and one alone in parentheses, (xs[0]) = v, is the element write. The collection may also be one no name holds, what a call, a literal or a group answers, as in a, f(xs)[0] = 1, 2, written as §8.14 writes it. A field is not a target, and a field written as one, a, s.f = 1, 2, is refused as §13.3 says. A starred target is any target, as the starred bullet above says.

  • for-loop unpacking (for a, b in <iter>:) takes each element of the finite iterand as a tuple of the target arity — the shape enumerate (§8.6), zip (§8.6), and a dict’s .items() (§8.6) produce, which is what makes this the idiomatic way to consume them — or as a list or a variadic tuple, whose length is checked as each element is reached, so the elements before a short one have run the body. Each target binds per iteration as an assignment in the enclosing scope, so, like the single loop variable, it holds the last element’s value after the loop (§6.1) and keeps its type as any re-assignment must. A trailing comma ends the list: for a, b, in xs: is for a, b in xs:, and for a, in xs: takes each element as a one-element tuple and binds a to its item. A for target, alone or in a list, may be an element or a slice: for xs[0] in ys: writes xs[0] with each element of ys before the body runs, as xs[0] = y would. Nested and starred targets are taken here too (for a, (b, c) in xs:, for a, *rest in xs:); with a star the target arity is a minimum rather than an exact width.

  • Comprehension-clause unpacking ([<result> for a, b in <iter>]) applies the same rule in expression position, in every clause of a list, set or dict comprehension — so the three projections above are consumed the same way a for statement consumes them: [k for k, v in inventory.items()]. Each element must be a tuple of the target arity, a list or a variadic tuple, refused with the same sentence the loop gives; with a starred target the arity is a minimum, as it is everywhere else. Nested and starred targets are taken here too ([c for a, (b, c) in xs], [r for a, *r in xs]). The targets are comprehension-local (§8.9) and a later clause may rebind one, which shadows it from that point.

One target list, and one exception. A target list of plain names, of nested targets, and of one optional starred target is available in all three positions above: one target list is defined, and every position that takes one takes the same one. The single exception is an element or slice target, available in an assignment and a for header but not in a comprehension clause, which takes names only. An element there is a parse error that names the for statement as the place it may go. A target list without its = is a parse error naming the targets as the parser read them, an element shown as xs[…], and a call, such as xs[0].copy(), is not a target. A target Python cannot assign to is a parse_error where it begins, as in Python, naming what it is, when the targets before an =, or between for and in, are no target list Python reads: [f()] = [1] is a parse_error at column 2, “cannot assign to function call”, and for b + 1 in xs: one at column 5, “cannot assign to expression”. The first such target is answered, after a conditional with no else among them unless a : follows its condition, and True, False and None are refused as the reserved words they are. In a chain, the targets weighed are those before the first = that follows the target lists Python reads, each before its own: x = f() = 1 is a parse_error at column 5, “cannot assign to function call”. A lambda’s header is read past to find its =, so a = lambda: 1 = 2 is one at column 5, “cannot assign to lambda”. Python reads such a statement first as expressions, and two answers from that reading come before this one. Where an expression and a , begin the statement, an = after one of the expressions that follow, each read as a named expression is (§10.1), is answered as a named expression’s = is, where what it would assign to begins: a, b c = 1 is a parse_error at column 6, and a, f() = 1, 2 one at column 4, “cannot assign to function call here”. Where no rule of Python’s answers, the statement is answered where Python’s reading of it as expressions stops: a, -b c = 1 = 2 is a parse_error at column 7. A target list that stops where those expressions do keeps the answer that names what it read. A for’s targets Python does not read, where it names none of them, are answered where its first reading of them stops: past each target it reads, and past what begins the first it does not, with any call, subscript or attribute after it. for b() < c in xs: is a parse_error at column 9. Where that reading stops at the in, as in for (b() < c) in xs:, the answer there, at column 15, is a loop variable missing before it. A bracket among the targets whose inside does not parse is read into as Python reads it, and answered where that reading stops: a, (b +), c = 1 is a parse_error at column 8, “expected an expression”, and a conditional with no else there is answered where it begins. A True, False or None Python names past a target the reader stops at is refused where it stands, as in for (b() < c, None) in xs:, at column 15.

A fixed tuple’s arity is in its type, so its check is static; a list’s length is a property of the value, so its check runs with the statement, and so does a variadic tuple<T, ...>’s (§8.13), whose length is not in its type either. Unpacking introduces no iteration, so termination (§9.5) is preserved.

8.12 Chained assignment

a = b = <expr> binds every target on the left to the one value of <expr>, which is evaluated once:

a = b = 0                            # a and b both 0
lo = hi = mid = start                # three names, one value
a = xs[0] = 5                        # a name and an element
t = a, b = 1, 2                      # a name and a target list

Each target is any target an assignment takes (§8.11), a name, an element or a slice, or a target list in parentheses, in brackets or in neither, and is written as that assignment writes it, so t = a, b = 1, 2 binds t to the tuple and a and b to its elements. A field is not a target here either, and is refused as §13.3 says. The targets are assigned left to right after the value is evaluated, as in CPython, so xs[a] = a = 1 writes xs at the a held before the statement. The value’s type is bound to every target, so a later b = "text" — reassigning a name first bound as an int — is a type_error like any other (the one-type-per-variable rule, §6.1). This is a convenience form for initialising several names to the same value; it introduces no iteration, so termination (§9.5) is preserved.

8.13 Type conversions

The built-in scalar conversions turn a value from one type into another:

Call Accepts Produces
str(x) any expressible value its text form (string) — total; the same rendering +/print use
int(x) string / int / real / bool, or a value holding a number (§5.6) an int
int(s, base) a string + an int base an int
float(x) string / int / real / bool, or a value holding a number a real
complex(x) string / int / real / bool / complex, or a value holding a number a complex
complex(re, im) two numbers, each an int / real / bool / complex a complex
bool(x) any value — every value has a truth value (§8.2) a bool — the value’s truthiness

int(x) and float(x) do not take a complex, as in Python: read .real or .imag (§5.2), each already a real.

  • str(x) never fails — it renders any value the way print/+ do (a number as its decimal, a bool as True/False, a datetime as its RFC 3339 text, a collection as its element preview, and a class instance as its class and subject, as §13.2 describes). Note that a bool renders True/False, capitalised and Python-faithful, whether written or read back from the store. Exercised in §C.29.

8.13.1 How a store-derived value renders

str(x) is total, so the types a read gives you back render too, and print accepts them (§8.7). They do not share one convention, because they are not one kind of thing: some are collections you might have asked for a million of, one is a report written to be read, and one is a single value wearing a result type. There are six, and this is the whole list for the types a read gives back — a handler binding is covered at the end of this section.

Value Renders as Example
claim_list, subject_set, string_list, result_type_set a counted preview 2 entries [e1, e2]
proof_tree its type name and size proof_tree(2)
diagnosis the explanation itself via rule dev_online: …
active_claim the value alone, by its class eng
verify_result its type name and verdict verify_result match=True
a read that found nothing the absence marker <empty>

The counted preview is <n> entry [ for one and <n> entries [ otherwise, then the elements comma-separated, then ]. An empty collection renders 0 entries []. It is bounded: at most 8 elements appear, and a longer collection ends with , +<k> more naming how many were left out — so printing a large read stays a fixed size. Each element renders by its own class, the same way it would if you bound it and printed it: the preview never disagrees with iteration.

A verify_result appends conflicted when the cell holds competing values — verify_result match=True conflicted — so a verdict from a contested location is never mistaken for a settled one.

A diagnosis renders the report it carries, which may span several lines. This is the one of the six that is not a summary: printing it is how the Python Reference teaches why_not and what_needs, and what it shows is the whole explanation.

<empty> is what a read that found nothing renders, and it belongs to the reads that answer with a thing rather than with a collection of things: current(…) on a cell holding no value in force, and why(…) where nothing was derived. A collection read does not use it — an empty get or subjects is 0 entries [], which is the counted preview doing its job at zero. The two spellings are worth telling apart: 0 entries [] says this collection is empty, and <empty> says there was nothing to answer with.

These renderings are for reading, not for parsing. They are stable enough to put in a transcript and are not a data format: to take a value out of a result, iterate the collection or read the field. Nothing here is promised to a program that scans the text.

A handler binding is not a read result and is not one of the six. str(e) on a caught refuse (§10.3) renders the kind, a colon and the message — refuse: cell holds 2 competing values — …. Read e.reason when you want the message on its own, and e.discriminator when you want to branch on what happened; both are stated in §10.3 and are what a program should use.

The words entry, entries and more belong to the rendering, as True and False do, and the type names in the table are the language’s own identifiers. - int(x) from a real truncates toward zero (int(3.9) is 3, int(-3.9) is -3); from a bool yields 1 / 0; from a string parses a decimal integer exactly as int(s, 10) below does: surrounding whitespace, a leading sign and _ digit separators placed as in an integer literal (§3.5.2) are allowed (int(" -7 ") is -7, int("1_000") is 1000). A string that is not a decimal integer (int("3.5"), int("abc")), or a value out of the 64-bit range, is a catchable refuse (§10.3) — Python’s ValueError. A real that is an infinity refuses with arithmetic_overflow and one that is a NaN with value_error, in Python’s words, cannot convert float infinity to integer and cannot convert float NaN to integer; so do round(x), math.floor, math.ceil and math.trunc of one. - int(s, base) parses the string s in an explicit radix. base is 2..36, or 0 to auto-detect the radix from a 0x / 0o / 0b prefix. A prefix is also accepted when it matches base; an optional leading sign and _ digit separators (between digits, or once right after a prefix) are allowed. A digit outside the base, an out-of-range base, a malformed separator, or a base-0 decimal with a leading zero refuses (int("ff", 16) is 255, int("0b101", 2) is 5, int("z", 36) is 35). An explicit base with a non-string value is a type_error. - float(x) from an int / bool widens to a real; from a string parses a decimal real, including exponent form (float("1e3") is 1000.0), tolerating surrounding whitespace and taking _ digit separators placed as in a real literal (§3.5.7) (float("1_000.5") is 1000.5). inf, infinity and nan, in any case and with an optional sign, read as Python reads them (float(" -Infinity ") is -inf), and a value past the largest real is the infinity, as in Python (float("1e400") is inf). A hexadecimal float (float("0x1p3")), any other non-numeric string, and a NaN with a payload (float("nan(123)")) are a catchable refuse (§10.3). - complex(x) from an int / real / bool is that number with a zero imaginary part (complex(3) prints (3+0j)); from a complex it is the value itself; and with no argument it is 0j. From a string it reads the text print shows for a complex: a real part, an imaginary part ending in j or J, or both with the imaginary part’s sign between them and no space inside (complex("3+4j"), complex("-2.5j"), complex("j")). Surrounding whitespace and one enclosing pair of parentheses are allowed, and each part takes what float takes. Any other string is a catchable refuse (§10.3) in the words of Python’s ValueError: complex() arg is a malformed string, or, where a _ is not between two digits, could not convert string to complex: and the string. A part spelled inf or nan, or past the range, is what float reads, as in Python: complex("inf") is (inf+0j), complex("nanj") is nanj and complex("1e400") is (inf+0j). - A string that int(s), int(s, base), float(s) or complex(s) reads is read as Python reads it past ASCII: a character s.isspace() calls whitespace pads the number as a space does, and a decimal digit of any script stands for its value, so int("٥5") is 55, float("١.٥") is 1.5, and int("\u00a05") is 5. A digit that is not decimal, such as ², is refused as any other character is, and a space inside the number is not padding. ASCII is read as it is, so the four ASCII separators \x1c..\x1f, which s.isspace() also names, are not stripped, as in Python. A refusal quotes the string as it was written. - complex(re, im) builds a complex from two numbers, computed as Python computes it. When both are int, real or bool, re is the real part and im the imaginary part, each keeping the sign of a zero: complex(1, 2) prints (1+2j) and complex(1, -0.0) prints (1-0j). Either may be a complex: the real part is then re’s real part less im’s imaginary part, and the imaginary part is im’s real part plus re’s imaginary part, so complex(1j, 2j) prints (-2+1j). A string in either position of the two-argument form is a type_error. - bool(x) returns the truthiness (§8.2) of any value as a bool — bool([]) is False, bool("x") is True. Every value has a truth value, so it is total, like str(x).

Each scalar conversion takes none or one argument, int also the two-argument int(s, base) form above, and complex none, one or two. With none each is the zero of its type, as in Python: int() is 0, float() is 0.0, str() is "", bool() is False and complex() is 0j. Any other count, or a non-convertible argument type, is a type_error.

Arguments by name. A built-in function takes by name exactly the arguments CPython lets a caller name, and places each in the position it names, so a named call is the positional call it spells: int(s, base=16), round(number=x, ndigits=2), complex(real=1, imag=2), pow(base=2, exp=10, mod=1000), str(object=x) and enumerate(iterable=xs, start=1), with the names in any order after the positional arguments. Each argument is evaluated where it is written, the positional ones first, whichever position it fills, so pow(exp=xs.pop(), base=xs.pop()) pops the exponent first, as in Python (§9.1). zip takes one keyword after its inputs, strict, which has no position (§8.6). The names given to dict are not parameters but keys, each the string written, whatever the language: dict(a=1, b=2) is {"a": 1, "b": 2}. After a positional argument they are written over the entries it holds, in order, so dict({"a": 1}, a=2, b=3) is {"a": 2, "b": 3}, and the result has the type a literal holding the same entries has. An omitted real is 0, as in Python: complex(imag=2) is 2j. int’s text cannot be named, as in Python, and the keyword arguments of sum, min, max, sorted and print are described with those functions, and a method’s in §8.6. Every other built-in function takes no argument by name. A name the function does not take, a name given twice, an argument given both by position and by name, and a required argument left empty are each a compile-time error in CPython’s words, such as abs() takes no keyword arguments and round() missing required argument 'number' (pos 1). Arguments are counted before any name is read, as CPython counts them, so a call with more arguments in all than the function takes is refused for its length whichever of them are named. After the count, a required argument left empty is found first, then an argument given both by position and by name, then a name the function does not take, in CPython’s order.

A built-in function given a count it does not take is a type_error at the function’s name, in the words of the TypeError CPython raises, which follow how the function is written in C and not what it does: len() takes exactly one argument (2 given), isinstance expected 2 arguments, got 1, range expected at most 3 arguments, got 4, list expected at most 1 argument, got 2, round() takes at most 2 arguments (3 given), and round() takes at most 2 keyword arguments (3 given) where every argument is named. len, isinstance and issubclass refuse any name before they count, as CPython’s do, and sorted, min and max count what they take by position before their names: sorted(key=abs, reverse=True, x=1) is sorted expected 1 argument, got 0. enumerate reads its names where they stand, as CPython’s does, and not in that order: with two arguments in all one name must be start, and with one it must be iterable, so enumerate(start=1) is 'start' is an invalid keyword argument for enumerate(). With more and nothing by position it lacks its iterable, enumerate() missing required argument 'iterable'. The count is checked where the unit is compiled, where Python raises as the call runs, so no handler around the call sees it. Two counts are taught what this language provides instead, since CPython’s words name what it does not: iter given a second argument, whose form calls a function until it answers a sentinel, and str given a second, which is an encoding, where no value is bytes.

The collection conversions build one collection from another:

Call Accepts Produces
list(x) any finite collection, a string, or a tuple of one type, of scalars or of class instances a list<T> — the elements (a string yields its characters)
set(x) any finite collection of hashable elements, a string, or a tuple of one type or of scalars a set<T> — the unique elements, sorted
dict(x) a dict<K, V>, or a finite collection of pairs: a list<tuple<K, V>>, a tuple of them, a string or an items view dict_items<K, V> (§5.1) a dict<K, V> — the mapping
tuple(x) any finite collection, a string, or a tuple of one type, of scalars or of class instances a variadic tuple<T, ...> — the elements as an immutable, runtime-length tuple

Called with no argument, each builds the empty collection, as in Python, typed as its literal is: list() is [], dict() is {}, tuple() is (), and set() is the empty set, which has no literal (§3.5.13). A second argument is a type_error.

  • list(x) collects the elements of a finite collection (list, set, dict — over its keys —, a dict view (§5.1), or a store-derived collection) into a new list; a string yields its characters as one-character strings (list("ab") is ["a", "b"]). A tuple is also accepted: list((1, 2, 3)) is [1, 2, 3]. A list<T> has one element type, which is what a for over the tuple binds (§5.1): the positions’ one type, value over scalars of several (§5.6), or the nearest common base of class instances. So list((1, "a")) is a list<value>. A tuple holding a collection beside a value of another type is a type_error (index it by position instead).

  • set(x) collects the elements into a set — deduped and sorted (§5.1), an int, a real and a bool of equal value being one element (§3.5.13); the elements must be hashable (a collection of unhashable elements is a type_error). The empty set is set(), as in Python, or set([]).

  • dict(x) builds a mapping from another dict — a new dict holding its entries in its order, as x.copy() does (§8.14) — or from any other finite collection of pairs, read as update reads one (§8.14): a pair is any collection of two, a key and a value, so a list, a tuple, an iterator or an items view of two-element tuples is the shape zip (§8.6) and a dict’s .items() (§5.1) produce, and dict(zip(keys, vals)) and dict(pairs) are the idiomatic constructions. A duplicate key keeps its first position and takes the last value (§8.9). An element that is no collection, or one not two long, is refused when it is read, catchably, in Python’s words (§10.3). What cannot be iterated at all is a type_error.

  • tuple(x) collects the elements into a variadic homogeneous tuple tuple<T, ...> — a tuple of runtime length whose every element is T. Unlike a fixed tuple<T1, …, Tn> (from a tuple literal), it is indexed by a runtime int (t[i] → T, out-of-range refuses) and unpacks as a list does, its length checked when the statement runs (§8.11). It compares (==) against another tuple whose elements agree with T. A fixed tuple is accepted too, which is how a fixed tuple is widened to a variadic one: tuple((1, 2)) is a tuple<int, ...>, its T what a for over it binds (§5.1). One holding a collection beside a value of another type is a type_error — there is no single T to collect — so index it by position instead. A tuple does not change, so tuple(xs) holds the elements xs held when it was called, whatever changes xs after.

repr(x) returns the repr text of any value as a string — a string keeps its quotes (repr("hi") is "'hi'"), a collection renders with repr elements; for most other values repr matches str. Past U+007F a string’s repr writes a character as itself when Python’s str.isprintable() accepts it, and escapes any other by its size as ascii(x) does. Those are the controls, the format characters, the private-use and unassigned codepoints, and every separator but the space: repr("a\xa0b") is "'a\\xa0b'", and len(repr("a\xa0b")) is 8, as in Python.

ascii(x) returns repr(x) with every character past U+007F escaped by its size — two hexadecimal digits up to U+00FF, four up to U+FFFF, eight beyond — as Python escapes it: ascii("é") is "'\\xe9'" and ascii(["日"]) is "['\\u65e5']". Where there is nothing to escape it equals repr(x).

bin(n), hex(n) and oct(n) write an int in base 2, 16 or 8 behind the prefix 0b, 0x or 0o, in lowercase digits and at no width: bin(5) is "0b101", hex(255) is "0xff", oct(0) is "0o0". A negative number is signed, never written in two’s complement — bin(-5) is "-0b101" — and a bool is the int it is, so bin(True) is "0b1". Each reads back through int(s, base), which takes the prefix (int(hex(255), 16) is 255). An argument of any other type is a type_error, and so is a count other than one, for all four.

format(value, spec) renders a value per the Python format mini-language, returning a string; it also powers an f-string {value:spec} (§3.5.10). format(value) takes the empty spec. The supported spec grammar is [[fill]align][sign][z][#][0][width][grouping][.precision][type]:

  • align — < left, > right, ^ center, = between a number’s sign and its digits; an optional fill character precedes it (default space), and it may be any one character, é or 中 as well as *. With no align, numbers are right-aligned and other values left-aligned. format(-5, "*=6") is "-****5". A string refuses = in Python’s words, '=' alignment not allowed in string format specifier, and a complex in Python’s words too, '=' alignment flag is not allowed in complex format specifier; a collection refuses it in ours.
  • sign — + writes a sign on a positive number too, a space writes a space there, and -, as with no sign, writes only a negative’s: format(5, "+") is "+5" and format(5, " ") is " 5". A string refuses each in Python’s words, Sign not allowed in string format specifier and Space not allowed in string format specifier, and c refuses one too, Sign not allowed with integer format specifier 'c'.
  • z — a real or a complex that writes as a negative zero writes no sign: format(-0.001, "z.1f") is "0.0". An int and a string refuse it in Python’s words, Negative zero coercion (z) not allowed in integer format specifier.
  • # — b, o, x and X write their prefix, format(5, "#b") being "0b101", and a real keeps its point and a g’s trailing zeros: format(1.0, "#.0f") is "1." and format(1.0, "#g") is "1.00000". A string refuses it, Alternate form (#) not allowed in string format specifier, and so does c, Alternate form (#) not allowed with integer format specifier 'c'.
  • 0 — the fill becomes 0, and a number written with no align is padded after its sign: format(-5, "05") is "-0005". Any other value with no align is left-aligned as before, so format("ab", "05") is "ab000". After a written fill the 0 is the first digit of the width, so format(5, "*>05") is "****5". An infinity or a NaN is padded with spaces instead, as C’s printf pads one: format(math.inf, "010") is " inf" and format(-math.inf, "010") is " -inf", where Python writes "0000000inf" and "-000000inf". A written fill of 0 is still the fill: format(math.inf, "0>10") is "0000000inf", as in Python.
  • width — the minimum field width in characters.
  • grouping — , separates a number’s digits in threes, and _ too, or in fours under b, o, x and X: format(1234567, ",") is "1,234,567" and format(255, "_b") is "1111_1111". A fill of 0 is grouped with them and may pass the width by one: format(1234, "08,") is "0,001,234". A type that takes no grouping refuses it in Python’s words, Cannot specify ',' with 'x'., and so does writing both, Cannot specify both ',' and '_'..
  • .precision — for a real, the digits it is written with: after the point under f, F, e, E and %, and significant digits under g, G, n and with no type. For a string, the maximum character count. A precision under an integer type is a catchable refuse in Python’s words, Precision not allowed in integer format specifier.
  • type — for an int or a bool: d and n (decimal), x / X (lower / upper hex), o (octal), b (binary), c (the character whose code point it is, as chr gives it), and each type a real takes, which writes the number as a real. For a real: f / F (fixed point), e / E (scientific, the exponent signed and at least two digits), g / G (fixed point, or scientific when the exponent is below −4 or at least the precision, with trailing zeros removed), n (as g) and % (the value times 100 in fixed point, then %); each takes a precision of 6 when none is written. For a string: s. With no type a value renders as str, and a precision with no type on a real counts significant digits as g does, but keeps the .0 of a whole number and turns scientific one digit sooner, as Python does: format(99.0, ".3") is "99.0" and format(100.0, ".3") is "1e+02". The types above are the whole set Python’s int, float and str take.

So format(3.14159, ".2f") is "3.14", format(42, ">5") is " 42", format(42, "05") is "00042", format(255, "x") is "ff", format(1234.5, ".2") is "1.2e+03", format(255, "e") is "2.550000e+02" and format(65, "c") is "A". The digits are the exact value of the real rounded to the nearest, a tie to the even digit: format(2.5, ".0f") is "2" and format(0.125, ".2f") is "0.12". A bool formats as the int it is once any spec is written: format(True, ">3") is " 1".

A type the value’s own type does not take is a catchable refuse in Python’s words, Unknown format code 'q' for object of type 'int'. So is a width or precision too long to read, Too many decimal digits in format string; a precision above 2147483647 where a number is written as a real or a complex, precision too big; and c of a number outside 0 … 0x10FFFF, %c arg not in range(0x110000). c of a lone surrogate, 0xD800 … 0xDFFF, is refused as chr refuses it, where Python answers, because a string here is UTF-8 and has no encoding for one. A collection (a list, tuple, dict, set, a dict’s views, a slice) takes no format spec: a non-empty one is refused, a catchable refuse in our own words, wherever it is written, in format, an f-string, str.format and __format__ called by name, and an empty one answers str of the value.

A complex formats as it does in Python. It is right-aligned by default, like a number. With f each part takes the precision and the parentheses are dropped: format(3 + 4j, ".2f") is "3.00+4.00j". A precision with no type counts significant digits in each part and keeps the rendered shape: format(3.14159 + 2j, ".3") is "(3.14+2j)". Zero padding, the 0 flag or a 0 fill, is a catchable refuse in Python’s words, Zero padding is not allowed in complex format specifier. A complex takes e, E, f, F, g, G and n, each part written as a real is under the same type, the imaginary one with its sign; with n or no type the parts take g. Any other type is refused in Python’s words, as for any value above.

8.14 Changing a collection

A list, a set and a dict are shared. A name, a parameter, an element of another collection and a loop variable that hold one hold the same collection: b = a binds b to the list a holds rather than to a copy of it, and a script called with a list receives that list. A statement that changes a collection changes it in place, so every holder sees the change, as in Python. Each of these statements answers nothing, and each is a statement of its own (rebind-stmt, del-stmt, §4.3), never an expression. The three methods that change a collection and answer a value follow the table.

Statement Receiver Changes the collection to
xs.append(v) list xs + [v]
xs.extend(c) list xs followed by the elements of c: a list, tuple, set, string, or a dict’s keys
xs.insert(i, v) list xs with v placed before position i; a negative i counts from the end, and the position is clamped to 0 … len(xs)
xs.remove(v) list xs without its first element equal to v
xs.sort(key=f, reverse=b) list sorted(xs, key=f, reverse=b) (§8.13); both keywords are optional, and a positional argument raises sort() takes no positional arguments, as in Python
xs.reverse() list xs in reverse order
c.clear() list, set, dict the empty collection of its kind
s.add(v) set s \| {v}
s.discard(v) set s - {v}
s.remove(v) set s - {v}, refusing when v is absent
s.update(c, …) set s with the elements of each c; with no c, s
s.difference_update(c, …) set s without the elements of any c; with no c, s
s.intersection_update(c, …) set s with only what every c holds; with no c, s
s.symmetric_difference_update(c) set s ^ set(c)
d.update(e) dict d with each entry of the dict e, or each pair e holds, written in order. A pair is any collection of two, a key and then a value, so d.update(["ab"]) writes d["a"] = "b". A pair of another length is dictionary update sequence element #0 has length 3; 2 is required, an element that is no collection cannot convert dictionary update sequence element #0 to a sequence and an e that is no collection 'int' object is not iterable, each a catchable refusal (§10.3) in Python’s words
d.update(k=v, …), d.update(e, k=v, …) dict d updated by e when it is given, then with each name written as a string key mapped to its value, in the order written: d.update(a=1) writes d["a"] = 1, whatever the language, as dict(a=1) builds {"a": 1} (§8.13). d.update() changes nothing
xs[i] = v list xs with position i holding v
d[k] = v dict d with k mapped to v; a key d already holds keeps its position
xs[i] op= v, d[k] op= v list, dict the element write of xs[i] op v or d[k] op v, with i or k evaluated once; an element that is itself a list or a set is changed in place, as op= changes one held by a name
del xs[i] list xs without position i
del d[k] dict d without the key k
xs[i:j] = c list xs with the elements from position i up to j replaced by the elements of c, which may be more or fewer; the bounds are read as a slice’s are (§5.3)
xs[i:j:k] = c list xs with each position the slice selects holding the next element of c, which must hold exactly as many; a step of 1 writes as the plain slice above does
xs[i:j] op= c list the slice write of xs[i:j] op c, with the bounds evaluated once
del xs[i:j], del xs[i:j:k] list xs without the positions the slice selects
xs[s] = c, xs[s] op= c, del xs[s] list with s a slice value (§5.3), what the same statement does with the bounds s holds written in the brackets: xs[slice(1, None, 2)] = c is xs[1::2] = c
xs += c list what xs.extend(c) changes it to, for any finite collection c (§4.3)
xs *= n list xs * n (§4.3)
s \|= t, s &= t, s -= t, s ^= t set s \| t, s & t, s - t, s ^ t (§4.3)

So a = [1], b = a, a.append(2) leaves b as [1, 2]; a script that appends to its parameter appends to the caller’s list; and row = [0], grid = [row, row], row.append(1) leaves grid as [[0, 1], [0, 1]]. An element read answers the element itself, so r = grid[0], r.append(2) changes the row grid holds.

Changing and answering. Three methods change a collection and answer a value. Each is an expression as well as a statement, and stands wherever an expression may: in an operator, an argument list, a comprehension, a condition, an f-string, or on an element.

Call Receiver Answers Changes the collection to
xs.pop() list its last element xs without it
xs.pop(i) list the element at position i; a negative i counts from the end xs without position i
s.pop() set its first element in the set’s iteration order s without it
d.pop(k) dict the value k maps to d without the key k
d.pop(k, default) dict the value k maps to, or default when d has no k d without the key k
d.setdefault(k, default) dict the value k maps to, or default when d has no k d with k mapped to default when it had no k, and unchanged otherwise
d.popitem() dict its last entry, as a (key, value) tuple d without that key

The answer is the element itself, as an element read’s is, so row = grid.pop() holds the row the list held. As a statement, the call changes the collection and the answer is discarded. So groups = {} filled by groups.setdefault(k, []).append(v) groups each v under its k: the default is written and then answered, and append changes the list the dict now holds.

# grouping.dpy — setdefault and pop change a collection and answer a value.
def group():
    groups = {}
    for w in ["ant", "bee", "asp", "cat"]:
        groups.setdefault(w[0], []).append(w)
    print(groups.pop("c", []))          # ['cat']
    words = groups["a"]
    print(words.pop(), words)           # asp ['ant']
    print(groups)                       # {'a': ['ant'], 'b': ['bee']}

Invoked, that prints ['cat'], then asp ['ant'], then {'a': ['ant'], 'b': ['bee']}, as Python does.

Through an element. A statement changes the collection its target reaches, which may be an element of another collection or what a method answers: m[i][j] = v, d[k].append(v), del m[i][j], d.get(k, []).sort(). Everything before the last subscript or the method is read once, as an expression is, and what it reaches is the collection changed — the one its holder holds, so the holder sees the change: after m = [[1, 2]], row = m[0] and m[0][0] = 5, both m and row show 5. Only the collection the target reaches changes, so d.get(k, []).append(v) on a dict without k appends to the default, which nothing holds, as in Python.

Through a value. An element or slice write may also begin with a value no name holds: what a call, a literal or a group answers, f(xs)[0] = v, m.rows(xs)[i][j] += 1 or [xs][0][0] = v. So may a method that changes a collection, and a del: f(xs).append(v), [xs][0].sort(), f(xs).pop(), del f(xs)[0] or del xs.copy()[0]. The value is read once, as an expression is, and the collection it reaches is changed, so a script that returns the list it was given hands back the caller’s list, and f(xs)[0] = 9 changes xs, as in Python. Parentheses around a name or an element are that name or element, so (xs).append(v) and del (xs)[0] change xs. Parentheses around a list of targets are that list, so del (a, b) is del a, b, as in Python, and refused as that is. A target Python cannot delete at all, one that is no name, attribute, element or slice, is a parse_error where it begins, as in Python, naming what it is, and the first such target is answered: del a, f() is a parse_error at column 8, “cannot delete function call”, and del xs[0] + 1, whose target is the sum, one at column 5. A conditional with no else among the targets is answered before any target is weighed. A target that ends in a call whose arguments do not parse is weighed as the value before the call, as Python backs off a trailer that fails: del x, None(, 1 is a parse_error at column 8, “cannot delete None”, where del x, f(, 1, whose f Python can delete, is one at column 10, inside the call. An = that begins a call’s argument, straight after its ( or a comma, names no keyword, so the call does not parse there: del f(=1) is a parse_error at column 7. A trailer after the element goes on with the target, as in Python: del xs[0].a is refused, an attribute being no element, and a grammar error inside a trailer is answered there, so del xs[0](1 +) is a parse_error at column 14. A value side by side after the last target is read whole before any target is weighed, as CPython reads its targets as expressions first, so del e < b d ( is a parse_error at column 13, the bracket never closed, where del e < b d is one at column 5, the comparison. A field is not changed this way: it belongs to an instance a name holds and is written whole, with remember (§13.3), so s.f[0] = v and f().y = v are refused.

As one target of several. An element or slice write may also be one target of a target list, a chained assignment or a for header (§8.11, §8.12), and is written there as this statement writes it: xs[i], xs[j] = xs[j], xs[i] swaps two elements. Its value may be a bare comma-list, as an assignment’s may (§3.5.11): ts[0] = 3, 4 writes the tuple (3, 4).

What builds a new collection. A list, set or dict literal, a comprehension, .copy(), list(c), set(c), dict(c), tuple(c), sorted(c), a slice read, and the operators +, *, |, &, - and ^ each build a new collection, so a change to it leaves the operands as they were. So a program that means to change a collection and keep the original takes a copy first: ys = xs.copy(), t = s.copy(), e = d.copy() or e = dict(d). The new collection is shallow, as Python’s is: its elements are the ones its operands hold, so grid2 = grid.copy() holds the same rows as grid. An assignment x = e binds x to what e answers — a new collection only when e builds one — and changes no collection x held before.

Typing. The changed collection must have the collection’s type, as every assignment must (§6.4). xs.append(v) is typed as xs + [v], so appending a string to a list<int> is a type_error naming the type change, exactly as xs += ["a"] is. s.add(v) is typed as s | {v}, and d[k] = v as a dict literal holding d’s entries and k: v. An int and a real meet as §5.6 says. A set element and a dict key must be hashable (§5.1). A method the receiver’s type does not take — append on a set, add on a list, any of them on a string or a tuple — is a type_error in Python’s words, 'set' object has no attribute 'append', and an element write or del on a string or a tuple is a type_error too, in Python’s words, as on any value that holds no element it could change: 'str' object does not support item assignment. Rebuild it from slices (s = s[:i] + c + s[i + 1:]). A method no value takes, called on what an expression answers, is told the same way: xs[0].foo() on a list<int> says 'int' object has no attribute 'foo', on a list<string> 'str' object has no attribute 'foo'. What only this language has names its own type, and a value names the receivers that take the method. A change read as a value answers None in Python, which no value here holds, so y = xs.append(1) is a type_error saying so. Beside a ** before a dict whose keys only the program knows (§8.15), a change read as a value and bound to no name waits for the program instead: the names are merged and refused as at its statement, the count is checked, the change is made, and only then is the value refused, in the same words. With opts empty, print(xs.append(2, **opts)) appends 2 to xs and is then refused; with opts holding x, it is list.append() takes no keyword arguments, and xs is as it was. So it is through a type’s name, list.append(xs, 2, **opts), and at a list’s sort, which reads the names sorted takes. A value bound from it, y = xs.append(2, **opts), is a type_error where the unit is compiled, whatever the mapping gives (§8.15). A slice write and del of a slice are a list’s alone, and on anything else are a type_error; a slice write’s value is any collection, typed as extend’s argument is. A change through an element keeps the element’s type as a change through a name keeps the name’s: m[0].append("a") on a list<list<int>> is a type_error naming m[…].

A list’s or a set’s pop answers the element type, a dict’s pop its value type and popitem a tuple<K, V>. A list’s pop takes at most one argument, an int; a set’s pop and popitem take none; a dict’s pop takes a key and an optional default, and setdefault a key and a default. Any other count is a type_error. A key fits the dict’s key type as d[k]’s does (§8.6). d.pop(k, default) answers the type d.get(k, default) does. setdefault is typed as the write d[k] = default, so its default must have the dict’s value type.

A collection first bound empty ([], set(), {}) has an element type nothing has supplied yet. Every name that holds it shares that element type, and the first statement that supplies one supplies it to all of them: after a = [], b = a and a.append(1), b is a list<int>. A script parameter supplies one too, so acc = [] passed to a script taking list<int> is a list<int> from then on, and so does the default of d.get(k, default), d.pop(k, default) or d.setdefault(k, default) for a dict’s value type: counts = {} filled by counts[w] = counts.get(w, 0) + 1 is a dict<string, int>, and groups = {} filled by groups.setdefault(w, []).append(n) a dict<string, list<int>>: an empty default is one type with the dict’s values, so what is appended to the answer types the dict. An empty collection written into one whose elements are collections of its kind takes their type in turn: grid.append([]) on a list<list<int>> and d[k] = {} on a dict<string, dict<string, int>> are accepted, and the new element holds ints from then on. A collection built from one still empty shares its element type as well — b = a.copy(), b = list(a), b = a[:], b = a + [] — because a loop can build it again after the source holds elements. A second element type through any of them is a type_error naming the name that supplied the first: after the lines above, b.append("x") is refused as b sharing its element type with a. A collection cannot be one of its own elements, at any depth: a.append(a) would need an element type that contains itself, which no type spells, so a program that would build one is a type_error.

An element read before any statement supplied one takes its type from the operator it meets. +, -, /, //, **, |, & and ^ join two values of one type, so the other operand supplies it: counts[w] += 1 under if w in counts: makes counts a dict<string, int>, as counts[w] = counts[w] + 1 does. x /= n stores a division’s answer where it read, so there the element is a real. * and % supply nothing, since 2 beside * fits an int, a string and a list alike and % also formats a string. A read nothing supplies a type for (v = d[k] with nothing yet written to d, or d[k] * 2) is a type_error. Its message names the annotation that would supply one: d: dict<string, int> = {}. A key or index nothing wrote still compiles, and d["a"] += 1 on an empty dict raises 'a' at runtime, catchably, as Python’s does.

Evaluation. Everything is evaluated before the collection changes, in the order Python evaluates it, except what a set’s update and difference_update and a dict’s update read one element at a time (below). An element or slice write evaluates its value first, then its target left to right: the chain, then the index, key or bounds. Every other statement evaluates its target first — a method call its arguments after the chain, left to right, and an augmented write its index, key or bounds once, then the read of the target, then its right-hand side. A call that changes a collection and answers a value runs where Python evaluates it within its expression (§9.1), so after xs = [1, 2], xs.pop() - xs.pop() is 1; in the right operand of and or or, or in a branch of a conditional expression, it runs only when that operand is evaluated.

One element at a time. A set’s update and difference_update, and a dict’s update, read an iterator (§8.6) or a generator expression (§8.9) given to them one element at a time, as Python’s do, and each element changes the collection before the next is read. A generator expression’s first source is evaluated with the call’s other arguments, as Python evaluates it where the generator is made, and the rest of it as each element is read. So a read of the collection being changed finds it as far as the call has changed it, and the read after its size has changed is a catchable refuse (§10.3) in Python’s words, Set changed size during iteration or dictionary changed size during iteration, discriminator changed_during_iteration. What was written before stays written: after s = {1, 2}, a caught s.update(x + 10 for x in s) leaves s as {1, 2, 11}, and a caught s.difference_update(iter(s)) leaves it as {2}. An earlier argument’s change is one a later argument’s first read finds, so s.update([5], iter(s)) is refused too. In a dict’s update a pair of the wrong length is refused naming its position in what gave it, after the pairs before it are written, and the names written after it (d.update(e, k=v)) are written once it has been read to its end. Every other argument is read whole when the call reaches it, and intersection_update and symmetric_difference_update read theirs whole before the set changes, as Python’s do.

Refusals. A change the collection cannot make is a catchable refuse (§10.3), whose reason is Python’s str(e):

  • xs.remove(v) with no element equal to v: list.remove(x): x not in list, discriminator value_not_in_list;
  • s.remove(v) with v absent: the repr of v, discriminator key_not_found;
  • del d[k] with k absent: the repr of k, discriminator key_not_found;
  • xs[i] = v or del xs[i] with i outside -len(xs) … len(xs) - 1: list assignment index out of range, discriminator index_out_of_bounds;
  • xs[i:j:k] = c with a step other than 1, whose slice selects a different number of positions than c holds: attempt to assign sequence of size N to extended slice of size M, discriminator value_error;
  • a slice write or del of a slice with a step of 0: slice step cannot be zero, discriminator slice_step_zero;
  • xs.pop() on an empty list: pop from empty list, and xs.pop(i) with i outside -len(xs) … len(xs) - 1: pop index out of range, both with discriminator index_out_of_bounds;
  • s.pop() on an empty set: 'pop from an empty set', discriminator key_not_found;
  • d.pop(k) with k absent and no default: the repr of k, discriminator key_not_found;
  • d.popitem() on an empty dict: 'popitem(): dictionary is empty', discriminator key_not_found;
  • a read in the target refuses as that read does: m[5][0] = v with a shorter m is list index out of range, and d["z"].append(v) without "z" the repr of "z".

A loop over a collection its body changes. A for reads a list as it stands at each step, as Python does: step i binds the element at position i when the step begins, and the loop ends when i reaches the list’s length. It never runs more steps than the list had elements when the loop began (§9.5), so a body that removes elements ends the loop sooner, as in Python, and a body that appends ends it at that count. A for over a dict or a set walks the keys or elements it held when the loop began; when the dict or set has a different size at a later step — the step after the last element included — that step is a catchable refuse (§10.3) in Python’s words, dictionary changed size during iteration or Set changed size during iteration, discriminator changed_during_iteration. Writing a value to a key the dict holds does not change its size. A comprehension walks its source the same way. Either over a dict view (§5.1) reads the dict at each step, taking the element at the position the walk has reached, so a value written during the loop is the one bound, and a change of size is refused as it is for the dict.

Divergences from Python, kept.

  • A loop that appends to the list it walks stops at the count the list had when the loop began, where Python walks every element appended and may never end: xs = [1, 2], for x in xs: xs.append(x) leaves xs as [1, 2, 1, 2]. The bound is what termination rests on (§9.5).
  • A list’s extend given an iterator or a generator expression over the list itself reads it to the length the list had when the call began, where Python’s reads every element it appends and may never end: xs = [1, 2], xs.extend(x for x in xs) leaves xs as [1, 2, 1, 2], the bound a loop keeps (§9.5).
  • A dict or set whose keys change while its size does not is walked as it stood when the loop began. Python’s walk then follows the layout of its hash table, and may bind a key added during the loop or refuse with dictionary keys changed during iteration. A dict view is walked by position in the dict as it stands at each step, so removing a key the walk has passed moves the rest back one place and the walk passes over one of them, where Python’s does not.
  • A collection built from one still empty shares its element type, and so does an empty default with the dict’s values (Typing, above), where Python lets the two differ.
  • An element read before anything wrote one takes its type from the operator beside it (Typing, above), so a later write of another type is refused, where Python holds both: after t[k] += 1, t[k] = 0.5 is refused naming t. A read beside * or %, or beside no operator, is refused naming the annotation, where Python runs it.
  • s.pop() answers the set’s first element in its sorted iteration order. Python answers whichever element its hash table holds first, which for strings differs from one run to the next. A program that needs a particular element names it: x = min(s), then s.remove(x).
  • d.setdefault(k) without a default is a type_error, where Python writes None: DKE Python has no None to write, as d.get(k) has none to answer.

Not statements of the language.

  • del x would unbind a name, and a name stays bound from its first binding to the end of its script (§6.1). Bind it to a new value instead.
# rebinding.dpy — a statement changes the collection a name holds, in place.
def tally(text: string) -> dict<string, int>:
    counts: dict<string, int> = {}
    order: list<string> = []
    for w in text.split():
        if w in counts:
            counts[w] += 1
        else:
            counts[w] = 1
            order.append(w)
    order.sort(key=len, reverse=True)
    del counts[order[0]]
    print(order)                            # ['not', 'to', 'be', 'or']
    return counts

def show():
    counts = tally("to be or not to be")
    print(counts)                           # {'to': 2, 'be': 2, 'or': 1}

Invoked with "to be or not to be", that prints ['not', 'to', 'be', 'or'] and returns {'to': 2, 'be': 2, 'or': 1}, as Python does. show() calls it so.

8.15 Spreading a collection

A * before an element of a list, a tuple or a set display spreads a collection into it, as in Python: the collection’s elements stand in the * element’s place, in the order a for walks them.

a = [1, 2]
print([*a, 3])                  # [1, 2, 3]
print((0, *a))                  # (0, 1, 2)
print({*a, *{2, 4}})            # {1, 2, 4}
print([*"ab", *{"k": 1}])       # ['a', 'b', 'k']
t = *a, 3                       # (1, 2, 3), a tuple written without brackets

What may be spread. Any value a for walks (§6.3) — a list, a set (in its sorted order), a dict (its keys), a dict view, a string (its characters), a variadic tuple, a store-derived collection, and an iterator (§8.6), which the spread reads to its end, so a second spread of it gives nothing. So may a fixed tuple whose positions differ in type, which a for does not walk: its length is part of its type, so each position keeps its own. Any other operand is a type_error that names the spread; Python raises a TypeError there as the program runs, “Value after * must be an iterable, not int”. The operand is a bitor-expr (§4.4), as it is in Python, so a comparison, a boolean operator or a conditional after it is a grammar error (§10.1).

Evaluation. A display’s elements and spreads evaluate left to right, each once (§9.1), and a spread reads its operand’s elements when it is reached. A spread of an empty collection adds nothing: [*[], 1] is [1]. The display builds a new collection, so ys = [*xs] is a list of its own, and a change made through ys is not seen through xs (§8.14). A set display keeps the first of equal elements, as it does when they are written out (§3.5.13): {*[1.0], 1} is {1.0}. Each spread is one operation at the meter, as a + [3] and list(a) are: the same copy is priced alike however it is spelled (§8.3).

Typing. A spread of a fixed tuple, tuple<T1, …, Tn>, is typed as its elements written out in its place: [*(1, "a")] is typed as [1, "a"] is, a list<value>. Any other spread is of a length known only as the program runs, and contributes its element type once, however many elements it holds. A list display is a list<T> and a set display a set<T> whose T joins what each element and each spread contributes, by the list-literal rule (§3.5.9); in a set, each contribution must be hashable (§3.5.13). A spread of the empty list [] contributes no type. A tuple display whose spreads are all of fixed tuples is the fixed tuple of its positions written out: (0, *(1, "a")) is a tuple<int, int, string>. One holding any other spread has no length in its type, so it is a variadic tuple<T, ...> whose T joins its contributions by the list-literal rule, as a concatenation with a variadic tuple joins (§8.4): (*xs, 3) with xs a list<int> is a tuple<int, ...>. Contributions that do not join are a type_error, as they are in a list display.

Where a spread stands. In a list, a tuple or a set display, a tuple written without brackets included: x = *a, 3, return 0, *a and for x in *a, *b:. And in a subscript’s index, which it makes a tuple as a comma does, as in Python: d[*k] is d[(*k,)]. A starred value is no tuple on its own. Standing alone as an assignment’s value, after return, or as a for’s iterable (y = *a, return *a, for x in *a:), it is a parse_error at the *, in Python’s words, “can’t use starred expression here”. Python’s compiler raises it once the whole source has been read, so a grammar error anywhere in the source is the answer instead. A trailing comma makes it a tuple: y = *a, is (*a,). A starred value alone in parentheses is a grammar error at the star (§10.1). As a comprehension’s element, [*x for x in xs], it is a parse_error at the *, in Python’s words, “iterable unpacking cannot be used in comprehension”. A * before an argument of print spreads its operand’s elements across print’s arguments, so print(0, *xs) writes 0 and then each element of xs, joined by sep (§8.7). Its operand is an expression, as in Python, so a conditional may follow the star: print(*xs if xs else ys). A spread of no collection is a type_error at its *, as in a display, in the words Python chooses by the call’s shape: as the call’s only positional argument, keywords beside it or not, “print() argument after * must be an iterable, not int”, and otherwise “Value after * must be an iterable, not int”. A script’s call takes one too, as the next paragraph says; so does a method’s call (§13.4), a call into a built-in namespace, a built-in function’s call and a built-in method’s call (below), and a * before the argument of a verb is refused (§10.1). A ** in a dict display is an entry of its own (§3.5.12), and in a script’s call a keyword argument for each entry (below).

A script’s call. A * before a positional argument of a call to a script (§6.8) spreads its operand’s elements across the call’s positional arguments, each filling the next unfilled parameter in its place, and a variadic parameter (§4.2) collects what is left over: given def f(a: int, b: int, c: int), f(1, *[2, 3]) and f(*(1, 2), 3) both bind a, b and c to 1, 2 and 3. The operand is an expression (§8.7), and a spread may follow a keyword argument, as in Python: f(c=3, *xs) fills a and b from xs. A fixed tuple’s length is part of its type, so a call whose spreads are all of fixed tuples has its arguments matched to its parameters at compile of the unit, as written arguments are (§6.8): f(*(1, 2)) is a type_error. A spread of any other collection has a length known only as the program runs, so its call’s arguments are matched as it runs, after every one of them has been evaluated, the positional ones and the spreads first and the keyword ones after, as Python evaluates them. A call they do not fit is then a catchable refusal (§10.3) with discriminator type_error, in the words of the TypeError Python raises there, checked in Python’s order: f() got multiple values for argument 'b' when a spread reaches a parameter a keyword argument also names, f() takes 3 positional arguments but 4 were given when there are more arguments than parameters and no variadic, and f() missing 2 required positional arguments: 'b' and 'c' when a parameter without a default is left unfilled. The count is still checked as written wherever it decides the call on its own: three written arguments to a script that takes two are a type_error whatever a spread beside them holds. Each element of such a spread must suit every parameter it could fill, which is each one from its position on that no keyword argument names, and so must each argument after it: given def s(a: int, b: string), s(*["a"], "b") is a type_error at the *. A spread of no collection is a type_error at its *, in Python’s words by the call’s shape, which names the script by its module, as Python does: __main__.f() argument after * must be an iterable, not int for a script of a unit that is no module, and m.f() for one of module m.

A ** at a script’s call. A ** before an argument of a call to a script or a method (§13.4) gives each entry of a dict as a keyword argument, as in Python: given def f(a: int, b: int = 2), f(**{"a": 1}) is f(a=1) and f(1, **{"b": 3}) is f(1, b=3). It stands among the keyword arguments, and several may. A dict’s keys are known only as the program runs, so its call’s arguments are matched then, as with a spread above, and a call they do not fit is a catchable refusal (§10.3) with discriminator type_error, in Python’s words and order: keywords must be strings where a key is no string and no mapping gives a key twice, __main__.f() got multiple values for keyword argument 'a' where two arguments name one parameter, which is where two mappings give the same key that is no string too, __main__.f() got multiple values for keyword argument '1', and before any key that is no string is refused, f() got an unexpected keyword argument 'z' where a key names none, f() got some positional-only arguments passed as keyword arguments: 'a' where it names one that takes only a position, and f() missing 1 required keyword-only argument: 'b' where a parameter after a * is left unfilled. Each value must suit every parameter it could fill, which is each one no other argument fills, and a dict display whose keys are all written has each value matched to the parameter its key names. An operand that is no dict is a type_error, in Python’s words, __main__.f() argument after ** must be a mapping, not int.

A ** at a builtin’s call. A ** before a dict display whose every key is a literal gives its keys as keyword arguments to a call of print, of a builtin, of a built-in type’s method, of sorted or a list’s sort, or of a function of a built-in namespace (§8.16–§8.18), as Python’s f(**{"k": v}) is its f(k=v): int("10", **{"base": 2}) is 2, xs.sort(**{"reverse": True}) sorts xs in reverse and math.isclose(**{"a": 1.0, "b": 1.0}) is True. Several may stand among the keyword arguments, and one that gives nothing gives the call nothing: len(xs, **{}) is len(xs). A key given twice in one display keeps its later value, as in Python; where the value it drops does something when it is read, as a call does, the call is a parse_error, since no name holds what it did. A key that is a number, a bool or None is a literal too, and is refused as Python refuses it. Such a call is checked where its unit is compiled, as a type_error in Python’s words and in Python’s order: first a keyword two arguments give, print() got multiple values for keyword argument 'sep', naming a method with its type (list.sort()) and a namespace’s function with the module Python defines it in (math.isclose(), _struct.pack()); then a key that is no string, keywords must be strings; then what the callee checks, 'zz' is an invalid keyword argument for print() or len() takes no keyword arguments. Where Python’s callee checks something first, it is checked first here: math.log refuses any keyword before it reads a key (log() takes no keyword arguments), as slice does (slice() takes no keyword arguments), min and max want an argument before they read a keyword, and a method Python names without its type through a dict, such as startswith, is named so (startswith() takes no keyword arguments). A name sorted does not take is named for sort(), which Python’s sorted passes it to. Any other operand after a ** at these calls, a name among them, is not read there yet, and the call is a parse_error that says so, except at print, at a builtin that takes no name, at a builtin that takes names, sorted among them, at a built-in value’s method, a change among them, at a function of a built-in namespace, and at the three methods a type answers itself.

A ** at print whose keys the program reads. At print, a ** before any operand that gives a dict is read where it is written among the keywords, and its keys are merged as the line is composed, in the order Python gathers a call’s keywords: each mapping’s keys where its ** stands, the names written in their places. A name given twice is refused as it is met, print() got multiple values for keyword argument 'sep', whether two mappings give it or one gives a name written beside it, and then, once every mapping is merged, a key that is no string, keywords must be strings, so that a name given twice is refused before one however the mappings are ordered. A key that is no string is itself a name given twice where two mappings give it, as a dict’s keys are equal (1, 1.0 and True are one key), and it is named as str writes it: with m and n both holding 1, print(**m, **n) is print() got multiple values for keyword argument '1', and with n holding True it is ... argument 'True', the later mapping’s spelling. The same holds at every call below, at a builtin, a built-in value’s method and a script’s call, and where each ** is a display of literal keys written out, which is refused where the unit is compiled, as a name written twice is. A display written beside a ** the program reads is merged with it as the program runs, its key that is no string held for the run: with n and k both holding "x", print(**{1: 2}, **n, **k) is print() got multiple values for keyword argument 'x' wherever the display stands among them, and keywords must be strings only where no name is given twice; a key the display and a mapping give is a name given twice (1 written, 1.0 held), named as str writes the later. Two displays written out that give one name are refused where the unit is compiled only where no ** the program reads is merged before the second giving: with n holding "x" and k holding "x", print(**n, **{"x": 1}, **{"z": 1}, **{"z": 2}) and print(**n, **k, **{1: 2}, **{1: 3}) are print() got multiple values for keyword argument 'x', as the program runs and catchably, since a mapping read before them can give a name twice first, and Python refuses the first it meets; print(**{"a": 1, "z": 1}, **p, **{"z": 2}) with p holding "a" names 'a'. With p giving no repeat the name is 'z', the same words, as the program runs. A display and a name=value that give one name beside such a ** are the same. Every mapping written out, and a ** read only after the repeat, keep the refusal where the unit is compiled. Then the first name print does not take, written or given, is refused in Python’s words alone, 'zz' is an invalid keyword argument for print(): beside such a **, a name written that print does not take waits for the merge, as in Python. Then sep, before end, must be a string or None, in the words §4.3 gives a written one, and a file the mapping gives is refused in the words a written one is given. Each of these is a catchable refusal (§10.3) with discriminator type_error. An operand that is no dict is a type_error where the unit is compiled, print() argument after ** must be a mapping, not list. A display there costs nothing of its own; every other ** is one operation at the meter, and the line is the operation it is when written out.

A ** at a builtin that takes no name. At a builtin that takes no keyword argument, such as len, abs, list, tuple, isinstance or divmod, a ** before any operand that gives a dict is read as Python reads it: the positional arguments first, in the order written, then each name, merged in the order written as at print. A name given twice is refused as it is met, len() got multiple values for keyword argument 'x', and then, once every mapping is merged, a key that is no string, keywords must be strings. Then any name at all is refused, len() takes no keyword arguments, before the arguments are counted. Each of these is a catchable refusal (§10.3) with discriminator type_error. A mapping that gives nothing leaves the call as written: len(xs, **opts) with opts empty is len(xs). An operand that is no dict is a type_error where the unit is compiled, len() argument after ** must be a mapping, not list. A count the builtin does not take is refused only where the mapping gives no name, since Python refuses a name first, and then as the program runs: with opts empty, len(xs, 2, **opts) is len() takes exactly one argument (2 given), and with opts holding x, len() takes no keyword arguments. Without such a ** that count is a type_error where the unit is compiled. So is an argument the builtin does not take, of a type or a value it refuses, as Python reads one only once the names are merged: with opts holding x, len(5, **opts) is len() takes no keyword arguments, and with opts empty object of type 'int' has no len(), as the program runs, in the words the call has without such a **, where it is a type_error as the unit is compiled; and so is isinstance given a second argument that names no class. Each such ** is one operation at the meter, and the call is the operation it is when written out.

A ** at a builtin that takes names. At int, round, pow, complex, str, zip, enumerate, sum, sorted, min and max, a ** before any operand that gives a dict is read as Python reads it, and the names it gives are the call’s, each as if written name=value. The positional arguments are read first, then each name, merged as at print, with the same two refusals. Then, where a name the callee does not take is among them, Python’s order holds: too many arguments in all, int() takes at most 2 arguments (3 given), which says keyword arguments where none was given by position and which zip counts among its names alone; then a name the call needs and lacks, pow() missing required argument 'exp' (pos 2), or one given by name and by position, argument for round() given by name ('number') and position (1); then the first such name in the order given, 'x' is an invalid keyword argument for int(). enumerate reads the names where they stand instead, as written (§8.13): enumerate(**opts) with opts holding start alone is 'start' is an invalid keyword argument for enumerate(). sorted, min and max count the names apart from their positional arguments, which are counted as written whatever the mapping gives, and sorted is refused past the merge under sort‘s name, as Python refuses it: sort() takes at most 2 keyword arguments (3 given), 'x' is an invalid keyword argument for sort(). A name written beside such a ** that the callee does not take waits for the merge, as Python merges before it counts or reads a name: with opts holding x, sorted(xs, x=1, **opts) is sorted() got multiple values for keyword argument 'x' and min(xs, x=1, **opts) is min() got multiple values for keyword argument 'x', with opts holding a key that is no string keywords must be strings at sorted, and with opts empty 'x' is an invalid keyword argument for sort(); min and max read their names in the order gathered, so a name they do not take that stands before a key that is no string is refused first. Without such a ** that name is a type_error where the unit is compiled. A key a mapping gives is never a function, since a dict holds none, and is refused as Python refuses it once there is an element to call it on, 'int' object is not callable; over no elements the call is the call without it. A default beside several positional arguments is refused once the names are read, Cannot specify a default for max() with multiple positional arguments. Otherwise the call is the call written out with those names, and refuses what that call refuses, as the program runs, in the words it is refused with where its unit is compiled. Each of these is a catchable refusal (§10.3) with discriminator type_error. A call whose type turns on which names are given is a type_error where the unit is compiled: round(x, **opts) over a real gives an int without ndigits and a real with it. A call that none of the names its mapping could give would let run is refused as the program runs, in the words of what the mapping gives, as Python’s is: sum(**opts) is sum() takes at least 1 positional argument (0 given), and given three names sum() takes at most 2 keyword arguments (3 given). dict takes every name as a key, in a new mapping: dict(**opts) holds the names given, and dict(pairs, **opts) holds pairs’ entries and then the names, written over any it already holds. Each such ** is one operation at the meter.

A ** at a built-in value’s method. At the method of a built-in value called as a value, and at one reached through its type (str.split(s, **opts)), a ** before any operand that gives a dict is read as Python reads it: the receiver and the positional arguments first, then each name, merged as at print, a name given twice naming the method with its type, str.split() got multiple values for keyword argument 'sep'. A string method that takes names, split, rsplit, splitlines or expandtabs, reads them as Python’s does, its receiver not counted, and names itself alone: split() takes at most 2 arguments (3 given), argument for split() given by name ('sep') and position (1), 'x' is an invalid keyword argument for split(). Any other method takes no name, and refuses any it is given in Python’s words: str.upper() takes no keyword arguments, after a key that is no string, keywords must be strings. A count such a method does not take is refused only where the mapping gives no name, as at a builtin that takes no name: with opts empty, s.upper(1, **opts) is str.upper() takes no arguments (1 given) as the program runs, and xs.count(1, 2, **opts) is list.count() takes exactly one argument (2 given). So is an argument such a method does not take, through its type too: with opts holding x, s.center(1.5, **opts) is str.center() takes no keyword arguments, and with opts empty 'float' object cannot be interpreted as an integer, as the program runs. A dict’s get of a key alone, which has no None to answer (§8.6), waits for the names too: with opts holding x it is dict.get() takes no keyword arguments, and with opts empty it is refused as the program runs in the words it has without such a **. Where Python’s method refuses any name before it reads one, it is named alone and its keys are not read, startswith() takes no keyword arguments; through its type it is named with it. Through its type, a receiver written of another type is refused once the names are merged, as the program runs, before the method reads a name or counts what follows: str.upper(5, **opts) is keywords must be strings where opts holds a key that is no string, and otherwise descriptor 'upper' for 'str' objects doesn't apply to a 'int' object. Through its type a call given no positional argument, only such a **, has no receiver, as one given a * of nothing has none: str.upper(**opts) is keywords must be strings where opts holds a key that is no string, str.upper() got multiple values for keyword argument 'x' where two mappings give x, and otherwise unbound method str.upper() needs an argument, as the program runs, after the names; a value bound from it is a type_error where the unit is compiled, as with a * of nothing. A mapping that gives nothing leaves the call as written. A method a bool has from int is named for bool at a call that unpacks, with a * or a **, as Python names a bound method: True.bit_length(**opts) is bool.bit_length() takes no keyword arguments, where True.bit_length(x=1) names int.bit_length(). Each of these is a catchable refusal (§10.3) with discriminator type_error, and each such ** is one operation at the meter.

A ** at a change. A statement that changes a collection reads such a ** too, after its receiver and its other arguments, and so does pop, setdefault or popitem wherever it stands, and any other change read as a value, which is made before the value is refused (§8.14). A list’s sort takes key and reverse as sorted does, counts them apart, and is named alone, as Python names it: sort() takes at most 2 keyword arguments (3 given), 'x' is an invalid keyword argument for sort(), while a name given twice names it with its type, list.sort() got multiple values for keyword argument 'reverse', as does a name written beside such a ** that the mapping gives too: xs.sort(x=1, **opts) is list.sort() got multiple values for keyword argument 'x', and a name it does not take waits for the merge, 'x' is an invalid keyword argument for sort() with opts empty. Given a positional argument too, it is refused in the words of what the mapping gives: sort() takes no positional arguments, or past two arguments in all sort() takes at most 2 arguments (3 given). A dict’s update takes each name as a key: d.update(pairs, **opts) writes pairs’ entries and then the names. A name given twice, dict.update() got multiple values for keyword argument 'b', is refused before anything is written, and a key that is no string, keywords must be strings, once pairs has been written and before any name is, as Python’s are. Any other change takes no name, and refuses any it is given as a method does above, before the collection changes: list.append() takes no keyword arguments, and at a set’s update, update() takes no keyword arguments. A name written beside such a ** is merged with what it gives and refused with them, as at print: with opts holding x, xs.append(5, x=1, **opts) is list.append() got multiple values for keyword argument 'x'. A count the change does not take is refused only where the mapping gives no name, since Python refuses a name first: with opts empty, xs.append(2, 3, **opts) is list.append() takes exactly one argument (2 given). So is an argument the change does not take, s.add([1], **opts) being set.add() takes no keyword arguments with opts holding x and unhashable type: 'list' with opts empty, and so is a dict’s setdefault of a key alone, which has no None to write (§8.14). So is a change that would give its receiver a second type, since a name keeps one type (§6.4): after s = {1}, s.add("a", **opts) is set.add() takes no keyword arguments with opts holding x, keywords must be strings with a key that is no string, and with opts empty the refusal §6.4 gives, assignment toschanges its type fromsettoset— a variable keeps one type across the function, as the program runs and not where the unit compiles. It is so at list.append, list.insert, list.extend, a set’s update and a dict’s update and setdefault, in a def as in a module. The statement refuses either way, so the receiver keeps its type after it; at a dict’s update, which takes its names as keys and so refuses none, the refusal waits for the merge too, a key that is no string is keywords must be strings, and nothing is written. A mapping that gives nothing leaves the change as written. Each of these is a catchable refusal (§10.3) with discriminator type_error, and each such ** is one operation at the meter. An operand that is no dict is a type_error where the unit is compiled, before a name written beside it, as Python gathers the names before it calls: list.append() argument after ** must be a mapping, not list. A receiver that has no such method is a type_error where the unit is compiled, whatever the mapping gives, as Python looks a method up before it reads an argument: 'tuple' object has no attribute 'sort'. Through a type’s name the method is found, and a receiver written of another type is refused as at a method above, once the names are merged: over a tuple t, list.sort(t, **opts) is descriptor 'sort' for 'list' objects doesn't apply to a 'tuple' object before a name sort does not take is read. Without such a **, a name written and a receiver of another type are each a type_error where the unit is compiled, the receiver’s first: through a list’s sort, over a tuple t, list.sort(t, 1), list.sort(t, x=1) and list.sort(t, key=len, reverse=True, x=1) are each descriptor 'sort' for 'list' objects doesn't apply to a 'tuple' object, before the positional argument is counted or the names are, as Python’s descriptor refuses a receiver before its method reads an argument.

A ** at a built-in namespace’s function. At a function of a built-in namespace (§8.16–§8.18), a ** before any operand that gives a dict is read as Python reads it: the positional arguments first, then each name, merged as at print, a name given twice naming the function with the module Python defines it in, math.isclose() got multiple values for keyword argument 'rel_tol' and _struct.pack() got multiple values for keyword argument 'x'. math.isclose reads its names as Python’s does and names itself alone: isclose() takes at most 4 keyword arguments (5 given), isclose() missing required argument 'b' (pos 2), argument for isclose() given by name ('a') and position (1), and then 'x' is an invalid keyword argument for isclose(). Any other function takes no name, and refuses any it is given, math.sqrt() takes no keyword arguments, after a key that is no string, keywords must be strings, except math.log, which refuses any name before it reads a key, log() takes no keyword arguments. A mapping that gives nothing leaves the call as written. The three methods a type answers itself, dict.fromkeys, str.maketrans and float.fromhex, read a ** the same way, named with their type: with opts holding x, dict.fromkeys(xs, 0, **opts) is dict.fromkeys() takes no keyword arguments, with a key that is no string keywords must be strings, and with opts empty dict.fromkeys(xs, 0). A count a function that takes no name does not take is refused only where the mapping gives no name, as the program runs, as at a builtin that takes no name: with opts empty, math.sqrt(x, 2, **opts) is math.sqrt() takes exactly one argument (2 given), and without such a ** that count is a type_error where the unit is compiled. So is an argument such a function does not take: with opts holding x, math.sqrt("a", **opts) is math.sqrt() takes no keyword arguments, and with opts empty it is refused as the program runs in the words it has without such a **, math.sqrt() takes a real number, not string. Each of these is a catchable refusal (§10.3) with discriminator type_error, an operand that is no dict is a type_error where the unit is compiled, math.sqrt() argument after ** must be a mapping, not list, and each such ** is one operation at the meter.

A * beside such a **. Beside a * whose length is known only as the program runs, such a ** is read where what is called takes no name: a builtin that takes no name, a function of a built-in namespace other than math.isclose, and a built-in value’s method or a change that takes none. Python’s order holds. Every argument is read, left to right; then the names are merged, a key that is no string refused, keywords must be strings, and a name given twice, str.upper() got multiple values for keyword argument 'x'; then the receiver, where the * gives it through its type, so that str.upper(*xs, **opts) is unbound method str.upper() needs an argument while xs is empty and descriptor 'upper' for 'str' objects doesn't apply to a 'int' object where it gives an int; then each name, any of which is refused as above; and then the count the * gives, as at a call with a * alone. So it is where nothing types what the * gives, as with xs = []; without such a ** that call is a type_error where the unit is compiled, in the same words. With opts holding x, len(*xs, **opts) is len() takes no keyword arguments whatever xs holds, and with opts empty, over two elements, len() takes exactly one argument (2 given). So is an argument the * gives that the call does not take: over ys = [1.5], s.center(*ys, **opts) is str.center() takes no keyword arguments with opts holding x, and 'float' object cannot be interpreted as an integer with opts empty. Each of these is a catchable refusal (§10.3) with discriminator type_error, and each such ** is one operation at the meter. A builtin that takes names reads it too, int, round, pow, complex, str, zip, enumerate, sum and dict, over the count the * gives, and Python’s order holds there as without a *: too many arguments in all, int() takes at most 2 arguments (3 given); then a name missing or given by name and by position at that count; then the first name the callee does not take, 'x' is an invalid keyword argument for int(); and otherwise what the call written out with that count refuses, so that with xs empty and opts holding base, int(*xs, **opts) is int() missing string argument. enumerate reads the names where they stand, over the count the * gives. sorted, min and max read it too, and count the * first, as Python’s do: over two elements sorted(*xs, **opts) is sorted expected 1 argument, got 2, and over none min(*xs, **opts) is min expected at least 1 argument, got 0, whatever opts holds; then the names, counted apart from the arguments and refused under sort’s name at sorted; then a default beside several arguments; then a key that is no function. A built-in value’s method that takes names, split, rsplit, splitlines and expandtabs, reads it as a builtin does, over the count the * gives, its receiver too where the * gives it; and math.isclose reads it in Argument Clinic’s order: too many arguments in all; then too many by position, isclose() takes exactly 2 positional arguments (3 given); then a name given by name and by position; then an argument missing; then a name it does not take. A list’s sort reads it too, in the words given at a change above, and counts what the * gives as arguments by position once the names are merged: past two in all, sort() takes at most 2 arguments (3 given), and otherwise any of them, sort() takes no positional arguments, before a name it does not take. A dict’s update counts what the * gives, update expected at most 1 argument, got 2, then writes its entries and then the names, as keys, and a key that is no string is refused once those entries are written and before any name is. Through its type, where the list sorted is the first thing the * gives, list.sort(*xs, **opts) merges the names before it takes the list from the *, as at every method so reached: with xs empty, a key that is no string is keywords must be strings and a name given twice is list.sort() got multiple values for keyword argument 'reverse', and only then is the call unbound method list.sort() needs an argument, or, where what the * gives first is no list, descriptor 'sort' for 'list' objects doesn't apply to a 'tuple' object. What follows the list is then counted as above, before a name it does not take. With the names written and no such **, a first value of a type that is no list is a type_error where the unit is compiled, in the same words, and the first thing every * and every positional argument gives is the list, as the paragraph on a * at a method says.

A value bound from a call that can only refuse. Where such a ** leaves a call for the run to refuse, and nothing types what the call would answer, a value bound from it is a type_error where the unit is compiled, in the call’s own words, whatever the mapping gives, since a bare assignment binds its name to the initializer’s static type (§6.4) and such a call has none. So it is through a type’s name at a receiver written of another type, where the method answers what that type holds: v = list.pop((1, 2), **opts) is descriptor 'pop' for 'list' objects doesn't apply to a 'tuple' object, and v = dict.setdefault(5, "a", 0, **opts) is descriptor 'setdefault' for 'dict' objects doesn't apply to a 'int' object. So it is at a count the callee does not take where no count it takes types the call, v = chr("a", 2, **opts) being chr() takes exactly one argument (2 given), and where nothing types what a * gives: over xs = [], v = list.pop(*xs, **opts) is cannot infer type of binding `v` — right-hand side did not typecheck to a known type. So it is for a change read as a value (§8.14), for a special name whose answer no value here holds (§8.20), and for an argument a callee that takes no name does not take, where what the callee answers turns on that argument or is what no value here holds, each in the words it is refused with without such a **: v = abs("a", **opts) is a type_error as v = abs("a") is, since abs answers the type it is given, and so is v = d.get(k, **opts), which has no None to answer (§8.6). Python merges the names first, so that where opts holds a key that is no string each of these is keywords must be strings; here the mapping’s words answer only where the same call is read as a statement or printed, which is refused as the program runs, as above. A handler around such a binding does not change this, since nothing has run when it is refused. Where the call fixes a type, as str.upper(5, **opts), len(xs, 2, **opts), len(5, **opts) and s.center(1.5, **opts) do, a value bound from it is refused as the program runs, as the call is.

A built-in namespace’s call. A * before a positional argument of a call into math, cmath, vector or struct (§8.16–§8.18) spreads its operand’s elements across the call’s positional arguments, as in a script’s call: given p = [1.0, 1.0], math.atan2(*p) answers what math.atan2(1.0, 1.0) answers, and struct.pack(f"<{len(xs)}q", *xs) packs every element of a list of integers xs. A call whose spreads are all of fixed tuples has its arguments counted at compile of the unit, as written ones are. With a spread of any other collection they are counted as the program runs, and a count the function does not take is a catchable refusal (§10.3) with discriminator type_error, in the words the compiler gives a call written out: math.atan2() takes 2 positional arguments but 3 were given. Each element must suit every parameter it could fill, as in a script’s call. math.isclose takes a and b by name beside a * too, and since which of them the * fills is known only as the program runs, a name is placed at the count the * gives, as Python places it: with xs holding one element, math.isclose(*xs, b=1.0) compares it with 1.0; with two it is argument for isclose() given by name ('b') and position (2), and with none isclose() missing required argument 'a' (pos 1), each a catchable refusal with discriminator type_error.

A built-in function’s call. A * before a positional argument of a built-in function spreads its operand’s elements across the call’s positional arguments, and every built-in function takes one: max(*xs), zip(*rows), range(*bounds), len(*[s]). Every argument is evaluated, left to right, before the function runs, and the call is then the call as written with that many arguments. A count Python’s function does not take is refused in the words of the TypeError Python raises: abs() takes exactly one argument (2 given), range expected at most 3 arguments, got 4, pow() missing required argument 'exp' (pos 2). A spread of a fixed tuple, or of a list display holding no spread, says how many arguments it gives, so its count is checked at compile of the unit: abs(*(1, 2)) is a type_error, and any other is a catchable refusal (§10.3) with discriminator type_error as the program runs. So is a count written out that a spread cannot make right, whatever it holds: three arguments and a spread to divmod. Each element must suit what the function takes at every count it could give: at compile, the call is checked as written at each count the function takes. A count no element could suit is refused as the program runs, in the words a written call is refused in: given xs = [1.5, 1.5], round(*xs) is a refusal naming the digit count, while round(*[1.5]) is 2. Where no count is suitable the call is a type_error at compile, as abs(*["a"]) is. max and min read one argument as the collection they compare, so max(*[5]) is the refusal 'int' object is not iterable, in Python’s words.

A call’s type is one type, so a function whose result’s type would depend on how many arguments a spread gives is a type_error at compile saying so, where Python decides as it runs: over a list<list<int>>, max with a spread gives an int for one list and a list<int> for two. Spreads, and arguments beside them, that hold types with no common type by the list-literal rule are a type_error, since which of them stands at each position is known only as the program runs. zip takes one column per element, so a row’s length is the program’s: zip(*rows) is an iterator<tuple<T, ...>>. isinstance and issubclass name a class, which is no value here (§13.9), so no spread gives one and either is refused saying so. A collection with no element type yet, xs = [] with nothing added, holds nothing, as *[] does. The operand is an expression, as in a script’s call, and a spread of no collection is a type_error at its *, in Python’s words by the call’s shape: max() argument after * must be an iterable, not int as the call’s only argument, and Value after * must be an iterable, not int otherwise. Each spread is one operation at the meter, and the call is the operation it is when written out (§8.3).

A keyword argument beside a built-in function’s * binds as Python binds it, written before the * or after it, and the call answers what it answers written out: int(*["ff"], base=16), round(*xs, ndigits=1), enumerate(*xs, start=1), sum(*xs, start=1), pow(*xs, mod=5), complex(*xs, imag=1), sorted(*xs, reverse=True), max(*xs, default=0), zip(*rows, strict=True) and dict(*xs, a=1). A count that does not fit the keyword is refused in the words of the TypeError Python raises, as a count is above: over two elements, pow(*xs, exp=2) raises argument for pow() given by name ('exp') and position (2), and max(*xs, default=0) raises Cannot specify a default for max() with multiple positional arguments. A name the function does not take is a type_error at compile, since every count would raise, where Python raises as the program runs, so no handler around the call sees it: abs(*xs, x=1), and pow(*[2], x=1), where Python finds the missing argument first. str takes no encoding or errors, since no value here is bytes. A spread of [] or of a list display gives a count this language reads at compile, so the refusal comes then and no handler around the call sees that either, where Python raises the same words as the program runs: xs = [], then abs(*xs). A spread of one value that is no collection is told as the value written out is, where the function reads a finite collection: xs = [1], then sorted(*xs), is sorted() requires a finite collection as sorted(1) is, where Python raises 'int' object is not iterable. A ** before a dict display beside such a * gives its keys as written keyword arguments, and one before any other dict is read as above; print reads both.

min and max with key alone compare the arguments, or what one argument holds, so max(*xs, key=abs) over one int raises 'int' object is not iterable, and over a list<list<int>> the call has no one type, as above. A * alone among the positional arguments is read after the keyword arguments’ values, as in Python, and one beside another positional argument in its turn, so a keyword’s value that changes the collection shows which. Two calls read it ahead of those values, where Python reads it after: a method’s call and a call into a namespace.

A built-in method’s call. A * before a positional argument of a built-in method’s call spreads its operand across the call’s arguments, as at a built-in function’s call: "-".join(*[xs]), "abc".replace(*pair), xs.index(2, *bounds). Through a type’s name the first thing a spread gives may be the receiver: str.upper(*["a"]), list.append(*[xs, 1]). A list display there is read as the tuple it spells, each position of its own type. The receiver is evaluated first, then every argument left to right, and the call is then the call as written with that many arguments. A spread giving no receiver raises unbound method str.upper() needs an argument.

A count a built-in method does not take is refused in the words of the TypeError Python raises, which follow how the method is written in C and not what it does: str.upper() takes no arguments (1 given), str.join() takes exactly one argument (0 given), replace expected at least 2 arguments, got 1, find() takes at least 1 argument (0 given), insert expected 2 arguments, got 1, pop expected at most 1 argument, got 2. Written out, or with a spread that says how many it gives, the count is checked at compile of the unit, and with any other spread it is a catchable refusal (§10.3) with discriminator type_error as the program runs, counting what the spread gave. So is more written out than the method takes beside such a spread, and a suitable count an element cannot suit: each element must suit what the method takes at every count it could give, and where it suits none the call is a type_error at compile. A method’s result that would have two types by count is a type_error saying so, as at a built-in function’s call. A name beside such a spread binds as Python binds it once the spread’s count is known, and the call answers what it answers written out: "a b c".split(*ys, maxsplit=1), "a b".split(*ys, sep=" "), s.splitlines(*ys, keepends=True). A count that does not fit the name is refused in the words of the TypeError Python raises, counting the name with the positional arguments: split() takes at most 2 arguments (3 given) past two in all, and argument for split() given by name ('sep') and position (1) where the spread fills the place the name was given for. A name the method does not take, or one beside which every count would raise, is a type_error at compile, where Python raises as the program runs: "a b".split(*ys, x=1), and "a b".startswith(*ys, x=1), which takes no names. A dict‘s update makes its names keys, as written out, and counts only what the spread gives: d.update(*ys, k=1). A list’s sort takes no positional argument, so xs.sort(*ys, reverse=True) sorts where ys gives nothing and otherwise raises sort() takes no positional arguments, or sort() takes at most 2 arguments (3 given) counting the names. A name it does not take is refused only after that count, whether the receiver is written before the call or through the type: xs.sort(*ys, x=1) and list.sort(xs, *ys, x=1) raise sort() takes no positional arguments where ys gives a value, and 'x' is an invalid keyword argument for sort() where it gives none, the values of the names read first, and where nothing types ys it gives nothing (§6.3) and the second is refused where the unit is compiled, after a receiver of another type is refused for its type. A positional argument written beside the * is counted with what the * gives, before any name: xs.sort(*ys, 5) and xs.sort(5, *ys), and the same through the type, raise sort() takes no positional arguments where ys gives one value or none, and sort() takes at most 2 arguments (3 given) where ys gives two, or one with a name beside it, the names counted; where nothing types ys the refusal is the compile’s, in the same words. A key= beside the * is applied only where the sort runs, after the count: xs.sort(*ys, key=len) over a list of ints raises sort() takes no positional arguments where ys gives one value, and sort() takes at most 2 arguments (3 given) where it gives two, the key never applied; where ys gives none the sort runs, and a key an element refuses (object of type 'int' has no len()) is raised as the sort applies it, none being raised where the list has no element. Each is decided as the program runs and is catchable. Where nothing types ys, and where the call has no *, a key the elements refuse is refused where the unit is compiled. Through the type’s name the first thing every * and every positional argument gives, in order, is the list sorted: list.sort(*ys), and list.sort(*ys, xs), which sorts xs where ys gives nothing and otherwise sorts the first thing ys gives and then raises sort() takes no positional arguments. Where nothing gives a first thing the call raises unbound method list.sort() needs an argument, before any name is read: list.sort(*ys, x=1) is that and not 'x' is an invalid keyword argument for sort(); and where the first thing is no list, descriptor 'sort' for 'list' objects doesn't apply to a 'int' object, before what follows it is counted, as with list.sort(*ys, 1, 2). What follows the list is then counted, sort() takes no positional arguments, or sort() takes at most 2 arguments (3 given) counting the names, and then the names, sort() takes at most 2 keyword arguments (3 given) and 'x' is an invalid keyword argument for sort(). Where nothing types what a * gives it gives nothing (§6.3) and the call is refused where the unit is compiled; where a * is typed, as ys[0:0] is, it may give nothing and the refusal is the program’s, a catchable refusal (§10.3). Where a * and the first argument written after it give values of different types, the unit cannot say what stands first and refuses it as it refuses a spread of values of different types. The spread and the names’ values are read before the count is checked, as in Python, and a key is called only after it is checked. A display there stands in the call as written and costs nothing of its own. Every other spread is one operation at the meter, and the call is the operation it is when written out (§8.3).

A spread adds no iteration to a program: it reads a finite value once, and an iterator no further than the length its source had when it was made (§8.6). So the termination guarantee (§9.5) is unaffected.

8.16 The math and cmath namespaces

import math
import cmath

math and cmath are Python’s modules of those names, provided by the language rather than stored. import math reads nothing from the store and runs nothing; it makes math.<name> available from its line to the end of the unit, as every import does (Python Reference §9.3). from math import sqrt, pi binds the names it lists, as a from import of a stored module does, and leaves math itself unbound. No stored module may be named math or cmath: compiling one is refused, so each name always means its namespace. Any other name Python gives a module of its own is an ordinary module name.

math

Name Signature Answers
math.sqrt(x) N → real the square root of x
math.exp(x) N → real e to the power x
math.log(x[, base]) N → real; (N, N) → real the natural logarithm of x; with base, the logarithm of x to that base, rounded once: math.log(1000, 10) is 3.0
math.log10(x), math.log2(x) N → real the logarithm of x to base 10, and to base 2
math.erf(x), math.erfc(x) N → real the error function of x, and its complement 1 - erf(x)
math.gamma(x), math.lgamma(x) N → real the gamma function of x, and the natural logarithm of its magnitude: math.gamma(5) is 24.0
math.sin(x), math.cos(x), math.tan(x) N → real of an angle x in radians
math.asin(x), math.acos(x), math.atan(x) N → real the angle in radians whose sine, cosine or tangent is x: from -π/2 to π/2 for asin and atan, and from 0 to π for acos
math.atan2(y, x) (N, N) → real the angle of the point (x, y), from -π to π
math.hypot(*coordinates) (N, …) → real the distance from the origin to the point with these coordinates, of which there may be any number: math.hypot(3, 4) is 5.0, and math.hypot() is 0.0
math.degrees(x), math.radians(x) N → real the angle x, given in radians, in degrees; and x, given in degrees, in radians
math.isclose(a, b, *, rel_tol=1e-09, abs_tol=0.0) (N, N) → bool whether a and b differ by at most rel_tol times the larger of their magnitudes, or by at most abs_tol
math.floor(x), math.ceil(x) N → int the greatest integer no larger than x, and the least no smaller; an int is its own, and a bool the int it is
math.trunc(x) N → int the integer nearest zero, with the same rules for an int, a bool and a real past what an int holds
math.sinh(x), math.cosh(x), math.tanh(x) N → real the hyperbolic sine, cosine and tangent of x
math.asinh(x), math.acosh(x), math.atanh(x) N → real the inverse hyperbolic sine, cosine (for x of 1 or more) and tangent (for x between -1 and 1, exclusive)
math.cbrt(x) N → real the cube root of x, a negative x included
math.exp2(x) N → real 2 to the power x
math.expm1(x), math.log1p(x) N → real e to the power x, less 1; and the natural logarithm of 1 plus x (for x above -1), each exact where x is near zero
math.pow(x, y) (N, N) → real x to the power y, always a real: math.pow(2, 10) is 1024.0
math.fabs(x) N → real the magnitude of x, as a real
math.copysign(x, y) (N, N) → real x with the sign of y, a zero’s sign included: math.copysign(1, -0.0) is -1.0
math.fmod(x, y), math.remainder(x, y) (N, N) → real the remainder of x divided by y with the sign of x; and x less y times the whole number nearest x / y, a tie going to the even one
math.modf(x) N → tuple<real, real> the fractional and the whole part of x, each with x’s sign: math.modf(-2.5) is (-0.5, -2.0)
math.frexp(x) N → tuple<real, int> the mantissa and exponent of x, x being the mantissa times 2 to the exponent and the mantissa’s magnitude at least 0.5 and under 1 (or zero): math.frexp(8.0) is (0.5, 4)
math.ldexp(x, i) (N, I) → real x times 2 to the power i, an int
math.nextafter(x, y) (N, N) → real the real after x in the direction of y, y itself where they are equal
math.ulp(x) N → real the gap between the magnitude of x and the next real; for the largest real, the gap below it
math.gcd(*integers), math.lcm(*integers) (I, …) → int the greatest common divisor and the least common multiple of any number of ints, none included: math.gcd() is 0, math.lcm() is 1, and a zero among them makes lcm 0
math.comb(n, k) (I, I) → int the number of ways to choose k of n, 0 where k is past n
math.perm(n[, k]) I → int; (I, I) → int the number of ordered choices of k of n, 0 where k is past n; with one argument, k is n, which Python writes as k=None and DKE Python, which has no None (§3.5.4), as no k
math.factorial(n) I → int the product of the whole numbers from 1 to n
math.isqrt(n) I → int the greatest int whose square is no larger than n
math.fsum(iterable) collection<N> → real the sum of the elements, each taken exactly: math.fsum([0.1] * 10) is 1.0
math.prod(iterable, *, start=1) collection<C> → N or complex the product of start and the elements: an int where each is an int, a complex where one is, and a real otherwise
math.dist(p, q) (collection<N>, collection<N>) → real the distance between the points p and q, of one length: math.dist([1, 2], [4, 6]) is 5.0
math.isinf(x), math.isnan(x), math.isfinite(x) N → bool whether x is an infinity, a NaN, or neither; an int and a bool are finite
math.pi, math.e, math.tau real the doubles nearest π, e and 2π
math.inf, math.nan real the positive infinity and the NaN (§3.5.7)

cmath

Name Signature Answers
cmath.exp(z) C → complex e to the power z
cmath.sqrt(z) C → complex the principal square root of z
cmath.phase(z) C → real the argument of z, from -π to π
cmath.polar(z) C → tuple<real, real> (abs(z), phase(z))
cmath.rect(r, phi) (N, N) → complex the complex of modulus r and argument phi
cmath.isinf(z), cmath.isnan(z), cmath.isfinite(z) C → bool whether a part of z is an infinity, whether a part is a NaN, and whether both parts are finite: cmath.isinf(complex(math.inf, math.nan)) and cmath.isnan of it are both True
cmath.pi, cmath.e, cmath.tau, cmath.inf, cmath.nan real as in math
cmath.infj, cmath.nanj complex the complex with a zero real part and an imaginary part that is inf, and nan

N is the numeric operand set of §8.3, {int, real, bool}, I is the integer set {int, bool}, and C adds complex to N. A math function given a complex, bar math.prod, which multiplies one, and any function given a value that is not a number, is a type_error, where Python raises TypeError. A function of I given a real is a type_error in Python’s words, 'float' object cannot be interpreted as an integer, refused where the unit is compiled wherever the argument is a real; math.ldexp’s exponent, given one, is Expected an int as second argument to ldexp.. The floor, ceiling or truncation of a real past what an int holds is a runtime refuse (§10.3), as round’s is, where Python answers a longer integer, and so is an int answer of gcd, lcm, comb, perm, factorial or prod that is past what an int holds, as any such int is (§8.3): math.factorial(21). Every intermediate step of them is exact, so an answer that fits is never refused for a step that did not.

Only math.isclose and math.prod take keyword arguments: rel_tol and abs_tol by name only, and a and b by position or by name, for the first; start by name only, for the second, whose count Python words as prod() takes exactly 1 positional argument (2 given) and prod() takes at most 2 arguments (3 given). math.hypot, math.gcd and math.lcm take any number of arguments, none included, so no count is refused. A call with more or fewer arguments than the function takes is a type_error at the function’s name, in the words of the TypeError Python raises, checked where the unit is compiled: math.sqrt() takes exactly one argument (2 given), atan2 expected 2 arguments, got 1, math.log requires 1 to 2 arguments, isclose() missing required argument 'b' (pos 2), isclose() takes exactly 2 positional arguments (3 given), and isclose() takes at most 4 keyword arguments (5 given) where every argument is named. A keyword the function does not take, a constant called (math.pi()), a function used as a value (f = math.sqrt, §8.8), a name the namespace does not have, a namespace used without its import, and a call on a line of its own, whose value would go nowhere, are each a parse_error.

Results. Each function follows Python’s own algorithm for it (CPython 3.11), and each power, logarithm, exponential, trigonometric function, inverse trigonometric function and modulus that algorithm uses is correctly rounded, as in §8.3. So a result is the same wherever it is computed, and departs from Python’s only where Python’s platform rounds one of those steps wrongly: math.atan2(-31.77378470442932, -2.721897422427844) is -1.6562525739043301, the exact angle rounded, where Python on a platform that rounds this one wrongly prints -1.65625257390433. log10, log2, erf, erfc, gamma and lgamma are likewise the exact value rounded once, where Python’s platform, or for gamma Python’s own approximation, may differ in the last digit, and so are sinh, cosh, tanh, asinh, acosh, atanh, cbrt, exp2, expm1, log1p and pow: math.cbrt(576.7985282082266) is 8.32417843718322, where Python on a platform that rounds it wrongly prints 8.324178437183217, and math.sinh(5.097328896223573) is 81.78912966062903, where such a platform prints 81.78912966062904. fabs, copysign, fmod, remainder, trunc, modf, frexp, ldexp, nextafter and ulp are exact on every platform, and the integer functions are exact. An int past 2**53 is given to each function as the int it is, where Python converts it to a real first, so the answer is the function’s exact value at the int, rounded once: math.sqrt(893270773857066291) is 945130030.1318684, where Python answers 945130030.1318682; math.sin(9007199254740993) is -0.9034039880133538, where Python answers -0.848925964814655; math.fmod(9007199254740993, 10.0) is 3.0, where Python answers 2.0; and math.isclose(9007199254740993, 9007199254740992, rel_tol=0.0) is False, where Python answers True. nextafter steps from the int itself and ulp is the gap where the int lies; frexp alone answers for the int rounded to a real, so that its mantissa stays under 1. Ten functions depart from Python’s algorithm, because it rounds where the answer need not be:

  • math.degrees(x) is x times 180/π and math.radians(x) is x times π/180, each exact product rounded once. Python multiplies by the constant already rounded to a real and rounds again, which gives another last digit in about one result in five for degrees and one in ten for radians: math.degrees(-880.7976600675347) is -50465.9885268683, where Python answers -50465.98852686831.
  • math.log(x, base) is the logarithm of x to base rounded once. Python divides the natural logarithm of x by that of base, each already rounded to a real, and rounds the quotient again, which gives another last digit in about one result in three: math.log(1000, 10) is 3.0, where Python answers 2.9999999999999996, and math.log(125, 5) is 3.0, where Python answers 3.0000000000000004. Wherever the exact logarithm is a real, as it is for every power of the base, that is the answer. In the rare case where the exact quotient lies so near the midpoint of two reals that the result could round either way, the call is not proven to round it correctly, and no such case is known.
  • math.hypot is the distance correctly rounded, of any number of coordinates: the exact sum of their squares, whose square root is rounded once. Python’s algorithm for three or more is not proven to round correctly. So math.hypot(*p) and math.dist(p, [0] * len(p)) are one answer, and an int coordinate is its own exact value where Python first rounds it to a real.
  • math.fsum(xs) is the exact sum of the elements rounded once, an int taken as the exact int it is, and a sum of nothing, or of terms that cancel, is 0.0. Python’s sum is exact too, but raises OverflowError: intermediate overflow in fsum where a partial sum passes the largest real, though the whole is a real: math.fsum([1e308, 1e308, -1e308]) is 1e+308. A sum past the largest real is inf or -inf, where Python raises the same OverflowError. Python rounds an int element to a real first, which gives another last digit: math.fsum([2 ** 53 + 1, 1, 1]) is 9007199254740996.0, where Python answers 9007199254740994.0. A sum that lies exactly half-way between two reals is the one with the even last digit: math.fsum([1.0, 2 ** -53]) is 1.0.
  • math.prod(xs) is the exact product of start and the elements rounded once, each part of a complex product likewise. Python rounds after every factor, which gives another last digit: math.prod([-2.6, 1.9, -4.0, -2.5]) is -49.4, where Python answers -49.39999999999999. A product whose factors pass through a real beyond range, or a zero, is the exact product: math.prod([1e200, 1e200, 0.0]) is 0.0, where Python answers nan because its partial product was inf; a product itself past the largest real is inf or -inf, as Python answers. Python multiplies whatever * takes, math.prod(['ab'], start=2) being 'abab'; DKE Python takes numbers, and anything else is a type_error. A product of ints is an int, with no step refused that does not matter to the answer (math.prod([2 ** 40, 2 ** 40, 0]) is 0).
  • math.dist(p, q) is the exact distance rounded once, from the exact differences of the coordinates. Python rounds each difference, which gives another last digit: math.dist([-1.7, 1.6, -9.6], [2.3, 2.6, -8.8]) is 4.199999999999999, where Python answers 4.2. A distance past the largest real is inf, as Python answers.
  • cmath.exp, cmath.rect and cmath.sqrt answer each part of the result, the real and the imaginary, as its exact value rounded once. Python composes parts it has already rounded, e to the power of the real part times the cosine of the imaginary part, the modulus times the cosine of the angle, a square root of a sum that holds another, and rounds again, which gives another last digit in about three calls in ten: cmath.exp(complex(1.3, 2.7)).imag is 1.5681835703495033, where Python answers 1.5681835703495035; cmath.rect(2.5, 0.7).real is 1.912105468211221, where Python answers 1.9121054682112213; and cmath.sqrt(complex(3, 7)).imag is 1.5191729832155239, where Python answers 1.5191729832155236. Where Python gives a part its own way, the part is Python’s: the imaginary part of cmath.exp of a real is that real’s, signed zero and all, a zero angle makes cmath.rect(r, phi) complex(r, r * phi), and a zero part of a product takes the product’s sign. In the rare case where an exact part lies so near the midpoint of two reals that it could round either way, the call is not proven to round it correctly, and no such case is known.

math.degrees(math.asin(0.5)) is 30.000000000000004, which is no error of either function: π/6 has no exact real, math.asin(0.5) is the nearest one to it, and 30.000000000000004 is the nearest one to that real times 180/π. The sign of a zero follows Python: math.atan2(-0.0, -1.0) is -3.141592653589793, math.asin(-0.0) is -0.0, cmath.phase(-1.0) is 3.141592653589793, and cmath.sqrt(complex(-4.0, -0.0)) is -2j.

Infinity and NaN. Each function takes an infinity or a NaN and answers IEEE 754’s value, as Python does where it answers: math.exp(math.inf) is inf, math.atan(math.inf) is 1.5707963267948966, math.fsum([math.nan, 1.0]) is nan, and cmath.exp(complex(-math.inf, 1)) is 0j. math.isinf, math.isnan and math.isfinite tell them apart. DKE Python departs where Python raises instead of answering, as §8.3’s Infinity and NaN rules, and at the sign of a NaN:

Case DKE Python Python 3.11
a result past the largest real from finite arguments: math.exp(1000), math.sinh(711), math.cosh(711), math.expm1(710), math.exp2(1024), math.ldexp(1.0, 1024), math.pow(10, 400), math.gamma(172), math.lgamma(1e306), math.fsum([1e308, 1e308]) inf OverflowError
the same in cmath: cmath.exp(800), cmath.exp(complex(710, 0.5)), cmath.polar(complex(1.7e308, 1.7e308)) (inf+0j), (inf+1.0710341440060488e+308j), (inf, 0.7853981633974483) OverflowError
an infinite argument with no value: math.fmod(math.inf, 2.0), math.remainder(math.inf, 2.0), math.sin, math.cos and math.tan of an infinity, math.asin(math.inf), math.acos(-math.inf), math.atanh(math.inf), and math.fsum([math.inf, -math.inf]) nan ValueError
-inf to a function defined only from some point upward: math.sqrt, math.log, math.log10, math.log2, math.log1p, math.acosh and math.gamma of -math.inf nan ValueError
the sign of a NaN: math.copysign(1.0, -math.nan) 1.0: there is one NaN, and it has no sign (§3.5.7) -1.0

With finite arguments, an argument outside the function’s domain still refuses, as in Python (below).

Faults. A call refuses at run time where Python raises, with Python’s message (§10.3):

Cause Discriminator Message
an argument outside the function’s domain: math.sqrt(-1), math.log(0), math.log10(0), math.asin(2), math.acosh(0.5), math.atanh(1), math.log1p(-1), math.fmod(1, 0), math.remainder(1, 0), math.pow(0, -1), math.pow(-8, 0.5), and a pole of the gamma function, math.gamma(0), math.gamma(-1), math.lgamma(-3) value_error math domain error
a negative argument to the counting functions: math.comb(-1, 2), math.perm(2, -1), math.factorial(-1), math.isqrt(-1); and math.dist given points of two lengths value_error n must be a non-negative integer, k must be a non-negative integer, factorial() not defined for negative values, isqrt() argument must be nonnegative, both points must have the same number of dimensions
an int answer past what an int holds: math.factorial(21), math.gcd(-2 ** 63), math.trunc(1e19) arithmetic_overflow integer overflow: `math.factorial` result exceeds the signed 64-bit range, with the function’s name, as §8.3 refuses an int past its range
an infinity or a NaN to math.floor, math.ceil or math.trunc, which no int is arithmetic_overflow for an infinity, value_error for a NaN cannot convert float infinity to integer, cannot convert float NaN to integer
a base of 1 to math.log division_by_zero float division by zero
a negative tolerance to math.isclose value_error tolerances must be non-negative

A result too small to represent is the nearest number that is, zero included, and no fault: math.exp(-1000) is 0.0, as in Python.

A constant is the literal it names, so it costs nothing at the meter, as a literal does. A function call is one operation, as every built-in call is.

8.17 The vector namespace

import vector

vector is provided by the language, as math and cmath are, and is imported and reserved as they are (§8.16): import vector reads nothing from the store, and no stored module may be named vector. Python has no module of this name. The name is reserved all the same so that import vector names the same namespace in every store: a module written against it cannot be changed by what another module in its store is called. Each of its two functions is defined below as a Python function, and answers what that function answers.

Name Signature Answers
vector.apply(xs, m, axes) (list<C>, list<list<C>>, list<int>) → list<C> xs with the square matrix m applied along the axes axes
vector.sumsq(xs, axes) (list<C>, list<int>) → list<real> the sum of the squared parts of the elements of xs, for each pattern of the axes axes

C is the operand set of §8.16, {int, real, bool, complex}.

Shape. xs holds 2ⁿ numbers for some n ≥ 0, and axis a is bit a of an index into it, so axis 0 is the lowest bit. axes names k different axes, each from 0 to n − 1, and m is 2ᵏ rows of 2ᵏ numbers. The first axis named is the highest bit of a row or column of m: with axes [a, b], row 2 of m is the element whose bit a is 1 and whose bit b is 0. With no axes, m is [[c]] and every element is multiplied by c.

Result. The answer is the list this Python function returns, element for element and class for class, signed zeros included:

import math
from fractions import Fraction


def apply(xs, m, axes):
    k = len(axes)
    off = [sum(1 << a for t, a in enumerate(axes) if j >> (k - 1 - t) & 1)
           for j in range(1 << k)]
    out = list(xs)
    for base in range(len(xs)):
        if any(base >> a & 1 for a in axes):
            continue
        for r in range(1 << k):
            terms = [(m[r][c], xs[base | off[c]]) for c in range(1 << k)]
            acc = terms[0][0] * terms[0][1]
            for a, x in terms[1:]:
                acc = acc + a * x
            out[base | off[r]] = exact(acc, terms)
    return out


def exact(acc, terms):
    if isinstance(acc, int) or not all(finite(v) for t in terms for v in t):
        return acc
    re = sum(part(a, 0) * part(x, 0) - part(a, 1) * part(x, 1) for a, x in terms)
    if isinstance(acc, float):
        return rounded(re, acc)
    im = sum(part(a, 0) * part(x, 1) + part(a, 1) * part(x, 0) for a, x in terms)
    return complex(rounded(re, acc.real), rounded(im, acc.imag))


def part(v, i):
    if isinstance(v, complex):
        return Fraction((v.real, v.imag)[i])
    return Fraction(v) if i == 0 else Fraction(0)


def finite(v):
    parts = (v.real, v.imag) if isinstance(v, complex) else (v,)
    return all(math.isfinite(p) for p in parts)


def rounded(q, step):
    if q == 0:
        return step if step == 0 else 0.0
    try:
        return float(q)
    except OverflowError:
        return math.inf if q > 0 else -math.inf

Each * and + in acc is the operator of §8.3, and an element keeps the class that arithmetic gives: vector.apply([1.0, 2.0], [[1, 0], [1j, 1]], [0]) is [1.0, (2+1j)]. An element of ints is that int. Any other whose operands are all finite is, in each part, the exact sum of the exact products rounded once, whatever order the products come in: vector.apply([1.0, 1.0, 1.0, 1.0], [[1e16, 1.0, -1e16, 1.0], [0.1, 0.2, 0.3, 0.4], [1.0, 1e16, 1.0, -1e16], [0.0, 0.0, 0.0, 1.0]], [0, 1]) is [2.0, 2.0, 1.0, 1.0], where the products added left to right give [1.0, 0.0, 1.0, 1.0]. A part whose exact value is zero takes the sign acc gives it where that is a zero, and +0 where it is not, and a part past the range of a real is inf or -inf. An element with an operand that is not finite is acc, IEEE 754’s value at each step. The result’s type is list<complex> where xs or m holds complex, and otherwise the type * gives an element of each.

An argument of another type is a type_error. A wrong number of arguments is a type_error too, as in §8.16, in the words Python gives a function written in Python: vector.apply() missing 2 required positional arguments: 'm' and 'axes' and vector.apply() takes 3 positional arguments but 4 were given. A function used as a value, a namespace used without its import, and a call on a line of its own are each a parse_error, as in §8.16.

Faults. A call refuses at run time, checking in the order of this table, and an axis’s two checks in the order axes names it:

Cause Discriminator Message
len(xs) is not a power of two, 0 included value_error vector.apply() needs a vector whose length is a power of two, and 3 is not one
an axis outside 0 to n − 1 value_error vector.apply(): axis 2 is out of range for a vector of 4 elements
an axis named twice value_error vector.apply(): axis 1 is named twice
m is not 2ᵏ rows of 2ᵏ value_error vector.apply(): the axes named take a 2 × 2 matrix
an int product or sum past the signed 64-bit range, as §8.3 refuses it arithmetic_overflow integer overflow:vector.applyresult exceeds the signed 64-bit range

A call is one operation, whatever the length of xs, as every built-in call is. Its result is built in full, so its elements count toward any limit a service sets on how much one run builds, as the elements of [0] * n do.

vector.sumsq. The companion to vector.apply: where apply carries a matrix along axes, sumsq adds along them, one sum for each pattern the named axes can make.

Shape. xs holds 2ⁿ numbers for some n ≥ 0 and axis a is bit a of an index into it, as for apply, and axes names k different axes, each from 0 to n − 1. The first axis named is the highest bit of a pattern: with axes [a, b], pattern 2 is the elements whose bit a is 1 and whose bit b is 0. With no axes there is one pattern and every element is in it. The answer holds the 2ᵏ sums, the first for the pattern 0.

Result. The answer is the list this Python function returns, element for element, signed zeros included:

from fractions import Fraction


def sumsq(xs, axes):
    k = len(axes)
    parts = [[] for _ in range(1 << k)]
    for i, x in enumerate(xs):
        j = 0
        for a in axes:
            j = j << 1 | (i >> a & 1)
        z = complex(x)
        parts[j] += [z.real, z.imag]
    return [sum_of_squares(p) for p in parts]


def sum_of_squares(p):
    if not all(math.isfinite(v) for v in p):
        return math.fsum(v * v for v in p)
    try:
        return float(sum(Fraction(v) ** 2 for v in p))
    except OverflowError:
        return math.inf

Each element is read as the complex number complex(x) is, so an int or a bool is its real part with a zero imaginary part, and each sum is the exact sum of the exact squares of the parts, rounded once, whatever the order of what it adds: vector.sumsq([-1.561131722497604, -2.099686097555672], []) is [6.845813963256903], where math.fsum over the squares, each already rounded, answers 6.845813963256904. A sum past the range of a real is inf, and so is one holding a square past it (1e200 * 1e200), and an element with a part that is nan makes its sum nan, as §8.16’s math.fsum answers each. vector.sumsq([3, 4], []) is [25.0], vector.sumsq([1, 2, 3, 4], [0]) is [10.0, 20.0], and vector.sumsq([1, 2, 3, 4], [1, 0]) is [1.0, 4.0, 9.0, 16.0] where [0, 1] makes [1.0, 9.0, 4.0, 16.0]. Every sum is a real whatever the numbers that make it, and the result’s type is list<real>.

An argument of another type is a type_error, and so is a wrong number of arguments, in the words Python gives a function written in Python: vector.sumsq() missing 1 required positional argument: 'axes' and vector.sumsq() takes 2 positional arguments but 3 were given. A function used as a value, a namespace used without its import, and a call on a line of its own are each a parse_error, as in §8.16.

Faults. A call refuses at run time, checking in the order of this table, and an axis’s two checks in the order axes names it; the first three are vector.apply’s, with vector.sumsq for its name:

Cause Discriminator Message
len(xs) is not a power of two, 0 included value_error vector.sumsq() needs a vector whose length is a power of two, and 3 is not one
an axis outside 0 to n − 1 value_error vector.sumsq(): axis 2 is out of range for a vector of 4 elements
an axis named twice value_error vector.sumsq(): axis 1 is named twice

The Python function does not refuse an axis out of range or named twice, which it reads as a bit that is never set or set again; the call refuses both, as apply does. Nothing else refuses: vector.sumsq([1e200, 1.0], [0]) is [inf, 1.0].

A call is one operation, whatever the length of xs, as every built-in call is. Its result is the 2ᵏ sums and nothing more, so only those elements count toward any limit a service sets on how much one run builds, as the elements of [0] * n do, and the length of xs adds nothing to them.

8.18 The struct namespace

import struct

struct is Python’s module of that name, provided by the language, and is imported and reserved as math is (§8.16): import struct reads nothing from the store, and no stored module may be named struct. It lays numbers out as the bytes of a blob (§3.5.8), and reads them back, by a format string. Each call answers what Python’s struct answers, in Python’s words where it refuses, except where a value is one DKE does not hold (below).

Name Signature Answers
struct.pack(format, v1, v2, …) (string, …) → blob the values laid out as the format describes
struct.unpack(format, buffer) (string, blob) → tuple the values the format reads from buffer, in order
struct.calcsize(format) string → int how many bytes the format lays out

The format. An optional first character chooses the byte order, the sizes and the alignment. Codes follow, each after an optional decimal count, and whitespace between codes is skipped.

First character Byte order Sizes Alignment
@, or none little-endian native native
= little-endian standard none
< little-endian standard none
> or ! big-endian standard none

Native layout is the one Python gives on a 64-bit x86 Linux host, wherever the program runs, so a format lays out the same bytes everywhere: with @, a code is preceded by the zero bytes that bring its offset to a multiple of its alignment.

Code Packs Unpacks to Standard size Native size, alignment
x no value, as a zero byte no value 1 1, 1
c a blob of one byte a blob of one byte 1 1, 1
b, B I int 1 1, 1
? any value, by its truth bool 1 1, 1
h, H I int 2 2, 2
i, I I int 4 4, 4
l, L I int 4 8, 8
q, Q I int 8 8, 8
n, N, P I int none 8, 8
e N real 2 2, 2
f N real 4 4, 4
d N real 8 8, 8
F C complex 8 8, 4
D C complex 16 16, 8
s a blob a blob 1 1, 1
p a blob a blob 1 1, 1

I is {int, bool}, and N and C are the operand sets of §8.16. A lowercase integer code is signed and its uppercase twin unsigned. e, f and d are IEEE 754 binary16, binary32 and binary64, and F and D a complex as two of f and two of d, the real part first; the two complex codes are Python 3.14’s. n, N and P have no standard size, so after =, <, > or ! they are not codes.

A count before a code repeats it, so "3h" lays out what "hhh" does, except before s and p, where it is the length of one blob: "5s" packs one blob into five bytes, cut short or padded with zero bytes, and unpacks five bytes as one blob. p writes the length it keeps in its first byte, at most 255, then that many bytes, and unpacks the length its first byte gives, at most its count less one. A count of 0 lays out no item, except 0s and 0p, which take a blob, lay out none of it and unpack an empty one, as Python 3.14 does.

Types. pack gives a blob and calcsize an int. The tuple unpack gives is typed from its format where the call writes it, and a pad byte, which gives no value, is not counted:

  • A string literal: a fixed tuple of each item’s type, in order, where it lays out 256 items or fewer, so struct.unpack("<hd?", b) is a tuple<int, real, bool>. Past 256, a format whose codes all unpack to one type T gives a tuple<T, ...>, and any other a tuple<value, ...>.
  • An f-string whose every hole is an int, and whose text’s codes all unpack to one type T: a tuple<T, ...>, so struct.unpack(f"<{n}D", b) is a tuple<complex, ...>.
  • Any other format: a tuple<value, ...>, each element of which is read as §5.6 reads a value.

A format that is not a string and a buffer that is not a blob are each a type_error, in these words: struct.pack() takes its format as a string, not int and struct.unpack() reads a blob, not string. A wrong number of arguments is a type_error in Python’s words, as in §8.16: unpack expected 2 arguments, got 1, missing format argument for a pack given none, and _struct.calcsize() takes exactly one argument (2 given). A function used as a value, a namespace used without its import, and a call on a line of its own are each a parse_error, as in §8.16. A * argument spreads into a call (§8.15).

Faults. A call refuses at run time where Python raises. It reads the whole format first, then counts the values, then packs each one left to right, so the fault it meets first is the one it refuses with:

Cause Discriminator Message
a format character outside ASCII value_error 'ascii' codec can't encode character '\xe9' in position 1: ordinal not in range(128)
a character that is not a code value_error bad char in struct format
a count with no code after it value_error repeat count given without format specifier
a format laying out more bytes than an int holds value_error total struct size too long
pack given more or fewer values than the format lays out value_error pack expected 2 items for packing (got 1)
unpack given a blob of another length value_error unpack requires a buffer of 4 bytes
a value the code does not pack value_error required argument is not an integer, and … not a float, … not a complex, char format requires a bytes object of length 1 and argument for 's' must be a bytes object
an int outside the code’s range value_error byte format requires -128 <= number <= 127, and its forms below
a number too large for e, f or F arithmetic_overflow float too large to pack with f format

An int outside a code’s range is refused in Python’s words, and Python’s differ by byte order as well as by code: struct.pack("<h", 32768) refuses with short format requires -32768 <= number <= 32767 and struct.pack(">h", 32768) with 'h' format requires -32768 <= number <= 32767, and struct.pack("<Q", -1) with argument out of range and struct.pack(">Q", -1) with int too large to convert.

Values DKE does not hold. An int is at most 2⁶³ − 1 (§5.1). So unpack refuses one answer Python gives: an unsigned integer above 2⁶³ − 1, with arithmetic_overflow and integer overflow:struct.unpackresult exceeds the signed 64-bit range.

Infinities and NaN. A real, and a part of a complex, may be an infinity or a NaN (§3.5.7), and each packs to its IEEE 754 encoding and unpacks from it, as in Python. There is one NaN: unpack reads every NaN bit pattern as it, and pack writes it as the quiet NaN with its sign bit clear, 7ff8000000000000 for d, 7fc00000 for f and 7e00 for e, most significant byte first. Python writes the bits of the NaN it was given, so where arithmetic made the NaN (inf - inf), Python on x86-64 writes fff8000000000000 and DKE Python 7ff8000000000000. A finite value too large for e, f or F is still refused as above; an infinity packs.

A call is one operation, however many values it packs or reads, as every built-in call is, and a * argument is one more (§8.15). The values unpack gives and the bytes of the blob pack builds count toward any budget a service sets on how much a run builds (§9.5), as a list’s elements and a string’s characters do.


8.19 Named expressions

named-expr := IDENT ':=' expr

A named expression binds the name to the value of expr when it is evaluated, and answers that value: if (n := len(xs)) > 2:, print(a := 1, a), [y for x in xs if (y := x * 2) > 2]. It stands in place of an expr where Python’s grammar reads one: in parentheses, as an element of a display or a comprehension, as an argument of a call, and as the whole condition of an if, an elif or a case’s guard, or the whole subject of a match. Anywhere else the := is a parse_error where it stands (§10.1), and its target MUST be a name.

# running_total.dpy — a running total, bound as each element is evaluated
def running() -> list<int>:
    total = 0
    return [total := total + x for x in [1, 2, 3]]

It binds as an assignment does (§6.1), so the name keeps its one type through it. It binds when it is evaluated, so what is evaluated before it reads the old value and what is evaluated after it reads the new one: print(n, (n := 2), n) prints the old value and then 2 twice. What a conditional expression, an and or an or does not evaluate binds nothing, and a later read of a name nothing else bound is a catchable refuse (§10). In a comprehension it binds in the scope that holds the comprehension, and the name keeps its last value after it. A comprehension’s variable cannot be rebound by one, and a comprehension’s iterable cannot hold one, as Python refuses.

It is a binding, so it costs no operation. Its value cannot be a read of the store, which needs a binding of its own (§7.4). A method whose body holds one is called as its stored script, so the name stays the method’s own. A rule’s condition and conclusion hold none (§14).

8.20 Special names on a built-in value

A special name begins and ends with two underscores. A value of a type Python has holds the special names its Python class holds, and one called by name answers as the spelling it stands for: xs.__len__() is len(xs), a.__add__(b) is a + b, a.__lt__(b) is a < b, x.__abs__() is abs(x), xs.__getitem__(k) is xs[k], xs.__contains__(v) is v in xs, x.__str__() is str(x), x.__floor__() is math.floor(x) and xs.__iter__() is iter(xs). So it is for every operator, comparison, conversion and built-in a special name stands for. A reflected name takes its operands the other way round: (2).__rpow__(10) is 10 ** 2. A call costs what its spelling costs.

# special_names.dpy — a special name answers as the spelling it stands for
def show():
    print([3, 1].__len__())         # 2
    print((2).__rsub__(10))         # 8
    print("ab".__mul__(2))          # abab
    print([1, 2].__contains__(2))   # True

A special name the value’s class does not hold is a type_error in Python’s words, 'int' object has no attribute '__len__'. So are a count of arguments its Python method does not take, expected 0 arguments, got 1, and a keyword argument, wrapper __len__() takes no keyword arguments.

A * or a ** at a special name. A * (§8.15) spreads its operand across a special name’s arguments, and a ** gives it names, as at any method. The name is looked up where the unit is compiled, as Python looks it up before it reads an argument. A ** display’s names are refused there too, in Python’s order: a name given twice; at a method, a key that is no string, keywords must be strings; then any name, a slot’s method refusing a key that is no string as a name. Otherwise the call is the program’s as it runs, in Python’s words for a call that unpacks, which calls the method bound to its receiver. Each * is read where it stands; the names a ** before any dict gives are merged, a name given twice naming a slot’s method by the class that defines it, int.__add__() got multiple values for keyword argument 'x' on a bool, and any other method by its receiver’s class, bool.__floor__() got multiple values for keyword argument 'x'; then any name is refused, by a slot’s method without reading a key, wrapper __len__() takes no keyword arguments, and by any other once no key is other than a string, float.__floor__() takes no keyword arguments; and then the count, expected 1 argument, got 0, or bool.__floor__() takes no arguments (1 given) where a call written out names int.__floor__(). An unhashable class’s __hash__, which is None, merges the names, None got multiple values for keyword argument 'x', refuses none, and is refused, 'NoneType' object is not callable. At each count the name takes, the call answers its spelling, and the call has the type every such count agrees on, as at a method (§8.15); where no count a * may give answers, the call is a type_error where the unit is compiled, in the words of the first that does not. With xs holding one element, print(n.__abs__(*xs)) is expected 0 arguments, got 1 as the program runs.

Where Python’s answer is one no value here holds, the call is a type_error saying what Python answers:

  • An operator’s special name given an operand its class does not take answers NotImplemented in Python, so that the operator asks the other operand. (1).__add__("a") is refused, naming +. Given a value (§5.6), the call is refused when the value may hold a class it would answer NotImplemented for, since which it answers cannot be read before the program runs.
  • __hash__ on a class that cannot be hashed (§5.1) is None in Python, and the call is refused as Python refuses it, 'NoneType' object is not callable. On one that can, it answers what hash() does, which DKE Python does not have.
  • __class__ reads the class, and a class is not a value. isinstance tests it (§13.9).
  • A special name read with no call is a method, which is not a value (§8.8).
  • The machinery Python keeps behind a value, __init__, __new__, __sizeof__, __reduce__, __getattribute__, __setattr__, __doc__ and the rest, is refused. A value here carries none of it.

Beside a ** before a dict whose keys only the program knows, wherever the value is bound to no name, a refusal in the list above that Python reaches only after the names waits for them: the names are merged and refused as above, the count is checked, and the call is then refused in the same words, as the program runs. With opts empty, print((1).__add__("a", **opts)) is refused naming +, and with opts holding x, wrapper __add__() takes no keyword arguments. So it is for __hash__. A value bound from such a call is a type_error where the unit is compiled, whatever the mapping gives (§8.15). __class__ and the machinery are looked up, and refused, where the unit is compiled, whatever the mapping gives.

A special name that changes its receiver is a statement, as the method it stands for is (§8.14). xs.__setitem__(k, v) is xs[k] = v, xs.__delitem__(k) is del xs[k], and an in-place name is its augmented assignment: xs.__iadd__(ys) is xs += ys and s.__ior__(t) is s |= t. Through an element it changes the element, as xs[0].append(v) does. Read as a value, __setitem__ and __delitem__ answer None, as append does, and an in-place name answers its changed receiver, which DKE Python reads only as a statement, so either is a type_error saying so. Beside such a ** and bound to no name, the change is made before the value is refused, as a change read as a value is (§8.14): print(xs.__setitem__(0, 5, **opts)) writes 5 and is then refused. A set’s in-place name given an operand that is not a set answers NotImplemented and changes nothing in Python. Here it is refused, as an operator’s special name is above.

A class this program declares holds no special name of its own (§13.4).

9. Dynamic semantics

9.1 Evaluation order

Expressions evaluate strictly, left to right. The sub-expressions of a binary operator evaluate before the operator; the index expression of [] evaluates before the indexing; an argument list evaluates left to right before the call. An argument given by name evaluates where it is written, whichever position it fills (§8.13). A method call’s receiver evaluates before its arguments; a slice’s start, end and step evaluate in that order; and a dict literal evaluates each key and then its value before the next key. A call of a stored script is ordered as any other expression is: it runs, and anything it prints appears, where this order reaches it. The short-circuit operators and, or are the only exceptions: the right operand evaluates only if needed. Statements within a suite execute in textual order; an assignment’s binding becomes visible immediately after its initializer evaluates.

9.2 Control flow

9.2.1 if

if <cond>: with an optional else:. The condition may be any value — it is tested for truthiness (§8.2), so if xs: / if s: / if n: branch on non-empty / non-zero exactly as CPython does.

9.2.2 for

for x in <iter>:. The <iter> is one of the six finite collections, a constructed list/set/dict, or a string (§6.3); x is bound to each element in order — for a string, each character (a one-character string); the body executes once per element. A store-derived collection is read once, at entry, so changing the store during the loop does not change the iteration. A list, set or dict the body changes is walked as §8.14 says: a list as it stands at each step, never past the count it had at entry, and a dict or set as it stood at entry, refusing a change of its size.

A for MAY carry a trailing else: clause:

for x in <iter>:
    S
else:
    Se

Se runs once, after the loop finishes, UNLESS the loop was left by a break (§9.2.7). An empty <iter> — zero iterations — still runs Se. Se executes in the scope enclosing the loop, where the loop variable x holds the last element bound (§6.1), or, after an empty <iter>, whatever it held before the loop. Because Se is straight-line, the termination guarantee (§9.5) is unaffected.

9.2.3 match

match <scrutinee>:. The scrutinee is evaluated once; control transfers to the case clause whose label matches the runtime discriminator of the scrutinee value. A match with a class set is exhaustive per §6.2, and one and only one clause runs; one without — over a value Python has, or over a value naming no class — runs at most one, and none when no pattern matches. When the scrutinee is a value (§5.6), the label matched is the runtime class of the stored content — one of those listed in §6.2. An arm whose label is an alternation (§6.2) matches when that class is any one of the alternatives. An arm with a sequence or a mapping pattern (§6.2) is taken when the scrutinee has the shape its label writes, and its bindings take the values at those positions. Named arms are tested IN SOURCE ORDER, and the first whose label matches and whose guard (§6.2) holds is the one that runs. A class may appear in more than one label only when every earlier arm naming it is guarded, so among class labels order matters exactly where guards are involved. A value pattern (§6.2) is tried where it is written, among class arms too: a literal matches when the scrutinee == it, _ matches anything, and a capture matches anything and assigns it to its name — before the guard is evaluated, keeping the value when the guard is false. If no named arm is taken, the case default: arm runs, wherever it is written in a match with no value pattern; beside one it is the last arm (§6.2). An arm’s as <var> binding, if present, receives the content typed at the arm’s class for the duration of the arm — and is in scope in that arm’s guard, so a guard may read it.

A guarded arm whose guard is false is not taken, and evaluation continues with the next arm. A match with a class set cannot fall out of the bottom of the statement: a guarded arm does not satisfy exhaustiveness (§6.2), so a match with no case default: must name every class in an UNGUARDED arm, and one of those always runs. A match without one falls out when no pattern matches, and control continues after it.

9.2.4 try

try:
    S
except refuse as r:
    Sr
except engine_error as e:
    Se
else:
    So
finally:
    Sf

Statements in S execute in order. If a statement raises a refuse, its result does NOT bind and control transfers to the first except refuse handler (if present) with r bound to a refuse_info. If a statement raises an engine_error, control transfers to the first except engine_error handler (if present) with e bound to an engine_error_info. If no handler for the raised category is present, the refuse / engine_error propagates out of the try and out of the script. After a handler completes, execution continues with the statement following the try.

Handlers may appear in any order and any number. Order does not decide which category a handler catches — that is fixed by the category it names — so a handler is selected by its category and, among handlers naming the same category, by position: the first wins and any later one for that category is unreachable. First-wins is CPython’s rule for duplicate except clauses, and DKE Python follows it.

An unreachable handler does not make a program ill-formed: it compiles, and the first handler runs. An implementation SHOULD name each unreachable handler in a note beside the result (§10.6), which is where it goes beyond CPython, whose compiler says nothing about one.

The except handlers MAY be followed by two optional clauses, in this order:

  • else: — permitted only when at least one except handler is present. So runs iff S completed with no refuse or engine_error. So is outside the protected region: a refuse or engine_error raised in So is NOT caught by this try’s handlers and propagates.
  • finally: — Sf runs on every exit path from the try: after a clean body, after a handler, after else, when a refuse or engine_error propagates uncaught, and when a break or continue in S leaves the try. Sf runs last. If Sf itself raises, or issues a break / continue, that outcome supersedes whatever the try was about to do.

A try MUST have at least one except handler or a finally clause; a bare try: with neither is a compile-time error. The clause order is try → except* → else → finally. A try with neither, and an else: with no except before it, are grammar errors (§10.1), as they are in CPython, so a syntax error below them is not the answer over them. Each is answered where a handler was to begin: at the else, or at the first token after the clauses, which below a dedent is column 0 of its line (§10.1), and at the end of the line the source ends on. The lines try:, pass and print(1 +) are a parse_error at line 3, column 1. An except refused for its category still counts as a handler, so except ValueError: is answered for its category.

9.2.5 branch block

with branch("<name>"):
    S

A branch block opens an isolated branch named <name>, executes S, and on normal completion commits the branch. If any statement in S raises an uncaught refuse or engine_error, the implementation MUST roll back the branch and re-raise.

A branch’s name begins with a letter or _ and continues with letters, digits, _ or -; it holds no ., space or other character, and cannot be empty. A name outside that rule is a refuse naming the character at fault, raised before S runs. A branch may not take the name of a world that exists (§7.1.2).

Leaving S early by return, break or continue is a normal completion and commits the branch, as reaching the end of S does. Only an uncaught refuse or engine_error rolls back. The distinction is between leaving and failing, not between leaving early and leaving at the end: a program that means to abandon the branch’s writes says so with rollback (§9.2.6).

9.2.6 commit / rollback

Both are bare statements. Written inside a branch block, they resolve to that block’s branch and terminate it. The block’s remaining statements still execute, and they execute with no branch open: a write among them is an ordinary durable write, outside the resolved branch and unaffected by it, and the implementation MUST report it as it reports any other write. Outside a branch block, each is a refuse (no active branch). Nested branching is forbidden, and so is a branch inside a world block (§9.2.9).

9.2.7 break / continue

Both are bare statements, valid only lexically inside a for loop (§9.2.2) — break or continue outside a loop is a compile-time error (not a runtime refuse). break stops the loop immediately; continue skips the rest of the current iteration and proceeds to the next element. Both may appear nested inside an if, match, or try block within the loop body, and each resolves to the innermost enclosing for loop; a break/continue in an inner loop does not affect an outer loop. Because a for iterates a finite collection and these statements only ever shorten the iteration (never extend it), the termination guarantee (§9.5) is preserved. Loop context does not cross a function boundary: a break/continue must be inside a loop in its own definition, not merely called from within a loop.

9.2.8 return

A return statement ends the enclosing script immediately. It bubbles past any enclosing for, match, if, or try — a return is not a loop signal, so it does not run a for … else and is not drained by a loop the way break / continue are (§9.2.7). One interaction is fixed: a return leaving a try still runs that try’s finally clause first (§9.2.4), and if the finally itself returns, breaks, or continues, that outcome supersedes the pending return.

  • Bare return (no expression) exits the script with no value. It is valid in any script. Statements after the taken return in the same suite do not run.
  • return <expr> (a value script only, §4.2) evaluates <expr> and exits, delivering the value. When the script was reached as a value-call in a caller (y = f(args), §4.4), the value binds the caller’s target; when the script was run at the top level (MCP Reference §4.2), the value is surfaced on the invocation result.

Because a return only ever shortens execution (it starts no iteration and scripts do not recurse, §9.7), the termination guarantee (§9.5) is preserved.

9.2.9 world block

with world(<name>):
    S

A world block runs S inside the world named <name> (§7.1.2), a string expression evaluated once, before S. Entering brings the world up to date with the store. S reads everything the store holds and everything the world holds, and every write in S lands in the world.

Leaving S by any path leaves the world: reaching its end, return, break, continue, or an uncaught refuse or engine_error. The statements after the block run outside it. Nothing is committed or rolled back on the way out: what S wrote before it ended stays in the world, and is merged or discarded only by merge_world or discard_world. A script called inside the block runs in the world.

A world block cannot open inside another world block or inside a branch block, and a branch block cannot open inside a world block. Written inside one another, these are compile-time errors; reached at run time, through a script called from inside one block that opens the other, each is a refuse. commit and rollback inside a world block, outside any branch, are refuses (no active branch), as anywhere else.

9.3 Refuses as control flow

A refuse is a recoverable runtime outcome of a verb call. Refuses arise from input that the store rejects (out-of-surface slot, incomplete slots, ambiguous match, invalid source slot, write conflict, …). The list of refuse reasons is defined by the target service; DKE Python guarantees only that a refused statement’s binding does not produce a value and that control transfers to the enclosing except refuse handler if present. A statement that would bind a value and refuses leaves the target identifier unbound; any subsequent reference within the same block is a refuse on the same path.

9.4 Engine errors

An engine_error indicates that the target service could not carry out a well-formed request made from the user’s source. Engine_errors are distinct from refuses; they indicate a service-side issue, not a user-side correction opportunity. They are caught by except engine_error.

9.5 Termination guarantee

DKE Python guarantees bounded termination (it is bounded-terminating). Every well-typed program terminates on every input. The guarantee follows by structural induction:

  • The only iteration construct is the for … in over a finite collection (§6.3). The collection’s length is determined at iterand evaluation; the loop runs at most that many times, whatever its body does to the collection (§8.14).
  • There is no while, no unbounded loop, no goto, no continuation.
  • next (§8.6) reads at most one element per call and is not a loop. Every call a run makes is reached from straight-line code or from a for over a finite collection, so a run makes a bounded number of them.
  • There is no recursion. A stored-script call is inlined at compile time (§9.7); a direct or indirect call cycle between scripts is a compile error.
  • All other control-flow constructs (if, match, try, branch) reduce body size, not increase it.

A target service MAY additionally impose execution budgets (statement count, wall-clock time, the amount of data one run may build) for operational reasons; such budgets are NOT part of the language specification. Note what termination does and does not promise: it bounds how LONG a program runs, not how much it HOLDS at once, and those are separate properties — a program can be bounded-terminating and still build a collection larger than the service can hold. A budget of this kind refuses the run (§10.3-class, reported as a refuse) rather than failing it, and is not catchable by try (§9.3): a handler able to swallow a resource limit could retry inside a bounded loop and defeat it.

9.6 Determinism contract

Given (i) the same DKE Python source, (ii) the same target store state at invocation start, and (iii) the same argument values, two invocations of one program MUST produce byte-identical transcripts. This holds across re-invocation, across implementation process restarts, and across implementation versions that share the same spec version (§11). Determinism applies to the response shape — the response header’s call frame (with its success verdict) and the ordered sequence of print outputs and store effects the body lists (MCP Reference §4.2); the exact prose of that human render is presentation, not a versioned contract (MCP Reference §4.2). Determinism does NOT constrain the wire encoding of the request or any internal representation.

9.7 No runtime recursion; inline expansion

DKE Python has no runtime function-call mechanism. A stored-script call <name>(args) (§4.3, §6.8) is compile-time inline expansion: each invocation site is replaced by the callee’s body with parameter bindings substituted. Consequently callees MUST be compiled before callers, cycles are forbidden (a compile error), and inlining is one-shot (recompiling a callee after the caller does not change the caller’s stored body until the caller is also recompiled).

A value-call in expression position (y = f(args), §4.4) follows the same model: it is not a runtime call either, and a value script that a caller reaches by value-call is subject to the identical compile-before-call, no-cycles rule. Because value delivery is a compile-time construction over the callee’s body, a value-call adds no recursion and no runtime call frame; the termination guarantee (§9.5) is unchanged.

A callee written below its caller compiles when the caller reaches it. A chain of such calls going more than 1000 levels is a type_error, and the same chain written callee first is not.


10. Error model

DKE Python distinguishes four error categories. Every conforming implementation MUST classify outcomes into one of these:

Category Stage Recoverable in source? Exit code (CLI)
parse_error static (lexer / parser) no 4
type_error static (typechecker) no 5
refuse runtime (verb call) yes (except refuse) 1 (uncaught)
engine_error runtime (verb call) yes (except engine_error) 2 (uncaught)

Exit code 0 indicates success; exit code 3 indicates a CLI argument error (not a language-level outcome).

10.1 parse_error

The source is not a well-formed token stream or does not match the grammar of §4. Examples:

  • A tab in indentation, or a dedent that matches no open level (§3.2).
  • Unterminated string literal.
  • A closing bracket that closes no bracket, or one of another kind.
  • A bracket still open at the end of the source.
  • Codepoint outside XID at identifier position.
  • A reserved word (§3.7) in user-identifier position.
  • An empty compile unit — no declarations after whitespace and comments.
  • A non-exhaustive match written with a missing arm (raised as type_error, §10.2) versus a malformed case header (parse_error).
  • A name argument that is not an identifier-shaped bare word (§6.5).

A conforming implementation MUST report the line and column of the first offending token. Both count from 1, and the column counts characters, not bytes, as Python counts the offset of a syntax error, so a line holding å or 汉 is placed where a reader counting in an editor lands. The line break that ends a line with a comment begins where the comment begins, as CPython’s tokenizer places it, so an error answered at that line break is answered there. The line x = 1 + # c is a parse_error at column 10. What is answered at the end of the source is answered where its final line break stands, one past the end of its last line, as CPython answers it, and not at the start of a line after it; a source with no final line break is answered where one would stand. The line if True:, ending the source, is a parse_error at column 9. A source whose final line break is a carriage-return / line-feed pair has one empty line more in CPython 3.11’s reading, so what is answered at its end is answered on that line: if True:, ending in the pair, is a parse_error at line 2, column 1. Answers CPython gives at a token with no column of its own count the column from zero instead, as CPython counts it. They are an unexpected indent (§3.2); a grammar error at a dedent a token follows, such as a block missing below a header whose next line returns to a shallower level, answered at that token; and the end of the source below a line that ends in no header’s :, as below a decorator, which is answered at column 0 of the line the source ends on. The three lines def f():, if x: and print(1) are a parse_error at line 3, column 0. The line @f, ending the source, is a parse_error at line 1, column 0. It is still met at the end of the source, after every form above it that is answered in source order.

A grammar error comes first. CPython reads a whole program before it runs any of it, so a SyntaxError anywhere is the answer, whatever else is wrong above it. A DKE Python parser also decides things that are not grammar — whether a qualified name is a class, what a type annotation names, the shape of isinstance’s second argument, whether a form CPython writes is one this language admits — and refuses at the first of them. When the program also breaks the grammar on a later line, a conforming implementation MUST report that grammar error in place of the refusal above it. The three lines x = 1, print(isinstance(x, 5)) and y = 1 + are a parse_error at line 3, “expected an expression”, and not at line 2. A grammar error is a place where the grammar requires a token or a word and the source holds something else: a missing ), : or line break, or an expression that is not there. A word CPython reserves too, written where a name or a value is read, is one as well: this language teaches it as a reserved word (§3.7), and CPython’s parser refuses it there. It is answered where CPython answers it, at the word, or, at the start of a statement, at the token after a word that begins a statement or an expression, as return does in return = 1. One after a . is answered at the word, as if is in xs.if (§3.7). A word only this language reserves, such as remember, is a name to CPython, and refusing it stays a decision. The two lines y = def and z = 1 + are a parse_error at line 1. Two values side by side inside a bracket, with nothing between them, are a grammar error too, answered as CPython answers a forgotten comma: where the first value begins, past parentheses that only group it, so [(a) b] is answered at the a. CPython answers some such pairs where the second begins, and so does a conforming implementation: a pair whose first value follows a conditional’s if or a for, begins with match, case or _ or with a name a string follows, is print alone, or is a name an import lists. A value after a conditional’s else is a first value like any other, so x = [1 if b else 2 3] is a parse_error at column 18. A name that begins the spelling of case or match, such as c or ma, counts as one of those soft keywords, since CPython 3.11.2 compares a name with a soft keyword over the name’s own length. After such a name, or a name a string follows, CPython reads on from the next token unless it is a (, and a pair it finds there is answered at its first value: x = [m.x 2] is a parse_error at column 10, at the 2, and x = [c[0] 2] at column 7, at the [. Where nothing it reads there has a second value after it, no value after the first is read on, even into a bracket the source never closes: x = [c < b d is a parse_error at column 12, at the d, as x = c < b d ( is at column 11. A pair it does find is read as any other: where its second value runs into a bracket the source never closes, the bracket is the answer, as x = [c d e is at column 5. A name begins that reading after a keyword Python reads an expression after, such as a statement’s if, while or del or a conditional’s else, and after a match that begins a statement: if c d e ( is a parse_error at column 10, at the (. No value is read on after a comprehension’s iterable or one of its conditions, which Python reads as one operand with no rule for a forgotten comma: x = [e for q in xs if e d is a parse_error at column 25, at the d, whether the bracket closes or not. A for statement’s iterable begins after its in and a raise’s cause after its from, and a name there begins a reading as one after if does: for q in c.x d ( is a parse_error at column 14, at the d, and raise e from e d e ( at column 20, at the (. A raise of an exception that is not built in is read as Python reads it before it is refused, so raise e d is a parse_error at column 9. A starred element of a statement is one value that only an operator that computes continues, so a comparison, a boolean operator or a conditional’s if after it is answered where it stands: *e < b d is a parse_error at column 4, at the <, and x, *e < b = 1 at column 7. A line that begins with a list, a set or a dict the source never closes, or one a bracket never closed follows, is read as Python reads it, so a grammar error inside it is the answer: [q for q in *a b ( is a parse_error at column 13, at the *. An = inside a bracket never closed, with nothing after the value it would assign, is answered as the bracket, since Python reads past that value to the end: x = [a, b = c is a parse_error at column 5. A line that begins with a * and assigns nothing, as *e, a does, is an expression standing alone, refused at its start as a, *e is, and read as Python reads it: *e, a if b is a parse_error at column 5, at the if. A starred element annotated alone is neither a tuple nor an expression, and is answered at the :, so *e: int is a parse_error at column 3; a tuple annotated is answered at its start, a trailing comma’s included, as a,: int is at column 1. Python reads an annotation after a : that follows an expression before it weighs the target, so where none reads there the : is answered, as f(a): is a parse_error at column 5, and where one reads the statement is answered at its start, as f(a): int is at column 1. A print(…) the line goes on past keeps no value, as a script’s call does, and what follows it is read as Python reads a trailer: print (a)[, 1 is a parse_error at column 11, and print (a)[0] is an expression statement, which runs. So is a script’s call the line goes on past: f(a)(, 1 is a parse_error at column 6. A dict’s ** is read as a list’s * is, so a grammar error after its operand is answered before the unpacking is refused: x = {**e < b} is a parse_error at column 10, at the <. After a line’s first element and a comma, Python reads every value on, side by side or not, so a bracket never closed after them is the answer whatever stands before them: y, (a) b c ( is a parse_error at column 12. A call written without its parentheses among them is answered at its word, unless its arguments are read on to that bracket: y, print 's' b 't' ( is a parse_error at column 4. Inside a rule (§14), a form only the rule’s grammar reads, such as known_absent(…), keeps its own answer. The line x = [1, 2 3] is a parse_error at column 9. Where a reading stops at a token CPython reads on from, the grammar error is answered where CPython stops. A . after a value with no name after it is answered at the token after the ., so x = a.-b is a parse_error at column 7, and a not after a value with no in after it at the token after the not, where not in would go on. Inside a bracket, a not that begins a second value is a forgotten comma, as above. A = inside a call’s parentheses, after an argument that is no name, is answered where the argument begins, past parentheses that only group the whole of it, as CPython answers an assignment to an expression: f(a.b=1) is a parse_error at column 3. True, False and None are no names, so f(True=1) is answered at the word. An argument that begins with a name and its = is a keyword argument, so a second = after its value is a grammar error where it stands: f(a=b=1) is a parse_error at column 6. Where CPython reads a named expression — in any other bracket, and after an if, an elif, a match or the if of a case’s guard — a = is answered the same way, where what it would assign to begins, when operands joined by arithmetic stand on each side of it and no = follows the second: if x = 1: is a parse_error at column 4, and x = [1, (a)=1] at column 10. A while’s condition is one too, read as Python reads it past the refusal of the while, so while a = 1: pass is a parse_error at column 7. Each operand is read as Python’s parser reads it, so one whose bracket does not parse inside matches no such rule, and the = is answered where it stands: if a = (b +): pass is a parse_error at column 6. Python reads that operand with its rules for an invalid one, so a grammar error those rules raise inside it is the answer, before the = is weighed: if a = {b:}: pass is a parse_error at column 10. An operand whose trailer the source never closes is read to the primary before the trailer, where Python’s reading of the trailer stops on the bracket’s line, so if a = b(: pass is a parse_error at column 4; where that reading reaches the end of the source or a later line, the bracket is answered, as if a = b( is at column 9. What it would assign to is named as CPython names it unless it is a name alone, which is asked whether == was meant, so x = [1, (a)=1] is told “cannot assign to name here”, and parentheses that only group it are read through. What begins with a list, a tuple, True, False or None before the =, and a = in a dict’s value, a slice, a comprehension’s clause, a lambda’s body or a conditional’s condition, are answered at the =, as CPython answers them. An await with a value after it, which Python reads as its keyword, is a value like any other among a statement’s targets, and is named as one: a, await b = 1 is a parse_error at column 4, “cannot assign to await expression here”, and a, await b += 1 one at column 1, where Python names the tuple. A value alone in parentheses after a * among them is a grammar error at the star, raised as Python reads the element and before any = is weighed: f(a), (*b) = 1 is a parse_error at column 8, “cannot use starred expression here”. Where CPython reads a named expression (§8.19) its value is read on, so a grammar error in it is reached: y = (a := 1 +) is a parse_error at column 14. Where CPython reads none, as in y = a := 1, the := is a grammar error where it stands. Where CPython reads a named expression, or the expressions a statement begins with, a target that is no name is a grammar error where it begins once a value follows the :=, named as CPython names it: y = (a.b := 1) is a parse_error at column 6, “cannot use assignment expressions with attribute”, and parentheses that only group the target are read through, so y = ((a) := 1) is one at column 7. Where Python’s reading of the value after the := reaches the end of the source inside a bracket never closed, the bracket is the answer, the one last opened, as y = (a.b := 1 is at column 5 and y = (a.b := (1 at column 13, inside the value, and so is a call’s: (a.b := f(1 is a parse_error at column 10. A bracket in the value that the source closes ends that reading at its close, so y = (a.b := f(1 +) keeps the target’s lesson at column 6, and a call the source never closes whose reading stops on its line is backed off to the value before it, as Python backs off a trailer that fails: (a.b := f(1: pass is a parse_error at column 2, “cannot use assignment expressions with attribute”. A named expression that begins a {…} makes it a set, so a : after its value is a grammar error where it stands: y = {a := 1: 2} is a parse_error at column 12. After a dict’s first entry, a key with no : after it is answered as CPython answers it: on the line the key begins, at the column of the key’s last character, inside parentheses that only group the whole key, so x = {1: 2, a.bc} is a parse_error at column 15. The key is the longest expression CPython parses there, so one whose first operand does not parse, as (3 +), is answered at its grammar error: x = {1: 2, (3 +)} is a parse_error at column 16. A group holding an = reads as no key, and Python’s reading of it as a named expression answers the = where what it would assign to begins: x = {b: c, (c=d)} is a parse_error at column 13. A call whose arguments stand in an order CPython’s grammar does not read, a positional one after a keyword one, is read only to its name, so x = {1: 2, f(c=d, g)} is a parse_error at column 12. A : after a key with no value after it is answered at the :. After ->, CPython reads the longest expression it can and requires the : there, so a return annotation that ends before its : is answered where it ends, and an empty one at the ->: def f() -> int + -: is a parse_error at column 16, “expected :”. A collection type written with angle brackets, such as list<int>, is this language’s own, and is read as one. Each part between the =s of a chained assignment (§8.12) is read as CPython reads an expression list and answered where that reading stops, so x = a b = 2 is a parse_error at column 7. In a case, a * takes a name to capture or _, and is answered at what follows it when that is neither; a starred pattern written alone, as in case *rest:, is answered at the token after its name, since a sequence pattern is written in brackets or with a comma (§6.2): case [*rest]: or case *rest,:. A program with no grammar error is refused at its first refusal. A grammar error below a refusal inside a @rule or @constraint is reached, and so is one later on the line of a refusal inside an operand: a name, a literal, a call or a bracketed group, with what indexes it, calls it or reads a field of it. A rule’s lines are read by the rule’s own grammar (§14), and where it stops in a conclusion’s value, in an if condition or after a constraint’s violated, the value or the condition is read again from where it begins, as Python reads an expression. A grammar error that reading meets past where the rule’s grammar stopped is the answer, so a conclusion k.g = 1 + is answered at the column past its +, and if k.a > 1 +: at its :, as Python answers them. So is one in a condition holding a premise refused as a decision, such as if k.a is:. Where Python’s grammar holds the value or the condition, as in k.g = 1 + 2, the rule’s grammar still refuses it where it stopped, and that is a grammar error there, as it is where the rule’s grammar requires a field or a line of the rule’s shape and the line holds something else. A grammar error later on the refusal’s line is reached when the refusal stands in a header too: in a parameter or its default, a return annotation, a base class, the type of a field or of a binding, the exception an except names or the class a case names, and at a name a definition already holds. In a parameter list Python reads, a ** parameter, a parameter after a starred one and a word this language reserves (§3.7) are read past as Python reads them, and in a method so are a special method’s name and a first parameter other than self. A grammar error is reached in a decorator refused at its name or in its arguments, which is read as Python reads a decorator, and in or after a lambda, which is read as Python reads one. On a line of a rule or a constraint, a refusal is stepped past where it stands, and the rule’s grammar reads the rest of the line. A * or ** this language refuses where an argument or an entry of a dict begins is read past as Python reads it. A * before an element of a list, a tuple or a set, bracketed or not, spreads a collection and is refused nowhere (§8.15). It is read as Python reads it all the same, so what this paragraph says of a refused star’s operand and of the grammar error after it holds of a spread’s too. A grammar error later in its bracket, later on its line or below it is then the answer. One with no value after it is that grammar error. One whose value stands alone in parentheses is a grammar error at the star, as in Python, ahead of any grammar error after it: y = (*a) is a parse_error at column 6, “cannot use starred expression here”, and so is del (*a). Two values side by side after an argument’s * or **, or after the * of a bracket’s first element, are answered at the first, past its *, where Python reads a whole expression; after a dict’s ** or a later element’s *, they are answered at the second. The line f(*a, 1 +) is a parse_error at column 10, “expected an expression”. A ** that begins a dict, with an operand and a comprehension’s clauses after it to the }, is a grammar error at the ** in Python’s words, “dict unpacking cannot be used in dict comprehension”, and a * before a dict’s value is one at the *, “cannot use a starred expression in a dictionary value”: y = {**a for a in b} is a parse_error at column 6, and y = {a: *b} at column 9. With no operand Python reads after the *, the grammar error is where its reading of one stops: y = {a: *} is a parse_error at column 10. The same holds where an element of a list, a tuple or a set begins, which Python reads a bitwise_or after: a keyword after the * begins none, and a comparison, and, or or a conditional’s if after its operand cannot go on, so y = [*not a] is a parse_error at column 7 and y = [*a < b] one at column 9. Just inside a bracket Python reads the * and an expression first, and answers that expression as it answers any other: where the reading runs into a bracket the source never closes, the bracket is the answer; a conditional with no else is a parse_error where it begins, as y = [*a if b] is at column 7; and a value straight after it is a forgotten comma where it begins, as y = [*a < b c] is at column 7. The same keyword is a parse_error at the word after the * or ** that begins a subscript’s index or a call’s argument, which Python reads an expression after, as y = f(*yield) is at column 8, unless another argument comes before a *, which Python answers where the arguments begin; and Python reads no slice after a starred index, so y = a[*b:1] is a parse_error at the :, column 9. Elsewhere in a list, a tuple or a set, and after a dict’s **, Python reads nothing on from a *’s operand: a value or a not straight after it is a parse_error where it stands, as y = [1, *a b is at column 12, and not the bracket the source never closes. A statement that begins with an element and a comma reads on all the same, so y, *a b ( is a parse_error at column 9, the (. Nor does Python read a not with no in after the operand of a bracket’s first *, or of a return’s or a yield’s *, as a comparison: y = [*a not ] is a parse_error at column 9, at the not. Its rule for a forgotten comma still reads a second value after the first, as above, and reads on into a bracket the source never closes where the source ends after the not. A prefix +, which this language refuses (§8.3), is read past as the sign Python reads: y = +a b is a parse_error at the b. A verb call this language refuses at an argument, for a keyword argument the verb does not take (§7.1) or for a second argument where it takes one operand, is read on to its ) as Python reads a call’s arguments, so a grammar error later in the call is the answer: pin(K.s.a, b, 1 +) is a parse_error at column 18. So is a verb call refused at a keyword argument it takes, written out of its place (§7.1). So is a call of a builtin, an aggregate query (§7.1.1) or a method refused at a keyword argument it does not take, or at an argument it does not take after its keyword arguments: y = len(x, a=1 +) is a parse_error at column 17. A verb call or a call of a builtin refused at one of its arguments for any other reason is read on from the argument after it, so a grammar error there is the answer: y = isinstance(1, 5, 1 +) is a parse_error at column 25. A grammar error inside the refused argument itself is not reached. A call refused at an argument more than it takes (§7.1) is read on from that argument, so a grammar error in it or after it is the answer: y = len(x, c, 1 +) is a parse_error at column 18. So is a verb call refused at a path argument that goes on as an expression (§7.1): y = current(K.s.a +, t) is a parse_error at column 20. So is one refused at a path argument that is no path, and a hypothesis (§15) refused at either: y = current("s", 1 +) is a parse_error at column 21. A call refused as short of an argument it takes (§7.1) is read past its ), so a grammar error below it is the answer: the lines remember(K.s.a) and z = 1 + are a parse_error at line 2. A * or ** argument after a verb’s or a builtin’s keyword arguments is refused as one is anywhere in a call. An argument out of order in a call (§7), a verb’s or a builtin’s among them, is a grammar error, as it is in Python. A * argument after a ** one is answered where the call’s first argument begins. So is a * that no expression follows, after another argument: f(1, *) is a parse_error at column 3. A positional argument after a keyword or a ** argument is answered once the rest of the call is read as Python reads it: at the ), or where that reading stops. Two values side by side, or an argument that assigns to an expression, in that rest are answered first, at their own place. f(a=1, b, c=2) is a parse_error at column 14, the ). So is a call that stands among an assignment’s or a del’s targets, which Python’s rule for its arguments raises in before any target is named: x, f(c=d, g) = 1 is a parse_error at column 12, and del f(c=d, g) one at column 13. Such a call side by side after another value among a statement’s elements is no call to Python, which reads its bracket as a tuple of named expressions and answers the first that assigns, at its name: y, foo b(c=d, g) is a parse_error at column 10, where y, foo b(c=d), a call Python reads, is one at column 8, at the b. A class’s bases are a call’s arguments to Python and are read as one, so an argument out of order among them is answered as it is in a call: class C(B, x=1, 2): is a parse_error at column 18. A base that is not a class’s name, which this language refuses (§13.8), is read past as Python reads the argument it is, so a grammar error after it is reached. A call refused at one of its arguments is read to its ), so the brackets after it are reached. What follows a refused value is read as Python reads it, each call’s arguments and each subscript’s parts, so a grammar error in the brackets after it is reached: x = datetime("bad")[1 +] is a parse_error at column 24. So are the arguments of a method call refused at its name, whether it is a method a built-in value or a module does not have or one a class does not declare: y = s.foo(1 +) is a parse_error at column 14. A statement refused at its first word is read as Python reads it, its body included, so a grammar error in it is reached: while True: x + is a parse_error at column 16. A while and an elif are reserved (§3.7), as they are in Python, so a line one begins is the header Python reads, before a ( too, and is refused at its first word as one whose condition opens with no bracket is: while (a): pass is a parse_error at column 1, and so are elif (a) = 1: pass and elif (a: pass, whose bracket is never closed, with no if above them. Where Python’s reading of the header raises first, that is the answer: in while (a) = b(: pass it raises at column 8, “cannot assign to name here”, and while.x.y = 1 is a parse_error at column 6, where no condition begins. An elif with no if above it is a grammar error at its word, as in Python, ahead of anything in its condition or its body and of a grammar error below it: elif a = b(: pass is a parse_error at column 1. A string not terminated below it, or a number Python’s tokenizer refuses, is the answer instead, at its own line; a bracket never closed below it is not. So is a class inside a block or a class body, which is declared only at the top level (§13.1), with its bases and its body: class D: inside an if, with x: int = 1 + in its body, is a parse_error at line 3, column 21. What a decorator refused at its name decorates is read as Python reads it, so a grammar error in it is reached: @nosuch above def f(): and y = 1 + in its body is a parse_error at line 3, column 12, and what follows a decorator must be a def or a class. A definition that reads as a rule or a constraint keeps the refusal, since its lines may be a rule’s grammar under a misspelled @rule or @constraint. A word only this language reserves, where Python reads a name, is read as that name past its refusal, so a grammar error after it is reached: remember = 1 + is a parse_error at column 15, and so is one in a definition, a class or a method that word names, after an except that binds it, or in a target list or a field that holds it. A True, False or None before a comma at a statement’s head begins the expressions Python reads the statement as before it weighs any target, and Python reads it there as the value it is, so a grammar error later on the line is reached: True, b + is a parse_error at column 10, and True, lambda x=1, y: 0 = 1 one at column 19. A parameter list or a lambda’s parameters that Python would not read are answered at the place Python names, ahead of a refusal in or around them and of this language’s own grammar past that place. Python names it at a parameter after a ** one, a second / or *, a / after a * or before any parameter, a * right after a / with no comma between, a default on a starred parameter, a parameter with no default after one with a default and before any *, a bare * that no named parameter can follow, and a parameter written in brackets. The line def f(**kw, y: int = 1 +): is a parse_error at column 13, ahead of the refusal of **kw. A grammar error in a default before that place comes first. Where Python names no place in such a list, it is answered at the first token Python cannot read, as def f(a, 1): is at column 10. An import refused past its first word — at a dotted module name, a *, the dots of a relative from, or a name an earlier import bound to something else — is read on as Python reads an import, so a grammar error later on its line is reached: import m.n o is a parse_error at column 12.

The lexer refuses two kinds of form, and only one of them stops the reading. The first kind is a form CPython reads as a token and this language does not admit:

  • a bytes literal;
  • an integer too large for int, or a real too large for real;
  • an escape CPython reads and a string here refuses: one that writes NUL, such as \0, or a surrogate;
  • indentation by tabs.

A conforming implementation reads past such a form as CPython reads its token, and reports a grammar error anywhere in the program, above the form, below it or later on its own line, in place of the refusal. The three lines x = 1e999, y = 1 + and z = 1 are a parse_error at line 2, “expected an expression”.

A ~ is a sign, which stands only before a value and never between two. x = 1 ~ 2 is a parse_error at the ~, and x = [1 ~2] is one at its first value, two values with no comma between them.

A @ after anything on its line reads as a % does, an operator of its level that stands only between two values, so x = @1 is a parse_error at the @, which no value stands before.

The second kind is a token the lexer cannot read at all, and CPython draws a line through it by kind, which a conforming implementation draws too. What CPython’s tokenizer refuses is reported before any grammar error in the program, wherever the two stand: an unterminated string, a malformed number, a character outside printable ASCII that no token begins with, a full-width form of an operator included, and a closing bracket that closes no bracket or one of another kind. That holds below a form of the first kind too. The three lines x = 'a', y = ) and z = 1 are a parse_error at line 2, “unmatched )”.

A malformed escape, and a printable character no token begins with, such as $, ?, a backquote or a ! that does not begin !=, are refused where CPython’s parser meets them. So is what an f-string is refused for, unless the string has no end: CPython reads an f-string as one token, and its parser reads the interpolations. That covers a " in an interpolation, an empty one, a single }, and whatever an interpolation holds that is refused, a closing bracket included. A conforming implementation answers such a form in source order with grammar errors. A grammar error above it is the answer, and so is one earlier on its own line. Otherwise the form is the answer, over a grammar error below it and over any form of the first kind, unless a token the previous paragraph names stands below it. A \ that does not end its line is answered in the same order, but nothing below it is read, so not even such a token is reached. A dedent that matches no open level of indentation is answered in the same order, and nothing below it is read either. The two lines y = 1 + and z = 1 $ 2 are a parse_error at line 1, “expected an expression”. A malformed escape and what an f-string is refused for are answered where CPython answers them: at the token after the strings written together with that string, which, at the end of a line, is where the line’s comment begins or else where the line ends. A dedent that matches no open level is answered one past the end of its line, and where that line ends in a \, one past the end of the line it continues to: the four lines def f():, if x:, \ and print(1) are a parse_error at line 4, column 9. Both are still ordered by where they begin. The line z = "\x4" + 1 is a parse_error at column 11. The three lines if True:, x = 1 and y = 2 are a parse_error at line 3, column 10. A grammar error inside the expression an interpolation holds, and a token there the lexer cannot read, are answered where CPython’s own parse of the expression answers them. CPython parses the expression alone, inside parentheses, so the interpolation’s { is column 1 and each byte after it one column more. The line y = f"x{g(1 +)}" is a parse_error at column 7, “expected an expression”, and the line y = f"{é +}" one at column 6, since é is two bytes. A closing bracket that closes no bracket or one of another kind, and a ! that begins no conversion, are refused as CPython reads the string, and are answered past it as above: the line y = f"{x)}" is a parse_error at column 12. CPython parses an f-string’s interpolations in turn, as its parser builds the string. So what an interpolation is refused for is ordered where that interpolation begins, at its {, and not where the string begins, and a grammar error in an earlier interpolation of the same string is the answer over it: the line y = f"{1 +}{0x}" is a parse_error at column 5. A ! that begins no conversion is refused once the expression before it has parsed, so a grammar error in that expression is the answer: the line y = f"{1 +!z}" is a parse_error at column 5 too. A # in an interpolation’s expression is refused and answered past the string: the line y = f"{x # c}" is a parse_error at column 15. A ; there is a grammar error at the ;: the line y = f"{x;}" is a parse_error at column 3. A ' or a !a in an interpolation is read as CPython reads it there, so a grammar error after it in the same interpolation is the answer: the line y = f"{d['k'] +}" is a parse_error at column 10. CPython decodes an f-string’s text as it reaches it as well, so a malformed escape after an interpolation gives way to a grammar error in it, and an escape this language refuses where CPython reads one, such as \0, is read past as CPython reads it there: the lines y = f"{1 +}\x4" and y = f"{1 +}\0" are each a parse_error at column 5. A \ the source ends after (§3.2) is answered where CPython’s parser meets the end: on reading past the last token. A grammar error is the answer over it if the parse raises it before reading that far. CPython reads one token past a value where it stops, and stops at a keyword that begins no value, a : or a -> without reading past it. So a grammar error at a token before the last is the answer, and so is one at such a keyword, : or -> standing last; one at a value standing last, or past the last token, is not. A malformed escape or an f-string refusal that would be answered past the \ gives way to it, since CPython meets the end before it builds that string. The line x = 1 2 \, ending the source, is a parse_error at column 10, and the line y = def \ at column 5.

A bracket still open at the end of the source is answered as never closed, at the innermost bracket open there. CPython reads the rest of a source after its parser fails, so a conforming implementation answers the bracket in place of a grammar error or a form answered in source order that stands on a later line than the bracket or at the end of the source. A grammar error or a form answered in source order on the bracket’s own line, short of the end, is the answer, and so is one above the bracket: on that line CPython’s answer turns on how far its error rules look ahead, which this specification follows only for two values side by side, below. A refusal of a form CPython reads gives way to the bracket wherever it stands, above the bracket, earlier on its line or below it, unless a grammar error the reading finds is the answer: CPython reads on past such a form and still ends inside the bracket. A \ that does not end its line ends the reading before the end is reached, so it is the answer, and a token CPython’s tokenizer refuses below the bracket is the answer too. Two values side by side are read on as CPython reads them: its rule for a forgotten comma reads the whole of the second value, and outside a bracket its rule for a Python 2 statement reads on after a name that begins an expression, through a list of values and the value after each. Where that reading runs to the end of the source inside a bracket never closed, one open around the values or one the reading opens, or stops on a later line than that bracket, the bracket is the answer: x = [1 2, x = [1 2 + 3, x = a b ( and x = a b c ( are answered as never closed, while in x = [1 2 3 the reading of 2 stops at the 3, and the two values are the answer. Nothing is read on after a name an import, a global or a nonlocal lists, a name a def or a class gives, a parameter’s name or a case’s pattern, which CPython reads as no expression. A word Python begins a statement with and reserves, which this language reads as a name (§3.7), ends Python’s reading of an expression: where one stands after a bracket never closed on the bracket’s line and other than the import of a from, it is the answer, naming the bracket and the word, as x = [pass is a parse_error at column 6. A header’s body is read as the statement it is, so the word that begins it stands before any bracket in it, and a word after a bracket never closed in the body is the answer as any other: if a: x = [pass is a parse_error at column 12. Where the answer stands at the word itself it keeps its own words, as x = [b for pass does at column 12, and a word on a later line than the bracket leaves the bracket the answer. Nor after a value that begins with a word CPython takes for a soft keyword: match.x d ( is a parse_error at column 9, where match x d (, with a value straight after the word, is one at column 11, never closed. The two lines z = (1, and 2 3 are a parse_error at line 1, “( was never closed”. The three lines y = 5(1), z = [1, and return 2 are a parse_error at line 2, “[ was never closed”, and the two lines y = 5(1) + [1, and return 2 are a parse_error at line 1, “[ was never closed”.

A keyword argument written twice. A call that names one keyword argument twice is refused where the name is written the second time, in CPython’s words: for f(a=1, a=2) the message reads “keyword argument repeated: a”. Of the names one call writes twice, the one written first is answered, so the line y = f(a=1, b=1, b=2, a=2) is a parse_error at column 22, at its second a. CPython finds a name written twice only once its parser has read the whole program, so a grammar error anywhere is answered before it: the two lines y = f(a=1, a=2) and z = 1 + are a parse_error at line 2. CPython checks a call before anything inside it, its arguments and the expression it calls, so of two calls that each write a name twice, the one holding the other is answered: the line y = f(g(a=1, a=2), b=1, b=2) is a parse_error at column 25, and the line y = h(b=1, b=2).m(a=1, a=2) at column 24. Otherwise the call that begins first is answered. A built-in function or a verb that refuses a name given twice in words of its own, as current refuses a second known_at=, refuses it at the same place. A call in an f-string’s interpolation is answered where the name is written in the line, and not at a column of the interpolation: the line y = f"x{g(a=1, a=2)}" is a parse_error at column 16.

What Python’s compiler refuses. CPython compiles a program only once its grammar has read it, and its compiler refuses programs of its own: a return or a yield outside a function, a break or a continue outside a loop, a parameter named twice, a global below a use of its name, __debug__ bound, more than twenty blocks nested, a keyword argument written twice, and others. Each is a parse_error here, at the place CPython answers, and in CPython’s words unless a lesson of this language stands at that same place. A grammar error anywhere comes first: the two lines break and z = 1 + are a parse_error at line 2. Every other refusal comes after: the two lines y = 5(1) and break are a parse_error at line 2. Of two refusals CPython’s compiler would both make, the one it reaches first is answered, and that need not be the one written first. It reads the names of every scope before it reads a statement, so the three lines break, def f(a, a): and pass are a parse_error at line 2, column 10. It reads an assignment’s value before its targets, so the line f(await a).b = yield is a parse_error at column 16. A position it reports counts the line’s bytes, where the grammar’s counts its characters, and it does so on the English surface alone, since no other surface has a CPython to answer as: the line def g(name, name): is a parse_error at column 15.

Where this reading stops short of Python’s. Past a nesting of 100, or 64 steps for a token, it reads only the calls, in the order they are written. A return in an except* handler is answered at the statement or at its value by what this language folds, which is not all Python folds: return 2 >> 1 at the return, where Python answers at the value, and return 1 / 0 at the value, where Python answers at the return. A break in the try of a try and finally inside an except* handler is answered where it is written, where Python has lost its place and answers at line -1. 1 beside 1.0 in a mapping pattern is answered at Python’s place in this language’s words. from __future__ import annotations as a binds nothing, since no value here is a feature, and a parameter’s x<b, a: d>e is this language’s type syntax, so def f(a: x<b, a: d>e): is told of the type at column 10.

How long and how deep. A chain of and, or or comparisons, the ifs and fors of a comprehension, the alternatives of a pattern and the elements of a sequence pattern read at any length, as in Python. Up to 2,996 elifs may lead to an if, along its own chain and any chain whose leg holds it, where Python’s compiler raises RecursionError at the top of a module, and the next is a parse_error where it is written; Python’s count falls by one for each block the if stands in, and this one does not. An expression nested past 500 levels any other way, by a prefix operator, a chain of +, of subscripts or of method calls, or a conditional expression in the else of another, is a parse_error at the level past it or, for a chain read whole, at its end, where Python reads about 2,990 levels, and 1,495 method calls. An or-pattern whose alternatives bind a name costs a level an alternative, so it stops at the same count, where Python has none. A chain of more than 32 operands is grouped evenly and not to the left, which gives the value Python gives, but it tells of another pair where its values have no type to join: of four operands written 1, 1, [1] and "a" and then 1 to make 32, the int and the list<int> at the second and, and to make 33, the list<int> and the string at the third.

Reading a program has a limit in work, counted and so the same on every host, which grows with the program’s length. Past it, a program Python refuses gets Python’s own answer, in place of any lesson of this language, and any other program is a parse_error at line 0, where Python raises MemoryError if its parser’s stack is what ran out. Nothing written by hand comes near it: among what reaches it are 70 dicts unpacked one inside another beside a syntax error, and a chain of tens of thousands of lambdas.

10.2 type_error

The source parses but violates a rule in §5 or §6. Examples:

  • Use of an unbound identifier — including the kind position of a verb path (§6.7).
  • A match whose case set does not cover the scrutinee’s discriminator set, contains a case not in the set, or repeats a case (§6.2).
  • A for … in over a non-collection (§6.3).
  • Binding a statement-only verb by assignment (§6.4).
  • <value>.<field> for a non-existent field, and <value>.<method>(…) for a method the class does not declare.
  • An operand-type mismatch outside the §8.4 string-concat overload — including a chained comparison (§4.6).
  • A non-string source argument (§7.3).

A conforming implementation MUST report the line and column of the offending construct, counted as §10.1 counts them.

10.3 refuse

A runtime outcome of a verb call (or built-in operation) that the target service rejects with a teaching message. Examples:

  • remember(K.s.a, v, src) where the cell already carries a different value under the same source.
  • remember(K.s.a, v, src, qualifiers={…}) where the cell already carries the same value under the same source with different qualifiers (§7.1).
  • remember(K.s.a, v, src, from, to) where the cell already carries the same value under the same source over a different window that overlaps this one (§7).
  • A second remember or update of one cell from one source, over an overlapping window, at a module’s top level (Python Reference §9.5).
  • A qualifiers= dict holding a name that is not a name, or more qualifiers than a claim carries (§7.1).
  • A read naming a kind that is not known: current, get, subjects, list_subjects, list_attributes, an aggregate (§7.1.1), or the field a hypothesis reads (§15) when none of its suppositions writes that kind. A kind is known while the store holds something under it, or while a class declares it (§13.1), in a module as anywhere else. A read of a known kind at a location that holds nothing does not refuse: it answers empty, or a collection with no elements. Writing is how a kind comes to exist, so a write never refuses for this reason. A removal is real (§7.1), so a kind whose last claim is forgotten is not known again until something is written under it; declare its class to read it while it holds nothing.
  • A single-claim read at a location holding competing values (§7.6): current, or the field suppose or suppose_absent reads (§15).
  • A write — remember, update, or a supposition — at a computed field (§13.7) or at a field a standing rule concludes (§14.3), unless that rule yields to a recorded value.
  • A verb path whose segment count does not match the verb’s slot shape (§7.2).
  • A percentile rank held by a name whose value is outside 0–100 (§7.1.1).
  • Index out of bounds on claim_list[i].
  • Division by zero; integer overflow.
  • commit outside a branch.
  • forget(K.s.a) where the address, or something under the named subject, is pinned. Lift the pin with unpin first.
  • forget(c), would_forget(c), dependents(c) or premises(c) naming a claim the store no longer holds, or a claim_id two claims share (§7.1).
  • pin, unpin, prefer or unprefer naming something the store does not hold, or asking for a state that already holds (§7.5).
  • A read given known_at= whose answer rests on a rule changed after that moment or one whose text at that moment the store holds no record of, on a value whose replacement has since been forgotten, or on more of the store than such a read may work over (§7.1).
  • A removal, from inside a branch, of data written before the branch began; from inside a world, of data the store holds outside it; and prefer or unprefer, inside either, at a field judged outside it (§7.1.2).
  • create_world naming a world that exists or an open branch; merge_world, discard_world, compare_worlds or a world block naming a world that does not exist; create_world, merge_world, discard_world or compare_worlds inside a world or branch block; and, at run time, a world or branch block opened inside a world block, or a world block inside a branch block (§9.2.9).
  • merge_world naming a world that holds a value at a field one of the store’s rules concludes, where the rule does not yield to a recorded value (§7.1.2).
  • A branch block naming a world that exists (§9.2.5).
  • A literal the store cannot hold — a malformed timestamp, duration, or byte string; a magnitude beyond its type; a name that cannot be an identifier.
  • A call that exceeds a limit the store enforces.

A refuse carries a refuse_info (reason, teaching_hint, discriminator). When wrapped in try: … except refuse as r:, control transfers to the handler with r bound. The exact reason taxonomy is defined by the target service; a conforming implementation MUST surface whatever reason strings the service returns without altering them.

10.4 engine_error

An engine_error carries an engine_error_info (reason, code). Caught by try: … except engine_error as e:.

When a call fails with engine_error, nothing the caller wrote is wrong. A condition the caller must fix — a pin to lift, a branch to leave, a literal to rewrite, a call to narrow — is a refuse (§10.3) and is caught by except refuse, not by except engine_error.

engine_error is RESIDUAL and promises nothing about a retry. The call was well formed, no rule declined it, it did not compile-fail, and it still did not complete. That is the whole of what the status carries. except engine_error takes every run-time failure the wire reports as engine_error, including those that until now passed by it (a value narrowed to a class it did not hold, a read of a method that returns nothing); a spent budget, a stopped run and an add-only refusal remain uncatchable.

Because retryability is not this layer’s to state. It is already answered, authoritatively and with the detail a client needs, at the transport: 429 RATE_LIMITED and 503 UPSTREAM_UNAVAILABLE, and JSON-RPC -32002 carrying Retry-After in seconds. A second retry signal here would be weaker than those and could only disagree with them — and the MCP Reference already warns that a client which cannot tell “wait” from “act” will back off when it should not.

What a caller CAN route on, stated so that “promises nothing” does not read as “tells you nothing”:

you want to know read
did the call complete status — success or not
is my source wrong compile_error, with diagnostics
must I change something before this can work refuse, and switch on refuse_reason
should I wait and repeat the transport code — never this status
anything else engine_error, and the message says what happened

Every failure reaches the caller on the status, so routing on the status alone still separates a completed call from a failed one without reading prose.

A store busy with another operation. A statement can reach the store while another operation is writing to it, for longer than the service waits for that operation to finish. The statement fails as an engine_error and keeps nothing. Its message says the store was busy with another operation, and that nothing in the call needs changing. That is the last row of the table working as written: the message says what happened. The service cannot know whether the other operation will have finished by the next call, so this adds no promise to the status. What a program’s earlier statements did stands, as it does after any failure.

10.5 Diagnostic format (informative)

A command-line surface prints diagnostics in the form

<class> <file>:<line>:<col>: <message>

where <class> is parse_error or type_error. The text after <class> is quality-of-implementation; a third-party tool MAY choose any format.

10.6 Notes

A program that compiles MAY come back with notes: statements about the source that did not stop it compiling, reported beside the result rather than instead of it. A note is not an error class. It changes nothing a program does: not whether it compiles, not what it does when run, and not the outcome a call reports. This implementation notes one thing, an except handler that can never run (§9.2.4), naming the line of the handler that runs instead. How a note reaches a caller is the service’s contract (MCP Reference §5).


11. Versioning and compatibility

11.1 Spec version

The DKE Python — Language Specification is at alpha · API 1.5 · rev N: the published surface carries the release stage, the API version it describes and one revision, rev N, stated on the Version line of every published document, in the footer of every page and by the service’s initialize answer. The revision moves by one whenever any published document’s content changes, whatever the change, and never otherwise, so two copies of the surface that state the same revision are the same surface and the higher of two revisions is the later one. Which revisions changed this document, and how, is recorded in Appendix E, from rev 1 (2026-10-11) forward; a revision absent from that list left this document as it was. Only the current revision is served. The same number is machine-readable: a page carries it as <meta name="dke-doc-revision">, the site’s llms.txt states it, and the service’s spec tool reports it as _meta.revision. The API version is MAJOR.MINOR, the second of the three facts the Version line states, and the service reports it as _meta.api_version on initialize and tools/list. From API 1.0, at rev 147 (2026-11-12), it moves so:

  • MAJOR increments when an answer a program or call already got, and got right, changes; when a valid input that was accepted is refused; or when the wire changes in a way a client built against it would notice: a tool, an argument or a field removed or renamed, or a response’s shape changed. MINOR returns to 0.
  • MINOR increments on an addition: a name, a built-in, a form, a tool, an argument or an input accepted that was not before, changing no answer a program already got.
  • A correction moves neither: a wrong answer made right, a refusal’s wording, or a document’s text moves the revision alone.

A source declares the MAJOR it is written for (§3.9.1). Within a MAJOR, a source that compiled at an earlier MINOR compiles at a later one and gets the same answers, save where an answer was wrong; across a MAJOR, source compatibility is not guaranteed. A document revision changes no source: an implementation accepts a conforming source by its structure (§4).

Each published document also states a version of its own, MAJOR.MINOR.PATCH, first on its Version line and followed by the revision it last changed in: **Version:** V, changed in rev R · alpha · API 1.5 · rev N. It moves only when that document’s text changes, by one step of the rule above applied to what the document specifies: MAJOR when what it specifies changes an answer, refuses an input that was accepted or changes the wire as that rule says; MINOR when it describes an addition; PATCH for a correction or a change of wording, which moves a document’s version where it moves no API version. The parts after the one that moves return to 0. A document’s version is never restarted, and from rev 148 each entry of its Revision history opens with the version that revision gave it. A page carries its document’s version as <meta name="doc-version">.

11.2 Reserved for future use

The following surface area is reserved and MUST NOT be used:

  • The names empty, refuse_info, engine_error_info as type-name tokens (§5.5).

11.3 Compatibility with the target service

A DKE Python program is portable across target services that implement DKE’s canonical verb surface verbatim. Behaviour differences caused by different target-service surfaces are not language differences; the language layer is the same.


12. Conformance

12.1 Conforming implementations

An implementation MAY conform as one of:

  • Parser/checker (static-only): accepts the language defined in §3–§6, rejects with the correct error class per §10.1 / §10.2, produces a typed AST. Sufficient for linters, formatters, syntax highlighters, and language servers.
  • Compiler/runtime (full): in addition, implements the lifecycle the MCP Reference specifies against a target service and the dynamic semantics (§9), satisfying the determinism contract (§9.6) and the termination guarantee (§9.5).

A conforming implementation MUST document which class it implements and which spec version it claims.

12.2 Conformance suite (informative)

A minimal conformance suite for a parser/checker exercises:

  • Every reserved word from §3.7 in user-identifier position → parse_error.
  • A tab in indentation and a bad dedent → parse_error.
  • An empty compile unit (whitespace and comments only) → parse_error.
  • An unterminated string → parse_error.
  • A non-XID codepoint at identifier head → parse_error.
  • A name argument that is not an identifier-shaped bare word → parse_error.
  • An unbound identifier → type_error.
  • A match on active_claim missing the empty case → type_error.
  • A match on active_claim with an extra case, and one with a duplicate case → type_error.
  • A for … in over an active_claim → type_error.
  • Binding a statement-only verb by assignment → type_error.
  • A chained comparison a < b < c → accepted, with b evaluated once.
  • A <claim>.nonexistent → type_error.
  • Every verb in §7.1 in its call form → accepted.
  • A module body with a top-level data declaration and no def → accepted (a module need not declare a script; Python Reference §9.1, §9.7).
  • An import <module> statement at module scope → accepted (Python Reference §9.3).
  • A cross-module qualified read module.Kind(subject).field → accepted (Python Reference §9.6).
  • A module-qualified construction module.Kind(subject), bound to a name or passed as an argument → accepted (§13.2, Python Reference §9.6).
  • A bare Kind(subject) whose class only an imported module declares, with no stored script of that name → type_error (§13.2).
  • Re-importing an already-loaded module → accepted; it reconciles (Python Reference §9.5).
  • A module top level writing one cell twice from one source over overlapping windows → refused at run time as a conflicting write, naming the cell (Python Reference §9.5).
  • An import cycle (a module that imports itself, directly or transitively) → rejected when the module is loaded (Python Reference §9.3).
  • An import of a module the store does not hold, then a call into it → refused at the import, naming the module; with no use of it → fails there when the program runs (Python Reference §9.3).

Implementations are encouraged to publish such a suite.


13. Classes and methods

A class groups a set of typed fields with the methods that operate on them, and gives programs a typed handle — an instance — for one named member of the class. Classes are an optional surface: a program that declares none is exactly a program without classes. Classes introduce no new types (§5) and no new verbs (§7), and they preserve the termination guarantee (§13.6). One class may extend another (§13.8); a method call on a reference whose class is not fixed until run time then runs the method of the instance’s actual class — a choice over the fixed, finite set of classes the program declares, never an open-ended lookup. class and return are reserved keywords (§3.7).

A top-level construct is either a script declaration (§4.2) or a class declaration (§13.1).

13.1 Class declarations

class-decl   := 'class' IDENT ( '(' IDENT? ')' )? ':' NEWLINE INDENT ( docstring class-member* | class-member+ ) DEDENT
class-member := field-decl | method-def
              | 'pass' NEWLINE                          # declares nothing
field-decl   := IDENT ':' scalar-name NEWLINE
              | computed-field                          # a field with an initializer (§13.7)

A class declaration is the keyword class, the class name, an optional base class in parentheses, a :, and an indented body of field declarations and method definitions. A class may name a single base class it extends — class Dog(Animal): — inheriting the base’s fields and methods (§13.8); naming more than one base is not supported.

A class body holds fields, computed fields (§13.7) and methods (§13.4), and nothing else. It may open with a docstring (§4.1), and pass in it declares nothing, so a subclass that adds nothing to its base is written class Dog(Animal): pass, and a class with no base may declare nothing at all. ... alone on its line is the same as pass here. A class holds no value of its own, so a class attribute such as LIMIT = 5 is not a member, and a field takes no default: an = after a field’s type declares a computed field. A class is declared, and a decorator written, only at the top level of a unit, never inside a class body.

A class exists from the line that declares it, as in Python. A line that runs as the module runs may not name one declared below it: a construction, a binding annotation or a class-match arm at the top of a unit, a parameter or return annotation, a base, or an argument of isinstance (§13.9) or issubclass (§13.10). Each is a parse_error, name 'K' is not defined, where Python raises that NameError as the module runs. A body runs when it is called, so the body of a def or a method may name a class declared anywhere in the unit, and two classes may name each other that way. A module statement that reaches such a body above the class, directly or through what it calls, is a type_error in the same words. Both are told at compile, so nothing above them has run, and a call past an if that would not have held is refused too.

A class body may name its own class, in a method’s signature as well. Python raises NameError for that signature unless the name is in a string, and here it is read either way. A signature may name any class of the unit in a string (§6.4), which Python never reads as a name. Written both ways in one signature, the class is refused as above.

A field is name: type whose type is a value-class scalar — a primitive (string, int, bool) or one of the value classes the engine stores as a cell value (datetime, duration, real, blob, scientific, complex). A field is a stored value slot the customer writes and reads. A field declared twice in one class keeps the type written last, as Python’s annotations do. A computed field installs a rule, so its name is declared once in a class: a second computed field of that name, or a plain field beside one, is refused. A method may read a field declared below it.

A field may instead carry an initializer — name: type = expr — making it a computed field whose value the engine derives from the class’s other fields and maintains automatically. A computed field’s type is int, real, bool, or complex, and the customer never writes it directly — a write there refuses. See §13.7.

class Sensor:
    temp: string
    # methods follow

Declaring a class introduces no storage and performs no action; it is a compile-time description the compiler uses to typecheck field access and method calls. Of field access it checks the field’s NAME — a field the class does not declare is a type_error (§13.3) — and not the class of a value written to it: the declared type is read for a computed field’s initializer (§13.7) and for the field list, and constrains no write. So a class is a closed schema over field names and an open one over field types.

13.2 Instances

An instance names one member of a class, identified by a string:

instance-ctor := IDENT '(' expr ')'   # the class name applied to a name expression

Sensor(room) is an instance of Sensor, bound by assignment:

s = Sensor(room)

Constructing an instance records nothing — it is not a write; it yields a typed reference for reading and writing the instance’s fields and calling its methods. Reading a field that has never been written yields empty (§13.3). Construction also runs no code. A class declares no constructor, and a method named __init__ is refused (§13.4), so a field gets its value from remember once the instance is constructed.

An instance is expressible. print, str, +, an f-string and repr render it, and so does a collection that holds one. It renders as its class and its subject, so print(Sensor("hall")) shows <Sensor 'hall'>, and two instances render apart whenever their subjects or classes differ. The class shown is the one the instance was constructed as, which is the class whose methods it runs (§13.8): an instance of Dog bound to a name annotated Animal renders <Dog 'rex'>. The subject is quoted the way a string inside a collection is (§8.7), and a class an imported module declares is written with its module, <m.Kind 'a'>.

This form matches no CPython output, deliberately. CPython shows an object’s memory address, which differs from run to run. A DKE Python render is deterministic, so the same program always prints the same line.

A class an imported module declares is constructed through its module (Python Reference §9.6). After import m, m.Kind(subject) is an instance of m’s Kind. You read and write its fields, pass it to a script, call its methods and test it with isinstance(x, m.Kind), exactly as with an instance of a class this unit declares. A parameter may be annotated m.Kind. It accepts an instance of Kind or of a subclass m declares, and a method called on it runs the method of the instance’s own class (§13.6). m.Kind is a type wherever a class this unit declares is one (§5.5), so an annotated binding, a collection’s element, key or value, and a return type may name it too, and accept what a parameter accepts.

A bare Kind(subject) whose class only an imported module declares is a type_error that names the qualified form. The exception is a stored script of that name, which the call reaches instead.

A write to a bare Kind in a unit that does not import the module declaring it does not reach the module’s kind: remember(Kind.s.f, …) writes a kind of its own named Kind, beside the module’s (Python Reference §9.6). That is a legitimate kind, so the write is accepted. It is also what a writer gets who meant the module’s kind and left off the import, so compile and run answer it with a note naming the module, after the result and as a notes entry. remember, update and an absence assert are noted, once per kind, at the first write; a read is not.

A class an earlier compile unit declared is reached the way that unit’s scripts are. Where a later unit calls those scripts by their bare names (§6.8), Kind(subject) constructs the class, and the instance is used as an instance of a class the later unit declares: its fields are read and written, it is passed to a script, a parameter may be annotated Kind, its methods are called and isinstance(x, Kind) tests it. Declaring a class that extends it is rejected, because a subclass needs the class’s body, which only the unit that declared it has. The name is the weakest binding it has — a class or script the later unit declares, a stored script, and an imported module that declares a class of that name each take it. A class declared again replaces the earlier one for the units compiled after it, and a script already compiled against the earlier class is unchanged.

13.3 Field access

Given an instance s of a class that declares a field f:

  • Read — s.f yields the current claim for that field: an active_claim when the field has a value, or empty when it does not. Consume it with match (§6.2) or .value (§5.2).

    reading = s.temp
  • Write — remember(s.f, <value>, <source>) records a new value for the field together with its provenance source (§7.3). It takes the validity window and qualifiers= that remember and update take on a path (§7.1), whether s was bound by construction or is a parameter of the class’s type:

    remember(s.temp, "21", "op1")
  • Verbs — every other verb that takes a K.s.a path (§7.1) takes s.f as that field’s cell, the one remember writes: current(s.f), get(s.f) and why(s.f) read it, as do verify, agreement, writers, pin, unpin, prefer, unprefer, why_not, what_needs, assert, forget and would_forget, through any name that holds an instance, and a hypothesis takes it as each field it supposes and as the field it reads (§15). The cell is that of the class the instance was constructed with, so through a parameter of a base class it is the subclass instance’s. A verb that takes a class and a subject — caveats, list_attributes — takes the instance itself as that subject, caveats(s), through any name that holds one, and list_attributes(s) takes the as-of time list_attributes(K.s) takes. An operand that takes a class and one of its fields — list_values, list_subjects, subjects, a fold — is given the class. An instance’s field at either is a type_error. Its message names the form that asks the question — caveats(s), or the class form — and get(s.f), which reads the field.

A value is stored with the class it has. A real field written with 1 + 0.0 holds 1.0. Written with the result of a script declared -> real that returned 1, it holds 1, because arithmetic converts an int and a return does not (§3.5.7). What decides the stored class is the construct that produced the number, not the field’s declaration. Appendix C.9 writes both, and the two compare equal (§8.1).

Field access is available on an instance variable and, inside a method body, on the explicit receiver self (§13.4). Reading or writing a field the class does not declare is a type_error.

A field is written only with remember, which names the value’s source, so a field written as a target is refused, naming remember: s.f = v, s.f += v, a field in a target list, a, s.f = 1, 2, and a field as one target of a chained assignment, x = s.f = 1. So is a field of a value no name holds, f().y = v, and a write into an element of a field, s.f[0] = v, since a field is written whole and never changed in place. Python’s grammar reads each as a write, so each is a refusal, and a parse_error later in the program is the answer in its place (§10.1).

An instance variable is a name bound so that the instance it holds is known where the name is used: by construction, as a parameter of a class’s type, by a call to a script declared to return the class, as the loop variable of a for over a list of instances or a tuple of them, as an element taken from such a list by index, as a second name for an instance variable, from a conditional expression whose two branches are instances of classes with a common base, or by a binding annotated with the class, item: Sensor = x. A list of instances is, here, a display of them, a name bound to one, a parameter of type list<Sensor> or *args: Sensor, a call to a script declared to return list<Sensor>, a list comprehension whose result is its own loop variable over one, a slice of one, or two of them joined with +. A script called may be one of the unit’s own, one stored before it, or an imported module’s, m.sensors(). An instance held in a dict or a tuple is followed too, when the container’s type is declared by a parameter, an annotated binding or a script’s return, when the container is a dict display whose values hold instances or a tuple display that holds one, or when it is taken from one that is: a dict’s value by key, a tuple’s element by a literal index or by unpacking, and each value of d.values() and each pair of d.items() a for takes. A name bound any other way still holds its instance, and passes it on, and a verb reaches the field’s cell through it, as through an instance variable (Verbs above). A field read through it, x.f, reads the field off the instance as the program runs, and so does one straight off an expression, xs.pop().n. A method called through it, or straight off such an expression, xs.pop().record("21", "op1"), runs the method of the instance’s own class, and a class that holds no such method raises 'Rock' object has no attribute 'record' when an instance of it is the one called. Its classes’ versions of the method answer one type, or the call is a type_error, unless it stands alone on its line. A class match over the name (§13.5) is a type_error that names the annotated binding to write first. The value d.get(key, default) returns is the usual case:

class Sensor:
    temp: string

def readings(rooms: dict<string, Sensor>):
    x = rooms.get("hall", Sensor("none"))
    item: Sensor = x
    reading = item.temp

No name has to be written between an instance and its member. A field or a method taken straight after a call or an index that would bind an instance variable is taken through the instance it yields, as through that name: reading = rooms["hall"].temp, sensors()[0].record("21", "op1"). The call or the index is evaluated once, where the expression is. A field read so is a read like any other, bound on its own line.

13.4 Methods

A method is a def inside a class body whose first parameter is the explicit receiver self. Inside the body, self.f names field f of the instance the method was called on.

method-def   := void-method | value-method
void-method  := 'def' IDENT '(' 'self' ( ',' param-item )* ')' ':' NEWLINE INDENT ( docstring stmt* | stmt+ ) DEDENT
value-method := 'def' IDENT '(' 'self' ( ',' param-item )* ')' '->' return-type ':' NEWLINE INDENT docstring? stmt* 'return' expr NEWLINE DEDENT
  • A void method — def name(self, params): — declares no return type and its body holds no return <expr>; it performs actions and yields no value, called as a statement.

    def record(self, v: string, src: string):
        remember(self.temp, v, src)
  • A value method — def name(self, params) -> type: — declares a return type and is called in expression position. With no -> type, a body holding a return <expr> gives the method its type, as a script’s does (§4.2). Its body is what a value script’s may be (§4.2), any statements, an if, a for, a match and an early return among them, and it returns a value of the declared type on every path (§6.9); self’s fields and the parameters are in scope. A return of another type is a type_error that names the method and both types. A field read has type active_claim, so a method returning one declares -> active_claim, or nothing. A value method returns any type a script may (§4.2), a collection included, so -> list<string> is a return type on a method as on a def. A value method may also declare -> value, and a script may too, by the same rule (§4.2), since Python has one construct for both. A store fold’s result is a value, which the method’s body or its caller narrows with match.

    def label(self) -> string:
        return "sensor"
    
    def combined(self, a: int, b: int) -> int:
        s = a + b                  # a local binding …
        return s * 2               # … then a tail return
    
    def sign(self, a: int) -> int:
        if a < 0:                  # any statement a script runs
            return -1
        return 1

A method is called on an instance with instance.method(args) — the self argument is supplied by the receiver, not written at the call:

s.record("21", "op1")           # void method, statement position
name = s.label()                # value method, expression position

A method’s arguments bind as a script’s do (§4.2): by position or by keyword, an omitted one takes the parameter’s default, and a parameter list holds the markers of §4.2, each with its own refusal. A keyword-only or a variadic parameter is bound by the method’s stored script, resolved from the receiver’s class as any call is.

A value method may also be called as a statement, s.label(). It runs, and its value is discarded, as a call statement discards a value script’s (§4.3).

A method call is resolved from the receiver’s class. When that class is fixed where the call is written, exactly one method body applies. When the receiver is held at a base-class type — a base-class parameter, or an element of a list of mixed subclasses — the call runs the method of the instance’s actual class (§13.8); because a class hierarchy is closed and finite, the candidate bodies are a bounded, program-declared set, not an open-ended lookup. Methods obey compile-before-call exactly like scripts (§6.8), so a method may call one written below it; method recursion — direct, mutual, or through an override and super (§13.8) — is not expressible, so the call graph is acyclic (§13.6). Argument count and types are checked against the method’s declared parameters.

A * before a positional argument of a method’s call spreads its operand’s elements across the call’s arguments, as in a script’s call (§8.15), and the call is checked as one is: given def m(self, x: int, y: int), b.m(*xs) binds x and y from xs. The receiver counts as a positional argument where Python counts it, so b.m(*[1, 2, 3]) is refused as the program runs with Box.m() takes 3 positional arguments but 4 were given. A refusal names the method as Python does, by the class whose body holds its def: a method Sub inherits from Box is Box.m, and so is the version a super() call reaches. A lone spread of no collection names it by its module too: __main__.Box.m() argument after * must be an iterable, not int. A method of a class another module declares is named by the class it is called on, so one that class inherits reads Sub.m() there, where Python reads Box.m().

Special methods. A method whose name begins and ends with two underscores — __init__, __str__, __eq__, __len__ and the rest of Python’s special methods — is a parse_error with a teaching hint. Python calls those methods on its own, on construction, on print, on ==, and DKE Python calls none of them, so a method of that name would never run. Each hint names the DKE way to do what the method was for:

  • __init__ — construction runs no code (§13.2). Give a field its value with remember(s.f, <value>, <source>) after constructing the instance, or in an ordinary method called then.
  • __str__ and __repr__ — an instance already renders, as its class and its subject (§13.2). To show something else, read a field into a name and build the string from it, or return it from an ordinary value method.
  • __eq__, __ne__, __lt__, __le__, __gt__ and __ge__ — instances are not compared or ordered by an operator (§8.1). Compare a field of each, or test the class with isinstance. x in xs over a list of instances already compares them by subject and class.
  • Any other — give the method an ordinary name and call it by that name.

13.5 Dispatch by class

When code must branch on which class an instance belongs to, it uses a match whose scrutinee is a bare instance variable and whose case arms name classes:

class-match := 'match' IDENT ':' NEWLINE INDENT class-case+ default-case? DEDENT
class-case  := 'case' IDENT ':' ( simple-stmt NEWLINE | NEWLINE INDENT stmt+ DEDENT )
default-case := 'case' 'default' ':' ( simple-stmt NEWLINE | NEWLINE INDENT stmt+ DEDENT )
match s:
    case Sensor:
        remember(s.temp, "22", "op1")
    case Actuator:
        remember(s.state, "on", "op1")

A match whose scrutinee is a name the compiler follows to an instance, and whose arms each name another class, with a catch-all last if any, is a class match; any other match is read by §6.2. Inside a case <Class>: arm the instance is treated as that class, so its fields and methods resolve to that class. Written with no parentheses the arm set is closed and exhaustive: it MUST cover the instance’s class — or, under inheritance (§13.8), every class the instance could belong to — or provide a case default: arm, and a class match that is neither is a parse_error. case <Class>():, Python’s class-pattern spelling, is the same arm, and is a test: a class match with any arm written so runs no arm when none holds, as in Python. A case names its class as a construction does (§13.2, Python Reference §9.6): a class the unit declares by its bare name, a class an imported module declares as <module>.<Class>. Class patterns over any other value, and what their parentheses hold, are §6.2’s. When the instance’s class is fixed where the match is written, the arm is chosen there; when the instance is held at a base-class type, it is chosen at run time, from the same fixed arm set. Either way the arm chosen is the first, in source order, whose class is the instance’s actual class or a base of it (§13.8) — a bounded selection over the program’s classes, never an unbounded lookup.

13.6 Termination

Classes preserve the termination guarantee of §9.5. Method dispatch — direct calls (§13.4), class matches (§13.5), and the dispatch on an instance’s actual class under inheritance (§13.8) — always selects from the fixed, finite set of classes and methods the program declares; even when the actual class is not known until run time, the selection is over that bounded set, so it introduces no unbounded lookup. Methods are non-recursive by construction; iteration remains the bounded for … in of §6.3. Every method body is therefore a finite, non-recursive program over bounded iteration.

13.7 Computed fields

A computed field is a field declared with an initializer:

computed-field := IDENT ':' computed-type '=' expr NEWLINE
computed-type  := 'int' | 'real' | 'float' | 'bool' | 'complex'

Its value is not written by the customer; the engine derives it from the class’s other fields and keeps it current as those fields change. A computed field’s declared type is int, real, bool, or complex.

class Order:
    price: int
    qty: int
    total: int = price * qty        # a computed value
    big: bool = total > 1000        # a computed flag

Given Order.o1.price = 10 and Order.o1.qty = 5, reading Order.o1.total yields 50; writing Order.o1.price = 200 re-derives it. The customer writes only price and qty; total and big are maintained automatically. A computed field reads back like any field (current, §7.1) with a derived provenance in place of a source.

A write to a computed field refuses. remember or update at a computed field, or a supposition of one (§15), is a refuse (§10.3) whose reason names the fields the value is derived from — write those instead. Nothing is recorded, so the field keeps its derived value and current reads it straight after the refused write. forget at a computed field is not refused: it removes only what was recorded there, and the derived value is untouched.

A computed field is not declared over recorded values. Installing a class whose computed field already holds values someone recorded refuses too, and the reason names those values: they would stand beside the derivation as a disagreement (§7.6) that no write could clear. Forget them, or give the computed field a name of its own. The refusal changes nothing recorded.

The initializer. Every identifier in the initializer must name another field of the same class declared earlier (a computed field may build on an earlier computed field), and the initializer must reference at least one such field. The form depends on the declared type:

  • An int field is an arithmetic expression over int and bool fields and integer literals using +, -, *, % (§8); a bool field is the number it is (§8.1). Division (/) is not written in an int field — its result may be fractional; declare the field real.
  • A real field is an arithmetic expression using +, -, *, /, % over int, bool and real fields and numeric literals (§8.3). Division is true division here as everywhere (§8.3): it yields a real whether or not it divides evenly, so n / 2 over 4 is 2.0.
  • A complex field is an arithmetic expression using +, -, *, / over int, bool, real and complex fields, numeric literals and imaginary literals (§8.3). It computes as a program does: a number beside a complex number is read as the complex number with a zero imaginary part, and the result is a complex, so gain * 2 over 1+2j is (2+4j) and gain + 1j over the int 3 is (3+1j). An expression none of whose operands is complex derives the number it computes, which the field holds as a declared complex holds a number (§3.5.15). The remainder % and the power ** are not written in one: a remainder has no meaning on a complex number, and no computed field derives a power. A subject whose expression divides by zero holds no value in the field, and a part past the range of a real is inf or -inf, as a program computes it.
  • A bool field is a single ordered comparison — <, <=, >, >= — of one earlier field against a numeric literal or another earlier field. Its left side is a single field: to compare a computed value, declare that value as its own field first (total: int = price * qty, then big: bool = total > 1000). The comparison ranks two values of one kind, as a program does: numbers (int, real, bool) against numbers, a string against a string. A complex number has no order (§3.5.15), so a bool field that compares a complex field or an imaginary literal is refused where it is declared. Where a field it reads holds a value it cannot rank against the other side — a blob, which has no order, or a value of another type, such as a string recorded where the field declares an int — the comparison has no answer, so the field holds neither True nor False for that subject, and why_not on it says so.

Because every reference points at an earlier field, a class’s computed fields form no cycle. Across compiles they can: a later compile may declare the class again with a computed field reading one that an earlier compile computes from it, and a rule may conclude into a field a computed field reads. Such a cycle is refused when it passes through a computed field whose initializer applies an operator (total: int = price * qty). A computed field that would close it with what the store already holds is refused at the compile that closes it, the refusal names the field and says its value would depend on itself, and nothing that compile declares is stored; a rule that would close it is refused as §14.5 says. A cycle whose computed fields only copy one other field (a: int = b) or compare (a bool field) is accepted: a copy holds a value some subject already holds, and a comparison holds True or False, so the cycle settles. A computed field participates in match (§6.2) and expressions (§8) exactly as a written field of the same type would.

Termination. Computed fields preserve the termination guarantee of §9.5: a derivation is a finite, acyclic expression over the class’s own fields, evaluated once per change; it introduces no loop and no recursion.

13.8 Inheritance

A class may extend one other class — its base class — named in parentheses after the class name:

class Animal:
    sound: string
    def speak(self):
        remember(self.sound, "generic", "op1")

class Dog(Animal):
    breed: string
    def speak(self):
        super().speak()
        remember(self.breed, "collie", "op1")

A class extends at most one base (single inheritance). Naming more than one — class C(A, B): — is a compile-time error, as is naming a base that is not a class declared earlier in the program, or naming the class itself. Empty parentheses, class A():, name no base: that is the class class A: declares, as in CPython.

Inherited members. A subclass has every field and method of its base, plus those it declares. Dog above has sound and speak from Animal and its own breed; an inherited method reads and writes the base’s fields on the subclass instance. Inheritance is transitive: a class three levels deep has the members of every class above it.

Overriding. A subclass may declare a method with the same name as one it inherits; the subclass’s version then applies to instances of the subclass. Above, Dog.speak overrides Animal.speak.

super. Inside a method, super().<name>(args) runs the base class’s version of <name> on the same instance — the inherited step — which an override can then extend. It is always the base’s version, never an override’s. When <name> returns a value, super().<name>(args) is that value and stands wherever a value does, as in return "z" + super().label(). When it returns nothing, the call is a statement of its own. super is available only inside a method body, and names the base of the class that declares the method, also where a subclass inherits or overrides that method. (Constructor chaining — a super().__init__(...) call — is not part of the language: construction takes only the instance name and runs no code (§13.2), and a method named __init__ is refused (§13.4).)

Substitutability. A subclass instance is accepted wherever its base class is expected: as a parameter typed with the base, as a binding used at the base type, as an element of a list whose element type is the base, and as the value returned by a script or method declared to return the base. No cast is written — a Dog value is an Animal value. A class that is not the named one and does not descend from it is refused where the name is written, and refused when the program is compiled rather than when it runs.

A list written with instances of several subclasses takes their nearest common base as its element type, so [Dog(d), Cat(c)] is a list<Animal> and may be passed where one is named. A list written where a type is declared for it takes that type wherever each element fits it. That holds at an annotated binding, as an argument, at a return and as a parameter’s default, so [Dog(d)] may be passed where a list<Animal> is named. A dict written with values of several subclasses takes their nearest common base as its value type, so {"d": Dog(d), "c": Cat(c)} is a dict<string, Animal>, and a dict written where a type is declared takes that type in the same positions. A display written inside a display joins so too, at any depth: [[Dog(d)], [Cat(c)]] is a list<list<Animal>>. Instances of classes that share no base class may be held in one list, dict or tuple, but join under no class. A method call through one of them runs the method of the instance’s own class, and a field read through one is a type_error, since no class says which field is meant. A list a name already holds does not widen: a list<Dog> is not a list<Animal>, for the reason §8.14 gives — the list is one object its holders share, and a holder reading it at the wider type could put a Cat where the narrower one reads Dogs.

Dispatch under inheritance. When a method is called on a reference whose class is fixed where the call is written, exactly one body applies (§13.4). When the reference is held at a base-class type — a base-typed parameter, or an element of a list of mixed subclasses — the call runs the method of the instance’s actual class:

class Cat(Animal):
    def speak(self):
        remember(self.sound, "meow", "op1")

def herd(a: Animal):
    a.speak()                        # runs Dog.speak, Cat.speak, or Animal.speak,
                                     # according to a's actual class

def demo(d1: string, c1: string):
    animals = [Dog(d1), Cat(c1)]     # a list of mixed subclasses
    for a in animals:
        a.speak()                    # each element runs its own class's speak

Value-returning methods (§13.4) dispatch the same way. Where the class of the instance a call reaches holds a method of that name that returns nothing, as an override written def m(self): pass beneath one that returns a value, the call runs that method, and reading its value is then refused as the line runs, naming the method as CPython names it: method `B.m` returns nothing — it cannot be used as a value. CPython hands the caller None, which DKE Python has no value for. No except handler takes the refusal, since CPython raises nothing there, and a finally runs. A call whose arguments that method does not take is refused the same way without running it. This holds wherever the read is written: in a method a subclass inherits, read from an instance of the subclass, in a script written above the class, and in an element’s call in a comprehension. A call whose arguments the method of the class it reaches does not take, where that method returns a value or the call stands alone as a statement, is refused as it reaches that class, in the words of CPython’s TypeError (B.m() takes 1 positional argument but 2 were given), and except refuse takes it; a class whose method takes them runs it as written. Where no class a read can reach holds a method that returns a value, or no class’s method takes a call’s arguments, the program is refused where it is compiled. Because a program’s classes form a closed, finite hierarchy, the set of methods any one call can reach is bounded and known from the program text; there is no open-ended lookup, and termination (§13.6) is preserved.

Reading a member under a base type. Through a base-typed reference you may read or write any field, and call any method, the base declares — these are present on every instance. Reading a field or calling a method that only a subclass declares first requires establishing the instance’s class, with isinstance (§13.9) or a class match (§13.5); otherwise it is a type_error.

Class match under a hierarchy. A class match (§13.5) on a base-typed instance is resolved at run time; a case <Class>: arm matches that class or any of its subclasses, first match winning. Written with no parentheses, the arm set stays closed and exhaustive over the hierarchy — cover every case or provide case default:.

def classify(a: Animal):
    match a:
        case Dog: print("kind=dog")
        case Cat: print("kind=cat")
        case default: print("kind=other")

13.9 isinstance

isinstance(x, C) reports whether the instance x belongs to class C — true when x’s class is C or any subclass of C, false otherwise. It returns a bool.

The second argument may also name a type, as the language spells it — int, bool, real, string, datetime, duration, blob, scientific, complex, null, list, dict, set, tuple or slice — and then tests a value of any type: isinstance(1, int) is True, and every value is an object. str and float, the conversions (§8.13), name the string and real they build, as in CPython. A bool is an int here, as a case int: arm takes one (§6.2), and an int is not a bool; list tests a constructed list, and a read result is not one. The argument may be a parenthesized tuple of classes and types, or a | union of them, and is true when any one is: isinstance(x, (int, real)) and isinstance(x, int | real). An empty or nested tuple is a parse_error. An argument left out, as between the commas of isinstance(x,,), or begun with a mark or a word no value begins with, as in isinstance(x, :), is a grammar error (§10.1). A class is tested over any value, and a value that is no instance of it is False: isinstance(1, Dog).

if isinstance(d, Dog):
    print("d is a Dog")
if isinstance(d, Animal):        # a Dog is an Animal
    print("d is an Animal")
if isinstance(an, Dog):          # a plain Animal is not a Dog
    print("an is a Dog")
else:
    print("an is not a Dog")

When x’s class is fixed where the call is written, the result is a compile-time constant; when x is held at a base type, it is decided from the instance’s actual class. A type is tested on the value when the call runs, never before: a name declared int may hold a bool, and isinstance of it with bool is True. Inside the taken branch of an isinstance test the reference is treated as the tested class, so its subclass-only fields and methods become accessible (§13.8). The test is the whole condition, and the narrowing ends with the branch: the else, and everything after it, see the declared class again. A tuple or union narrows to the nearest class its alternatives share — (Puppy, Hound) to Dog — and alternatives sharing no class, or a type named among them, narrow nothing. A test against a class the reference already has, or against one of its bases, leaves the reference alone. A name holding a value is narrowed by a test against types, to the tested class, and there the test need not be the whole condition: an and narrows what follows it, and an or of two tests narrows to what they join to. Where the test fails, the else reads such a name at the classes it leaves (§5.6).

13.10 issubclass

issubclass(A, B) reports whether the class A is B or a subclass of B — true when B is A itself or a base A reaches through its chain of bases, at any depth, false otherwise. It returns a bool. A class is a subclass of itself, as in Python.

class Animal:
    name: string
class Dog(Animal):
    pass
print(issubclass(Dog, Animal))       # True: a Dog extends Animal
print(issubclass(Dog, Dog))          # True: a class is a subclass of itself
print(issubclass(Animal, Dog))       # False: an Animal need not be a Dog

Either argument may name a type instead, as isinstance’s second argument does (§13.9), and str and float name the string and real they build. A type is a subclass of itself and of no other type, except that a bool is an int, as it is everywhere in this language: issubclass(bool, int) is True and issubclass(int, bool) is False. A class is never a subclass of a type, and a type never one of a class. The second argument may be a parenthesized tuple of classes and types, or a | union of them, and is true when any one is: issubclass(Dog, (Cat, Animal)) and issubclass(bool, int | real). An empty or nested tuple is a parse_error, as it is for isinstance. An argument left out, as in issubclass(,), is a grammar error, as it is for isinstance.

A class is not a value in this language, so each argument is a name written where the call is. The first names one class or one type, and anything else there — an instance, a name bound to a value, a call, a tuple or a union — is a parse_error that says so: issubclass(x, int) is a parse_error. To ask which class an instance belongs to, use isinstance (§13.9). A class either argument names must be declared above the call, or be one an imported module declares, written m.Kind, as a class isinstance names must be. Both being fixed where the call is written, the result is a compile-time constant. A keyword argument is a type_error.


14. Standing rules

A standing rule is a named statement that derives facts automatically. It states a condition over the stored data and a conclusion to record whenever that condition holds. Once defined, a rule’s conclusion always reflects the current data: it is in force exactly while its condition holds, so a derived fact is never stale. A computed field (§13.7) derives one field of one class from that class’s own fields; a standing rule is written on its own and is named.

14.1 The rule block

A rule is a def marked with the @rule decorator and a name no other declaration in its unit holds (§4.2), then a for clause naming the subjects it ranges over, an if clause stating the condition, and an indented conclusion:

@rule
def needs_review():
    for o in Order:
        if o.total > 1000 and absent(o.shipment):
            o.review = True
rule-decl   := '@rule' ( '(' 'kind' '=' rule-kind ')' )? NEWLINE
               'def' IDENT '(' ')' ':' NEWLINE
               INDENT
                 'for' subject-binding ( ',' subject-binding )* ':' NEWLINE
                 INDENT
                   'if' premise ( 'and' premise )* ':' NEWLINE
                     INDENT conclusion NEWLINE DEDENT
                 DEDENT
               DEDENT
subject-binding := IDENT 'in' kind-name
kind-name       := IDENT   # the kind whose fields the name reads
rule-kind       := '"definition"' | '"causal"' | '"empirical"' | '"policy"' | '"heuristic"'

The for clause gives each subject the rule is about a name, and the kind whose fields it reads under that name (for o in Order). Every name the rule uses — in a premise or in its conclusion — is one of those subject variables. The @rule marker distinguishes a rule from an ordinary script; for, in, if, and and read here exactly as they do elsewhere in the language, and absent names one of the rule surface’s three negative premises (§14.2).

The premises, not the kind, say which subjects a name ranges over. A name whose fields a ranging premise reads (§14.5 lists them) ranges over the subjects recorded with those fields. A name reached only through a join or a relation premise ranges over the subjects the join or the relation names — the values that field holds, the pairs that relation holds — whether or not anything of its kind is recorded for them. So in for e in E, x in N, y in N: if e.a == x and e.b == y: Link.x.y = True, an E naming p and w concludes Link.p.w although no N named w is recorded. A rule that wants only recorded Ns reads one of their fields: with and y.name == y.name added, it concludes only for a y recorded with a name.

A rule may bind more than one subject, separated by commas, and then relate them with a join premise (§14.2). Each name must be distinct. The bindings are a conjunction and carry no order: they are separated by commas rather than written as nested for blocks precisely because nesting would suggest an iteration sequence, and a rule states a condition over the whole store rather than describing a walk through it. A nested for inside a rule is not accepted.

@rule
def taint_spreads():
    for f in Fn, c in Call, g in Fn:
        if f.tainted == True and c.caller == f and c.callee == g:
            g.tainted = True

Read plainly: whenever a tainted function f is the caller of some call c, and c’s callee is g, then g is tainted too. Because the conclusion writes the same field the first premise reads, the rule applies to its own results, and the taint reaches everything the call graph connects — not to a fixed number of steps. Two of the three subjects are named by the join rather than compared: c and g are found through f, which is what lets one rule follow a chain.

A rule’s kind. @rule(kind="causal") says what kind of relation the rule states, one of five: "definition" (the conclusion is what the premises mean), "causal" (what the premises describe brings the conclusion about), "empirical" (the two have been observed together), "policy" (the conclusion is decided rather than discovered) and "heuristic" (a rule of thumb). The kind is for the reader. It changes nothing the rule concludes, holds or refuses: a rule written with a kind and the same rule written without one conclude the same claims. It reads back on the rule’s conclusions as .rule_kind (§5.2), in the rule listing (§14.7) and in the module’s information. The five words are the same in every language, as trust’s are; the keyword kind is spelled in the unit’s language. A recompile that changes the kind changes it, and one that leaves it out removes it. @rule takes no other argument.

# rule-kind.dpy — a rule says what kind of relation it states, and its conclusion reads it.
@rule(kind="policy")
def needs_review():
    for o in Order:
        if o.total > 1000:
            o.review = True

def show_kind():
    remember(Order.o1.total, 1200, "till")
    r = current(Order.o1.review)
    print("review: " + str(r.value) + ", " + r.rule_kind)
OK script show_kind(): 1 claim written, 1 print
  remember Order.o1.total = 1200
  print review: True, policy

14.2 Premises

The if clause is one or more premises joined by and; the rule holds only when every premise holds:

premise    := comparison | equality | join | field-cmp | relation | absence
            | not-asserted | known-absent | nonexistence | source-test | trust-test
            | text-test
comparison := IDENT '.' IDENT ord-op ( '-'? ( INT | REAL ) | SCIENTIFIC
                                     | STRING )
                                                 # o.total > 1000, o.price > 19.99,
                                                 #   o.mass > scientific("1.0e4"), o.name < "m"
equality   := IDENT '.' IDENT eq-op literal      # o.lic == "MIT", o.total != 5
join       := IDENT '.' IDENT eq-op IDENT        # c.caller == f
field-cmp  := IDENT '.' IDENT ( eq-op | ord-op ) IDENT '.' IDENT
                                                 # o.delivered > o.deadline
relation   := IDENT '.' IDENT '.' IDENT eq-op literal
                                                 # Reach.x.y == True
absence    := 'absent' '(' IDENT '.' IDENT ( '.' IDENT )? ')'
                                                 # absent(o.shipment), absent(Gap.x.y)
not-asserted := 'not_asserted' '(' IDENT '.' IDENT ( '.' IDENT )? ')'
                                                 # not_asserted(b.flies)
known-absent := 'known_absent' '(' IDENT '.' IDENT ( '.' IDENT )? ')'
                                                 # known_absent(p.chip)
nonexistence := 'none' '(' IDENT 'in' IDENT ':' premise ( 'and' premise )* ')'
                                                 # none(c in Call: c.caller == f)
source-test := 'source' '(' IDENT '.' IDENT ')' ( eq-op STRING | '==' IDENT )
                                                 # source(r.temp) == "lab",
                                                 #   source(r.temp) == s
trust-test := 'trust' '(' IDENT '.' IDENT ')' eq-op ( '"asserted"' | '"derived"' )
                                                 # trust(s.label) == "derived"
text-test  := text-cell '.' ( 'startswith' | 'endswith' ) '(' text-part ')'
            | text-part 'in' text-cell
                                                 # r.path.startswith("/srv/"),
                                                 #   t.name.endswith(".test"), "admin" in a.role
text-cell  := IDENT '.' IDENT ( '.' IDENT )?
text-part  := STRING | IDENT '.' IDENT
eq-op      := '==' | '!='
ord-op     := '<' | '<=' | '>' | '>='
literal    := STRING | '-'? ( INT | REAL ) | SCIENTIFIC | complex | BLOB | 'True' | 'False'
                                                 # §3.5; the same set `suppose` takes (§15)
complex    := '-'? IMAG | '-'? ( INT | REAL ) ( '+' | '-' ) IMAG
                                                 # 2j, 1+2j, -0.5-1.5j (§3.5.15)

Premises that name the same field through the same name read one value of it. A field can hold more than one value at once, since two sources that disagree each hold one there (§7.6). Premises that name one field (r.t in r.t > 30 and r.t < 40) are tested together against one of its values, never each against whichever value suits it. A reading whose values are 50 and 20 therefore does not satisfy r.t > 30 and r.t < 40: each value meets one bound and neither meets both. Record a 35 and the premises hold through that value. The rule covers every premise that names a field — a comparison, an equality, a field comparison, a join, a source or trust test — so a join names the subject that the value the other premises accepted refers to, and a source test is about where that value came from. A fold over the field in the conclusion folds only the values the premises accepted (§14.3). Where several values pass, the rule holds through each of them (§14.4).

  • A comparison (o.total > 1000) holds when the field, read as a number, stands in the given order to the numeric literal. The bound may be negative (r.temp < -10), so a threshold over a signed quantity — a temperature, a balance, an elevation — is expressible. It need not be a whole number either: o.price > 19.99, r.ratio > 0.75 and s.temp > 36.6 are thresholds a rule can state. What is compared is the number, not the way either side was written, so a fractional bound reads a whole-numbered field and a whole bound reads a fractional one. The bound may also be a quoted string (o.name < "m"), in which case the field is read as text and ordered alphabetically, by Unicode code point. The two readings do not mix: a text bound compares only against fields holding text, and a numeric bound only against fields holding numbers, so a field carrying both never answers by accident. A bound written in scientific notation (o.mass > scientific("1.0e4")) is a numeric bound and orders by magnitude, so it reads a field recorded in scientific notation the way a plain numeric bound reads a plain number — scientific("9.0e2") is below a scientific("1.0e4") bound, whatever the two look like side by side.

  • An equality (o.lic == "MIT") holds when the field holds exactly that value. It takes the same literals a conclusion takes: a quoted string, a number — whole or fractional, negative included — a number in scientific notation (scientific("1.0e5")), a complex number (1+2j), a blob (blob("00")), or True / False. A blob is opaque bytes and a complex number has no order, so these are the two literals that carry == and != and not the ordered operators: <, <=, > and >= say nothing about one, and a rule that writes one is refused where it is written. A complex number is written as a case label writes one (§6.2): an imaginary literal, or a number, + or -, and an imaginary literal, either with a leading -. An equality against one holds when the field holds a value equal to it, as a program’s == decides (§8.1): that complex number, both parts equal, or a number equal to it. So z == 1+0j holds for a field holding 1, 1.0 or True, and z == 1 for one holding 1+0j. A complex number with a non-zero imaginary part equals no number, and a number in scientific notation is not equal to a complex number, here as in a program. Equality over numbers is over the number, so o.total == 5 holds for a field recorded as 5 and for one recorded as 5.0; a field holding the text "5" is a string, not the number, and does not hold.

  • An inequality (o.total != 5) is the exact complement of == over values that are recorded: it holds when the field holds some value that is not the given one — including a value of another kind, so a field holding the text "5" does hold o.total != 5, the same reading that keeps it out of o.total == 5. A field with no value recorded, or one recorded as explicitly empty, holds NEITHER == nor !=: comparing against nothing is not a comparison. Ask that question with absent(...), which is about whether a value exists rather than what it is.

  • A join (c.caller == f) holds when the field holds the subject of another subject the rule binds. The right side is a subject name from the for clause rather than a value, which is how a rule relates two subjects: it reads a field as a reference and follows it. A field written with remember holds a subject exactly when the value recorded there is that subject’s name, so a join and a plain equality are the same test — what differs is that a join says which subject the answer is about, and so lets the conclusion be about that subject instead.

    A join with == also ranges the subject on its right, over the subjects the field names whether or not anything of its kind is recorded for them (§14.1), which is what allows a conclusion about a subject no premise compared. != does not: it says which subjects are excluded, which names no definite set of subjects (§14.5).

    The right side of a join is a subject. To compare against a second field, write that field — see the next entry, which is a different premise.

  • A field comparison (o.delivered > o.deadline) holds when the two fields’ stored values stand in the given order. Both operator families apply: the ordered ones compare as §8.1 does — numerically between two numbers, chronologically between two instants, by length between two durations, alphabetically between two texts — and == / != compare for equality, as a program’s == does, so a field holding 1+0j equals one holding 1. Two values of different classes stand in no order, so a duration against a number or a number against a text does not hold; a bool is a number here, as it is in a program. One token tells a join from a field comparison: a bare subject on the right is a join, a subject followed by a field is this.

    The two fields may belong to the same subject or to different subjects the same for binds:

@rule
def late():
    for o in Order:
        if o.delivered > o.deadline:
            o.late = True

@rule
def over():
    for c in Charge, l in Limit:
        if c.amount > l.cap:
            c.over = True

A field comparison requires both fields to have a value. A subject with no deadline recorded has nothing to be late against, so the rule does not conclude about it — the same reading == and != already have, and absent(...) remains the way to ask whether a value exists at all.

The right side names one subject and one field. A specific subject (Order.o1.total) cannot be named there: a rule ranges over the subjects it binds, and asking about one named subject is what current(...) answers.

  • A text test asks about part of a text, in the spellings a program uses for the same question: r.path.startswith("/srv/") holds when the field’s text begins with the part, t.name.endswith(".test") when it ends with it, and "admin" in a.role when it holds the part anywhere. A field holds one value at a time, so in over a field asks for a substring, as it does over a string in a program. The part is a quoted string or a field, written var.field for a subject the rule’s for binds, on either side of in:
@rule
def admin_path():
    for r in Request:
        if "admin" in r.path:
            r.admin = True

@rule
def served():
    for r in Request:
        if r.path.startswith("/srv/") and absent(r.admin):
            r.public = True

@rule
def under():
    for r in Request, s in Share:
        if r.path.startswith(s.prefix):
            r.shared = True

A text test cannot be negated: not in, and not before startswith or endswith, are not premises, and a rule that writes one is refused. Ask the positive question in a rule of its own, as admin_path does, and test the absence of what it concludes, as served does.

The test is code point by code point: no case folding, no normalization, no locale, so "Admin" does not hold "admin", as in a program. The empty string is a part of every text, at its start, its end and within it. Only a text holds a part: a number, a datetime, a duration or any other value holds none, even where its written form contains the characters, and a field with no value holds none either. A part that is a field holds for a text only when that field holds a text too. The tested text may be a pair’s field, written as a relation premise writes it (Name.x.y.startswith("a")).

A text test says which subjects the rule applies to, as a comparison does, and it reads the same value of its field as every other premise on that field (above). A test asks about one part: a tuple of parts, which startswith takes in a program, is refused, and so is a window after the part (startswith("a", 1)); write a premise for each part a rule needs, and a second rule for a part that would do instead. There is no regular-expression premise.

  • An absence (absent(o.shipment)) holds when the subject has no value recorded for that field. It is one of the premise grammar’s three negative forms — absence, not-asserted and nonexistence — and it means the plain absence of information, not a field recorded as false or empty. A field asserted absent with assert(K.s.a, src) holds a recorded absence, so absent(...) does not hold over it — and neither does not_asserted(...), since a person recorded it. known_absent(...) is the premise that holds over it.

  • A not-asserted (not_asserted(b.flies)) holds when nobody has recorded a value for that field. It is the narrower of the two that ask about ONE cell (the third, none(...), asks about a whole relation): absent(...) asks whether the cell is empty, this asks only whether a person put something there, so a value a rule concluded leaves it holding while absent(...) goes false. Like an absence it is shape-only — there is no not_asserted(b.flies, True) form, because did a person record this particular value is a different question and is answered by reading the field.

    This is what lets a rule state a default and yield to a recorded fact. Birds fly, unless someone has said otherwise about this bird is one rule:

@rule
def birds_fly():
    for b in Bird:
        if b.species == "avian" and not_asserted(b.flies):
            b.flies = True

Record b.flies = False for a penguin and the rule stops concluding about that bird — not because the conclusion was overwritten, but because its premise stopped holding. Remove that record and the default returns. No conflict is created and nothing has to be ordered, which is the difference between a default and a fight between two sources.

A rule may name its own conclusion in not_asserted(...), and this is the one place that is allowed. absent(b.flies) in the rule above would be self-cancelling — the rule’s own conclusion would falsify its own premise — and is refused with a message pointing here. not_asserted(...) reads only what people wrote, and a rule’s conclusion is not that, so the question stays answerable no matter how many times the rule fires.

  • A known absence (known_absent(p.chip)) holds when a source has recorded the field as known to be absent, with assert(K.s.a, src). It is the positive form beside the two negatives above, and the three tell a field’s three states apart: absent(...) holds when it has no value at all, not_asserted(...) when nobody recorded one, and known_absent(...) when a source recorded that there is none. A value does not satisfy it, and neither does a field nobody wrote. It reads a claim, so it says which subjects the rule applies to (§14.5).

    It reads the same claim as every other premise on its field, so a source test beside it asks who recorded the absence:

@rule
def vet_found_no_chip():
    for p in Pet:
        if known_absent(p.chip) and source(p.chip) == "vet":
            p.needs_chip = True

This concludes for a pet the vet recorded with no chip. It does not conclude for one where the owner recorded the absence and the vet recorded a number, whichever premise is written first. A recorded absence has no value, so a conclusion may not copy or fold the field this premise tests (§14.3).

  • Over a pair. Each of the three also takes a pair, written as a relation premise writes one: absent(Gap.x.y) asks about the field y names on the subject x of Gap, both names bound by the rule’s for. It asks about that one cell exactly as the one-field form does, so absent(Gap.x.y) holds when nothing is recorded there, not_asserted(Gap.x.y) when nobody recorded anything there, and known_absent(Gap.x.y) when a source recorded that there is nothing. A value recorded at a pair later stops a conclusion drawn from its absence, and removing it lets the conclusion return.

    not_asserted over a pair states a default for every pair and yields to the ones a person records:

@rule
def default_priority():
    for x in Task, y in Task:
        if x.open == True and y.open == True and not_asserted(Priority.x.y):
            Priority.x.y = "unranked"

absent(...) and not_asserted(...) over a pair say which subjects the rule applies to no more than the one-field forms do, so another premise must name both (§14.5); known_absent(...) over a pair names both. Because the field is whichever one y names, a rule that tests absent(Gap.x.y) may not conclude any field of Gap: it could record the very field it tested. The same pair concluded is answered with not_asserted(...), as for one field.

  • A nonexistence (none(c in Call: c.caller == f and c.tainted == True)) holds when no subject of that kind satisfies every premise inside it. Where an absence asks about one cell of one subject, this asks about a whole set — “none of its callees is tainted”, “no order of this customer is unpaid” — which is the difference between naming a fact and quantifying over a relation. The inner premises are the ordinary premise grammar and may name ONE of the outer rule’s subject variables, so c.caller == f is what ties the inner set to the subject being concluded about.

    It is answered for each subject of the variable it names. That variable need not be the one the rule concludes about. A rule over for r in Req, u in User: that tests r.owner == u and none(f in Flag: f.user == u) and concludes r.open = True asks, for each request and each of its owners, whether that owner has no flag — so a request owned by two users is open when either of them has none. A none(...) whose inner premises name two of the outer rule’s subject variables would have to be answered for each pair of subjects, and it is refused, naming both. One that names none of them is a condition on the store as a whole, and it holds or fails alike for every subject the rule concludes about.

    It holds for a subject the set never mentions. A function with no callees at all satisfies “none of its callees is tainted” — vacuously, and that is the intended reading. This is why a none(...) is enough on its own to say which subjects a rule applies to (§14.5): the subjects it concludes about are precisely the ones no inner subject reaches, so they can be reached by nothing else.

    What it records. The rule is answered in two stages, and the intermediate is an ordinary derived field on the subject the quantifier is answered for — the outer subject variable its inner premises name, or the subject the rule concludes about when they name none. It is named after the RULE (<rule>__none<N>, numbered from 1 in the order the none(...) premises are written): a rule is_safe concluding f.safe records f.is_safe__none1, and a rule open_req over requests and users as above records u.open_req__none1 on each user. So two rules concluding one field never share one. A rule compiled in a module that records it on a kind the module does not declare puts the module’s name first, since two modules may each name a rule alike: is_safe in a module audit, over a Fn no module declares, records f.audit__is_safe__none1. It is visible wherever any derived field is — list, current, checkup, and in why of the field itself (the conclusion is drawn where it is absent, so it is no element of the conclusion’s why) — and it holds True exactly for the subjects the none(...) excludes. Nothing about it is hidden, and reading it is a supported way to see which subjects the quantifier found.

    The first stage is an auxiliary rule, named after the rule as the field is: a rule myrule whose first none(...) premise needs one defines myrule__none1. That is the name a DIAGNOSTIC can print — a none(...) whose inner premises do not range-restrict is refused with “could not define rule myrule__none1 — it does not say which subjects it applies to” (§14.5). A reader meeting that name is looking at the auxiliary rule for their own myrule, which records the field of that name. A none(...) whose inner premises never mention the name it binds asks nothing about that name’s subjects, and is refused naming myrule itself.

  • A source test (source(r.temp) == "lab") holds for a value whose source is the one named — the text a write gave as its source (§7.3), quoted, so source(r.temp) == "field crew" names a source with a space in it. It is about where a value came from, not what it is: a field three sources disagree on holds source(r.temp) == "lab" through the value the lab recorded, and every other premise on r.temp, and a copy of it in the conclusion, reads that same value. source(r.temp) != "lab" holds through a value some other source recorded. It does not say the lab did not record the value: a value the lab and a second source both recorded holds != through the second source’s record of it.

    A value a rule concluded has no source a person gave it, so it satisfies no source test with either operator. trust(...) is how a rule asks about those.

    A source joined to a subject. The right side may be a subject the for binds: source(r.temp) == s holds when the value’s source is that subject’s name, which relates a value to what the store says about the one who recorded it:

@rule
def vetted():
    for r in Reading, s in Station:
        if source(r.temp) == s and s.accredited == True:
            r.vetted = r.temp

A reading recorded by a station the store holds as accredited is vetted, and forgetting the accreditation withdraws the conclusion. With no other premise on s, for s in Station still means a Station: the join holds only for a source the store holds a Station subject by that name for. Only == joins; source(r.temp) != s is refused.

  • A trust test (trust(s.label) == "asserted") holds for a value a person recorded, and trust(s.label) == "derived" for one a rule concluded — the two words .trust reads back (§5.2), spelled in English on every surface. != selects the other. A field holds recorded and concluded values together only where a rule yields to a recorded value (§14.3), and that is where the test tells the two apart — the defaults from the values recorded over them:
@rule
def label_default():
    for x in Sensor:
        if not_asserted(x.label):
            x.label = x.name

@rule
def defaulted():
    for x in Sensor:
        if trust(x.label) == "derived":
            x.defaulted = True

Both tests take one field of a bound subject; a relation (Reach.x.y) is refused. Neither stands in a constraint (§14.10), whose premises are the comparisons and absences a read can answer.

14.3 The conclusion

conclusion := cell '=' ( literal | '-' ( INT | REAL ) | fold | copy | text )
                                                 # o.review = True, r.adjustment = -5,
                                                 #   r.rate = 0.75, c.order_count = count(o),
                                                 #   d.owner_name = p.name,
                                                 #   d.line = f"{d.title} cites {d.cites}"
            | 'assert' '(' cell ')'              # assert(p.chip) — known to be absent
cell       := IDENT '.' IDENT                    # a subject and one of its fields
            | IDENT '.' IDENT '.' IDENT          # a kind, a subject, a field
copy       := IDENT '.' IDENT                    # p.name — a field of a bound subject
            | IDENT '.' IDENT '.' IDENT          # Dist.x.y — a relation's value for a pair
text       := <an f-string (§3.5.10), each field a copy>
                                                 # f"{p.name} ({d.room})"
fold       := fold-name '(' IDENT ')'            # count(o) — fold the subjects
            | fold-name '(' IDENT '.' IDENT ')'  # sum(o.amount) — fold one field
fold-name  := <a single-operand fold of §7.1.1>  # count, sum, avg, argmax, …

Whenever the condition holds, the engine records the named field of the named subject as the given literal, which may be negative (r.adjustment = -5), need not be a whole number (r.rate = 0.75), and may be written in scientific notation (s.avogadro = scientific("6.02e23")), as a complex number (s.gain = 1+2j) or as a blob (d.marker = blob("beef")). The field then holds a value of that kind, and reads back as that kind. As with a computed field, the customer never writes that field directly: a write there refuses exactly as it does at a computed field (§13.7), and its reason names the rule and the fields its condition reads. Where the conclusion covers every subject of a field the condition also reads, as p.email = q.email does over one class, no value can be written to that field for the rule to read. The reason says so instead of naming that field as one to write: give the conclusion a field of its own, or let the rule yield. The one exception is a rule that yields — one whose condition includes not_asserted(...) on the very field it concludes (§14.2). That rule states that a recorded value wins, so recording one there is accepted, and the rule stops concluding for that subject. A relation conclusion (Rel.x.y = …) concludes at every field of its kind, and yields at the one its not_asserted(x.f) names: a value may be recorded at f, and every other field stays the rule’s.

A rule is refused on the same terms as a computed field: installing a rule whose conclusion would land on a field that already holds recorded values refuses, naming them, unless the rule yields. Forget those values, or conclude at another field; the refusal changes nothing recorded, and forget there is never refused.

A cell can be named in two lengths. The two-segment form, o.review, names a field of a subject the for clause bound, and is what most rules use. The three-segment form is the same Kind.subject.field path used everywhere else in the language — remember(Order.o1.total, …) — except that the last two segments may be bound names rather than literals. That is what lets a rule record a fact about a pair:

@rule
def reaches_direct():
    for c in Call, x in Fn, y in Fn:
        if c.caller == x and c.callee == y:
            Reach.x.y = True

@rule
def reaches_further():
    for c in Call, x in Fn, y in Fn, z in Fn:
        if Reach.x.y == True and c.caller == y and c.callee == z:
            Reach.x.z = True

The first rule records every direct edge; the second joins a known reach to a further edge and records the longer one. Because the second reads the same relation it writes, it applies to its own results and settles on everything the graph connects — not on a fixed number of steps. Reading a relation with a bound name in the field position asks for every cell of that subject, so Reach.x.y in a premise finds each y that x reaches rather than requiring one to be known in advance.

A cycle needs nothing further. A subject that reaches itself is in one, so the query is the relation read with the same name twice:

@rule
def in_cycle():
    for x in Fn:
        if Reach.x.x == True:
            x.cyclic = True

A name used twice in one cell agrees with itself rather than standing for two things: Reach.x.x holds only where the subject and the field are the same name, which is what makes this a cycle test and not a list of everything reachable.

Every name a conclusion uses is one the for clause binds, as a Python name is bound before it is used. A name the for does not bind is refused, and the refusal names it. A join or a relation premise binds no name: it ranges one, deciding which subjects the name stands for (§14.5). A relation premise ranges both of its names, so a name it reads in the field position needs no comparison of its own, and may be a conclusion’s subject as well as its field. This rule records a relation backwards:

# reached-by.dpy — a relation read backwards: what reaches each function.
@rule
def reached_by():
    for x in Fn, y in Fn:
        if Reach.x.y == True:
            Back.y.x = True

def show_reached_by():
    remember(Reach.main.parse, True, "graph")
    b = current(Back.parse.main)
    print("parse is reached from main: " + str(b.value))
OK script show_reached_by(): 1 claim written, 1 print
  remember Reach.main.parse = True
  print parse is reached from main: True

As §14.1 says, the premises and not the kind say which subjects a name ranges over, so neither main nor parse needs to be recorded as an Fn.

A conclusion may copy a value the rule read. d.owner_name = p.name records, for each device, the name of the person its owner names. The right side is a field of a subject the for clause binds, or the value a relation holds for a pair of them, written as a premise writes it, and the value recorded is the value read there, of the same kind: a copied datetime is a datetime, a copied blob is a blob, a copied 5 is the number and not the text.

# owner-name.dpy — a device records its owner's name, read across the join.
@rule
def owner_name():
    for d in Device, p in Person:
        if d.owner == p:
            d.owner_name = p.name

def show_owner():
    remember(Device.d1.owner, "ann", "inventory")
    remember(Person.ann.name, "Ann Lee", "hr")
    n = current(Device.d1.owner_name)
    print("owner: " + str(n.value))
OK script show_owner(): 2 claims written, 1 print
  remember Device.d1.owner = "ann"
  remember Person.ann.name = "Ann Lee"
  print owner: Ann Lee
  • A copy reads the field, so the field’s presence is a premise. A device whose owner has no name recorded is not concluded about, the reading a field comparison has (§14.2). The copy also ranges the subject it reads (§14.5), so a copy that yields needs no comparison: if not_asserted(s.label): s.label = s.name labels every sensor that has a name, and a label someone records wins.
  • A copy records every value the field holds. A field that two sources disagree on holds two values, and the rule records one conclusion from each, so the disagreement travels into the copied field (§14.4). A premise on the copied field and the copy read one value of it (§14.2): if r.t > 30: r.hot = r.t copies only the values above 30.
  • A copied value is valid while the value it was read from is. A value recorded with a validity window is copied with that window, narrowed to the overlap with the other premises’ windows (§14.4).
  • A relation’s value is copied as a field’s is. r.dist = Dist.x.y copies the value Dist holds for the pair x, y, and Km.x.y = Dist.x.y carries it into another relation, pair for pair. Both names must be bound, as in a relation premise (§14.2), so a path naming a specific subject (Order.o1.total) is not copied. Everything above holds for it: the pair’s presence is a premise, the copy ranges both names, every value the pair holds is copied, and a premise on the same pair reads the value the copy does — if Dist.x.y < 6: r.near = Dist.x.y copies only the distances under 6.
  • A conclusion cannot read a field its rule tests with known_absent. A recorded absence has no value, and a premise and the conclusion on one field read one value (§14.2), so a copy (p.copied = p.chip), a fold (count(p.chip)) or a text written from it (f"{p.chip}") has nothing to carry. The rule is refused when you define it. To conclude that a field is known to be absent, write assert(p.chip) (below).
  • A conclusion cannot copy what it concludes. A copy of the concluded field on the same subject (x.a = x.a), or of the concluded pair (Km.x.y = Km.x.y), would be its own only source, so it could never record anything. The same field of another subject is how a value travels along a link:
@rule
def same_name():
    for x in Member, y in Member:
        if x.same_as == y and not_asserted(x.name):
            x.name = y.name

A member linked to one whose name is recorded takes that name; a member linked to that member takes it too, however long the chain; and a member whose own name is recorded keeps it. - Copying makes no new values. Every copy records a value the store already holds, so rules may copy into each other without threatening termination (§14.6). A copy that feeds a fold’s own group is that fold’s cycle, and is refused as one (§14.5).

A conclusion may write a text from values the rule read. d.line = f"{d.title} cites Version {d.cites}." records, for each document the condition holds for, one sentence written from two of its fields. The right side is an f-string (§3.5.10) whose fields are each what a copy’s right side is: a field of a subject the for clause binds, or the value a relation holds for a pair of them. The text recorded is the one a program’s f-string prints over the same values, and it is a conclusion like any other: it is rewritten when a field it reads is revised, it goes when the condition stops holding or a field it reads does, why explains it by the claims it was written from, and a write at the field refuses as it does at any concluded field.

# cites-line.dpy — a sentence about each document, written from two of its fields.
@rule
def cites_line():
    for d in Doc:
        if d.status == "published":
            d.line = f"{d.title} cites Version {d.cites}."

def show_line():
    remember(Doc.security.status, "published", "web")
    remember(Doc.security.title, "Level 2 Security", "web")
    remember(Doc.security.cites, "1.9", "web")
    a = current(Doc.security.line)
    print(str(a.value))
    update(Doc.security.cites, "1.10", "web")
    b = current(Doc.security.line)
    print(str(b.value))
OK script show_line(): 4 claims written, 2 prints
  remember Doc.security.status = "published"
  remember Doc.security.title = "Level 2 Security"
  remember Doc.security.cites = "1.9"
  print Level 2 Security cites Version 1.9.
  update Doc.security.cites = "1.10"
  print Level 2 Security cites Version 1.10.
  • A written text is a string, whatever it reads as. n.shown = f"{n.count}" over a count of 4 records the text "4", where the copy n.shown = n.count records the number. An f-string with no field is a string literal, and is the literal conclusion above.
  • Each field is read as a copy reads its one. Its presence is a premise, so a document with no version recorded has no sentence; the text ranges each subject it reads a field of (§14.5); a field that two sources disagree on gives one text for each value it holds; a premise on a field and the text read one value of it (§14.2); and the text is valid while every value it was written from is.
  • A field holds a string or an int. Those are written as a program writes them, a text as itself and a whole number as its digits. Where a field holds a value of another type — a real, a bool, a datetime — no text is drawn for that subject, and why_not on the concluded field names the field and the value it holds.
  • A field is written plain. {d.title} and {Dist.x.y} are the whole of what a field may be: a conversion ({d.title!r}), a format ({d.count:>5}), a debug field ({d.title=}), a subject alone ({d}) and any other expression ({d.count + 1}) are each refused when you define the rule. A conclusion computes nothing, so a value that needs working out is concluded into a field of its own, and that field is named.
  • A text has a ceiling of 65,536 bytes. A text that would be longer is not drawn, for that subject alone, and why_not says so.
  • A text cannot be written from itself. The concluded field on the same subject (x.a = f"{x.a}!") is refused where it is written, and a rule that closes a cycle through a written text is refused as §14.5 says.

A conclusion may record that a field is known to be absent. Written in the conclusion’s place, assert(p.chip) records, for each subject the condition holds for, what assert(K.s.a, src) records when a program writes it (§7.1): that the field is known to be absent. It takes no source, because the rule is the source of what it concludes, and a second argument is refused. The three-segment form records it about a pair, as assert(Gap.x.y).

@rule
def strays_have_no_chip():
    for p in Pet:
        if p.status == "stray":
            assert(p.chip)

A known_absent(...) premise holds over what this records, absent(...) does not, and not_asserted(...) does, since no person recorded it (§14.2). It is a conclusion like the others: it goes when the condition stops holding, a write at the field refuses as it does at any concluded field, and a value another rule concludes at the same field is held beside it, so the field is in disagreement (§7.6).

A conclusion may fold the subjects it ranges over instead of naming a literal. c.order_count = count(o) records, for each customer, how many orders met the condition with that customer. The for clause ranges over pairs, the conclusion names one of them, and the fold is taken over the other — grouped by the subject the conclusion is about:

@rule
def order_count():
    for c in Customer, o in Order:
        if o.customer == c:
            c.order_count = count(o)

@rule
def order_total():
    for c in Customer, o in Order:
        if o.customer == c:
            c.total = sum(o.amount)

@rule
def regular():
    for c in Customer:
        if c.order_count > 3:
            c.regular = True

A customer with four orders gets 4; a customer with one gets 1. That per-subject answer is the difference from an aggregate query (§7.1.1), which folds a whole attribute across every subject of a kind and yields a single number: count(Order.customer) is how many orders exist, and count(o) here is how many are this customer’s.

Two shapes, by what is folded:

Written Folded
count(o) the bound subjects themselves — a bare subject variable
sum(o.amount) one field of those subjects

Which folds may appear. The single-operand folds of §7.1.1 — every family there except ranked and two-attribute. percentile(o.amount, 90) and slope(o.x, o.y) need an argument a conclusion has nowhere to put, and are refused here rather than admitted half-formed. The rest keep their §7.1.1 meanings exactly, the identity and positional folds included: argmax(o.amount) records the subject holding this group’s largest amount, and distinct(o.region) records how many distinct regions this group covers.

A subject missing the folded field contributes nothing to a fold over that field, and still counts toward count(o). Four orders of which three carry an amount give count(o) of 4 and sum(o.amount) over the three. Naming a field makes its presence a premise — the same reading a field comparison has (§14.2).

A fold ranges over the premises’ MATCHES, and a location holding more than one value in force matches once per value. §7.6 already states the general rule — a standing rule reads every value at a location in disagreement and derives from each — and a fold is where it becomes visible as arithmetic. Take two orders, and let a second source also record that the first belongs to this customer. That order now matches twice, and count(o) answers 3 for two orders.

Each fold follows the claims of the field IT reads, so the two shapes move independently, and the difference is worth knowing before reading a number:

what holds two values in force count(o) sum(o.amount)
nothing 2 30
the joined field (o.customer) 3 30 — unchanged
the folded field (o.amount) too 3 50 — the amount is added twice

count(o) folds the join’s matches, so a second claim on the joined field raises it. sum(o.amount) folds the amount’s own claims and does not see the join’s extra match at all; only a second claim on the amount adds it again.

A premise on the folded field decides which of its values are folded. Premises and the fold that name one field read one value of it (§14.2), so if o.customer == c and o.amount > 10: c.big = sum(o.amount) folds only the amounts above 10. An order whose amount is 5 from one source and 15 from another contributes 15, not 20.

For the number of distinct subjects, fold distinct(o), which answers 2 over the same data and is what subjects(Order.customer, c) counts in program position. Nothing here is special to folds — an ordinary conclusion is derived once per matching value too — but a literal conclusion records the same value twice and looks unchanged, while a count does not.

The folded name must be bound by the for clause, like every other name a rule uses. Folding a name the rule does not range over is refused, and the refusal names that name.

A fold that declines when its rule is defined refuses the definition, and nothing else. Defining a rule whose fold declines over the values its field already holds (§14.9 — a string where the rule sums, say) is refused, and the refusal says the fold cannot read a value there: correct or remove that value, or fold with one that takes it, such as count. The refusal is that rule’s alone. A compile that defines only rules whose folds take what is recorded succeeds beside a rule that declines, and a program that writes such a value is not refused either: the declining rule keeps the last value it concluded, read with maintained set to False (§5.2).

The third rule above is the ordinary case that makes the first two worth having: a derived field is a premise like any other (§14.4), so “a customer with more than three orders is a regular” is one comparison against a field nobody wrote. A fold may not, directly or through other rules, feed a premise that the same fold depends on — see §14.5.

14.4 What a rule does

Defining a rule takes effect immediately and continuously:

  • A rule’s conclusion is in force exactly while its condition holds over undisputed data — evaluated over the data present at definition time and kept current as data is written or removed (including when a value is first recorded for a field an absent(...) premise required to be absent). A conclusion in force carries a derived provenance (§7.1) in place of a source.
  • When a conclusion’s condition ceases to hold, the conclusion ceases to be in force; when it comes to hold, the conclusion is in force. A rule therefore states an invariant over the data, not a one-time action.
  • A premise in disagreement carries the disagreement into the conclusion (§7.6). While a premise location holds claims from different sources with different values, a rule reading it derives once from each value that satisfies its condition, and every such conclusion is in force. This holds whether the disagreement arose before or after the conclusion was first derived: a conclusion already standing is not withdrawn when its premise comes into dispute, and the contesting value’s conclusion joins it. Conclusions collapse back to one when the disagreement clears — where the disagreeing claims were written, by updateing or forgetting one of them; where they are themselves conclusions, neither reaches, since a conclusion forget withdraws is recorded again while its condition holds and an update adds a further claim beside the two, so that disagreement clears by changing the rules that produce it. The outcome depends on the data, never on the order the claims arrived: recording the disagreement first and the rule second reaches the same conclusions as the reverse.

A derived field reads back exactly like any other field (current/get, §7.1); its value reflects the rules currently in force.

A conclusion is valid while its premises are. A premise may carry a validity window (§7.1), and a conclusion drawn from windowed premises is valid over the time they are all valid together — the overlap of their windows, and no wider. Nothing else about the rule changes: the conclusion is in force exactly while its condition holds, and its window says which times that condition is being asked about.

# window-overlap.dpy — a conclusion drawn from two windowed premises is valid
# over the time both premises are valid, and not outside it.
@rule
def flagged():
    for e in Emp:
        if e.status == "active" and e.dept == "eng":
            e.flagged = True

def overlap_report():
    remember(Emp.e1.status, "active", "hr",
             datetime("2020-01-01T00:00:00Z"), datetime("2022-12-31T00:00:00Z"))
    remember(Emp.e1.dept, "eng", "hr",
             datetime("2021-01-01T00:00:00Z"), datetime("2023-12-31T00:00:00Z"))
    inside = get(Emp.e1.flagged, datetime("2021-06-01T00:00:00Z"))
    print("inside the overlap: " + str(len(inside)))
    after = get(Emp.e1.flagged, datetime("2023-06-01T00:00:00Z"))
    print("after it: " + str(len(after)))
OK script overlap_report(): 2 claims written, 2 prints
  remember Emp.e1.status = "active" valid_from=datetime("2020-01-01T00:00:00Z") valid_to=datetime("2022-12-31T00:00:00Z")
  remember Emp.e1.dept = "eng" valid_from=datetime("2021-01-01T00:00:00Z") valid_to=datetime("2023-12-31T00:00:00Z")
  print inside the overlap: 1
  print after it: 0

The two premises overlap from 2021-01-01 to 2022-12-31, so that is when the conclusion is valid. An as-of read (§7.1) inside the overlap finds it and one outside does not, which is why the second read above answers empty although dept is still valid then. A bare read answers about the present, so once the overlap has passed the conclusion is absent from the present — the correct answer, and not a rule that stopped working.

A premise carrying no window is valid at all times and therefore narrows nothing: a conclusion drawn from one windowed and one unwindowed premise takes the windowed one’s bounds. A premise with one open bound narrows only the side it names.

Premises that never overlap conclude nothing. Where the windows have no time in common there is no moment at which the condition holds, so the rule records no conclusion at all — at no time, the present included. That is not an error and is not reported as one: the rule is well-formed, it applied, and the data gave it nothing to conclude. why_not (§7.1) on the cell names the premise that is not in force at the time asked about, which is what tells this apart from a rule that never applied and from a cell no rule concludes.

A window carries through a chain. A conclusion is an ordinary premise to any rule that reads it, and it carries its own window when it is read that way. A rule concluding from a conclusion is valid over the same overlap the first one was, and a chain of rules narrows to the time every fact along it is valid.

14.5 Well-formed rules

Two things make a rule usable; a rule that lacks either is rejected when you define it, with a message that names the rule.

  • A rule must say which subjects it applies to — every subject it binds, not just one of them. A subject the for binds and nothing else in the rule mentions is not used, and the message says so: remove it, or relate it to the others with a join. A subject only the conclusion mentions has no premise to say which subjects it concludes about, unless the conclusion copies from it (below). A subject whose only mention is an absence test never says which subjects, so the rule has nothing to work over (for o in Order … if absent(o.shipment): … — which orders?). The same applies to a not_asserted(...), for the same reason and not a second one: it names one cell and describes subjects without saying where to find them. Over a pair (absent(Gap.x.y)) the two forms range neither name, for that reason. In the defeasible rule of §14.2 the b.species == "avian" premise is what ranges b, and a default worth stating almost always has such a premise already — the population it is a default FOR. These premises range a subject: a comparison, a join (either side, == only — a != join says which subjects are excluded, which is the same non-range an absence has), a relation premise, which ranges both of its names, a known absence (over a pair, both of its names), a nonexistence, and a source or trust test, which reads the field it names (§14.2). A source joined to a subject (source(r.temp) == s) ranges that subject too, over the subjects the store holds of its kind. Give each subject at least one of those. A conclusion that copies a field (§14.3) ranges the subject it copies from as well: it reads that field, so the set is the subjects that have it. A copy of a relation ranges both of its names, as a relation premise does. A conclusion that writes a text ranges each subject it reads a field of, for the same reason.

    A nonexistence ranges where an absence does not, and the difference is not arbitrary even though both are negative: an absence names one cell and nothing else, so it describes subjects without saying where to find them. A none(...) carries a whole inner rule, and the subjects it concludes about are exactly the ones that inner rule does not reach — a set the engine can enumerate, because it is the kind’s own subjects minus the ones the inner premises matched. So the range is not an extra requirement placed on the author; it falls out of what the premise means. This is what lets “a function is safe when none of its callees is tainted” be written as it reads, with no unrelated anchor field invented to give the rule something to stand on.

  • A rule’s outcome must settle. A rule — alone or together with others — must not chase its own tail: if recording its conclusion would remove the very condition that produced it, there is no answer to settle on, and the rule is rejected. You may build rules on stored data and on one another freely; what is ruled out is a set whose outcome cannot settle, which is a set that cancels itself and the two below. A relation conclusion (Rel.x.y = …, or assert(Rel.x.y)) can record any field of its kind, since the field is whatever y names, so it counts as concluding every one of them: a rule that tests absent(r.f) on a subject of that kind and concludes a relation into it is rejected for this reason, whichever field f is. Conclude the pair into a kind of its own, whose fields the rule does not test, and nothing it records can undo the absence. An absence over a pair (absent(Gap.x.y)) is the same fact from the other side: it tests whichever field of Gap y names, so a rule that tests it and concludes any field of Gap, of a subject of that kind or of a pair, is rejected for this reason. not_asserted(Gap.x.y) reads only what people wrote, so it may stand beside such a conclusion.

    A fold (§14.3) settles on the same terms, for a second reason. A fold reads a whole group before it can answer, so a fold whose own result feeds — directly or through other rules — a premise that fold depends on is asking for a number that changes as soon as it is written. That set is rejected at definition too. Counting from data other rules derive is fine; what is ruled out is a count that is a member of what it counts.

    A computed field (§13.7) that calculates settles on the same terms, for a third reason. A computed field whose initializer applies an operator (total: int = price * qty) works its value out from its inputs, so a rule whose conclusion feeds — directly or through other rules — an input of such a field, while the rule depends on that field’s value, would make the value depend on itself. That set is rejected at definition too. A rule may read such a field and may conclude into its inputs; what is ruled out is doing both around one cycle. A field that copies one other field or compares (a bool field) may sit on a cycle with rules, since its value settles (§13.7).

    A text a rule writes (§14.3) settles on the same terms, for a fourth reason. A conclusion that writes a text makes a new value from the ones it reads, so a text that fed, directly or through other rules, a field it is written from would be written from itself, and each writing would be longer than the last. A rule that closes such a cycle is rejected at definition, whichever rule of the cycle it is, and the message names the text. Other rules may read a written text freely; what is ruled out is a text that is one of its own sources.

14.6 Termination

Standing rules preserve the termination guarantee of §9.5. A rule ranges over finitely many stored subjects; its premises are tested, not iterated; and §14.5 rules out the four constructions under which the outcome could fail to settle, a set that cancels itself, a fold that counts its own result, a computed field that calculates from its own value and a text written from itself. Every set of rules therefore reaches a fixed set of derived facts in finite time.

14.7 list(target = rules)

list(target = "rules")

Lists the names of the standing rules you wrote with @rule, one per line — the rule-authoring counterpart of list(target = scripts) (MCP Reference §9.2). A rule with a kind (§14.1) is listed with the kind after its name, heat.warm (causal). Rule names come back in ascending name order, the same guarantee §5.1 gives the enumerating reads a program can call. A rule a module defines is listed under its module.rule surface name and sorts under that.

Only the rule names, with their kinds, are returned; the listing carries no premises or conclusions. To see what a rule does, keep your own source — the service stores no rule source. list(target = rules) is wire-only and its response is rendered text, not a typed value; there is no expression-position form of it inside a program (unlike the script listing, which a program also reads, as list_scripts(), a string_list).

The listing is authored-only. A computed field (§13.7) is a standing rule — it fires, it derives, and it can stop concluding — and it is not listed here, because it has no name you gave it: its address is the field it fills. So an empty listing means you have written no @rule, not that nothing is reasoning over your store. A store declaring area: int = w * h and no @rule lists nothing here and reports rules=1 from checkup (§14.9), which counts every standing rule in force. Read the two together: list(target = rules) answers what did I name, checkup answers what is standing, and the difference between them is the computed fields.

14.8 forget_rule(name)

forget_rule(name = "<module>.<rule>")

Removes one standing rule and everything it derived. name is the qualified module.rule name that list(target = rules) (§14.7) prints. The removal is hard, the same as any other removal (§7.1): the rule stops firing and leaves the rule listing, and the facts it concluded are reclaimed.

Facts a removed rule concluded do not all disappear. A conclusion that another standing rule still supports is re-derived and stays — only the support this rule contributed is withdrawn. So removing one of two rules that agree leaves their shared conclusion standing, on the surviving rule.

Removing a rule does not touch the facts it read. Premises are your data; only conclusions are the rule’s.

An address with no rule behind it removes nothing, and the call succeeds, saying the rule is not stored. forget_rule is wire-only — there is no expression-position form inside a program — and it needs a read-write-delete key, as every removal does.

To bring a removed rule back, compile its module again: the rule returns and re-derives its conclusions over the facts that are still there. This holds of every rule in a store, and a removed rule stays removed until then.

14.9 checkup

checkup

Reports the store against the standing rules and constraints in force over it. It takes no arguments, changes nothing, and needs only a read key.

It exists because a rule that never fired and a rule that fired and found nothing produce the same empty result. Where the reasoning came from a module the reader did not write, nothing else in this specification distinguishes the two: §14.7 gives rule names and no premises, and the data verbs (§7.1) describe the store without reference to the reasoning over it.

The response is a counts line followed by findings, one per line, in a closed set of forms:

Form Holds when
unmet <Kind>.<field> no-such-kind some standing rule’s or constraint’s premise reads that cell and the store holds no kind of that name
unmet <Kind>.<field> no-values the kind is present and no claim stands at that field for any subject
unmet <Kind>.<field> not-in-force the kind is present and a claim stands at that field, and every such claim carries a validity window (§7.1) that does not cover the time asked about
unread <Kind> the kind holds claims and no standing rule’s or constraint’s premise reads it
near <A> ~ <B> A is unread, B is the kind of an unmet no-such-kind cell, and their names differ only slightly
violated <name> <n> <subject> … a standing constraint (§14.10) does not hold; <n> is how many subjects breach it, and at most five of them are listed
counts rules=<n> unfirable=<n> unreadable=<n> declined=<n> kinds=<n> unread=<n> constraints=<n> unevaluable=<n> violated=<n> always; rules counts every standing rule in force, the computed fields a class declares (§13.7) included — see below, and note list(target = rules) (§14.7) names only the @rule ones; unfirable counts rules with at least one unmet premise, unreadable counts rules this version of the service cannot read (see below), declined counts rules whose fold cannot take the values it reads (see below), unevaluable counts constraints with at least one unmet premise — an invariant that could not be tested, which violated=0 alone would report as one that holds — and violated counts constraints that do not hold

Kinds are named in the addressable form (<module>.<Kind> for a kind a module declares), which is the form §7.1’s verbs take.

Findings name cells and never rules. A cell is part of a module’s interface — info_module (MCP Reference §9.4) already enumerates the classes a module declares — while which premise belongs to which rule is part of its body, which is not readable. Rules are therefore counted, not named.

A premise that asks for an absence. absent(...), not_asserted(...) and none(...) (§14.2) hold where a field has no value, or none a person recorded, so an empty field meets them. The unmet forms above are about the premises that need a value. A premise that asks for an absence raises no unmet finding and does not count its rule unfirable. A field that holds what it excludes keeps the rule from concluding for the subjects that hold it, and for no other, while unmet and unfirable say what no subject can meet. So that is not a finding here either, and why_not (§5.2) says it of one subject. The premise still reads its kind, so a kind read only by such a premise is not unread.

The rule a none(...) is answered through. A none(...) is answered by an auxiliary rule (§14.2) whose premises are the ones inside its parentheses. It stands over the store as any rule does, so rules counts it, and it asks for an absence as the none(...) does: its premises raise no unmet finding and it is never counted unfirable. That it cannot fire is the none(...) holding for every subject, so a finding naming the cells inside the parentheses would ask for exactly what the none(...) excludes, and recording it would withdraw the conclusions the none(...) let stand. Those cells still read their kind, so a kind read only inside a none(...) is not unread, and a rule that reads one of them as a value raises it as any rule does.

Which rules are counted. Every standing rule in force over the store: the rules you wrote with @rule and the computed fields your classes declare (§13.7). A computed field is a rule you did not name, and it is counted here for the reason this verb exists — it fires, it derives, and when a premise of it goes unmet it stops concluding silently. list(target = rules) (§14.7) is authored-only, so rules= is at least as large as that listing and the difference between them is exactly the computed fields. The two verbs answer different questions and are meant to be read together.

So the mixed store below is not a contradiction, and both readings are right. Compiled as a module named shapes (MCP Reference §9.1), so the names below carry its prefix:

class Box:
    w: int
    h: int
    area: int = w * h      # a computed field — counted, never listed
    big: bool

@rule
def bigbox():              # an authored rule — counted AND listed
    for b in Box:
        if b.area > 100:
            b.big = True

Over an empty store, list(target = rules) names one rule, shapes.bigbox, while checkup reports rules=2 unfirable=2 and names three unmet cells — shapes.Box.w and shapes.Box.h, the computed field’s premises, and shapes.Box.area, the authored rule’s. Both rules are represented and neither is named: a cell is what you can act on. Write a w and an h and the same store reports rules=2 unfirable=0, both rules having concluded. A store declaring the class and no @rule at all reads rules: none from one verb and rules=1 from the other, in the same second. Appendix C.30 is this class as a runnable program.

Unreadable rules. A rule is stored when its module is compiled, in a form that the version of the service which compiled it reads. A rule stored by another version may be one this version cannot read. Such a rule still stands, and rules counts it, but it never fires, whatever the store holds. unreadable counts these rules and nothing else reports them. The report does not read their premises, so none of their cells is called unmet and none of them is counted unfirable: a finding about a rule that cannot conclude would ask for data that changes nothing. A store whose rules were all compiled by this version reports unreadable=0.

Declined rules. A conclusion that folds (§14.3) takes the values its folded field holds under §7.1.1’s rule: a fold that compares by value declines while that field holds a value it cannot take — a string where the rule sums, say — and count takes any value. While one fold of a rule declines, the rule concludes nothing, at any of its conclusions, until those values change: a count written beside the declining sum in the same rule stops with it. declined counts these rules, and rules still counts them. Their premises are read as any rule’s are, so a declined rule with an unmet premise is counted unfirable too, and filling that cell leaves it declined. The rule is not named, for the reason above; why_not and what_needs on any of its conclusions name it. A computed bool comparing values that cannot be ranked against each other is not counted here: it concludes for one subject and not the next, which is a fact about that subject’s value, and why_not reports it for that subject. Nor is a computed field whose arithmetic has no answer for one subject’s values — a divisor of zero, say — for the same reason.

A violated finding names the constraint and the offending subjects, and not the cell — the mirror of that rule rather than an exception to it. An unmet finding is about the store’s SHAPE, so a cell is the right noun and naming the rule would map a premise to a body. A violated finding is about one named invariant the author declared, so the constraint is the right noun, and naming the cell would map a condition to that constraint’s body in exactly the way the paragraph above forbids. The subjects are what make it actionable: knowing that stock_never_negative broke is worth little without knowing which products broke it, and a subject id is data the reader already owns. The COUNT is exact and the list is capped at five, so a constraint breached by a whole kind reports the scale of the breach without putting the store’s subject list in a report meant to be read.

checkup is wire-only: there is no expression-position form inside a program, and using the name in a program body is an ordinary identifier. An empty store yields the counts line alone. Listing order is contract on both axes. Findings are grouped by form, in this order — the counts line, then every unmet, then unread, then near, then violated — and within each group in ascending name order. A form with no findings contributes no lines rather than a heading.


14.10 @constraint

A constraint watches a condition and reports. It concludes nothing, writes nothing, and can never fire:

@constraint
def stock_never_negative():
    for p in Product:
        if p.stock < 0:
            violated

It reads like a rule with violated where the conclusion would be, and that is what it is: the same for and if clauses, and no head. violated is a statement, not a value — it marks the block as a constraint body and is a reserved word only in that position.

A breach does not become data. The alternative without this construct is a rule concluding p.invalid = True, which records facts ABOUT the store INSIDE the store, and then feeds them to other rules as though they were observations. A constraint has no conclusion to record, so there is nothing for a later rule to read. That is the whole difference, and it is why a constraint is not simply a rule whose head you ignore.

A constraint is evaluated by checkup (§14.9), which reports one violated finding per constraint that does not hold. It is never evaluated on write: recording a fact that breaches an invariant is not refused, because a store mid-edit is routinely inconsistent and a write that fails on someone else’s invariant is a worse failure than a report that says so.

@constraint
def every_order_has_a_customer():
    for o in Order:
        if absent(o.customer):
            violated

Premises are a subset of the §14.2 grammar, and the subset is the whole of what a constraint can be asked. Two shapes are admitted:

premise example
a comparison against a literal p.stock < 0, p.tier == "gold"
an absence absent(o.customer)

Several joined by and are allowed, and hold when a subject satisfies all of them. Premises on one field hold through one value of it, as a rule’s do (§14.2): p.stock > 0 and p.stock < 10 is violated by a product holding a stock between the two, not by one holding 20 and -5. A premise that orders a string field against a quoted bound (<, <=, >, >=) is judged by codepoint order, alone or with absent(), and a constraint with one is reported violated exactly when a @rule with the same premise would conclude; before, such a constraint held silently. Anything else — a premise relating two subjects, a quantifier, a premise that selects on how a value came to be recorded — is refused when the constraint is defined, naming the two shapes that are accepted.

constraint-decl    := '@constraint' NEWLINE 'def' IDENT '(' ')' ':' NEWLINE INDENT
                        'for' subject-binding ':' NEWLINE INDENT
                          'if' constraint-premise ( 'and' constraint-premise )* ':' NEWLINE INDENT
                            'violated' NEWLINE
                          DEDENT
                        DEDENT
                      DEDENT
constraint-premise := IDENT '.' IDENT ( ord-op | eq-op ) literal          # a comparison against a literal
                    | IDENT '.' IDENT '.' IDENT eq-op literal             # the same, over a relation (§14.2)
                    | 'absent' '(' IDENT '.' IDENT ')'                    # an absence

A constraint also binds exactly one subject variable. A rule may relate two kinds (for c in Customer, o in Order); a constraint may not.

Both restrictions have one cause: a constraint asks only what a read of the store can answer, and the premises above are exactly those a read can express. To check something outside that, conclude it into a field with a @rule and write the constraint over that field. The rule does the reasoning, the constraint does the watching, and neither grows the other’s job.

Naming. A constraint shares the unit’s one name space with scripts, rules and classes (§4.2), and list(target = rules) (§14.7) does not list it — a constraint is not a rule and a caller enumerating rules to reason about derivation would be misled by one. checkup’s counts line reports how many stand, and the qualified name it prints is also the address forget_constraint (§14.11) takes.

Termination is not a question a constraint raises. It derives nothing, so it cannot feed itself, and its evaluation is a bounded pass over the subjects of one kind.

14.11 forget_constraint(name)

forget_constraint(name = "<module>.<constraint>")

Removes one standing constraint. name is the qualified module.constraint name that checkup (§14.9) prints in a violated finding and counts on its constraints= line.

Nothing else changes, and that is the whole of why this verb is separate from forget_rule (§14.8). A constraint concludes nothing, so there is no conclusion to withdraw and no fact to reclaim. Removing a rule raises a question about the facts it derived; removing a constraint raises none, because it derived none. After the removal the store holds exactly what it held before, and the only difference is that checkup no longer reports on that invariant.

The data the constraint watched is untouched — including the subjects that breached it. A breach was never recorded (§14.10), so there is nothing to clean up: the products with negative stock are still there, and their stock is still negative. Stopping the watch is not fixing the breach, and this verb does not pretend otherwise.

An address with no constraint behind it removes nothing, and the call succeeds, saying the constraint is not stored. forget_constraint is wire-only — there is no expression-position form inside a program — and it needs a read-write-delete key, as every removal does.

To bring a removed constraint back, compile its module again. forget_module (MCP Reference §9.4) removes a constraint too, but with the module’s classes, its scripts, its other constraints and the data in its kinds.


15. Hypothesis

A hypothesis reads what a value would be under supposed facts, without changing anything. suppose(<field>, <value>, …, <field>) sets each named field to the value beside it, lets your standing rules (§14) and computed fields (§13.7) run as if those were true, reads the final field, and then discards the suppositions — the store is left exactly as it was.

r = suppose(Order.o1.total, 2000, Order.o1.review)

A scenario usually needs more than one fact to be true at once. Give as many <field>, <value> pairs as it takes; the field to read is always the last argument, alone.

r = suppose(Order.o1.total, 2000,
            Order.o1.shipment, "sent",
            Order.o1.review)

Read: “if the order totalled 2000 and had shipped, would it need review?” A rule requiring both conditions is answered by supposing both at once.

Read: “if Order.o1.total were 2000, what would Order.o1.review be?” If a rule sets review when total > 1000, r is True — yet afterward Order.o1.total keeps its real value (or stays unset) and no review is recorded. Nothing the hypothesis touches persists.

hypothesis := 'suppose' '(' ( path-expr ',' literal ',' )+ path-expr ')'
  • Each supposition <field>, <value> sets one field to one literal value. A hypothesis takes 1 to 16 of them. They apply left to right, so two suppositions naming the same field leave the later one standing — the same rule an ordinary sequence of writes follows. A value taken from a variable is refused.
  • The limit of 16 is a cost bound, not a semantic one: the total cost grows faster than the count does. A scenario written by hand does not approach 16; the limit exists so that a program which would has a compile error naming the number rather than an invisible cliff at run time.
  • A single hypothesis supposes facts into place or supposes them away (§15.1), not both. Mixing the two in one hypothesis is not available.
  • Every <field>, supposed or read, is a K.s.a cell written as a verb’s is (§7): a segment held by a string name in scope is that name’s value, and an instance’s field s.f is that instance’s cell (§13.3), through any name that holds one.
  • A <field> that is no path, or goes on past one as an expression, is a parse_error at it, in a hypothesis of either kind (§15.1), as a verb’s path argument is (§7.1): r = suppose(Order.o1.total + 1, 2000, Order.o1.review) is one at column 13, “suppose takes a path here”. Python reads such a call, so a grammar error in it, later in the call, or below it, is the answer instead (§10.1).
  • The read <field> names the field whose value is returned — a plain stored field, or one a rule or computed field derives from the supposition.
  • The result is a value (§5.6), bound to a name and consumed with match like a cell read or an aggregate (§7.1.1). Concatenating it into a print renders it directly. If the read field has no value under the supposition, suppose produces no value — a catchable condition (guard it with a try / except handler, or read a field a rule is known to derive).
  • The read field is in one of three states under the suppositions, and suppose answers each the way current (§7.1) answers it over the real store: one value, which is the result; no value, the catchable condition above; or competing values (§7.6), which refuses with current’s own reason and teaching hint. It never picks one of the competing values for you. A disagreement that a rule derives only under the suppositions counts, because it is part of the state the hypothesis describes, and there the refusal is itself the answer: under those facts, the field would be in disagreement. get reads the full claim set of the real store; there is no hypothetical get.

A hypothesis never writes: the suppositions and everything derived from them are discarded when suppose returns, so reading under a hypothesis has no side effect — safe for preview, comparison, and what-if exploration. It preserves the termination guarantee of §9.5: each supposition is one bounded reasoning pass, the number of them is fixed where the call is written, and there is one read.

15.1 Supposing a fact away

suppose_absent(<field>, …, <field>) asks the opposite question: what would this field say if those facts did not exist? It supposes each named field’s fact away, lets your standing rules and computed fields run without them, reads the final field, and discards the suppositions.

r = suppose_absent(Order.o1.total, Order.o1.review)

As with suppose, name as many fields as the question needs. The field to read is the last argument, alone — so suppose_absent(a, b, c) supposes away a and b, and reads c.

r = suppose_absent(Order.o1.total, Order.o1.shipment, Order.o1.review)

Read: “if Order.o1.total were not recorded, what would Order.o1.review be?” If a rule sets review from total, r is empty — the conclusion has nothing left to stand on. Afterwards Order.o1.total still holds its real value and review still holds its derived one; nothing was removed.

absence-hypothesis := 'suppose_absent' '(' ( path-expr ',' )+ path-expr ')'
  • Each supposition <field> names one field whose fact is supposed away. As with suppose, a hypothesis takes 1 to 16 of them, applied left to right, and each field is written as suppose’s are.
  • The read <field> names the field to read under that absence — a stored field, or one a rule or computed field derives.
  • The result is an active claim (§7.1) that may be empty, consumed with match exactly like current, whose third state it shares: a read field holding competing values refuses rather than returning one of them (§15):
r = suppose_absent(Order.o1.total, Order.o1.review)
match r:
    case active_claim:
        print("review would still say", r.value)
    case empty:
        print("review would disappear")

This is the one place where the two hypothesis verbs differ in what they hand back, and the difference is the point. suppose asks what would this value be, so a missing answer is an edge case and arrives as a catchable condition. suppose_absent asks what would survive, and “nothing” is the answer it exists to give — so it arrives as an ordinary empty read rather than as an error you must catch to learn what you asked.

Why the verb exists. forget (§7.1) cannot be undone. dependents tells you what points at a fact; it does not tell you what the answers become without it. suppose_absent is the rehearsal: run it over the fields you care about before you remove anything, and see what disappears while it is still there to put back.

Like suppose, it never writes, and it preserves the same termination guarantee: one bounded reasoning pass per supposition, a count fixed where the call is written, and one read.


Appendix A — Full grammar in one block

# §3.4
identifier := XID_Start XID_Continue*     # read in NFKC

# §3.5.1
string-literal := '"' string-char* '"'
                | '"""' triple-char* '"""'   # triple-quoted (may span lines)
                | raw-prefix '"' raw-char* '"'             # raw
                | raw-prefix '"""' raw-triple-char* '"""'  # raw, triple-quoted
raw-prefix     := 'r' | 'R'
raw-char       := <any UTF-8 codepoint except '"', '\' and a line feed>
                | '\' <any UTF-8 codepoint>   # both kept: the backslash escapes nothing
raw-triple-char := <any UTF-8 codepoint except '\' and the closing '"""'>
                | '\' <any UTF-8 codepoint>
triple-char    := <any UTF-8 codepoint except '\' and the closing '"""'>
                | <a line feed, U+000A>      # written as itself, not escaped
                | string-escape              # the escapes of string-char
string-char    := <any UTF-8 codepoint except '"', '\' and a line feed>
                | string-escape
string-escape  := '\\'                   # backslash
                | '\"'                   # double quote
                | '\' <a single quote>   # single quote
                | '\n'                   # line feed        (U+000A)
                | '\t'                   # horizontal tab   (U+0009)
                | '\r'                   # carriage return  (U+000D)
                | '\a'                   # bell             (U+0007)
                | '\b'                   # backspace        (U+0008)
                | '\f'                   # form feed        (U+000C)
                | '\v'                   # vertical tab     (U+000B)
                | '\x' hex hex           # one codepoint <= U+00FF, two hex digits
                | '\u' hex{4}            # one codepoint ≤ U+FFFF, UTF-8 encoded
                | '\U' hex{8}            # one codepoint ≤ U+10FFFF, UTF-8 encoded
                | '\N{' name '}'         # a codepoint by its Unicode name
                | '\' octal octal? octal?   # one codepoint, up to three octal digits
                | '\' <a line feed>      # nothing: the string goes on at the next line
                | '\' <any other character>   # both kept: the backslash escapes nothing
octal          := '0'..'7'
hex            := '0'..'9' | 'a'..'f' | 'A'..'F'
name           := ( 'A'..'Z' | 'a'..'z' | '0'..'9' | ' ' | '-' )+   # a Unicode character name, matched case-insensitively

# §3.5.2
int-literal := dec-int | hex-int | oct-int | bin-int
dec-int     := dec-digit ( '_'? dec-digit )*
hex-int     := '0' ( 'x' | 'X' ) '_'? hex-digit ( '_'? hex-digit )*
oct-int     := '0' ( 'o' | 'O' ) '_'? oct-digit ( '_'? oct-digit )*
bin-int     := '0' ( 'b' | 'B' ) '_'? bin-digit ( '_'? bin-digit )*
dec-digit   := '0'..'9'
oct-digit   := '0'..'7'
bin-digit   := '0' | '1'

# §3.5.3
bool-literal := 'True' | 'False'

# §3.5.5
datetime-literal := 'datetime' '(' '"' rfc3339-utc '"' ')'
rfc3339-utc      := date 'T' time frac? 'Z'
date             := dec-digit dec-digit dec-digit dec-digit '-'
                    dec-digit dec-digit '-' dec-digit dec-digit   # YYYY-MM-DD
time             := dec-digit dec-digit ':' dec-digit dec-digit
                    ':' dec-digit dec-digit                       # HH:MM:SS
frac             := '.' dec-digit{1,9}          # 1..9 fractional-second digits

# §3.5.6
duration-literal := 'duration' '(' '"' iso8601-duration '"' ')'
iso8601-duration := '-'? 'P' days? ( 'T' hours? minutes? seconds? )?  # >= one term
days             := dec-digit+ fraction? 'D'
hours            := dec-digit+ fraction? 'H'
minutes          := dec-digit+ fraction? 'M'
seconds          := dec-digit+ fraction? 'S'
fraction         := '.' dec-digit{1,9}   # lowest-order term only

# §3.5.7
real-literal := int-part '.' frac-part? exponent?  # 3.5, 5. (trailing dot), 1.5e-3
              | '.' frac-part exponent?             # .5 (leading dot), .5e2
              | int-part exponent                   # 1e5 (exponent, no point)
int-part     := dec-digit ('_'? dec-digit)*
frac-part    := dec-digit ('_'? dec-digit)*
exponent     := ('e' | 'E') ('+' | '-')? dec-digit ('_'? dec-digit)*

# §3.5.8
blob-literal := hex-blob | b64-blob
hex-blob     := 'blob' '(' '"' ( hex-digit hex-digit )* '"' ')'
b64-blob     := 'blob_b64' '(' '"' base64-char* '"' ')'
hex-digit    := dec-digit | 'a'..'f' | 'A'..'F'
base64-char  := 'A'..'Z' | 'a'..'z' | dec-digit | '+' | '/' | '='

# §3.5.9
list-literal := '[' ( display-item ( ',' display-item )* ','? )? ']'
display-item := expr | '*' bitor-expr        # an element, or a spread (§8.15)

# §3.5.10
fstring-literal := fstring-prefix '"' ( fstring-char | replacement-field )* '"'
fstring-prefix  := 'f' | 'F'
                 | raw-prefix ( 'f' | 'F' ) | ( 'f' | 'F' ) raw-prefix   # raw, §3.5.1
fstring-char    := string-char (§3.5.1)   #  '{{' and '}}' denote literal '{' and '}'
replacement-field := '{' expr '='? ( '!' ('r' | 's') )? ( ':' format-spec )? '}'
format-spec       := ( fill? align )? sign? 'z'? '#'? '0'? width? grouping? ( '.' precision )? format-type?  # §8.13
fill              := <any one character>
align             := '<' | '>' | '^' | '='
sign              := '+' | '-' | ' '
width             := dec-digit+
grouping          := ',' | '_'
precision         := dec-digit+
format-type       := 's' | 'd' | 'x' | 'X' | 'o' | 'b' | 'c' | 'n'
                   | 'e' | 'E' | 'f' | 'F' | 'g' | 'G' | '%'

# §3.5.11
tuple-literal := '(' ( display-item ( ',' display-item )* ','? )? ')'   # a `*` item alone needs its comma (§8.15)

# §3.5.12
dict-literal := '{' ( dict-item ( ',' dict-item )* ','? )? '}'
dict-item    := expr ':' expr | '**' bitor-expr

# §3.5.13
set-literal := '{' display-item ( ',' display-item )* ','? '}'

# §3.5.14
scientific-literal := 'scientific' '(' '"' scientific-numeral '"' ')'
scientific-numeral := '-'? mantissa ( 'e' | 'E' ) '-'? dec-digit+
mantissa           := dec-digit+ ( '.' dec-digit+ )?    # a nonzero magnitude

# §3.5.15
imag-literal := ( dec-int | real-literal ) ( 'j' | 'J' )

# §3.9.1
declaration   := '#' decl-blank* 'dke:' decl-blank* decl-setting decl-blank+ decl-setting decl-blank*
decl-setting  := decl-key '=' decl-name ('.' decl-version)?
decl-key      := 'edition' | 'language'
decl-name     := ('a'..'z')+
decl-version  := ('1'..'9') ('0'..'9')*
decl-blank    := ' ' | <a tab>

# §4.1
source       := option-verb NEWLINE | module-body
module-body  := docstring? top-item+
docstring    := STRING NEWLINE                           # documents what it opens; nothing runs it
top-item     := decl | class-decl | rule-decl            # decl §4.2, class-decl §13.1, rule-decl §14
              | constraint-decl                          # constraint-decl §14.10
              | import-stmt | stmt                       # import-stmt §4.3, stmt §4.3 (incl. the four writes) — see Modules
option-verb  := '--' IDENT IDENT?

# §4.2
decl    := 'def' IDENT '(' params? ')' ( '->' return-type )? ':' NEWLINE INDENT ( docstring stmt* | stmt+ ) DEDENT
return-type := type | 'value' | 'None'              # `value`: a store fold's result, narrowed by the caller (§4.2); `None`: what no annotation says
params    := param-item ( ',' param-item )*             # which lists are read: §4.2
param-item := param | var-param
           | '/'                                        # what stands before it takes no keyword (§4.2)
           | '*'                                        # what follows it takes no position (§4.2)
param     := IDENT ':' param-type ( '=' const-expr )?   # a default value
var-param := '*' IDENT ':' binding-type                  # variadic: surplus positionals → tuple<type, …>
const-expr := expr                                      # a literal, or an operation over literals
param-type := binding-type
            | module-name '.' IDENT                     # a class an imported module declares (§13.2)
binding-type := type
type    := type-name
         | IDENT                                   # a class name (§13.1), in EVERY type position (§5.5)
         | 'list' '<' type '>' | 'set' '<' type '>'
         | 'dict' '<' type ',' type '>' | 'tuple' '<' type ( ',' type )* '>'
         | 'iterator' '<' type '>'                                          # §8.6
         | 'dict_keys' '<' type '>' | 'dict_values' '<' type '>'           # dict views (§5.1)
         | 'dict_items' '<' type ',' type '>'
         | collection-word '[' type ( ',' type )* ( ',' '...' )? ']'       # any of them in CPython's brackets, the same type (§5.5)
type-name := scalar-name
           | 'active_claim' | 'claim_list' | 'proof_tree' | 'diagnosis'   # read-result types (§5.1)
           | 'subject_set' | 'string_list' | 'verify_result' | 'result_type_set'
           | 'slice'                                                       # a slice value (§5.3)
           | 'range'                                                       # a range of ints (§5.3)
scalar-name := 'string' | 'int' | 'bool'                                 # primitives
             | 'datetime' | 'duration' | 'real' | 'blob' | 'scientific'  # value-class scalars (§5.5)
             | 'complex'
             | 'str' | 'float'                                           # CPython's names for `string` and `real` (§5.5)
collection-word := 'list' | 'set' | 'dict' | 'tuple' | 'iterator'
                 | 'dict_keys' | 'dict_values' | 'dict_items'

# §4.3
stmt         := simple-stmt ( ';' simple-stmt )* ';'? NEWLINE
              | block-stmt

simple-stmt  := assign-stmt
              | print-stmt
              | call-stmt
              | verb-call-stmt
              | commit-stmt
              | rollback-stmt
              | import-stmt
              | return-stmt                                  # §9.2.8
              | raise-stmt
              | assert-stmt
              | rebind-stmt                                  # changes a collection, §8.14
              | del-stmt                                     # §8.14
              | 'pass'                                       # a no-op
              | '...'                                        # a no-op, as `pass` is
              | expr-list                                    # an expression statement: it runs, and nothing keeps its value
              | 'break'                                      # loop control, §9.2.7
              | 'continue'

block-stmt   := match-stmt
              | class-match                                  # dispatch by class, §13.5
              | for-stmt
              | if-stmt
              | try-stmt
              | branch-block
              | world-block                                  # §9.2.9

assign-stmt      := IDENT '=' expr-list
                  | IDENT ':' binding-type                     # an annotation with no value; it binds nothing
                  | IDENT ':' binding-type '=' expr-list       # annotated assignment; the type must agree with the initializer, and only a name takes one (§6.4)
                  | chain-target ( '=' chain-target )+ '=' expr-list  # chained assignment; every target binds one value, left to right (§8.12)
                  | IDENT aug-op expr-list                     # augmented assignment on a bare name; `x op= e` is `x op e`, changed in place on a list or set (a field names a source, so it uses `remember(...)`, not `op=`)
                  | top-target ( ',' top-target )+ '=' expr-list                # unpacking a tuple or list; nested targets + one optional `*rest` (§8.11)
                  | target-group '=' expr-list                                  # a parenthesised or bracketed target list (§8.11)
expr-list        := display-item ( ',' display-item )* ','?     # a bare comma-list; holding a comma, it is a tuple (§3.5.11); a `*` item alone is none (§8.15)
top-target       := unpack-target | '*' unpack-target                           # a target, or THE single starred target (≤ one per list), which may be any target
unpack-target    := IDENT | element-target | target-group      # a name, an element or a slice, or a nested target list, which may hold its own star
target-group     := '(' top-target ( ',' top-target )* ','? ')'   # in parentheses; one target alone in them is that target
                  | '[' top-target ( ',' top-target )* ','? ']'   # in brackets; `[a]` is a list of one target (§8.11)
                  | '(' ')' | '[' ']'                           # a list of no targets; the value must hold nothing (§8.11)
chain-target     := top-target ( ',' top-target )* ','?        # a target or a target list, as an assignment takes one (§8.12)
element-target   := write-target subscript                      # an element or a slice, written as an element write writes it (§8.14)
                  | postfix-expr subscript                      # an element of what an expression answers, which no name holds (§8.14)
aug-op           := '+=' | '-=' | '*=' | '/=' | '%=' | '//=' | '**=' | '|=' | '&=' | '^=' | '@='
rebind-stmt      := write-target '.' rebind-method '(' arg-list? ')'  # changes the collection the target reaches; a statement, never an expression (§8.14)
                  | write-target '.' answer-method '(' arg-list? ')'  # changes the collection and discards what the call answers; the call is an expression too (§8.14)
                  | postfix-expr '.' ( rebind-method | answer-method ) '(' arg-list? ')'  # the same, on what an expression answers, which no name holds (§8.14)
                  | element-target '=' expr-list                # an element or slice write; changes the collection the target reaches (§8.14)
                  | element-target aug-op expr-list             # `c[k] op= e` is `c[k] = c[k] op e`, the target and `k` evaluated once
write-target     := IDENT ( subscript | '.' IDENT '(' arg-list? ')' )*  # a bound name, then any chain of subscripts and method calls; what it reaches is the collection changed (§8.14)
subscript        := '[' expr ( ',' expr )* ','? ']'             # an index or a key; a comma makes it a tuple, `d[1, 2]` is `d[(1, 2)]` (§4.4)
                  | '[' expr? ':' expr? ( ':' expr? )? ']'      # a slice; only a list's is written (§8.14)
rebind-method    := 'append' | 'extend' | 'insert' | 'remove' | 'sort' | 'reverse' | 'clear' | 'add' | 'discard' | 'update'
answer-method    := 'pop' | 'setdefault' | 'popitem'           # changes the collection and answers a value (§8.14)
del-stmt         := 'del' write-target subscript                # removes one element, entry or slice from the collection the target reaches (§8.14)
                  | 'del' postfix-expr subscript                # an element of what an expression answers, which no name holds (§8.14)
                  | 'del' '(' element-target ')'                # in parentheses, that element (§8.14)
print-stmt       := 'print' '(' ( print-arg ( ',' print-arg )* )? ')'
print-arg        := expr                                       # a value to render
                  | '*' expr                                   # each element of a collection, as an argument of its own (§8.15)
                  | ( 'sep' | 'end' | 'flush' ) '=' expr       # a keyword, read after the arguments
                  | 'file' '=' 'None'                          # the default, which is the line itself
                  | '**' dict-literal                          # its keys as keywords, each key a literal (§8.15)
raise-stmt       := 'raise' ( ( EXC-NAME ( '(' ( expr ( ',' expr )* ','? )? ')' )? | IDENT ) ( 'from' expr )? )?        # a catchable refuse; any other value after `raise` is read as a raise and is a parse_error at the value (§4.3)
assert-stmt      := 'assert' expr ( ',' expr )?                # Python assertion (the absence-assertion verb, §7.1, is separate)
call-stmt        := IDENT '(' arg-list? ')'
verb-call-stmt   := IDENT '(' arg-list? ')'                    # a built-in verb call (§7); the head IDENT is a verb head-word
commit-stmt      := 'commit'
rollback-stmt    := 'rollback'
import-stmt      := 'import' import-item ( ',' import-item )*   # load each module, Python Reference §9.3; `math` and `cmath` name the built-in namespaces, §8.16, `vector` one more, §8.17, and `struct` one more, §8.18
                  | 'from' IDENT 'import' import-names         # bind names a module declares
                  | 'from' IDENT 'import' '(' import-names ','? ')'
import-names     := import-name ( ',' import-name )*
import-name      := IDENT ( 'as' IDENT )?                      # bind the name, or bind it as the name after `as`
import-item      := module-name ( 'as' IDENT )?                # bind the module, or bind it as the name after `as`
module-name      := IDENT ( '.' IDENT )*
return-stmt      := 'return' expr-list?                        # early exit / value, §9.2.8

match-stmt   := 'match' expr-list ':' NEWLINE INDENT case-clause+ DEDENT   # two or more items, or one and a comma, are the tuple written without its parentheses (§3.5.11), as CPython reads a subject
case-clause  := 'case' class-label ( 'if' expr )? ':' arm-body            # §6.2
              | 'case' elem-pattern ( 'if' expr )? ':' arm-body           # structural: an alternative is a `pattern`, §6.2
              | 'case' value-pattern ( 'if' expr )? ':' arm-body          # value patterns, §6.2
              | 'case' 'default' ':' arm-body                             # never guarded, §6.2
class-label  := IDENT ( '|' IDENT )*                    # alternatives, §6.2
              | IDENT 'as' IDENT                        # `as` binds ONE class, never across `|`
value-pattern := value-alt ( '|' value-alt )* ( 'as' IDENT )?
value-alt    := case-literal | '_' | IDENT | class-pattern   # IDENT: a capture, a name that is no
                                                       #   class label, and alone; `_` last
class-pattern := IDENT ( '.' IDENT )? '(' ( elem-pattern ( ',' elem-pattern )* ','? )? ')'   # a class tested, §6.2
case-literal := '-'? ( INT | REAL ) | STRING+ | 'True' | 'False'
              | '-'? IMAG | '-'? ( INT | REAL ) ( '+' | '-' ) IMAG      # a complex, §3.5.15
arm-body     := simple-stmt NEWLINE | NEWLINE INDENT stmt+ DEDENT
pattern      := seq-pattern | map-pattern               # a SHAPE label, §6.2
seq-pattern  := '[' ( elem-pattern ( ',' elem-pattern )* ( ',' '*' IDENT )?
                    | '*' IDENT )? ']'                  # a `*` MUST be last
map-pattern  := '{' ( case-literal ':' elem-pattern ( ',' case-literal ':' elem-pattern )* )? '}'
                                                       # no key named twice, §6.2
elem-pattern := elem-alt ( '|' elem-alt )* ( 'as' IDENT )?   # the same names bound by
                                                       #   each; only the last may match every
                                                       #   value; IDENT is not `_`
elem-alt     := IDENT | '_' | case-literal | pattern | class-pattern   # one per §6.2 element row, plus
                                                       #   `'*' IDENT` above, whose position
                                                       #   is constrained, and the `|` and the
                                                       #   `as` above
for-stmt     := 'for' top-target ( ',' top-target )* ','? 'in' expr ':' NEWLINE INDENT stmt+ DEDENT   # a target list unpacks each element — the SAME list an assignment takes (§8.11)
#            (an element target among them is written each iteration; `top-target` is defined with `assign-stmt` above)
if-stmt      := 'if' expr ':' NEWLINE INDENT stmt+ DEDENT
               ( 'elif' expr ':' NEWLINE INDENT stmt+ DEDENT )*
               ( 'else' ':' NEWLINE INDENT stmt+ DEDENT )?
try-stmt     := 'try' ':' NEWLINE INDENT stmt+ DEDENT handler*
handler      := 'except' 'refuse' ( 'as' IDENT )? ':' NEWLINE INDENT stmt+ DEDENT
             | 'except' 'engine_error' ( 'as' IDENT )? ':' NEWLINE INDENT stmt+ DEDENT
branch-block := 'with' 'branch' '(' STRING ')' ':' NEWLINE INDENT stmt+ DEDENT
world-block  := 'with' 'world' '(' expr ')' ':' NEWLINE INDENT stmt+ DEDENT   # `world` is a soft keyword here

arg-list     := arg ( ',' arg )*
arg          := expr                             # positional
              | '*' expr                         # a spread: each element a positional argument of its own (§8.15)
              | IDENT '=' expr                   # a keyword argument

# §4.4
expr         := cond-expr
cond-expr    := or-expr ( 'if' or-expr 'else' expr )?   # ternary; right-assoc; lower than `or`
or-expr      := and-expr ( 'or' and-expr )*
and-expr     := cmp-expr ( 'and' cmp-expr )*
cmp-expr     := bitor-expr ( cmp-op bitor-expr )*
cmp-op       := '==' | '!=' | '<' | '>' | '<=' | '>=' | 'in' | 'not' 'in' | 'is' | 'is' 'not'
bitor-expr   := bitxor-expr ( '|' bitxor-expr )*    # set union, §8.6
bitxor-expr  := bitand-expr ( '^' bitand-expr )*    # set symmetric difference
bitand-expr  := add-expr ( '&' add-expr )*          # set intersection
add-expr     := mul-expr ( ( '+' | '-' ) mul-expr )*
mul-expr     := unary-expr ( ( '*' | '/' | '//' | '%' | '@' ) unary-expr )*
unary-expr   := ( 'not' | '-' | '+' ) unary-expr
              | pow-expr
pow-expr     := postfix-expr ( '**' unary-expr )?   # right-assoc; tighter than unary `-`
postfix-expr := primary-expr postfix*
postfix      := '.' IDENT '(' arg-list? ')'   # method call (§13.4; string methods §5.3)
              | '.' IDENT                      # field access
              | '[' display-item ( ',' display-item )* ','? ']'   # indexing (string / collection, §5.3); a comma or a `*` makes the index a tuple (§8.15)
              | '[' expr? ':' expr? ( ':' expr? )? ']'   # slice (string / list / tuple, §5.3)
primary-expr := STRING | INT | 'True' | 'False'
              | fstring-literal                # formatted string literal (§3.5.10)
              | list-display                   # list literal / comprehension (§3.5.9, §8.9)
              | brace-display                  # dict/set literal or comprehension (§3.5.12, §3.5.13, §8.9)
              | tuple-display                  # tuple literal / grouping (§3.5.11)
              | builtin-call                   # zip / enumerate (§8.6), abs / round (§8.3)
              | namespace-member               # math / cmath (§8.16), vector (§8.17), struct (§8.18)
              | IDENT '(' expr ')'             # instance constructor (§13.2)
              | module-name '.' IDENT '(' expr ')'   # module-qualified construction (Python Reference §9.6)
              | value-call                     # value-script invocation (§6.8)
              | hypothesis                     # suppose (§15)
              | absence-hypothesis             # suppose_absent (§15.1)
              | IDENT
              | path-expr
              | IDENT '(' arg-list? ')'         # a built-in read verb call, §7
value-call   := IDENT '(' arg-list? ')'        # a value script's call, in expression position
list-display := '[' ( display-item ( ',' display-item )* ','? )? ']'          # list literal (§3.5.9); a spread, §8.15
              | '[' expr ( 'for' IDENT 'in' expr ( 'if' expr )* )+ ']'   # list comprehension (§8.9)
brace-display := '{' ( dict-item ( ',' dict-item )* ','? )? '}'      # dict literal (§3.5.12)
              | '{' display-item ( ',' display-item )* ','? '}'              # set literal (§3.5.13)
              | '{' expr ( 'for' IDENT 'in' expr ( 'if' expr )* )+ '}'           # set comprehension (§8.9)
              | '{' expr ':' expr ( 'for' IDENT 'in' expr ( 'if' expr )* )+ '}'  # dict comprehension (§8.9)
tuple-display := '(' expr ')'                  # grouping (no comma)
              | '(' ( display-item ( ',' display-item )* ','? )? ')'   # tuple literal (§3.5.11); a `*` item alone needs its comma
builtin-call := ( 'zip' | 'enumerate' | 'reversed' | 'range' | 'abs' | 'round' | 'divmod' | 'str' | 'int' | 'float' | 'list' | 'set' | 'dict' | 'isinstance' | 'issubclass' ) '(' ( ns-arg ( ',' ns-arg )* )? ')'   # zip/enumerate/reversed/range §8.6, abs/round/divmod §8.3, conversions §8.13, isinstance §13.9, issubclass §13.10; a spread §8.15
namespace-member := ( 'math' | 'cmath' | 'vector' | 'struct' ) '.' IDENT ( '(' ( ns-arg ( ',' ns-arg )* ( ',' IDENT '=' expr )* )? ')' )?   # §8.16–§8.18; a constant takes no call, a function takes one
ns-arg           := expr | '*' expr                           # a value, or a spread of its elements (§8.15)
path-expr    := IDENT ( '.' IDENT )+           # K.s.a slot-grammar dotted path

# §8.9
comp-clause        := 'for' comp-target ( ',' comp-target )* ','? 'in' iter-expr ( 'if' cond-expr )*
comp-target        := name-target | '*' name-target     # THE single starred target (<= one per list), a name or a nesting
name-target        := IDENT | '(' comp-target ( ',' comp-target )* ','? ')'   # a name, or a nesting of names, which may hold its own star; an element target needs a `for` statement (§8.11)
                    | '[' comp-target ( ',' comp-target )* ','? ']'   # a nesting in brackets
                    | '(' ')' | '[' ']'                               # a nesting of no names; each element must hold nothing
iter-expr          := or-expr   # a conditional expression needs parentheses here
list-comprehension := '[' expr comp-clause+ ']'
set-comprehension  := '{' expr comp-clause+ '}'
dict-comprehension := '{' expr ':' expr comp-clause+ '}'
generator-argument := expr comp-clause+           # bare: the only argument of a call that reads it whole
                    | '(' expr comp-clause+ ')'   # parenthesized: an argument of such a call

# §8.19
named-expr := IDENT ':=' expr

# §13.1
class-decl   := 'class' IDENT ( '(' IDENT? ')' )? ':' NEWLINE INDENT ( docstring class-member* | class-member+ ) DEDENT
class-member := field-decl | method-def
              | 'pass' NEWLINE                          # declares nothing
field-decl   := IDENT ':' scalar-name NEWLINE
              | computed-field                          # a field with an initializer (§13.7)

# §13.2
instance-ctor := IDENT '(' expr ')'   # the class name applied to a name expression

# §13.4
method-def   := void-method | value-method
void-method  := 'def' IDENT '(' 'self' ( ',' param-item )* ')' ':' NEWLINE INDENT ( docstring stmt* | stmt+ ) DEDENT
value-method := 'def' IDENT '(' 'self' ( ',' param-item )* ')' '->' return-type ':' NEWLINE INDENT docstring? stmt* 'return' expr NEWLINE DEDENT

# §13.5
class-match := 'match' IDENT ':' NEWLINE INDENT class-case+ default-case? DEDENT
class-case  := 'case' IDENT ':' ( simple-stmt NEWLINE | NEWLINE INDENT stmt+ DEDENT )
default-case := 'case' 'default' ':' ( simple-stmt NEWLINE | NEWLINE INDENT stmt+ DEDENT )

# §13.7
computed-field := IDENT ':' computed-type '=' expr NEWLINE
computed-type  := 'int' | 'real' | 'float' | 'bool' | 'complex'

# §14.1
rule-decl   := '@rule' ( '(' 'kind' '=' rule-kind ')' )? NEWLINE
               'def' IDENT '(' ')' ':' NEWLINE
               INDENT
                 'for' subject-binding ( ',' subject-binding )* ':' NEWLINE
                 INDENT
                   'if' premise ( 'and' premise )* ':' NEWLINE
                     INDENT conclusion NEWLINE DEDENT
                 DEDENT
               DEDENT
subject-binding := IDENT 'in' kind-name
kind-name       := IDENT   # the kind whose fields the name reads
rule-kind       := '"definition"' | '"causal"' | '"empirical"' | '"policy"' | '"heuristic"'

# §14.2
premise    := comparison | equality | join | field-cmp | relation | absence
            | not-asserted | known-absent | nonexistence | source-test | trust-test
            | text-test
comparison := IDENT '.' IDENT ord-op ( '-'? ( INT | REAL ) | SCIENTIFIC
                                     | STRING )
                                                 # o.total > 1000, o.price > 19.99,
                                                 #   o.mass > scientific("1.0e4"), o.name < "m"
equality   := IDENT '.' IDENT eq-op literal      # o.lic == "MIT", o.total != 5
join       := IDENT '.' IDENT eq-op IDENT        # c.caller == f
field-cmp  := IDENT '.' IDENT ( eq-op | ord-op ) IDENT '.' IDENT
                                                 # o.delivered > o.deadline
relation   := IDENT '.' IDENT '.' IDENT eq-op literal
                                                 # Reach.x.y == True
absence    := 'absent' '(' IDENT '.' IDENT ( '.' IDENT )? ')'
                                                 # absent(o.shipment), absent(Gap.x.y)
not-asserted := 'not_asserted' '(' IDENT '.' IDENT ( '.' IDENT )? ')'
                                                 # not_asserted(b.flies)
known-absent := 'known_absent' '(' IDENT '.' IDENT ( '.' IDENT )? ')'
                                                 # known_absent(p.chip)
nonexistence := 'none' '(' IDENT 'in' IDENT ':' premise ( 'and' premise )* ')'
                                                 # none(c in Call: c.caller == f)
source-test := 'source' '(' IDENT '.' IDENT ')' ( eq-op STRING | '==' IDENT )
                                                 # source(r.temp) == "lab",
                                                 #   source(r.temp) == s
trust-test := 'trust' '(' IDENT '.' IDENT ')' eq-op ( '"asserted"' | '"derived"' )
                                                 # trust(s.label) == "derived"
text-test  := text-cell '.' ( 'startswith' | 'endswith' ) '(' text-part ')'
            | text-part 'in' text-cell
                                                 # r.path.startswith("/srv/"),
                                                 #   t.name.endswith(".test"), "admin" in a.role
text-cell  := IDENT '.' IDENT ( '.' IDENT )?
text-part  := STRING | IDENT '.' IDENT
eq-op      := '==' | '!='
ord-op     := '<' | '<=' | '>' | '>='
literal    := STRING | '-'? ( INT | REAL ) | SCIENTIFIC | complex | BLOB | 'True' | 'False'
                                                 # §3.5; the same set `suppose` takes (§15)
complex    := '-'? IMAG | '-'? ( INT | REAL ) ( '+' | '-' ) IMAG
                                                 # 2j, 1+2j, -0.5-1.5j (§3.5.15)

# §14.3
conclusion := cell '=' ( literal | '-' ( INT | REAL ) | fold | copy | text )
                                                 # o.review = True, r.adjustment = -5,
                                                 #   r.rate = 0.75, c.order_count = count(o),
                                                 #   d.owner_name = p.name,
                                                 #   d.line = f"{d.title} cites {d.cites}"
            | 'assert' '(' cell ')'              # assert(p.chip) — known to be absent
cell       := IDENT '.' IDENT                    # a subject and one of its fields
            | IDENT '.' IDENT '.' IDENT          # a kind, a subject, a field
copy       := IDENT '.' IDENT                    # p.name — a field of a bound subject
            | IDENT '.' IDENT '.' IDENT          # Dist.x.y — a relation's value for a pair
text       := <an f-string (§3.5.10), each field a copy>
                                                 # f"{p.name} ({d.room})"
fold       := fold-name '(' IDENT ')'            # count(o) — fold the subjects
            | fold-name '(' IDENT '.' IDENT ')'  # sum(o.amount) — fold one field
fold-name  := <a single-operand fold of §7.1.1>  # count, sum, avg, argmax, …

# §14.10
constraint-decl    := '@constraint' NEWLINE 'def' IDENT '(' ')' ':' NEWLINE INDENT
                        'for' subject-binding ':' NEWLINE INDENT
                          'if' constraint-premise ( 'and' constraint-premise )* ':' NEWLINE INDENT
                            'violated' NEWLINE
                          DEDENT
                        DEDENT
                      DEDENT
constraint-premise := IDENT '.' IDENT ( ord-op | eq-op ) literal          # a comparison against a literal
                    | IDENT '.' IDENT '.' IDENT eq-op literal             # the same, over a relation (§14.2)
                    | 'absent' '(' IDENT '.' IDENT ')'                    # an absence

# §15
hypothesis := 'suppose' '(' ( path-expr ',' literal ',' )+ path-expr ')'

# §15.1
absence-hypothesis := 'suppose_absent' '(' ( path-expr ',' )+ path-expr ')'

Appendix B — Reserved words (complete list)

and        as         assert     async
await      break      class      continue
def        del        elif       else
except     False      finally    for
from       global     if         import
in         is         lambda     None
nonlocal   not        null       or
pass       raise      return     True
try        while      with       yield

The type names of §5.5 (string, int, claim_list, …) are contextual, not reserved; programs SHOULD nonetheless avoid them as user identifiers.

import (Python Reference §9.3) is a reserved word (§3.7), as it is in Python, and begins the import statement at statement head, immediately before a module name.


Appendix C — Sample programs (informative)

These sample programs are self-contained: each takes no arguments and names its own subjects and values inline, so it runs exactly as shown — paste it, run it, read the result. Parameterised scripts (a signature with typed parameters, run with positional arguments over the wire) are specified normatively in MCP Reference §4.2 and the grammar (§4); the samples here stay self-contained so they need no external invocation input.

C.1 Trivial smoke

# smoke-trivial.dpy — a write, an exhaustive match, and a length read.
def smoke_trivial():
    remember(Cat.felix.color, "orange", "op1")
    cur = current(Cat.felix.color)
    match cur:
        case active_claim:
            print("color = " + str(cur.value) + " (per " + str(cur.source) + ")")
        case empty:
            print("no color claim")
    hist = get(Cat.felix.color)
    print("history has " + str(hist.length) + " claim(s)")

Run it as a one-shot with run (MCP Reference §4.2).

C.2 Audit walk

# audit-subject.dpy — read cluster + length composition.
def audit_subject():
    remember(Cat.felix.color, "orange", "audit")
    print("subject Cat.felix")
    attrs = list_attributes(Cat)
    print("attribute count = " + str(attrs.length))
    cur = current(Cat.felix.color)
    print("current color inspected")
    hist = get(Cat.felix.color)
    print("history depth = " + str(hist.length))

C.3 Try with refuse handling under a branch

# experimental write under a branch with explicit rollback.
def try_paint():
    with branch("experiment"):
        try:
            remember(Cat.felix.color, "tigret", "audit")
            after = current(Cat.felix.color)
            if after.value == "tigret":
                commit
            else:
                rollback
        except refuse as r:
            print("paint refused: " + str(r.reason))
            rollback

C.4 Classes and methods (§13)

# classes.dpy — a class with a typed field, a void method, and a value
# method; a def binds an instance and calls both.
class Sensor:
    temp: string
    def record(self, v: string, src: string):
        remember(self.temp, v, src)    # field write
    def label(self) -> string:
        return "sensor"                # value method

def demo():
    s = Sensor("room-a")               # instance binding
    s.record("21", "op1")              # void method call
    name = s.label()                   # value method call
    reading = s.temp                   # field read
    match reading:
        case active_claim:
            print(name + " = " + str(reading.value) + " per " + str(reading.source))
        case empty:
            print(name + ": no reading")

C.5 Datetime round-trip (§3.5.5, §5.6)

# datetime-round-trip.dpy — a datetime value keeps its type across write/read.
def datetime_round_trip():
    remember(Event.launch.at, datetime("2026-01-02T03:04:05Z"), "ops")
    cur = current(Event.launch.at)
    match cur.value:
        case datetime as d:
            print("launch at " + str(d))
        case default:
            print("not a datetime")

C.6 Duration round-trip (§3.5.6, §5.6)

# duration-round-trip.dpy — a duration keeps its type and reads back canonical.
def duration_round_trip():
    remember(Task.build.budget, duration("PT90M"), "ops")
    cur = current(Task.build.budget)
    match cur.value:
        case duration as u:
            print("budget = " + str(u))
        case default:
            print("not a duration")

C.7 Real round-trip (§3.5.7, §5.6)

# real-round-trip.dpy — a real keeps its type and reads back in canonical form.
def real_round_trip():
    remember(Sensor.a.reading, 3.5, "ops")
    cur = current(Sensor.a.reading)
    match cur.value:
        case real as r:
            print("reading = " + str(r))
        case default:
            print("not a real")

C.8 Blob round-trip (§3.5.8, §5.6)

# blob-round-trip.dpy — a blob keeps its type and reads back as canonical hex.
def blob_round_trip():
    remember(Doc.d1.payload, blob("48656C6C6F"), "ops")
    cur = current(Doc.d1.payload)
    match cur.value:
        case blob as b:
            print("payload = " + str(b))
        case default:
            print("not a blob")

C.9 Real arithmetic (§3.5.7, §8.3, §13.3)

# real-arithmetic.dpy — reals compute; arithmetic promotes an int to a real,
# while a declared return and a selection hand back the int they were given, so
# one real field can hold 1 and 1.0; a computed real writes through and reads
# back as a real.
class Meter:
    reading: real

def one() -> real:
    return 1
def real_arithmetic():
    sum = 2.5 + 1.25
    print("sum = " + str(sum))                # 3.75
    print("half = " + str((7.0 / 2)))         # 3.5 — int promotes, true division
    print("scaled = " + str((sum * 2)))       # 7.5 — real * int promotes
    remember(Sensor.a.total, sum, "ops")
    cur = current(Sensor.a.total)
    match cur.value:
        case real as r:
            print("stored = " + str(r))       # 3.75 — still a real
        case default:
            print("not a real")
    if sum > 3.0:
        print("above threshold")
    v = one()
    w = 1 + 0.0
    remember(Meter.k.reading, v, "ops")
    remember(Meter.j.reading, w, "ops")
    t = 1 if True else 2.5
    print(v, w, t, v == w)                    # 1 1.0 1 True

Invoked, it writes the two numbers into the one real field as they were produced:

OK script real_arithmetic(): 3 claims written, 6 prints
  print sum = 3.75
  print half = 3.5
  print scaled = 7.5
  remember Sensor.a.total = 3.75
  print stored = 3.75
  print above threshold
  remember Meter.k.reading = 1
  remember Meter.j.reading = 1.0
  print 1 1.0 1 True

C.10 Base64 blob input (§3.5.8)

# base64-blob.dpy — a blob written with the base64 spelling reads back as the
# same canonical hex as the equivalent blob("…") hex form.
def base64_blob():
    remember(Doc.d1.payload, blob_b64("SGVsbG8="), "ops")
    cur = current(Doc.d1.payload)
    match cur.value:
        case blob as b:
            print("payload = " + str(b))          # 48656C6C6F — canonical hex
        case default:
            print("not a blob")

C.11 As-of read (§7.1)

# as-of-read.dpy — current/get take an optional datetime to read a value as of
# another time. For an always-valid fact that is the present value; with a
# validity window an interior time selects the value valid then.
def as_of_read():
    remember(Rate.usd.pct, "3.0", "src")
    past = current(Rate.usd.pct, datetime("2020-01-01T00:00:00Z"))
    print("rate as of 2020: " + str(past.value))

C.12 Validity windows (§7.1)

# validity-window.dpy — two writes at one cell with non-overlapping validity
# windows (distinct sources) model a value that changed over time; two as-of
# reads at interior times select the two values.
def validity_window():
    remember(Sensor.room1.status, "cold", "reading-2019", datetime("2019-01-01T00:00:00Z"), datetime("2021-01-01T00:00:00Z"))
    remember(Sensor.room1.status, "warm", "reading-2021", datetime("2021-01-01T00:00:00Z"), datetime("2023-01-01T00:00:00Z"))
    past = current(Sensor.room1.status, datetime("2020-06-01T00:00:00Z"))
    print("status as of 2020: " + str(past.value))
    recent = current(Sensor.room1.status, datetime("2022-06-01T00:00:00Z"))
    print("status as of 2022: " + str(recent.value))

C.12.1 Reading back a scheduled commitment (§7.1)

# scheduled-commitment.dpy — a fact whose validity window has not yet opened is
# not part of the present, so the traversal verbs report nothing about it until
# they are given the time to answer as of. The same anchor that reads a cell
# reads the enumerations leading to it, so walking the enumerations reaches the
# fact.
def scheduled_commitment():
    remember(Release.v2.ships, "2027-03-01", "roadmap",
             datetime("2027-01-01T00:00:00Z"), datetime("2027-12-31T00:00:00Z"))
    when = datetime("2027-06-01T00:00:00Z")
    subs = list_subjects(Release, when)
    print("subjects: " + str(subs))
    attrs = list_attributes(Release.v2, when)
    print("attributes: " + str(attrs))
    vals = list_values(Release.ships, when)
    print("values: " + str(vals))

C.13 Aggregate query (§7.1.1)

# aggregate-demo.dpy — read-only summary statistics over a stored attribute.
def aggregate_demo():
    remember(Reading.r1.value, 10, "sensor")
    remember(Reading.r2.value, 20, "sensor")
    remember(Reading.r3.value, 30, "sensor")
    mean = avg(Reading.value)
    print("mean = " + str(mean))
    n = count(Reading.value)
    print("readings = " + str(n))
    # identity / positional folds (§7.1.1): distinct values, which subject holds
    # the max, and the earliest-recorded reading.
    kinds = distinct(Reading.value)
    print("distinct = " + str(kinds))
    hottest = argmax(Reading.value)
    print("hottest subject = " + str(hottest))
    oldest = first(Reading.value)
    print("first recorded = " + str(oldest))
# aggregate-folds-demo.dpy — every fold of §7.1.1 once, over values none of
# them declines: four readings, each with the hour it was taken.
def aggregate_folds_demo():
    remember(Reading.r1.value, 10, "sensor")
    remember(Reading.r2.value, 20, "sensor")
    remember(Reading.r3.value, 30, "sensor")
    remember(Reading.r4.value, 50, "sensor")
    remember(Reading.r1.hour, 1, "sensor")
    remember(Reading.r2.hour, 2, "sensor")
    remember(Reading.r3.hour, 3, "sensor")
    remember(Reading.r4.hour, 5, "sensor")
    a = count(Reading.value)
    b = sum(Reading.value)
    c = product(Reading.value)
    d = avg(Reading.value)
    e = median(Reading.value)
    f = midpoint(Reading.value)
    g = mode(Reading.value)
    h = geomean(Reading.value)
    i = harmmean(Reading.value)
    j = rms(Reading.value)
    print("totals and centre: " + str([a, b, c, d, e, f, g, h, i, j]))
    k = min(Reading.value)
    l = max(Reading.value)
    m = span(Reading.value)
    n = variance(Reading.value)
    o = stdev(Reading.value)
    p = iqr(Reading.value)
    q = mad(Reading.value)
    r = cv(Reading.value)
    print("spread: " + str([k, l, m, n, o, p, q, r]))
    s = skewness(Reading.value)
    t = kurtosis(Reading.value)
    u = entropy(Reading.value)
    v = percentile(Reading.value, 90)
    print("shape and rank: " + str([s, t, u, v]))
    w = distinct(Reading.value)
    x = argmin(Reading.value)
    y = argmax(Reading.value)
    z = first(Reading.value)
    zz = last(Reading.value)
    print("identity: " + str([w, x, y, z, zz]))
    c1 = covariance(Reading.hour, Reading.value)
    c2 = correlation(Reading.hour, Reading.value)
    c3 = slope(Reading.hour, Reading.value)
    c4 = intercept(Reading.hour, Reading.value)
    c5 = rsquared(Reading.hour, Reading.value)
    print("against the hour: " + str([c1, c2, c3, c4, c5]))

C.14 Computed field (§13.7)

# computed-field-demo.dpy — a field the engine derives and maintains.
# Declaring `total` with an initializer installs a derivation: whenever an
# Order's price and qty are known, total is price × qty.
class Order:
    price: int
    qty: int
    total: int = price * qty

def computed_demo():
    remember(Order.o1.price, 10, "sales")
    remember(Order.o1.qty, 5, "sales")
    t = current(Order.o1.total)
    print("total = " + str(t.value))

C.15 Standing rule (§14)

# standing-rule-demo.dpy — a named rule the engine applies automatically.
# `needs_review` flags any order over 1000 with no shipment recorded; the
# engine sets `review` on its own (and would withdraw it if a shipment were
# later recorded). The demo writes one qualifying order and reads the flag.
@rule
def needs_review():
    for o in Order:
        if o.total > 1000 and absent(o.shipment):
            o.review = True

def rule_demo():
    remember(Order.o1.total, 1500, "sales")
    r = current(Order.o1.review)
    print("review = " + str(r.value))

C.16 Hypothesis (§15)

# hypothesis-demo.dpy — read a value under supposed facts, without committing.
# The rule flags an order for review when it is over 1000 AND has shipped. One
# supposition cannot satisfy a rule that needs two facts, so the second call
# supposes both — recording nothing either way.
@rule
def needs_review():
    for o in Order:
        if o.total > 1000 and o.shipment == "sent":
            o.review = True

def hypothesis_demo():
    remember(Order.o1.shipment, "sent", "ops")
    would = suppose(Order.o1.total, 2000, Order.o1.review)
    print("would review = " + str(would))
    both = suppose(Order.o1.total, 2000,
                   Order.o1.shipment, "sent",
                   Order.o1.review)
    print("under both = " + str(both))

C.17 A data module (Python Reference §9.1, §9.2)

# catalog.dpy — a module: a class schema and a data declaration. The body runs
# on import (Python Reference §9.1); it declares no callable script (Python Reference §9.7).
class Product:
    price: int
remember(Product.widget.price, 500, "price-list")

Load it by name (service surface, Python Reference §9.3):

import catalog

C.18 Importing a module and a module-qualified read (Python Reference §9.3, §9.6)

# reporter.dpy — imports the catalog module (Python Reference §9.3) and reads one of its cells
# with the module-qualified construction (Python Reference §9.6).
import catalog
def price_report():
    p = catalog.Product("widget").price
    print("widget price = " + str(p.value))
import reporter

Invoking reporter’s script by its module-qualified address reporter.price_report (Python Reference §9.6) against a store that has imported catalog (§C.17). The response header names the script’s own call frame (price_report()); the module qualifier is the address, not part of the frame:

OK script price_report(): 1 print
  print widget price = 500

C.19 String slicing, search, and replace (§5.2, §5.3, §8.6)

# string-ops.dpy — char access, slicing, search, tests, case, and trimming.
def string_ops():
    email = "  Merlin@Langsyn.org  "
    trimmed = email.strip()
    at = trimmed.find("@")
    print("local = " + str(trimmed[:at]).lower())
    print("domain = " + str(trimmed[at + 1:]))
    print("first char = " + str(trimmed[0]))
    print("length = " + str(trimmed.length))
    print("upper = " + str(trimmed.upper()))
    print("masked = " + str(trimmed.replace("@", " at ")))
    print("is org = " + str(trimmed.endswith(".org")))
    print("starts M = " + str(trimmed.startswith("Merlin")))
    print("dots = " + str(trimmed.count(".")))
    print("has at = " + str(("@" in trimmed)))
    print("no spam = " + str(("spam" not in trimmed)))
    print("tidy = [" + str("**Merlin**".strip("*")) + "]")
    print("reversed = " + str(trimmed[::-1]))
    print("every 2nd = " + str(trimmed[::2]))
    print("last = " + str(trimmed[-1]))
    print("tld = " + str(trimmed[-3:]))
    print("digits only = " + str("12345".isdigit()))
    print("all caps = " + str(trimmed.isupper()))
    print("at index = " + str(trimmed.index("@")))

Run it as a one-shot with run (MCP Reference §4.2).


C.20 Constructed lists, comprehensions, and split/join (§3.5.9, §5.1, §5.3, §8.6, §8.9)

# collections.dpy — list literals, indexing/slicing, membership,
# comprehensions, and the split/join string bridge.
def collections():
    nums = [3, 1, 4, 1, 5]
    print("nums = " + str(nums))
    print("count = " + str(nums.length))
    print("last = " + str(nums[-1]))
    head = nums[:2]
    print("head = " + str(head))
    print("has 4 = " + str((4 in nums)))
    evens = [n for n in nums if n % 2 == 0]
    print("evens = " + str(evens))
    words = "merlin,are,langsyn".split(",")
    print("words = " + str(words))
    caps = [w.upper() for w in words]
    print("joined = " + str(" ".join(caps)))
    print("same = " + str(([1, 2] == [1, 2])))

Running it as a one-shot with run (MCP Reference §4.2) produces:

OK script collections(): 9 prints
  print nums = [3, 1, 4, 1, 5]
  print count = 5
  print last = 5
  print head = [3, 1]
  print has 4 = True
  print evens = [4]
  print words = ['merlin', 'are', 'langsyn']
  print joined = MERLIN ARE LANGSYN
  print same = True

C.21 Tuples, dicts, and sets (§3.5.11, §3.5.12, §3.5.13, §5.1, §5.3, §8.1, §8.6)

# collections2.dpy — tuple, dict, and set literals with their reads.
def collections2():
    point = (3, 4)
    print("point = " + str(point))
    print("x = " + str(point[0]))
    ages = {"ana": 30, "bo": 25}
    print("ages = " + str(ages))
    print("ana = " + str(ages["ana"]))
    print("has bo = " + str(("bo" in ages)))
    print("same = " + str((ages == {"bo": 25, "ana": 30})))
    print("names = " + str(ages.keys()))
    tags = {"red", "green", "red", "blue"}
    print("tags = " + str(tags))
    print("size = " + str(tags.length))
    warm = {"red", "orange"}
    print("shared = " + str(tags.intersection(warm)))

Running it as a one-shot with run (MCP Reference §4.2) produces:

OK script collections2(): 10 prints
  print point = (3, 4)
  print x = 3
  print ages = {'ana': 30, 'bo': 25}
  print ana = 30
  print has bo = True
  print same = True
  print names = dict_keys(['ana', 'bo'])
  print tags = {'blue', 'green', 'red'}
  print size = 3
  print shared = {'red'}

C.22 Dict and set comprehensions (§8.9)

# comprehensions2.dpy — build a dict and a set by bounded iteration.
def comprehensions2():
    nums = [1, 2, 3, 4]
    squares = {n: n * n for n in nums}
    print("squares = " + str(squares))
    parity = {n % 2 for n in nums}
    print("parity = " + str(parity))

Running it as a one-shot with run (MCP Reference §4.2) produces:

OK script comprehensions2(): 2 prints
  print squares = {1: 1, 2: 4, 3: 9, 4: 16}
  print parity = {0, 1}

C.23 zip, enumerate, and partition (§8.6)

# zipping.dpy — pair collections into tuples, read the pairs once, and split a
# string once.
def zipping():
    names = ["ana", "bo"]
    scores = [90, 85]
    paired = zip(names, scores)
    print("paired = " + str(paired))
    print("read = " + str(list(paired)))
    print("again = " + str(list(paired)))
    ranked = list(enumerate(names))
    print("ranked = " + str(ranked))
    parts = "user@example.com".partition("@")
    print("parts = " + str(parts))
    print("domain = " + str(parts[2]))

Running it as a one-shot with run (MCP Reference §4.2) produces:

OK script zipping(): 6 prints
  print paired = <zip object>
  print read = [('ana', 90), ('bo', 85)]
  print again = []
  print ranked = [(0, 'ana'), (1, 'bo')]
  print parts = ('user', '@', 'example.com')
  print domain = example.com

C.24 Value scripts and value-calls (§4.2, §4.4, §9.2.8)

# value-scripts.dpy — value-returning scripts compose by expression: one is
# nested inside another's arguments, and a branch-covered value script returns
# a string on every path.
def doubled(n: int) -> int:
    return n * 2
def summed(a: int, b: int) -> int:
    return a + b
def classify(n: int) -> string:
    if n > 100:
        return "big"
    else:
        return "small"
def value_scripts():
    total = summed(doubled(10), doubled(11))
    print("total = " + str(total))
    print("nested = " + str(doubled(doubled(5))))
    print("size = " + str(classify(total)))

Running it as a one-shot with run (MCP Reference §4.2) produces:

OK script value_scripts(): 3 prints
  print total = 42
  print nested = 20
  print size = small

C.25 Inheritance, override, super, dispatch, and isinstance (§13.8, §13.9)

# inheritance.dpy — a subclass extends a base, overrides a method and calls
# super, dispatches polymorphically through a base-typed parameter, and tests an
# instance's class with isinstance and a class match.
class Animal:
    sound: string
    def speak(self):
        remember(self.sound, "generic", "op1")
    def describe(self) -> string:
        return "an-animal"

class Dog(Animal):
    breed: string
    def speak(self):
        super().speak()
        remember(self.breed, "collie", "op1")
    def describe(self) -> string:
        return "a-dog"

class Cat(Animal):
    def describe(self) -> string:
        return "a-cat"

def label(a: Animal):
    print("desc=" + str(a.describe()))

def inheritance():
    animals = [Dog("d1"), Cat("c1")]
    for a in animals:
        label(a)
    d = Dog("d2")
    d.speak()
    br = d.breed
    match br:
        case active_claim: print("breed=" + str(br.value))
        case empty: print("breed=?")
    print("is-animal=" + str(isinstance(d, Animal)))
    c = Cat("c2")
    match c:
        case Dog: print("class=dog")
        case Cat: print("class=cat")
        case default: print("class=other")

Running it as a one-shot with run (MCP Reference §4.2) produces:

OK script inheritance(): 2 claims written, 5 prints
  print desc=a-dog
  print desc=a-cat
  remember Dog.d2.sound = "generic"
  remember Dog.d2.breed = "collie"
  print breed=collie
  print is-animal=True
  print class=cat

C.26 Temporal canonical form, equality, and ordering (§3.5.5, §3.5.6, §8.1)

# temporal.dpy — datetimes and durations read back canonical, compare by value
# rather than by spelling, and order (chronologically for instants, by length for
# spans). `PT90M` and `PT1H30M` denote one span, so neither is shorter.
def temporal():
    start = datetime("2026-01-02T03:04:05Z")
    end = datetime("2026-01-02T03:04:05.500Z")
    short = duration("PT90M")
    same = duration("PT1H30M")
    longer = duration("PT2H")
    print("end-canonical=" + str(end))
    print("span-canonical=" + str(short))
    print("earlier=" + str(start < end))
    print("same-span=" + str(short == same))
    print("neither-shorter=" + str(short < same))
    print("shorter-than=" + str(short < longer))
    print("ordered=" + str(sorted([longer, short])))

Running it as a one-shot with run (MCP Reference §4.2) produces:

OK script temporal(): 7 prints
  print end-canonical=2026-01-02T03:04:05.5Z
  print span-canonical=PT1H30M
  print earlier=True
  print same-span=True
  print neither-shorter=False
  print shorter-than=True
  print ordered=[PT1H30M, PT2H]

C.27 Temporal arithmetic (§3.5.5, §3.5.6, §8.3)

# temporal-math.dpy — shift an instant by a length, measure the length between
# two instants, and scale a length. Subtracting forwards from backwards gives a
# negative span, and a shift respects the calendar.
def temporal_math():
    start = datetime("2026-01-02T09:00:00Z")
    shift = duration("PT7H30M")
    finish = start + shift
    print("finish=" + str(finish))
    print("elapsed=" + str(finish - start))
    print("double=" + str(shift * 2))
    print("back=" + str(start - finish))
    print("leap=" + str(datetime("2024-02-28T12:00:00Z") + duration("P1D")))

Running it as a one-shot with run (MCP Reference §4.2) produces:

OK script temporal_math(): 5 prints
  print finish=2026-01-02T16:30:00Z
  print elapsed=PT7H30M
  print double=PT15H
  print back=-PT7H30M
  print leap=2024-02-29T12:00:00Z

C.28 Quotient and remainder, and reading backwards (§8.3, §8.6)

# quotient.dpy — split a division into its quotient and remainder in one call,
# and walk a collection backwards. Both halves of divmod floor together, so a
# negative dividend keeps the remainder positive. `reversed` returns an iterator,
# which `list` and `join` each read to the end.
def quotient():
    print("even = " + str(divmod(9, 3)))
    print("odd = " + str(divmod(7, 2)))
    print("negative = " + str(divmod(-7, 2)))
    minutes = 135
    hm = divmod(minutes, 60)
    print("duration = " + str(hm[0]) + "h" + str(hm[1]) + "m")
    print("countdown = " + str(list(reversed([1, 2, 3]))))
    print("backwards = " + "".join(reversed("stressed")))

Running it as a one-shot with run (MCP Reference §4.2) produces:

OK script quotient(): 6 prints
  print even = (3, 0)
  print odd = (3, 1)
  print negative = (-4, 1)
  print duration = 2h15m
  print countdown = [3, 2, 1]
  print backwards = desserts

C.29 Type conversions and the spelling of a rendered bool (§8.13)

# conversions.dpy — every conversion §8.13 defines, in one script. A bool
# renders `True`/`False`, the Python spelling, whether it was written as a
# literal or read back from the store: `stored` renders a bool read from the
# store, and `verified` renders a whole result type plus one of its declared
# `string` fields.
def conversions():
    print("bool = " + str(True) + " " + str(False))
    print("int = " + str(int(3.9)) + " " + str(int(-3.9)) + " " + str(int(True)) + " " + str(int("  -7 ")))
    print("float = " + str(float(1)) + " " + str(float("2.5")) + " " + str(float(True)))
    print("str = " + str(42) + " " + str(2.5))
    print("roundtrip = " + str(bool(str(True) == "True")))
    remember(Flag.f1.on, True, "ops")
    stored = get(Flag.f1.on)
    print("stored = " + str(stored))
    checked = verify(Flag.f1.on, True)
    print("verified = " + str(checked) + " " + checked.actual)

Running it as a one-shot with run (MCP Reference §4.2) produces:

OK script conversions(): 1 claim written, 7 prints
  print bool = True False
  print int = 3 -3 1 -7
  print float = 1.0 2.5 1.0
  print str = 42 2.5
  print roundtrip = True
  remember Flag.f1.on = True
  print stored = 1 entry [True]
  print verified = verify_result match=True True

C.30 A class whose reasoning is counted two ways (§13.7, §14.7, §14.9)

# mixed-reasoning.dpy — one class carrying BOTH kinds of standing rule: a
# computed field the engine derives, and an @rule you named. Compiled plainly
# rather than as a module, so the names below are bare: `list(target = rules)` (§14.7)
# names only `bigbox`, while `checkup` (§14.9) reports `rules=2`. Both are
# right — a computed field is a standing rule with no name you gave it.
class Box:
    w: int
    h: int
    area: int = w * h
    big: bool

@rule
def bigbox():
    for b in Box:
        if b.area > 100:
            b.big = True

def mixed_demo():
    remember(Box.b1.w, 20, "yard")
    remember(Box.b1.h, 30, "yard")
    a = current(Box.b1.area)
    print("area = " + str(a.value))
    f = current(Box.b1.big)
    print("big = " + str(f.value))

Running mixed_demo as a one-shot with run (MCP Reference §4.2) produces:

OK script mixed_demo(): 2 claims written, 2 prints
  remember Box.b1.w = 20
  remember Box.b1.h = 30
  print area = 600
  print big = True

Both values read derived: area from the computed field, big from the @rule. Against that store list(target = rules) answers bigbox and checkup answers counts rules=2 unfirable=0 unreadable=0 declined=0 kinds=1 unread=0 constraints=0 unevaluable=0 violated=0.

C.31 Turning a point with math and cmath (§8.16)

# rotation.dpy — turn the point 3 + 4j a quarter turn, then move between its
# two forms. Multiplying by cmath.exp(1j * theta) turns a complex number by the
# angle theta; polar gives a modulus and an angle, and rect gives them back.
# Each step is rounded, so the quarter turn is not exact in its last digit.
import math
import cmath

def rotation():
    p = 3 + 4j
    q = p * cmath.exp(1j * math.pi / 2)
    print("rotated = " + str(q))
    r, phi = cmath.polar(p)
    print("modulus = " + str(r))
    print("angle = " + str(phi) + " = " + str(math.atan2(4, 3)))
    print("back = " + str(cmath.rect(r, phi)))
    print("close = " + str(math.isclose(q.real, -4.0)))
    print("root = " + str(cmath.sqrt(-4)))

Running it as a one-shot with run (MCP Reference §4.2) produces:

OK script rotation(): 6 prints
  print rotated = (-4+3.0000000000000004j)
  print modulus = 5.0
  print angle = 0.9272952180016122 = 0.9272952180016122
  print back = (3+4j)
  print close = True
  print root = 2j

C.32 Transforming a list with vector (§8.17)

# transform.dpy — four readings, where bits 0 and 1 of an element's index are
# its two axes. [[0.75, 0.25], [0.25, 0.75]] blends each pair, a quarter of
# each into the other, applied along axis 0 and then axis 1. The 4 × 4 matrix
# keeps a running total: along [1, 0] it runs in the list's own order, and
# along [0, 1] it takes the middle two the other way round, because the first
# axis named is the high bit of a row. The last line adds the first half of a
# list of ints into the second, and ints stay ints.
import vector

def transform():
    blend = [[0.75, 0.25], [0.25, 0.75]]
    xs = vector.apply([4.0, 8.0, 0.0, 4.0], blend, [0])
    print("blended = " + str(vector.apply(xs, blend, [1])))
    upto = [[1, 0, 0, 0], [1, 1, 0, 0], [1, 1, 1, 0], [1, 1, 1, 1]]
    print("totals = " + str(vector.apply([10, 20, 30, 40], upto, [1, 0])))
    print("other order = " + str(vector.apply([10, 20, 30, 40], upto, [0, 1])))
    print("added = " + str(vector.apply([1, 2, 3, 4], [[1, 0], [1, 1]], [1])))

Running it as a one-shot with run (MCP Reference §4.2) produces:

OK script transform(): 4 prints
  print blended = [4.0, 6.0, 2.0, 4.0]
  print totals = [10, 30, 60, 100]
  print other order = [10, 60, 40, 100]
  print added = [1, 2, 4, 6]

C.33 Numbers held as one blob with struct (§8.18)

# packing.dpy — numbers packed into one blob by a format, and read back.
# `<` lays the values out little-endian with no padding; `@` aligns each to its
# size, so the same three codes take more bytes. A list of complexes packs to
# sixteen bytes each, and unpacks to the same numbers, signed zeros included.
import struct

def packing():
    b = struct.pack("<hq?", -2, 2 ** 40, True)
    print("bytes = " + str(b))
    print("sizes = " + str(struct.calcsize("<hq?")) + " and " + str(struct.calcsize("@hq?")))
    n, big, flag = struct.unpack("<hq?", b)
    print("back = " + str(n) + " " + str(big) + " " + str(flag))
    zs = [0.5 + 1j, complex(-0.0, -2.5)]
    kept = struct.pack(f"<{len(zs)}D", *zs)
    print("kept in " + str(len(kept)) + " bytes")
    print("back = " + str(list(struct.unpack(f"<{len(kept) // 16}D", kept))))

Running it as a one-shot with run (MCP Reference §4.2) produces:

OK script packing(): 5 prints
  print bytes = FEFF000000000001000001
  print sizes = 11 and 17
  print back = -2 1099511627776 True
  print kept in 32 bytes
  print back = [(0.5+1j), (-0-2.5j)]

Appendix D — Glossary

  • DKE — the service that compiles and executes DKE Python. The target of DKE Python programs in production deployments at dke.langsyn.net.
  • Canonical verb surface — the wire-public verb vocabulary published with DKE; the in-language verbs in §7.1 are drawn from it.
  • Class — a declared grouping of typed fields and the methods that operate on them (§13.1).
  • Instance — a typed handle to one named member of a class, written Class(subject) (§13.2).
  • Method — a def in a class body with an explicit self receiver; void or value-returning, resolved from the receiver’s class, and from the instance’s actual class when it is held at a base-class type (§13.4, §13.8).
  • Class match — a match whose scrutinee is a name the compiler follows to an instance and whose cases name classes; written with no (), closed and exhaustive (§13.5).
  • Module — a .dpy source file, whose body of declarations and statements runs once when the module is compiled or imported (Python Reference §9.1); a module is a namespace for the names it introduces (Python Reference §9.6).
  • Compile — the service-surface operation (MCP Reference §4.1) that submits a module: it stores the module under a name and runs its body, so its scripts become callable by name.
  • Import — the in-language operation that loads an already-stored module by name, running its body and making its names available under its namespace (Python Reference §9.3).
  • Run — the service-surface operation (MCP Reference §4.2) that executes a program and returns a transcript, either an inline source one-shot (not stored) or an already-stored script by name.
  • Slot grammar — the dotted-path notation K.s.a for addressing a store cell by kind, subject, and attribute.
  • Discriminator type — a type whose values carry a .kind string naming which case a match would dispatch to.
  • Finite collection — a type whose values have a determinable .length and whose for … in iteration runs that many times.
  • Refuse — a recoverable runtime outcome of a verb call, recoverable in source via except refuse.
  • Engine error — a runtime outcome from the target service layer, recoverable in source via except engine_error.
  • Bounded termination — the termination guarantee every DKE Python program carries (§9.5): bounded iteration over finite sets with no recursion (also called bounded-terminating).

Appendix E — Revision history

  • 18.3.8 · rev 171 (2026-11-15) — The Version line states API 1.5: a module may state its version on a top-level __version__ line, which a compile given no version records and info_module reports (Python Reference §9.1), and an addition moves the API’s MINOR.

  • 18.3.7 · rev 170 (2026-11-15) — §8.6: sum, min and max read a range from its bounds and a generator of ints one element at a time, building nothing, so sum(range(10 ** 6)) is 499999500000 under any limit a service sets on what a run builds; a generator’s elements from its first that is not an int are held, so a sum holding a real is still rounded once; and any and all hold none of a generator and stop at the deciding element. What 2026-11-14’s changes did, written down. Text only, so the API stays 1.4.

  • 18.3.6 · rev 169 (2026-11-15) — §7.1.1: every fold’s formula is written, with where it yields no value. Among what was unwritten: midpoint is the mean of the least and the greatest value alone, harmmean and geomean count every value and have no value over a zero or a negative one, cv keeps the sign of the mean and has none where it is zero, mad has no scale factor, percentile interpolates at rank (n − 1) · p / 100, slope(K.x, K.y) is the least-squares slope of the second attribute against the first, and entropy is the int 0 over equal values. A fold with no value refuses with aggregate_empty, which the section now names. Appendix C.13 gains a unit calling every fold. Text only: no answer changes, so the API stays 1.4.

  • 18.3.5 · rev 168 (2026-11-15) — §8.16: a math function given an int past 2^53 takes it as the int it is, and answers the function’s exact value at it rounded once, where the int was converted to a real first, as Python converts it: math.sin(9007199254740993) is -0.9034039880133538, where it answered -0.848925964814655, and math.isclose(9007199254740993, 9007199254740992, rel_tol=0.0) is False, where it answered True. nextafter steps from the int itself and ulp is the gap where it lies; frexp keeps the int rounded to a real. §8.3’s exception for the math functions is withdrawn. A correction, so the API stays 1.4.

  • 18.3.4 · rev 167 (2026-11-15) — §8.3: ** with an int past 2^53 as its base or its exponent, beside a real or as an int to a negative int literal power, is the exact power rounded once, where the int was converted to a real first, as Python converts it: (-1.0000000000000002) ** 9007199254740993 is -7.38905609893065, where the odd exponent rounded to an even real and the answer was 7.389056098930649. 18.2.3’s exception for ** is withdrawn; the math functions keep theirs. A correction, so the API stays 1.4.

  • 18.3.3 · rev 166 (2026-11-15) — §8.17: an element of vector.apply that is not an int and whose operands are all finite is, in each part, the exact sum of the exact products rounded once, as the section’s function now states, where it was the products added left to right, rounded at each step: vector.apply([1.0, 1.0, 1.0, 1.0], [[1e16, 1.0, -1e16, 1.0], [0.1, 0.2, 0.3, 0.4], [1.0, 1e16, 1.0, -1e16], [0.0, 0.0, 0.0, 1.0]], [0, 1]) is [2.0, 2.0, 1.0, 1.0], where it answered [1.0, 0.0, 1.0, 1.0]. An exactly zero part keeps the sign the left to right sum gives where that is a zero. A correction, so the API stays 1.4.

  • 18.3.2 · rev 165 (2026-11-15) — §8.17: each sum vector.sumsq answers is the exact sum of the exact squares of the parts, rounded once, as the section’s function now states, where it was math.fsum over the squares, each already rounded: vector.sumsq([-1.561131722497604, -2.099686097555672], []) is [6.845813963256903], where it answered [6.845813963256904]. A correction, so the API stays 1.4.

  • 18.3.1 · rev 164 (2026-11-15) — §8.3: a complex product, quotient and power of operands whose parts are all finite, a power’s base not zero, answer each part as its exact value rounded once to the nearest real, inf or -inf only past the range, and a sum holding a complex is, in each part, that part’s exact sum rounded once, as the section now states. Python’s algorithms round at every step: (0.1+0.2j) * (3-4j) is (1.1+0.2j), where it answered (1.1+0.20000000000000007j); (1 + 1j) ** 101 is (-1125899906842624-1125899906842624j), where it answered (-1125899906842634.5-1125899906842629.5j); sum([0.1j] * 10) is 1j, where it answered 0.9999999999999999j. A part whose exact value is zero takes the sign Python gives it where Python answers a zero there, and +0 otherwise, so (-8) ** 0.5 is 2.8284271247461903j, its real part exactly zero, where it was (1.7319121124709868e-16+2.8284271247461903j). complex(-1, 0) ** 1e308 is (1+0j), its exact value, where it answered C99 Annex G’s (nan+nanj) for a phase past the range. A correction, so the API stays 1.4.

  • 18.3.0 · rev 163 (2026-11-14) — §4.2, §5.1, §5.2, §8.6: range is a type of its own, the ints from start up to stop, step apart, held as its three bounds and never built. A for and a comprehension walk it, so a count to a million runs within a run’s budget where it was refused; building its ints, as list(range(n)) does, is charged as before. Where a program holds or prints one it answers as CPython’s does: len, an index, a slice (a range), ==, in, .index and .count, hashing, .start / .stop / .step, and its repr, range(0, 3), where it printed [0, 1, 2]. Adding two ranges, ordering them, repeating a sequence by one, and a list’s methods on one are refused in CPython’s words, where a range was read as a list. 'range' is a type name a parameter and a binding may declare. An addition, so the API is 1.4; the answers that change were wrong.

  • 18.2.11 · rev 162 (2026-11-14) — §3.4: a store holds one kind of any names that differ only in the case of a letter A to Z, and a write that would make another is refused naming the kind held, with nothing written; letters beyond A to Z are not compared this way. Such a write was accepted and its value read back under the kind already held, while the name written read as empty: with Cat held, remember(cat.s.a, 2, "src") was read back by current(Cat.s.a). A correction, so the API stays 1.3.

  • 18.2.10 · rev 161 (2026-11-14) — §7.1.1: a fold that returns a duration or a datetime yields its exact value rounded once to a nanosecond, half away from zero, which the section now states. Past about 104 days, or on a half a real cannot hold, avg, median, midpoint, percentile, iqr, mad, stdev, rms, geomean, harmmean, a temporal slope and intercept, and avg, percentile, iqr, mad and stdev over datetimes, rounded to a real first and could answer a nanosecond or two off: over PT0S and PT15638400.000000001S, avg yields P90DT12H0.000000001S where it yielded P90DT12H. A correction, so the API stays 1.3.

  • 18.2.9 · rev 160 (2026-11-14) — §7.1.1: median over an even count of datetimes, and midpoint over datetimes, halve the gap between two instants exactly, so they are their exact values rounded once to a nanosecond, as the section states. Past about 104 days they halved the gap as a real and could answer a nanosecond early: over 2026-01-01T00:00:00Z and 2026-07-01T00:00:00.000000001Z each now yields 2026-04-01T12:00:00.000000001Z. A correction, so the API stays 1.3.

  • 18.2.8 · rev 159 (2026-11-14) — §7.1.1: entropy is its exact value rounded once. It was computed as ln N − Σ c · ln c / N, rounding each logarithm, product, sum and quotient, so over a group whose values repeat it was often a unit or more off in the last place: over six values held once, once, twice, five, three and five times it now yields 1.6110656763229676, where it yielded 1.6110656763229678, as Python’s own formula over a list does. §8.3 no longer counts entropy among the answers computed through correctly rounded steps. A correction, so the API stays 1.3.

  • 18.2.7 · rev 158 (2026-11-14) — §7.1.1: percentile, iqr and mad are their exact values rounded once, and so are median, midpoint and span, every int entering as itself. percentile rounded its rank and then each step of the interpolation, iqr subtracted two quantiles so rounded, and mad took each deviation from a rounded centre, so each could be units off in the last place and an iqr near 1 over a column near 10⁸ from its eighth digit; an int past 2^53 was rounded before any of the six saw it. Beside an infinity, a percentile’s interpolation is that infinity, where it was NaN. A correction, so the API stays 1.3.

  • 18.2.6 · rev 157 (2026-11-14) — §7.1.1: variance, stdev, cv, skewness, kurtosis and the two-attribute folds are their exact values rounded once, every int entering as itself. They centred on the exactly rounded mean and summed in an order of their own, so each could be units off in the last place, and over equal values a spread that is exactly 0 could be a small number instead; variance and stdev now answer what statistics.pvariance and statistics.pstdev give. entropy alone keeps an order of its own. The worked example of a variance over durations of one and two minutes said 3600 seconds squared, where the fold answers 900, its value; the example now says 900. A correction, so the API stays 1.3.

  • 18.2.5 · rev 156 (2026-11-13) — §7.1.1: product, geomean and harmmean are their exact values rounded once, in no order, every int entering as itself. product multiplied in an order of its own, harmmean rounded every reciprocal and every addition, and geomean was computed as statistics.geometric_mean computes it, through rounded logarithms; each could be a unit off in the last place, geomean more. Over ten values of 0.1, geomean is now 0.1. A product over durations multiplies the spans in seconds exactly. §8.3: geomean no longer computes through logarithms. A correction, so the API stays 1.3.

  • 18.2.4 · rev 155 (2026-11-13) — §7.1.1: avg is the exact mean rounded once, and rms the exact root mean square rounded once with every square exact, where each was computed as statistics.fmean and math.sqrt(statistics.fmean([x * x for x in xs])) compute them and could be a unit off in the last place; an rms over values whose squares fall below the least real is no longer 0.0. An int enters a fold’s sum as itself, so sum, avg and rms over a column holding an int past 2^53 are exact too. The other folds that centre on the mean are unchanged. A correction, so the API stays 1.3.

  • 18.2.3 · rev 154 (2026-11-13) — §8.3: +, -, *, /, //, % and divmod with an int and a real are the exact value of the operation rounded once. An int past 2^53 was converted to a real first, as Python converts it, and the result rounded again: up to two units in the last place for + and //, and a remainder that lost every digit of the true one for %. 18.2.1’s exact floor held for two reals; for an int beside a real it holds from this revision. ** and the math functions given such an int still convert it first. A correction, so the API stays 1.3.

  • 18.2.2 · rev 153 (2026-11-13) — §8.3: / over two ints is their exact quotient rounded once, as Python’s is; a pair past 2^53 that did not divide evenly was divided as two reals each already rounded, and was off in the last place. A correction, so the API stays 1.3.

  • 18.2.1 · rev 152 (2026-11-13) — §8.3: a // with a real operand, and the quotient of divmod, are the floor of the exact quotient rounded once. A floor past the range of int is a real, where // refused it, and a zero dividend keeps the quotient’s sign, where 0.0 // -3.0 was 0.0; the infinite and NaN cases of §8.3’s table are unchanged. A correction, so the API stays 1.3.

  • 18.2.0 · rev 151 (2026-11-13) — §14.2: a premise may test part of a text, in the spellings a program uses for the same question: r.path.startswith("/srv/") holds when the field’s text begins with the part, t.name.endswith(".test") when it ends with it, and "admin" in a.role when it holds it anywhere. The part is a quoted string or a field of a subject the rule binds, on either side of in. The test is code point by code point, with no case folding; the empty string is a part of every text; only a text holds a part, so a number or a datetime holds none whatever its written form contains. A tuple of parts, a window after the part and a negated test (not in, or not before a method) are refused. An addition, so the API is 1.3, and the Version line, §1.3 and §11.1 state it.

  • 18.1.1 · rev 150 (2026-11-13) — The API is 1.2: calls made with keys that may only read run on one store together, where every call on a store ran alone (MCP §9.8), and an addition moves the API’s MINOR. The Version line, §1.3 and §11.1 state it. The language did not change.

  • 18.1.0 · rev 149 (2026-11-13) — §14.3: a conclusion may write a text from values the rule read, a fourth thing its right side may be: an f-string whose fields are each what a copy’s right side is, d.line = f"{d.title} cites Version {d.cites}.". The text is the one a program’s f-string prints over the same values and is a string whatever it reads as. Each field is read as a copy reads its one: its presence is a premise, its subject is ranged, a field holding several values gives one text for each, and the text is valid while every value it was written from is. A field holds a string or an int; where it holds another type no text is drawn for that subject, and why_not names the field and the value. A conversion, a format, a debug field, a subject alone and any other expression in a field are refused when the rule is defined, and a text longer than 65,536 bytes is not drawn. §14.5 gains its fourth reason a rule’s outcome could fail to settle, a text written from itself, and §14.6 counts it: a rule that closes a cycle through a written text is refused when it is defined. An f-string in a conclusion was refused until this revision. The API is 1.1, the addition moving MINOR, and the Version line, §1.3 and §11.1 state it.

  • 18.0.0 · rev 148 (2026-11-12) — The Specification states a version of its own again, first on the Version line and followed by the revision it last changed in, and §1.3 and §11.1 say how it moves: one step of the API’s rule applied to what the document specifies, a change of wording moving PATCH. The number continues the one this Specification last stated, 12.0, rather than starting over; the versions between were not published. From this revision each entry here opens with the version it made. Nothing else changed.

  • rev 147 (2026-11-12) — The API version is MAJOR.MINOR, at 1.0, and the Version line and §1.3 state API 1.0. §11.1 says when each part moves — MAJOR when an answer a program already got right changes, a valid input that was accepted is refused, or the wire changes in a way a client would notice; MINOR on an addition; neither on a correction — where it said how a numbered release would be tracked, were one adopted. §3.9.1: a declared edition version is the API’s MAJOR, so python.1 is valid at API 1.0 and every later 1.N.

  • rev 146 (2026-11-12) — §7.1.1: a fold whose formula, computed as Python computes it, passes the largest real on the way answers its value where that value is a real: over 1e308 and 1.5e308, avg yields 1.25e+308, where statistics.fmean raises OverflowError, and rms, the moments, cv, harmmean, product and the two-attribute folds likewise. A fold whose value is past the largest real yields inf or -inf.

  • rev 142 (2026-11-12) — A real, and a part of a complex, may be an infinity or a NaN, everywhere Python’s float may (§3.5.7, §3.5.15). 1e400 is inf; float() and complex() read inf, infinity and nan; math.inf, math.nan, math.isinf, math.isnan, math.isfinite, cmath.inf, cmath.nan, cmath.infj, cmath.nanj, cmath.isinf, cmath.isnan and cmath.isfinite are added (§8.16). A result past the largest real is inf or -inf where it refused, and an operation with an infinite or NaN operand answers IEEE 754’s value, with each departure from Python stated: §8.3 Infinity and NaN (**, round(x, n), a non-finite dividend over a zero divisor, inf // 1, -1 % inf, -1 // inf, and a complex part that is its exact value where Python’s algorithm loses it), §8.16 Infinity and NaN (the math and cmath functions), §8.1 Infinity and NaN (one NaN, found by value in a collection, sorted after inf, and nan from min and max), §8.13 (the 0 flag pads an infinity with spaces), §8.18 (one NaN packs as 7ff8…). A cell holds every real exactly, -0.0 and a complex’s signed zero parts included. int, round, math.floor, math.ceil, math.trunc and as_integer_ratio refuse an infinity and a NaN in Python’s words; abs of a complex past the range is inf.

  • rev 141 (2026-11-11) — §8.16: math gains the thirty names of Python 3.11 it lacked, each answering the exact value rounded once and refusing in Python’s words: gcd, lcm, comb, perm, factorial, isqrt and prod; sinh, cosh, tanh, asinh, acosh, atanh, cbrt, exp2, expm1, log1p and pow; fabs, copysign, fmod, remainder, trunc, modf, frexp, ldexp, nextafter and ulp; fsum and dist. fsum, prod and dist take every operand exactly and round once, so math.fsum([1e308, 1e308, -1e308]) is 1e+308 where Python raises OverflowError, and math.hypot of three or more coordinates is the exact distance math.dist gives, where it was Python’s own algorithm. math.prod takes start by name only and takes numbers only. An int answer past what an int holds is refused as §8.3 refuses one.

  • rev 140 (2026-11-11) — §8.15: two ** displays written out that give one name, with a ** the program reads merged before the second giving, are refused as the program runs, in the order Python merges, and are catchable: with n holding "x", print(**n, **{"x": 1}, **{"z": 1}, **{"z": 2}), len(xs, **n, **{"x": 1}, **{"z": 1}, **{"z": 2}) and d.update(**n, **k, **{1: 2}, **{1: 3}) (with k holding "x") are ... got multiple values for keyword argument 'x', where they named 'z' or '1' where the unit was compiled, and print(**n, **k1, **{"z": 1}, **{"z": 2}) names 'z' as the program runs. A display and a name=value giving one name beside such a ** are the same. Where every mapping is written out, or no ** is read before the second giving, the unit is compiled as before.

  • rev 139 (2026-11-11) — §8.15: a ** display written beside a ** whose keys the program reads, whose key is no string, is merged with the mappings as the program runs, as Python merges a call’s keywords: with n and k both holding "x", print(**{1: 2}, **n, **k), len(xs, **{1: 2}, **n, **k), sorted(xs, **n, **{1: 2}, **k), s.upper(**n, **k, **{1: 2}) and d.update(**{1: 2}, **n, **k) are print() got multiple values for keyword argument 'x' wherever the display stands, and with m holding 1, print(**{1.0: 2}, **m) is print() got multiple values for keyword argument '1', where they were keywords must be strings where the unit is compiled. keywords must be strings answers only where no name is given twice, as the program runs, and a dict’s update refuses it once it has merged. Where every mapping is written out the unit is compiled as before.

  • rev 138 (2026-11-11) — §8.15: a list’s sort given a * and a key= the elements refuse counts its arguments before it applies the key, as Python does: with ys giving one value, xs.sort(*ys, key=len) over a list of ints answers sort() takes no positional arguments, and with two values sort() takes at most 2 arguments (3 given), where it answered object of type 'int' has no len(), refused where the unit was compiled; with ys giving none the sort runs and the key’s refusal is raised as it is applied to an element, none where the list has none. Each is decided as the program runs and is catchable. A * nothing types gives nothing, and a sort with no * has no count to wait for; both keep the key’s refusal where the unit is compiled, in the same words.

  • rev 137 (2026-11-11) — §8.15: a list’s sort given a * and a positional argument written beside it counts every argument first, as Python does: with ys giving one value, xs.sort(*ys, 5, x=1) answers sort() takes at most 2 arguments (3 given), where it answered sort() takes no positional arguments, refused where the unit was compiled; with ys giving two, xs.sort(*ys, 5) answers sort() takes at most 2 arguments (3 given), where it answered for no positional arguments. Each is decided as the program runs and is catchable, every argument read first. A * nothing types gives nothing and keeps its refusal where the unit is compiled, in the same words.

  • rev 136 (2026-11-11) — §8.15: a list’s sort whose receiver is written, xs.sort(*ys, x=1) or list.sort(xs, *ys, x=1), counts what a typed * gives before it names a keyword sort does not take, as Python does: with ys giving one value it is sort() takes no positional arguments, where it was 'x' is an invalid keyword argument for sort(); it takeskey,reverse`, refused where the unit was compiled; withysgiving none it is‘x’ is an invalid keyword argument for sort(), as the program runs and catchable, without the hint. Too many names are counted the same way,sort() takes at most 2 arguments (3 given)beside a value andsort() takes at most 2 keyword arguments (3 given)beside none. A*` nothing types gives nothing and keeps its refusal where the unit is compiled, in the same words, after a receiver of another type is refused for its type.

  • rev 135 (2026-11-11) — §8.15: a key that is no string which two ** mappings of a call give is a name given twice, refused before any key is refused as no string, as Python merges a call’s keywords: with m and n both holding 1, print(**m, **n), len(xs, **m, **n), sorted(xs, **m, **n), s.split(**m, **n) and a script’s f(**m, **n) are print() got multiple values for keyword argument '1' (m.f() for a script of module m), where they were keywords must be strings, and slice(1, **m, **n) is the same where it was slice() takes no keyword arguments. Keys are equal as a dict’s are (1, 1.0 and True are one), and the key is named as str writes the later one. A script’s call refuses a name given twice by a later mapping before a key that is no string an earlier one gave, f(**k, **m, **n) with m and n both holding "a". Written out as displays, print(**{1: 2}, **{1: 3}) is refused where the unit is compiled, in the same words, where it was keywords must be strings.

  • rev 134 (2026-11-10) — §3.9.1, new: a source MAY declare on its first line the edition and natural language it is written in, each with an optional version, # dke: edition=python.1 language=eng.1. The edition’s version is the API; the language’s is that language’s vocabulary, which moves when a word is removed or respelled and never when one is added. A declared name supplies the parameter a call omits; a parameter naming another value, a version the service does not serve, and a first line that opens # dke: without matching the form are each refused before the source is compiled; a version left out is the one served, and latest is not a version. §3.3: that first-line comment is read. §3.9, §4.1: the language is set by the call’s language parameter, which the declaration may supply, where they said a source does not declare it.

  • rev 130 (2026-11-10) — §14.10: a @constraint premise that orders a string field against a quoted bound (<, <=, >, >=) is now judged, alone or with absent(), and is reported violated exactly when a @rule with the same premise would conclude; before, such a constraint held silently. §5.1: the proof_tree a why yields is the derivation alone, the conclusion and then the claims it rests on; the earlier, superseded values of the cell are no longer elements of it, and remain readable through get. §10.4: except engine_error takes every run-time failure the wire reports as engine_error, including those that until now passed by it (a value narrowed to a class it did not hold, a read of a method that returns nothing); a spent budget, a stopped run and an add-only refusal remain uncatchable. §8.13: a collection (a list, tuple, dict, set, a dict’s views, a slice) takes no format spec; a non-empty one is refused wherever it is written, in format, an f-string, str.format and __format__ called by name, and an empty one answers str of the value. §3.4: a name does not contain U+00B7, U+0387, U+013F or U+0140; each is refused wherever a name would hold it, as an unexpected character that the message names by its code point. No module of the DKE Library changes its stored size.

  • rev 128 (2026-11-09) — §8.16: cmath.exp, cmath.rect and cmath.sqrt give each part of the result, real and imaginary, as its exact value rounded once, where CPython 3.11 rounds the parts it composes and again, so cmath.exp(complex(1.3, 2.7)).imag is 1.5681835703495033 where CPython answers 1.5681835703495035; a part CPython gives its own way stays CPython’s. Seven functions now depart from CPython’s algorithm, not four. Example C.31’s cmath.rect of cmath.polar(3 + 4j) gives back (3+4j), where CPython answers (3.0000000000000004+3.9999999999999996j).

  • rev 127 (2026-11-09) — §8.3: sum over numbers holding a real, and no complex, is the exact sum of the start and every element rounded once, so sum([0.1] * 10) is 1.0 and sum([1e16, 1.0, -1e16]) is 1.0 where CPython 3.11’s sum answers 0.9999999999999999 and 0.0; an int enters as the integer it is; a sum is refused as arithmetic_overflow only when its rounded value, or an all-int total, is out of range, so a running total that leaves the range on the way no longer refuses it; a zero sum is -0.0 only when the start and every element are. §7.1.1: the store fold’s sum and the list’s agree.

  • rev 122 (2026-11-08) — §8.16: math has log10 and log2, erf and erfc, and gamma and lgamma, each the exact value rounded once, refusing a zero or negative argument to a logarithm and a pole of the gamma function with math domain error and a result past the finite range with math range error. math.log(x, base) is the logarithm of x to base rounded once, where Python’s divides two rounded logarithms and rounds again, so math.log(1000, 10) is 3.0 where Python answers 2.9999999999999996; the section says where a last digit may differ from Python’s.

  • rev 120 (2026-11-08) — §8.16: math has asin, acos and atan, each correctly rounded, and refusing outside its domain with math domain error; hypot, the distance from the origin over any number of coordinates, correctly rounded for two and Python’s own algorithm for any other number; and degrees and radians, each the exact product rounded once. degrees, radians and hypot of two coordinates depart from Python’s algorithm, which rounds twice, so a last digit may differ from Python’s, and the section says where. A result past the largest real, where Python answers an infinity, refuses with arithmetic_overflow.

  • rev 113 (2026-11-06) — §3.5.8: a write’s transcript line gives a blob’s hex whole up to 65,536 characters, and past that its length and first 8 bytes, marked as shortened; print and str(…) are never shortened. The forms a transcript gives every value past that length are stated in the MCP Reference, §4.2.

  • rev 112 (2026-11-06) — §3.5.8: a blob is cut, joined and read a byte at a time as Python’s bytes are: b[i:j] with a whole slice (negative bounds, a step) answers a blob, b1 + b2 a blob, and b[i] an int, defined by three Python functions stated in full. Each is one operation and builds no cell; a position past either end is refused as index_out_of_bounds and a step of zero as slice_step_zero; “It is not otherwise operated on” now follows the three.

  • rev 111 (2026-11-06) — §8.17: vector.sumsq(xs, axes) sums along axes, as vector.apply carries a matrix along them: for each pattern of the named axes it answers the value Python’s math.fsum gives over the squared real and imaginary parts of every element whose index has that pattern, a list<real> of 2ᵏ sums, defined by a Python function stated in full. It costs one operation, builds the 2ᵏ sums alone, and refuses an axis out of range or named twice as vector.apply does and a square or a sum past the range of a real as arithmetic_overflow, where Python answers an infinity or raises OverflowError.

  • rev 110 (2026-11-05) — §8.15: dict.fromkeys, str.maketrans and float.fromhex, the three methods a type answers itself, read a ** as every callee that takes no name does: with opts holding x, dict.fromkeys(xs, 0, **opts) is dict.fromkeys() takes no keyword arguments, with a key that is no string keywords must be strings, and with opts empty the call as written; a display of literal keys is refused where the unit is compiled. It was a parse_error that ** spreads a collection the call does not read.

  • rev 109 (2026-11-05) — §8.15: a change that would give its receiver a second type, beside a ** whose keys are known only as the program runs, waits for the run, as the names do: s.add("a", **opts) after s = {1} is set.add() takes no keyword arguments with opts holding x, keywords must be strings with a key that is no string, and with opts empty the refusal a name’s one type gives, where the unit refused it at compile whatever opts held. At list.append, list.insert, list.extend, a set’s update and a dict’s update and setdefault. The statement refuses either way, so the receiver keeps its type; a dict’s update writes nothing before it refuses.

  • rev 108 (2026-11-05) — §8.15: slice called beside a ** refuses any name before it reads a key, as math.log does, so slice(1, **m) with m holding a key that is no string is slice() takes no keyword arguments, where it was keywords must be strings; the same where the mapping is written, slice(1, **{1: 2}), and where a count too long or a * stands beside it.

  • rev 107 (2026-11-05) — §8.15: beside a ** before a dict whose keys are known only as the program runs, every mapping is merged before a key that is no string is refused, as Python merges a call’s keywords: a name given twice is refused first, at print, a builtin, sorted, min, max, a built-in value’s method and a list’s sort, so print(1, **m, **n, **k) with m holding 1 and n and k both x is print() got multiple values for keyword argument 'x', where it was keywords must be strings. A name written beside such a ** that sorted, min, max or a list’s sort does not take waits for the merge too: sorted(xs, x=1, **opts) is sorted() got multiple values for keyword argument 'x' where opts holds x, keywords must be strings where it holds a key that is no string, and 'x' is an invalid keyword argument for sort() where it is empty. It was a type_error where the unit is compiled, and still is without such a **.

  • rev 106 (2026-11-05) — §8.15: a method reached through its type’s name, given only a ** before a dict whose keys are known only as the program runs and no positional argument, is read as a call with no receiver, as Python reads it: the names are merged, a key that is no string is keywords must be strings and a name given twice str.upper() got multiple values for keyword argument 'x', and then the call is unbound method str.upper() needs an argument, as the program runs. It was a parse_error.

  • rev 105 (2026-11-05) — §8.15: a list’s sort reached through its type takes its list from the first thing every * and every positional argument gives, in order, and counts and names what follows it only after, as Python does: list.sort(*e, xs) sorts xs where e gives nothing, list.sort(*e, x=1) is unbound method list.sort() needs an argument and not 'x' is an invalid keyword argument for sort(), and with a list given list.sort(*ys, xs) is sort() takes no positional arguments, as the program runs where a * is typed. It counted and named what stood after a * as written, whatever the * gave. A list written of another type is refused for its type before what follows it is counted or named: list.sort(t, 1) over a tuple t is descriptor 'sort' for 'list' objects doesn't apply to a 'tuple' object. It was sort() takes no positional arguments.

  • rev 104 (2026-11-05) — §8.15: beside a ** before any dict whose keys are known only as the program runs, an argument a builtin, a function of a built-in namespace, a built-in value’s method or a change that takes no name does not take, of a type or a value it refuses, is refused as the program runs once the names are merged and none is given, in the words the call has without such a **, after the names in Python’s words: with opts holding x, len(5, **opts) is len() takes no keyword arguments, and with opts empty object of type 'int' has no len(). So is isinstance given a second argument that names no class, and a dict’s get or setdefault of a key alone, which has no None to answer. Each was a type_error where the unit is compiled, and still is without such a **. A value bound from such a call is refused as the program runs where the call fixes a type, v = len(5, **opts), and stays a type_error where the unit is compiled where what the callee answers turns on that argument or is what no value here holds, v = abs("a", **opts).

  • rev 103 (2026-11-05) — §8.14, §8.15, §8.20: beside a ** before a dict whose keys only the program knows, a change read as a value and bound to no name is made, its names refused first as at its statement, and the value is then refused as the program runs: with opts empty, print(xs.append(2, **opts)) appends 2 and is refused, and with opts holding x it is list.append() takes no keyword arguments. So is a special name whose answer no value here holds, print((1).__add__("a", **opts)), and __setitem__, __delitem__ or an in-place name read as a value, which makes its change first. Each was a type_error where the unit is compiled, and stays one where the value is bound, or without such a **. §8.20: a special name beside a * or such a ** is read as at any method, in Python’s words for a call that unpacks: the names merged and refused, by a slot’s method without reading a key, and then the count, bool.__floor__() takes no arguments (1 given). Beside a * it was refused as the program ran in words that were not Python’s, and beside such a ** it was a parse_error. A ** display’s names at a special name are refused in Python’s order and words, bool.__floor__() takes no keyword arguments.

  • rev 102 (2026-11-04) — §8.15: beside a ** before any dict whose keys are known only as the program runs, a value bound from a call the run can only refuse, where nothing types what the call would answer, is a type_error where the unit is compiled, in the call’s own words, whatever the mapping gives, since a bare assignment binds its name to the initializer’s static type (§6.4): v = list.pop((1, 2), **opts) is descriptor 'pop' for 'list' objects doesn't apply to a 'tuple' object, v = chr("a", 2, **opts) is chr() takes exactly one argument (2 given), and, where nothing types what a * gives, v = list.pop(*xs, **opts) is cannot infer type of binding `v` — right-hand side did not typecheck to a known type. The same call read as a statement or printed is refused as the program runs. This states what rev 101 already did.

  • rev 101 (2026-11-04) — §8.15: beside a ** before any dict whose keys are known only as the program runs, a count that a builtin, a function of a built-in namespace or a built-in value’s method taking no name does not take is refused only where the mapping gives no name, as the program runs, since Python refuses a name first: with opts empty, len(xs, 2, **opts) is len() takes exactly one argument (2 given), and with opts holding x, len() takes no keyword arguments. It was a type_error where the unit was compiled, whatever the mapping gave, and stays one without such a **.

  • rev 100 (2026-11-04) — §8.15: beside a ** before any dict whose keys are known only as the program runs, a receiver written of another type than the type its method is reached through is refused once the names are merged, as Python’s descriptor refuses it: str.upper(5, **opts) is keywords must be strings where opts holds a key that is no string, and otherwise descriptor 'upper' for 'str' objects doesn't apply to a 'int' object, as the program runs, before the method reads a name or counts what follows; and at a change a name written beside such a ** is merged with what it gives, so that xs.append(5, x=1, **opts) is list.append() got multiple values for keyword argument 'x' where opts holds x. Each was a type_error where the unit is compiled, whatever the mapping held, and still is without such a **.

  • rev 99 (2026-11-04) — §8.15: beside a ** before any dict whose keys are known only as the program runs, a method reached through its type’s name, where its receiver is the first thing a * gives, merges the names before it takes the receiver, as Python does: a key that is no string is keywords must be strings and a name given twice is str.upper() got multiple values for keyword argument 'x', and only then is the call unbound method str.upper() needs an argument, or, where what the * gives first is of another type, descriptor 'upper' for 'str' objects doesn't apply to a 'int' object, each as the program runs. It took the receiver first, except at list.sort, which merged the names first where the * gave no list and refused a value of another type where the unit is compiled. So it is where nothing types what the * gives, as with xs = [], which was a type_error where the unit is compiled and still is without such a **.

  • rev 98 (2026-11-04) — §8.15: a ** before any dict beside a * whose length is known only as the program runs is read at list.sort where the list sorted is the first thing the * gives, the names merged before the list is taken from the *. It was a parse_error. §8.17: says why vector is reserved though Python has no module of that name.

  • rev 97 (2026-11-04) — §8.15: a ** before any dict beside a * whose length is known only as the program runs is read at a list’s sort, which counts what the * gives as arguments by position once the names are merged and refuses any, and at a dict’s update, which writes what the * gives and then the names, as keys. It was a parse_error at a list’s sort and a type_error at a dict’s update, and stays a parse_error at list.sort where the list is the first thing the * gives.

  • rev 96 (2026-11-04) — §8.15: a ** before any dict beside a * whose length is known only as the program runs is read at a built-in value’s method that takes names, split, rsplit, splitlines and expandtabs, and at math.isclose, in Argument Clinic’s order. It was a parse_error, and stays one at a list’s sort. math.isclose takes a and b by name beside a *, placed at the count the * gives, where math.isclose(*xs, b=1.0) was a parse_error.

  • rev 95 (2026-11-04) — §8.15: a ** before any dict, such as a name that holds one, beside a * whose length is known only as the program runs, is read at sorted, min and max, which count the * first, as Python’s do: sorted takes one argument and min and max at least one, whatever the mapping gives; then the names, then a default beside several arguments, then a key that is no function. It was a parse_error, and stays one at math.isclose and a method that takes names.

  • rev 94 (2026-11-04) — §8.15: a ** before any dict, such as a name that holds one, beside a * whose length is known only as the program runs, is read at a builtin that takes names, int, round, pow, complex, str, zip, enumerate, sum and dict, over the count the * gives, in Python’s order: too many arguments in all, then a name missing or given by name and by position, then the first name the callee does not take, then what the call written out with that count refuses. It was a parse_error, and stays one at sorted, min, max, math.isclose and a method that takes names.

  • rev 93 (2026-11-04) — §8.15: a ** before any dict, such as a name that holds one, beside a * whose length is known only as the program runs, is read where what is called takes no name: a builtin, a function of a built-in namespace, a built-in value’s method or a change. Python’s order holds: every argument, then the receiver where the * gives it, then the names, any of which is refused, then the count. It was a parse_error, and stays one where what is called takes names. A bool’s method that int defines is named for bool at a call that unpacks, bool.bit_length() takes no keyword arguments, where it was named for int; and at a change an operand that is no dict is refused before a name written beside it.

  • rev 92 (2026-11-04) — §8.15: a ** before any dict, such as a name that holds one, at a function of a built-in namespace is read as Python reads it: math.isclose takes its names as Python’s does, named alone, and any other function refuses any name, math.log before it reads a key; a name given twice names the function with the module Python defines it in. It was a parse_error. A namespace’s function is named so where a keyword is written too: struct’s are _struct.pack() and the like, where they were struct.pack().

  • rev 91 (2026-11-04) — §8.15: a ** before any dict, such as a name that holds one, at a method that changes a collection is read as Python reads it: a list’s sort takes key and reverse as sorted does, a dict’s update takes every name as a key after the entries it is given, and any other change refuses any name before the collection changes, a count it does not take refused only where the mapping gives no name. It was a parse_error. A call that none of the names its mapping could give would let run is refused as the program runs, in the words of what the mapping gives, as Python’s is; it was a type_error where the unit is compiled, in the words for a mapping that gives none, which enumerate(**opts) given start alone contradicted.

  • rev 90 (2026-11-04) — §8.15: a ** before any dict, such as a name that holds one, at a built-in value’s method called as a value, and through its type, is read as Python reads it. split, rsplit, splitlines and expandtabs read the names as Python’s do, their receiver not counted, and name themselves alone; any other method refuses any name, named with its type or, where Python’s call refuses a name before it reads one, alone, startswith() takes no keyword arguments. It was a parse_error. A method that changes its receiver does not read one yet.

  • rev 89 (2026-11-04) — §8.15: a ** before any dict, such as a name that holds one, at sorted, min and max gives the call its names as if written, counted apart from the positional arguments, which are counted as written, and sorted is refused past the merge under sort’s name, as Python’s is. A key the mapping gives is never a function, and is refused 'int' object is not callable once there is an element to call it on. It was a parse_error. The sentence saying a ** beside a built-in function’s * is a parse_error now names the one call that reads both, print.

  • rev 88 (2026-11-04) — §8.13: enumerate reads its names where they stand, as Python’s does: with two arguments in all one name must be start, and with one it must be iterable, so enumerate(start=1) is 'start' is an invalid keyword argument for enumerate(). It was told it lacked its iterable. The sentence quoting that refusal with (pos 1) was wrong for a call with more names, which Python and this language answer without it. §8.15: a ** before any dict, such as a name that holds one, at enumerate and sum gives the call its names as if written, enumerate’s read where they stand. It was a parse_error. A call that none of the names its mapping could give would let run is a type_error where the unit is compiled, in the words it meets when the mapping gives none.

  • rev 87 (2026-11-04) — §8.15: a ** before any dict, such as a name that holds one, at a builtin that takes names (int, round, pow, complex, str, zip) gives the call its names as if written, and at dict its keys, in a new mapping; where a name the callee does not take is among them, Python’s order holds: too many arguments in all, then a name missing or given by name and by position, then the first such name. Otherwise the call refuses what the call written out does, as the program runs. A call whose type turns on the names given is a type_error where the unit is compiled. It was a parse_error.

  • rev 86 (2026-11-04) — §8.15: a ** before any dict, such as a name that holds one, at a builtin that takes no name is read after the positional arguments, as Python reads it: a key that is no string or a name given twice is refused as it is met, then any name at all, before the arguments are counted, each a catchable type_error; a mapping that gives nothing leaves the call as written. It was a parse_error.

  • rev 85 (2026-11-04) — §4.3, §8.15: a ** at print before any dict, such as a name that holds one, gives its keys as keywords when the line is composed, merged and checked as Python does: a key that is no string or a name given twice first, then the first name print does not take, written or given, then sep before end, each a catchable type_error. It was a parse_error. A name written beside such a ** that print does not take is refused after the merge, where it was refused as the unit compiled.

  • rev 83 (2026-11-03) — §7.1, §8.13, §8.16, §8.17, §8.18: a count a built-in function or a namespace function does not take is a type_error at its name in Python’s words, before any argument is read: len() takes exactly one argument (2 given), math.log requires 1 to 2 arguments, missing format argument, and a count of names in Argument Clinic’s, round() takes at most 2 keyword arguments (3 given) where nothing is given by position. A namespace function’s count was a parse_error.

  • rev 82 (2026-11-03) — §8.6, §8.7: a keyword print, sorted or a list’s sort does not take is a type_error in Python’s words, the first one written, 'x' is an invalid keyword argument for print(), and named for sort() from sorted, as Python names it; at a list’s sort it is refused after the positional arguments are counted.

  • rev 81 (2026-11-03) — §8.15: a * whose length is known only as the program runs reads beside a name at a built-in type’s method, a dict’s update and a list’s sort, as in "a b c".split(*ys, maxsplit=1) and xs.sort(*ys, reverse=True): the name binds as Python binds it once the count is known, and a count that does not fit it is a catchable refusal in Python’s words as the program runs, counting the name with the positional arguments. At sort the spread and reverse’s value are read before the count is checked, and through list.sort the first thing the spread gives is the list sorted.

  • rev 79 (2026-11-03) — §8.15: a ** before a dict display whose keys are all literals gives its keys as keyword arguments to print, a builtin, a built-in type’s method, sorted, a list’s sort and a built-in namespace’s function, as Python binds f(**{"k": v}) as f(k=v); a call it does not fit is a type_error in Python’s words and order. §4.3: print’s file may be given None, its default. A math function’s keyword given twice, by name and position, or not taken, is told in Python’s words.

  • rev 78 (2026-11-03) — §13.8: a read through a family of classes runs as Python’s does until it reaches a class whose method returns nothing, wherever it is written: in a method a subclass inherits, in a script written above the class and in a comprehension. A call whose arguments the method of the class it reaches does not take is refused as it reaches that class, in the words of Python’s TypeError, and except refuse takes it. A read no class can answer, and a call no class takes, stay refused when the program compiles.

  • rev 77 (2026-11-03) — §8.3: pow with a modulus given an operand that is not an integer is refused in Python’s words for the class whose power Python asks first. §8.4, §8.14: xs += c on a list takes any finite collection, as extend does. §8.16: math.floor and math.ceil. §8.20: a real’s __floor__ and __ceil__ answer as the two do.

  • rev 76 (2026-11-03) — §8.20: a special name a built-in value holds, called by name, answers as the spelling it stands for, a reflected one with its operands the other way round, and one the value does not hold is told so in Python’s words. Where Python answers NotImplemented, a method, a class or the machinery behind a value, the call is a type_error saying so. __setitem__, __delitem__ and the in-place names are statements, as the changes they stand for are.

  • rev 75 (2026-11-03) — §7.1.1: a fold over real values is exactly rounded where it adds: sum is math.fsum’s value, avg statistics.fmean’s, rms the root of statistics.fmean over the squares and geomean statistics.geometric_mean’s; the folds that select or interpolate answer what the same formula answers over a list, and the rest can differ from a Python computation over a list in the last place; covariance is the population figure. §8.3: sum over a list adds left to right, as CPython 3.11’s does, beside the store fold. §5.1: why an iterator or a values view is not a key or a set element.

  • rev 74 (2026-11-03) — §5.3, §6.2: an argument of a type a string’s method does not take, a list’s or a tuple’s index bound, a list’s insert position and a slice’s indices argument are refused in CPython’s sentence, which differs by method as CPython’s does, where the argument is of a class CPython has, and join over what is no iterable of strings names the first position that holds none; a sequence or mapping pattern that can never match, as one alternative of several, is left out of its arm, which stands on the others, where it was refused.

  • rev 73 (2026-11-03) — §3.5.12, §3.5.13, §8: a set literal, a dict literal’s keys and a key written into a dict that would hold a tuple beside a scalar, or tuples whose positions do not join, are refused naming the two types, as a list literal’s elements are, since a value holds a scalar. §8 opens with how an operator given operands it does not take is refused: in CPython’s sentence, naming CPython’s classes, where CPython refuses the same operands, and naming the operand types the operator takes where an operand is of a type CPython has no class for.

  • rev 72 (2026-11-03) — §8.6, §8.9, §8.14: a set’s update and difference_update, and a dict’s update, read an iterator or a generator expression one element at a time, changing the collection by each element before the next, as CPython’s do, so one over the collection being changed is refused at its next read in CPython’s words and what was written before stays; a dict’s pair of the wrong length is named by its position after the pairs before it are written. §8.9 names a list’s extend and the in-place set and dict methods among the calls that read a generator expression, and §8.14 states a list’s extend over the list itself, which stops at the length it began with, as a kept divergence.

  • rev 71 (2026-11-02) — §5.1, §8.3, §8.6: |, ^, union and symmetric_difference across scalar element types build a set<value>, as CPython builds a mixed set, and a set<value> takes another scalar in place; a keys or items view takes any collection beside it on either side of &, |, - and ^, an intersection keeping of two equal numbers the one CPython keeps, and a number beside a view is refused in CPython’s words.

  • rev 70 (2026-11-02) — §13.8: a value read through a call that reaches a class whose method returns nothing runs that method and is then refused as the line runs, naming the method as CPython does; no except takes the refusal. A read reaching only classes that return a value answers as before.

  • rev 69 (2026-11-02) — §5.6, §13.9: a value built from scalars holds their classes, and where a test of a name holding one fails, the name is read at the classes left to it: in the else, in a negated test’s branch, in a conditional’s second branch, after the test in an or, and in a match arm below arms naming classes. A branch no value can take is never run and not read, and an operator meeting a value of known classes runs where it takes each class. §6.2: an arm below arms naming classes reads the scrutinee at what their tests failing leave it, and a guard narrows a name its own pattern binds. §8.10: two scalars that are not both numbers join to a value holding their classes, where an int beside a string was a type_error.

  • rev 68 (2026-11-02) — §5.6, §13.9: a name holding a value is narrowed by an isinstance test against types, in the branch it takes, after it in an and, after a comprehension’s if and in a conditional’s first branch, and an or of two tests narrows to what they join to. §6.2: a match over a value whose class arms all write Python’s parentheses is Python’s, every arm a test and none required, case int(n): and a capture binding the value, an arm naming classes narrows a scrutinee written as a name, and a guard narrows its arm’s body. §8.10: a scalar beside a value joins to the value.

  • rev 67 (2026-11-02) — §5.1, §5.3, §8.11, §8.13: a tuple of scalars, or of class instances, iterates, converts and is indexed and sliced at a computed position as a list of its elements is; only one holding a collection beside a value of another type is not. dict(x) reads any finite collection of pairs as update does, a fault in a pair raised as it runs. §5.3: an index of a type Python has that is no int is told in Python’s words. §13.1: a field declared twice keeps the type written last, and a computed field is still declared once.

  • rev 66 (2026-11-02) — §6.2: a match over a value Python has is Python’s: every arm a test, tried in the order written, and none need run. It reads any scrutinee, a shape arm needs no catch-all, and an arm that cannot match compiles and never runs. Class patterns (case int():, case C():, case m.C():) test as isinstance tests, at an arm’s head or inside another pattern, a type that reads itself holds one pattern, and past that the test raises Python’s type_error. A class named with no (), over a value no class match follows, is told it captures in Python. §6.9: a match definitely returns only if an arm takes every value. §9.2.3, §13.5: case <Class>(): in a class match is a test, and only arms with no () are exhaustive.

  • rev 65 (2026-11-02) — §3.5.4, §4.2, §4.3, §6.8: a script with no -> type whose body holds a return <expr> is a value script of the type its returns give. §6.9: a suite definitely returns if any statement of it does, and the rule names raise and try. §8.8: the name of a def, a method or a module read with no call is told no value holds it. §9.7: a chain of calls below going more than 1000 levels is a type_error. §13.4: so a method’s.

  • rev 64 (2026-11-02) — §3.4: a name nothing binds is told in Python’s words. §8.14: a method a value does not hold is told in Python’s words, and a change read as a value says it answers None.

  • rev 63 (2026-11-02) — §4.2, §4.3: a call that does not fit is told in Python’s words and order, a method’s count holding its receiver, and one no type is known for only as it runs. §8.8: a call of a value is a type_error in Python’s words, or told as it runs, and no longer a parse_error. §8.14: an element write or del on what takes neither is told in Python’s words. §13.9: a class tests any value, and every value is an object. Newest first. The revision is the surface’s — one number stated by every published document, the web pages and the service wire, moving whenever any document changes (§11.1). This list records the revisions at which this document changed and what changed in it; a revision absent here left this document as it was. The Version line at the top states the current revision. The history starts at rev 1.

  • rev 62 (2026-11-01) — §13.1: a class exists from the line that declares it, a body may name one declared anywhere in the unit, and a line that runs before it is told name 'K' is not defined. §6.4: a type may be written as a string, and a construction alone is a statement. §13.1: a method may read a field declared below it.

  • rev 61 (2026-11-01) — §4.1, §6.8: a declaration may call one above or below it in its unit, the unit compiling a callee first, and only a call leading back to its caller is refused; a module’s own statement reaching a def below it is told name 'g' is not defined. §13.1, §13.3: a method called or a field read through a value the compiler does not follow runs against the instance’s own class, and a class holding no such method raises in Python’s words. §13.4: a value method’s body holds any statement, and a method may call one written below it. §13.8: a method call through classes sharing no base runs the instance’s own.

  • rev 60 (2026-11-01) — §5.1: what no dict or set could hold is told in Python’s words, unhashable type: 'list', when it is written and as the program runs when it is looked up. §8.6: any, all and a set’s intersection, issuperset and isdisjoint stop reading an iterator where CPython’s do.

  • rev 59 (2026-11-01) — §8.15: a * at a built-in method’s call, through a type’s name the receiver too, and a count a built-in method does not take in Python’s words. §8.6, §8.14: a set’s union, intersection, difference and update take any number of collections, with difference_update, intersection_update and symmetric_difference_update beside them, d.update() changes nothing, and sort’s positional argument raises as Python’s does. §8.9: a generator expression right after a *.

  • rev 58 (2026-11-01) — §3.5.10, §8.13: a format spec’s sign, z, # and grouping format as Python’s do, and a collection refuses them. §8.6: a method through its type’s name, str.upper("a"), is the method on its first argument. §8.3, §8.14: a dict’s |= and update read any collection of pairs. §8.15: a ** at a script’s or a method’s call gives each entry as a keyword argument.

  • rev 57 (2026-11-01) — §3.5.10, §5.3: a string’s .format and .format_map read and answer as Python’s do, and a template’s rules are stated. §3.5.12, §8.3: a dict’s **, | and |= merge. §4.3: raise holds several arguments. §5.3: translate takes a list, a tuple or a string as its table. §8.3: % with a string on its left formats as Python’s does, and @ and @= read and are refused naming their operands. §8.6: a type’s own three methods are reached through a value. §10.1: @ left the forms the lexer refuses.

  • rev 56 (2026-11-01) — §8.19: a named expression (:=) reads, where Python’s grammar reads one, and binds when it is evaluated. §10.1 no longer says it is refused. §4.3: print’s sep and end are any expression that gives a string, read after the arguments in the order written, None as the whole of either is its default, and flush is read and changes nothing. A condition of assert may begin with a bracket, and §4.3 says when the verb is read instead.

  • rev 55 (2026-11-01) — §5.3, §8.6: s.translate(table) reads. str.maketrans, dict.fromkeys and float.fromhex read, called on the type’s own name, as a statement and as a key=. .format_map is told what .format is.

  • rev 54 (2026-11-01) — §10.1: what reaches the limit on the work of reading. Reading a call nested in a call no longer doubles at each level, so 20 method calls nested in one another read like any others. What reaches the limit now is 70 dicts unpacked one inside another beside a syntax error.

  • rev 53 (2026-11-01) — §8.6: a real takes is_integer and hex, an int or a bool takes bit_length and bit_count, and each takes as_integer_ratio. §5.2: an int and a bool have .numerator and .denominator.

  • rev 52 (2026-11-01) — §10.1: reading a program has a limit in work, counted and so the same on every host. Past it, a program Python refuses gets Python’s answer, and any other is a parse_error at line 0.

  • rev 51 (2026-11-01) — §8.9: a comprehension’s if filters are each asked only for a truth value, so their types need not join. §10.1: how long a chain reads, the count of elif a chain holds, and where other nesting stops.

  • rev 50 (2026-10-31) — §8.11: a string, a set, a dict (its keys) and a dict’s view unpack into a target list, in an assignment, a for and a comprehension clause, where each was a type_error; in a comprehension clause a group in a fixed tuple’s slot reads an iterator, a view, a set or a dict where it stands. What stays a type_error is stated.

  • rev 49 (2026-10-31) — §8.11: a comprehension clause checks a starred group’s width over what its star absorbs, in Python’s words, where it told the list’s count.

  • rev 48 (2026-10-31) — §4.4: a comprehension’s later source or if that reads a name a later clause binds is a type_error, whatever an outer scope holds under the name, where it read the outer value; the words are Python’s, a free variable’s from a comprehension inside it, and a module called is 'module' object is not callable. §8.3: key= takes a method of a built-in type, key=str.lower, which reverses the sentence that called it a parse_error, and a key over what is no collection is told in Python’s words. §8.15: what a spread’s compile refusal gives up, and where a ** is told. §10.1: where the reading of what Python’s compiler refuses stops short.

  • rev 47 (2026-10-31) — §3.4: a name is read in Normalization Form KC, as Python reads it, so every spelling of one form is one name, where any other form than NFC was refused; a keyword is compared as written, a name that keeps a combining mark with no letter to compose into may name a local and nothing a store holds, and a string is not read. §6.1: a value’s refusal for a combining mark no longer calls it a literal spelling. §10.1: a spelling NFC would change is no longer a form the lexer refuses, and a position CPython’s compiler reports counts bytes on the English surface. Appendix A: the same.

  • rev 46 (2026-10-31) — §4.3: a return outside a script is refused where it ended the program, and from __future__ import is read as Python reads it. §4.4: __debug__ reads as True and cannot be bound. §10.1: what Python’s compiler refuses is answered at its place and in its order, ahead of every refusal but a grammar error, where a decision before the call stood.

  • rev 45 (2026-10-31) — §8.15: a keyword argument beside a built-in function’s * binds as Python binds it, where it was refused, and a count that does not fit the keyword is refused in the words of the TypeError Python raises. A * alone among the positional arguments is read after the keyword arguments’ values, and a collection with no element type yet holds nothing.

  • rev 44 (2026-10-31) — §4.4: a name a body binds anywhere is that body’s from its first line, a comprehension’s variable takes a built-in’s name through the comprehension but for its first source, and a capture, an import or a handler’s name takes one as an assignment does. A class the unit has declared holds its name ahead of a built-in.

  • rev 43 (2026-10-31) — §4.4: a name the program binds now comes before a built-in, print or a verb of that name, where a built-in’s name always won. §8.15: a * before a built-in function’s argument spreads the collection, checked as the call written out at each count it could give. §8.6: zip() gives nothing. §8.13: format(value) takes the empty spec.

  • rev 42 (2026-10-31) — §4.2: a sentence is gone that described starting a script in a way the wire does not offer. A stored script is called from a program.

  • rev 41 (2026-10-31) — §4.2: a parameter list reads a / and a bare * as Python’s do, a parameter may follow a variadic, and the listing writes a list as it was written. §4.3: raise alone raises what is being handled, raise e raises what e caught, and from takes None or a caught exception. §13.4: a method’s arguments bind as a script’s do.

  • rev 40 (2026-10-31) — §3.5.1: a backslash reads as CPython’s: octal digits name a codepoint, a backslash and a line feed are nothing, and before any other character both are kept. §4.1, §4.3, §6.4: an expression alone is a statement whose value nothing keeps, a builtin there is one, ... alone does nothing, and an annotation with no value binds nothing. §4.2, §5.5, §13.7: -> None says what no annotation says, str and float are read, and a collection type takes either bracket. §4.3: an import may list modules and take as. §4.4: prefix + is an operator. §13.1: a class with no base may declare nothing. §10.1: those forms are no longer refused.

  • rev 39 (2026-10-31) — §3.5.1: a string opens with either quote, a prefix u or U changes nothing, an f-string may be triple-quoted, and its field may hold a string in the other quote and ask for !a. §3.6, §4.3: a ; ends a simple statement, and a block’s simple statements may stand on its header’s line. §3.7: the reserved words are Python’s keywords and null; this language’s own words are names wherever their form does not stand. §8.7: a printed collection of strings reads back as written. §10.1: those forms are no longer refused.

  • rev 38 (2026-10-31) — §10.1: what a source ending in a carriage-return / line-feed pair answers at its end, and the line a dedent on a continued line is answered past.

  • rev 37 (2026-10-29) — §3.5.1, §3.5.5, §3.5.6, §3.5.8, §3.5.14: a typed value has one spelling, its factory call. §3.5.14: a numeral with no exponent or with an all-zero mantissa is a parse_error, and a magnitude too large to represent is refused at write. §3.8: a typed value has no token class of its own. §5.2, §14.4: a written window is printed back as factory calls, valid_from=datetime("…"). §6.2: case datetime("…"): reads as a class pattern and is refused. §7.1, §7.1.2, §7.5: refusal codes are snake_case, derived_field, not_recorded, limit_exceeded, scope_forbidden, asserted_field and already_in_effect among them.

  • rev 36 (2026-10-29) — §3.7: del, from, import and yield are reserved words, as they are in Python, so every keyword Python reserves is reserved. §4.3: del is a reserved word. Appendix B: import is a reserved word.

  • rev 35 (2026-10-29) — §3.7: async, await, elif, global, is, nonlocal, pass, raise and while are reserved words, as they are in Python, and of Python’s keywords only del, from, import and yield are not. §4.3: pass and raise name nothing. §8.1: is names nothing. §8.8: await and async name nothing. §10.1: a line a while or an elif begins is the header Python reads, before a ( too.

  • rev 34 (2026-10-29) — §3.7: lambda is a reserved word, as it is in Python, and a lambda that begins a path is read as a lambda. §8.8: where the word begins no lambda Python reads, the parse_error is where Python answers it.

  • rev 33 (2026-10-23) — §3.5: each prefix literal spelling denotes the same literal as its factory call. §3.9: the natural language is set by the language parameter alone. A name is bound by bare assignment. Compiling a module, forgetting a script and reading a script’s metadata are wire operations, named in the MCP Reference §9.2. A constructed value is evaluated as the program runs and is never stored. result_type_set sorts by type name. The DerivedField condition names the rule whose text at that moment the store holds no record of. Appendix E describes this document from rev 1.

  • rev 32 (2026-10-21) — §14.2: absent(...), not_asserted(...) and known_absent(...) take a pair, absent(Gap.x.y), in the grammar and in a new Over a pair item; §14.5 says what a pair ranges and that a rule testing an absence over a pair may not conclude a field of its kind.

  • rev 30 (2026-10-17) — §13.8 says constructor chaining is not part of the language, and that construction takes only the instance name and runs no code.

  • rev 29 (2026-10-17) — §7.1 says that compiling a module again removes what its text no longer declares, that this is a removal, and that a read-write key’s compile that would remove is refused with ScopeForbidden, naming what would go and leaving the store as it was.

  • rev 28 (2026-10-17) — §7.1 says a value restore brings back is in force as a new claim at the current instant, the word this document uses for a point in time.

  • rev 26 (2026-10-17) — The header states Version, Status, Audience and the Markdown copy, one field to a line. §1.3 and §11.1 state the version with its revision, alpha · API 1 · rev N. §7.5, Write-verb semantics, is a heading of its own again; it had run into the last paragraph of §7.4. Citations of the MCP Reference and the Python Reference name them so and link to the section they cite. written_by and writers (§5.2, §7.2) name store keys. §8.3’s example of a power that refuses at run time is x ** 0.5 with x a negative real; the literal (-8) ** 0.5 computes, as the same paragraph says. Struck-through passages and the amendment notes of §10.4 are removed, and the page states only what holds.

  • rev 21 (2026-10-14) — This appendix records revisions 13 to 20 as one entry. Nothing else changed.

  • rev 13 to rev 20 (2026-10-13 to 2026-10-14) — every revision in this range but rev 16: new sections and examples in §3, §4, §5, §6, §8, §10 and Appendices A and C, and the sections around them brought into line. A few arithmetic answers are correctly rounded where Python’s platform rounds them wrongly, and so differ from Python’s in the last digit (§8.3).

  • rev 12 (2026-10-12) — §8.7: a * after print or exec written without its parentheses is read as a sign is; a starred or awaited value among its arguments is read as any other; the values after a list that is its first argument are read as the call’s; and what Python raises inside a call that trails a first argument its rule for a forgotten comma read no value of stands, inside a call’s trailer or a subscript’s. §8.10 and §8.14: a condition with no else, or a del target, that ends in a call whose arguments do not parse is weighed as the value before the call; an = that begins a call’s argument names no keyword. §8.11 and §10.1: a starred value alone in parentheses among the targets is taught as Python teaches it; the bracket a :=’s value opens and the source never closes is the answer, a bracket the value closes is read to its close, and a call it never closes whose reading stops on its line is backed off; a call with its arguments out of order side by side after a value among a statement’s elements is answered at the first of them that assigns.

  • rev 11 (2026-10-12) — §8.8: a call of None is read as a call of any other value is, its arguments and what follows them. §10.1: an elif with no if above it is a grammar error at its word, ahead of anything in its line and of a grammar error below it bar a string not terminated or a number refused; a while or an elif line no statement reading the word as a name takes, a form this language does not write among them, is the header Python reads, with no : after a call as with one; an await among a statement’s targets is named as the value it is, and a starred value alone in parentheses among them is answered at its star; a group holding an = reads as no dict key; a call whose arguments stand out of order among an assignment’s or a del’s targets is answered as in any call; a script’s call the line goes on past reads what follows it as a trailer; a bracket never closed is the answer past a := whose value runs into it; and the words excepting a header’s body from the rule for a word after a bracket never closed are struck, a body being read as the statement it is.

  • rev 10 (2026-10-12) — §8.7: a decorator’s print or exec written without its parentheses is that error over a later grammar error, and a character no token begins with does not stand in its place. §8.8: the arguments of a call of a value, and what follows them, are read as CPython reads a call’s, and the bracket answered where that reading reaches the end. §8.9: a generator beside another argument gives way to the call’s bracket never closed where CPython reads the arguments to the end, and an empty place after a keyword argument is answered at its comma. §10.1: a : after an expression that no annotation follows is answered at the :; a call of print the line goes on past keeps no value; and a word Python begins a statement with, after a bracket never closed on its line, is the answer.

  • rev 9 (2026-10-12) — §10.1: a while or an elif before a (, which is read as a call, is the header Python reads where a : stands after the call at the line’s own depth, and is refused at its first word; a grammar error Python’s rules for an invalid value raise inside the value of an = where Python reads a named expression is the answer, before the = is weighed; and that value is read to the primary before a trailer the source never closes, where Python’s reading of the trailer stops on its line.

  • rev 8 (2026-10-12) — §4.3: a comma, a : or a ** in a lambda’s header is no part of what its bracket holds, so a group holding the lambda is named for it, and a { holding one alone is a set display. §8.8: a lambda where Python reads none, as an operator’s operand, is answered at the word with no header too. §10.1: a while’s condition is read as a named expression, as an if’s is; an = whose operand does not parse as Python’s parser reads it is answered at the =; and a True, False or None before a comma at a statement’s head is read as the value it is, so a grammar error later on the line is reached.

  • rev 7 (2026-10-12) — §8.7: a print or exec that CPython reads as a call missing its parentheses is answered at the word over a grammar error on a later line, bar an error CPython raises on its first reading — a bracket never closed, a string not terminated, a number its tokenizer refuses, an error in an f-string’s field, a dict’s own error at a key that reads — and bar a word among the targets a del, a for or an as names. Inside a bracket an = after the word’s values makes no target of them, a keyword argument’s name included. A first argument’s call whose arguments stand out of CPython’s order is read to the first out of place, and the word is the answer. §10.1: in a dict, a call whose arguments stand out of CPython’s order is read only to its name, and a key with no : after it is answered there.

  • rev 6 (2026-10-12) — §8.7: a print or exec before a [, on a line a grammar error fails past the word, is a call missing its parentheses, answered at the word, where the [ opens a list that reads and stands among no targets before an =. A grammar error raised among the values read after the word is the answer instead, and inside a bracket a value straight after the list is a forgotten comma at the list. An await and its value after the word are its first argument. §8.8: after such a print, the call is the answer over an await.

  • rev 5 (2026-10-11) — §7.1: a key scoped read-add reads and writes only what adds to the store. An update over a value its own source holds is refused when it runs, with ScopeForbidden, and the program ends there, as does a commit that would replace a value the store holds. Removal, the marking verbs, restoring or dropping a checkpoint, and compiling over a module the store holds are refused before any statement runs.

  • rev 4 (2026-10-11) — The document names itself by its title where it said “DKE Python Spec”, and its closing line states no release, the Version line being where the release is stated. §1: the MIT grant is spelled licence. Cross-references take the published documents’ short names, the ones the footer uses — Python Reference, MCP Reference. Nothing else changed.

  • rev 3 (2026-10-11) — §1.3, §11.1 and this appendix: the revision is the surface’s, one number shared by every published document, the web pages and the service wire, moving whenever any document changes; this appendix records the revisions at which this document changed. The Version line moves from rev 1 to it. Nothing else changed.

  • rev 1 (2026-10-11) — The document states its revision: the Version line carries rev N, §1.3 names it, §11.1 says what it promises and where a program reads it, and this appendix is added. §10.1: the arguments of a print or exec written without parentheses are read as CPython reads them — a - or + after the word begins an expression, and where a value stands beside it outside a bracket the word is the call; inside a bracket a value beside an argument is answered at the argument; a run of names before the word is read through to the word, which is the call; a later argument that runs into a bracket never closed is that bracket; nothing is read on after a first argument that is a conditional or a lambda unless a string or a sign follows the word. The del passage: the targets are read to a bracket never closed before any is weighed. The never-closed paragraph: a decision met inside an argument gives way to the bracket.


End of the DKE Python — Language Specification.