DKE Python — Language Reference
Version: 13.4.0, changed in rev 171 · alpha · API 1.5 · rev 171
Status: Published reference for DKE Python.
Audience: Programmers writing DKE Python — the programmer’s day-to-day lookup for verbs, types, statements, operators, classes, and modules.
This document as Markdown: dke-python-ref.md — the same text this page is rendered from, for readers and tools that would rather have the source than the page.
1. About this reference
This is the reference manual for DKE Python, the language a DKE customer writes and submits to the DKE service at dke.. It is the published description of the language: what each construct means, what it produces, and how to write it — one entry at a time, with a worked example for each.
Read it alongside its two companions:
- the Python Tutorial, which teaches the same language in task order, from a first program to classes, provenance and modules;
- the MCP Reference, which specifies the transport that carries a program to the service — the tool surface, request shapes, the response envelope and the error model.
If you come to DKE Python with a distinction in mind (unknown against false, disagreement against uncertainty, a hypothesis against a fact), start at §15, which names the verbs behind each.
Where this Reference and the running service appear to differ, tell us: one of the two is wrong, and it matters which.
The language and this document are open under the MIT licence. The DKE service that runs DKE Python is LangSyn’s proprietary product — the language is published; the implementation is not.
DKE Python is a Python-flavoured surface over DKE’s knowledge-operation semantics: def declarations, bare assignment, match/case, try/except … as, and an explicit self receiver. It is indentation-significant. Source files use the extension .dpy.
Its standing rules (§8.8) are Datalog-class — the terminating fragment of logic programming — because a service that runs rules its customers submit has to promise that every one of them terminates.
2. A program at a glance
A DKE Python source file is a module: one or more def declarations. Every declaration is stored under its own name and is independently invocable.
# greet.dpy — a one-declaration module.
def greet(K: string, s: string):
remember(K.s.mood, "cheerful", "op1") # write a claim
m = current(K.s.mood) # read it back (bare assignment)
match m: # consume the discriminator
case active_claim:
print("mood = " + str(m.value))
case empty:
print("no mood recorded")Reading the shape:
def greet(K: string, s: string):— a declaration (§7.1): thedefkeyword, name, typed parameters (name: type), a:and an indented suite.remember(…),current(…)— verb calls (§5): the built-in verb surface written as function calls with a dottedK.slot path as the first argument.s. a match/case— a statement (§7.8): exhaustive discriminator dispatch.m.— field access (§11) on anvalue active_claim.
Compile it as the module greet, then run it at the wire surface with run(source), where the source imports the module and calls the script: import greet, then greet. (MCP Reference §4.2). In another program, call it as any function: greet(K, s).
Learn from inside the language. Send a unit whose body is --help for orientation, or --help <topic> for one of header, declarations, statements, expressions, types, verbs, scripts. Classes, rules and constraints are under declarations.
--help verbs3. Natural language
The natural language a source is written in — which keyword vocabulary its statements use, and the language its responses render in — is set by the language parameter supplied when the source is submitted.
When the
languageparameter is omitted, the language is the one the source declares (below), and when it declares none, English (eng), which is the language this Reference describes. The parameter’s accepted values belong to the wire contract and are listed in MCP Reference §8.1; a value outside that set is declined before the source is compiled.This Reference describes the English surface. Every keyword, verb, operator spelling and rendered response shown here is the
engone.The natural language is a property of how you talk to the service, set per call, and the call decides it.
A source may say what it is written in, on its first line, so that a file read later carries it:
# dke: edition=python.1 language=eng.1 print("written in English, at API 1")The line is a comment, and optional.
python.is DKE Python at API MAJOR 1, API 1.0 and every later 1.N, and1 eng.is version 1 of the English vocabulary, which moves only when a word is removed or respelled. A declared name supplies the parameter a call leaves out; a parameter naming another value is refused; a version, where one is written, must be the one the service serves, and a version left out is that one. A file meant to be compiled later should write both versions. The form and the rules are in the Python Specification §3.9.1.1
4. Types
DKE Python has a closed vocabulary of built-in types, and one way to add to it: a class you declare is a type (§4.1, §8), and a subclass is accepted wherever its base is named (§8.10). There are no user-defined generics and no record types beyond classes. Programs compose by operating on the store through verbs.
4.1 The base types
| Type | Produced by | Category | Notes |
|---|---|---|---|
string |
literals, user data, values | primitive | no ordering; == / != only |
int |
literals, .length, counts |
primitive | signed 64-bit |
bool |
comparison + logical results, True / False |
primitive | |
value |
active_claim. (a claim’s stored content) |
tagged union | int | bool | string | datetime | duration | real | null | blob | scientific | complex; consume with match (§7.8) |
datetime |
datetime("…Z") literal, a datetime value read |
value class | a UTC instant; renders as RFC 3339; a write-verb value + case datetime as d: |
duration |
duration("…") literal, a duration value read |
value class | a length of time; renders as ISO 8601 (PnDTnHnMnS); a write-verb value + case duration as u: |
real |
3.5 literal, a real value read |
value class | a fractional number, written 3.5 or with an exponent 1e5 (Python Specification §3.5.7); renders exactly as CPython repr(float) (fixed, or scientific for extreme magnitudes); computes under + - * / // % ** (§6.3) + ordered comparison (§6.1); a write-verb value + case real as r: |
blob |
blob("…") / blob_b64("…") literal, a blob value read |
value class | an opaque byte string, written blob("…") or blob_b64("…") (Python Specification §3.5.8); renders as canonical uppercase hex; a write-verb value + case blob as b: |
scientific |
scientific("…") literal, a scientific value read |
value class | a number carrying its significant figures, written scientific("6.022e23") (Python Specification §3.5.14); reads back canonical (60.220e21 → 6.0220e22), with 4-s.f. and 5-s.f. of the same magnitude distinct; a write-verb value + case scientific as s: |
complex |
4j literal, 3 + 4j, a complex value read |
value class | a number with a real part and an imaginary part (Python Specification §3.5.15); renders as CPython repr(complex) ((3+4j), 4j); computes under + - * / ** (§6.3), compares with == / != and has no order (§6.1); a write-verb value + case complex as z: |
slice |
slice(stop), slice(start, stop[, step]) |
sequence selector | three bounds, each an int or open (None); xs[s] selects as xs[a:b:c] does; .start / .stop / .step and .indices(n); renders as CPython repr(slice) (slice(1, 5, None)); == / !=, no order, not hashable, not a write-verb value (§6.5) |
range |
range(stop), range(start, stop[, step]) |
integer sequence | the ints from start up to stop, step apart, held as three int bounds and never built; a for walks it; len, an index, a slice (a range), in, .index(x) and .count(x); .start / .stop / .step; renders as CPython repr(range) (range(0, 3)) |
active_claim |
current(K., a field read |
discriminator | one live claim, or empty |
claim_list |
get(K., list_values(K., caveats(K., dependents(h), premises(h), conflicts() |
finite collection | element active_claim |
proof_tree |
why(K., why(K. |
finite collection | element active_claim (the derivation’s participating claims); also fields .root, .truncated |
diagnosis |
why_not(K., what_needs(K. |
finite collection | element string (the lines of the explanation) |
subject_set |
subjects(K., list_subjects(K), list_subjects(K. |
finite collection | element string |
string_list |
list_categories(), list_attributes(K), list_pinned(), list_scripts() |
finite collection | element string |
verify_result |
verify(K. |
discriminator | fields .match, .actual, .expected, .conflicted |
result_type_set |
list_result_types() |
finite collection | element string |
refuse_info |
the binding of except refuse as e (§10) |
handler binding | fields .reason, .teaching_hint, .discriminator (§11.2) |
engine_error_info |
the binding of except engine_error as e (§10) |
handler binding | fields .reason, .code (§11.2) |
empty |
the second arm of an active_claim match |
discriminator sentinel | case empty: only, not a type name |
Beyond the base types there are four constructed type formers — values a program builds, which exist only while it runs and are never stored: list<T> (an ordered sequence, §6.7), tuple<T1, …, TN> (a fixed-length heterogeneous sequence, §6.8), dict<K, V> (a mapping from a hashable key — a scalar or a tuple of hashables — to a value, kept in insertion order, §6.9), and set<T> (a collection of unique hashable elements — a scalar or a tuple of hashables — presented sorted, §6.10). A list<T> is produced by a list literal, a comprehension, s., and a list slice; the other three by their own literals (§6.8–§6.10) and comprehensions (§6.11) — none by a read verb. Each shares the read surface it can support (index, .length, for … in, membership) with per-type refinements, and all render in print / +. None is one of the base types — they are type formers over them. A fifth, iterator<T>, is what zip, enumerate, reversed and iter return: a one-pass reader over a collection, with no length and no position (§6.12). Three more, dict_keys<K>, dict_values<V> and dict_items<K, V>, are the views a dict’s .keys(), .values() and .items() return, which read the dict as it stands whenever they are read (§6.9).
This table is the type vocabulary. empty has a row because you write it in a match arm, and it is not one of the types: it names the absent arm of an active_claim, and the compiler refuses it as an annotation in exactly those words.
Listing order is part of the contract. Each finite collection has a defined order, and claim_list has two — 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. |
the claim in force first, then the superseded ones oldest-first |
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 (canonical name, §5.5) |
Ascending name order is the order sorted() gives — codepoint order, not locale collation — the same sorted order a constructed set is presented in. So xs == sorted(list(xs)) holds for every row that says ascending, and sorting such a read again is dead code. This fixes what the order is, not only that it does not change between runs: the two structural rows bind just as tightly, so a proof_tree always leads with its conclusion and a diagnosis always reads top to bottom as written.
Three groupings drive the static rules:
- Primitives —
string,int,bool. - Finite collections —
claim_list,string_list,subject_set,result_type_set,proof_tree(all store-derived), anddiagnosis, the explanationwhy_notandwhat_needsgive of an absence, plus the client-builtlist<T>. These, together with a constructeddict(walked over its keys) andset(walked in sorted order), are the types afor … inmay iterate (§7.7); every one also has a.length(§11).claim_listandproof_treeyieldactive_claimelements; the other three store-derived collections anddiagnosisyieldstring; alist<T>yieldsT. Aproof_treeadditionally offers a.rootaccessor (§11.2). (Atuplejoins them when its elements have one type to bind — always for a variadic tuple, and for a fixed one whose positions share a type, are all scalars or are all class instances; a fixed tuple holding a collection beside a value of another type is not afor … initerand, §6.8.) - Discriminators —
active_claim(+ itsemptycase) andverify_result. Consumed by amatch, alongside thevalueunion. - The
valueunion — the stored content of a claim (active_claim.), one of these classes:value int,bool,string,datetime,duration,real,null,blob,scientificorcomplex. All but one are write-producible —int,bool,string,datetime,duration,real,blob,scientific,complex(a program writes one and reads it back at that class). The remaining class,null(an explicit absent value), is read-only: it is asserted viaassert(§5.1), no value literal produces it, but a claim carrying one is discriminable on read. Consume the union with a valuematch(§7.8) or anisinstancetest (§8.11) to recover the typed content, or render it directly. datetime— a UTC instant written with thedatetime("…Z")literal (e.g.datetime("2026-01-02T03:04:05Z"), RFC 3339 with a requiredZand optional 1–9 fractional-second digits). A write verb stores it typed and it reads back as thedatetimeclass ofvalue; it renders as its RFC 3339 text.duration— a length of time written with theduration("…")literal (e.g.duration("PT1H30M"), an ISO 8601 durationPnDTnHnMnS; calendar years and months are not accepted). The lowest-order term may carry a decimal fraction of up to nine digits, so durations are exact to the nanosecond (duration("PT0.5S")); only the last term may be fractional, and a span must fit within about ±292 years. A write verb stores it typed and it reads back as thedurationclass ofvalue; it renders as ISO 8601 in a canonical form with trailing fractional zeros removed, soduration("PT90M")reads back asPT1H30Mandduration("PT1.500S")asPT1.5S.real— a fractional number written as a decimal literal3.5, a leading-dot.5(==0.5), or a trailing-dot5.(==5.0);_may separate digits as in1_000.5. A lone.with no adjacent digit is the field operator, so.nameis a field access, and5.is the realname 5.0thenname(a parse error), not a field on an integer. A literal may also carry an exponent1e5,1.5e-3,2E10,.5e2(e/E, optional sign, digits — a real even without a point;1ewith no digit stays an integer1). A write verb stores it typed and it reads back as therealclass ofvalue. It renders exactly as CPythonrepr(float): shortest round-trip, fixed-point for a decimal exponent in[-4, 15](trailing zeros dropped —3.50→3.5,5.0→5.0), scientific (1e+20,1.5e-08) beyond that,-0.0for negative zero, andinf,-infandnanfor the two infinities and a NaN — the same text on the wire, on write-through, and on read-back: a cell holds every real exactly,-0.0, the infinities and a NaN included. An exponent past the top of the finite range is the infinity,1e400beinginf, as in Python; one past the bottom (1e-400) underflows to0.0. There is no NaN literal:math.andnan float("nan")make one, andmath.an infinity (§6.14). A result past the largestinf realisinfor-inf, and an operation with an infinite or NaN operand answers IEEE 754’s value (§6.3, Infinity and NaN). Acase real as r:arm bindsr : real. Reals compute:+ - * /and the ordered comparisons accept them and promoteint → real;/is always true division and always yields areal(§6.3).blob— an opaque byte string written as a hex literalblob("48656C6C6F")(an even count of hex digits) or a base64 literalblob_b64("SGVsbG8=")(RFC 4648; the same bytes) — two input spellings for one byte space; the emptyblob("")/blob_b64("")is a valid zero-byte blob. A write verb stores it typed and it reads back as theblobclass ofvaluein a single canonical uppercase-hex form regardless of the input spelling (a value writtenblob_b64("SGVsbG8=")reads back48656C6C6F); it renders as that hex text whichever spelling produced it, and the two spellings and a read of the same bytes are one value, equal by their bytes wherever blobs compare (blob_b64("AQI=") == blob("0102")isTrue, and a set or dict holds them as one element or key). A blob indexes, slices and joins as Python’sbytesdo (§6.5). Acase blob as b:arm bindsb : blob. There is no bytes literal:b"abc"is refused naming the blob it denotes,blob("616263")(Python Specification §3.5.8).scientific— a number carrying its significant figures, written with thescientific("…")factory (e.g.scientific("6.022e23")) — a mantissa and a required exponent. A write verb stores it typed and it reads back as thescientificclass ofvalue, in a canonical form with the mantissa normalized to one leading digit and the significant figures preserved, soscientific("60.220e21")reads back6.0220e22. The figures are part of the value:scientific("6.022e23")(four figures) andscientific("6.0220e23")(five) are the same magnitude but distinct values — the property arealcannot hold. Acase scientific as s:arm bindss : scientific.complex— a number with a real part and an imaginary part. An imaginary literal is a decimal number followed byj(4j,1.5j,1e3j), and a complex with a real part is written as the sum it is,3 + 4j, as in Python. It renders as CPython does:4jwhen the real part is a positive zero, otherwise both parts in parentheses,(3+4j)..realand.imagread the two parts, each areal, andz.is the conjugate. It computes underconjugate() + - * /beside a complex or a number (§6.3), compares with==and!=(1 + 0j == 1isTrue), and has no order:<and its kin refuse one, as in Python, and so dosorted,minandmax. A write verb stores it typed and it reads back as thecomplexclass ofvalue; a cell holds zero without a sign, so-0jreads back0j. Acase complex as z:arm bindsz : complex. A declaredcomplextakes a number too and keeps its class (z: complex = 1prints1), and so does a collection:[1, 2j]is alist<complex>whose elements keep their classes, sosum([1, 2j])is(1+2j), and it still refusessorted. Every number answers.real,.imagand.conjugate()too, as in Python (§11.2): it is its own real part and its own conjugate, aboolthe int it is, and its imaginary part is a zero of its class. Arealalso answers.is_integer()and.hex(); anintor aboolanswers.bit_length()and.bit_count()and has.numeratorand.denominator; and each answers.as_integer_ratio(), which refuses a ratio past the signed 64-bit range.
There is no None: every name holds one type, and no type means “a value or nothing”. To assert that a stored value is known to be absent, use assert(K. (§5.1), which carries no value; a parameter that may be omitted takes a default of its own type; a script with no result ends with a bare return. null is written only as an open bound of a validity window and as the case null: arm that matches one, and None only as a whole argument of slice(...), where it leaves that bound open (§6.5). Both words are reserved: neither can be bound as a name, and reading either where a value goes is refused with this lesson. A type is written in four places — a parameter, a binding annotation, a collection’s element type, and a script’s or value method’s return — and the same names are valid in all four. They are any of the value-class scalars (string, int, bool, datetime, duration, real, blob, scientific, complex), a read-result type (claim_list, active_claim, …, for calls between procedures), a collection type (list<int>, dict<string, int>, …), or a declared class (§8), which stands for an instance of that class. CPython’s spellings are read too: str is string, float is real, list[int] is list<int> and dict[str, int] is dict<string, int>. A return annotation of None alone, -> None:, says what no annotation says. A class name is a type wherever any other is, so list<Animal> names a list of animals and -> Animal a script that answers one; which instances fit such a name is §8. A value-class-scalar parameter supplied over the wire is passed as its canonical text (a datetime as RFC 3339 UTC, a duration as ISO 8601, a real as a decimal with a point, a blob as hex) and refuses if malformed. A class field (§8) is typed by a value-class scalar only — a field holds one stored value. Neither empty nor null is a type — both name absence; value arises only from .value field access.
4.2 Handler bindings
The except refuse as <var> / except engine_error as <var> handlers of a try (§7.9) each bind a value whose fields are all string and always present — a field with nothing to say holds "", since the language has no absent value. A refuse binds .reason, .teaching_hint, and .discriminator; an engine error binds .reason and .code. The fields are listed in §11.2. These types cannot be written as parameter types.
.reason is written for a reader and is what a program should report. The .discriminator of a refuse and the .code of an engine error play the same role for a program that must branch rather than report: each is a short tag naming which failure this is. Two kinds of .discriminator are part of this contract. A refusal you raised yourself with raise <ExcType>(…) (§7.15) carries the exception name your own source wrote. A refusal the store raises because a call cannot succeed as written carries the code the MCP Reference publishes for it as refuse_reason, such as conflicting_write, pinned_address or invalid_value, and a program may branch on those codes as a client of the service does. Every other tag is not part of this contract and is not enumerated here — branch on the tags you have observed and keep a fallback arm.
.teaching_hint suggests how to correct the call, and is empty when there is no suggestion. It is an aid to a human reader — never a second reason, and never something to branch on.
5. Verbs
The built-in verb surface is written as function calls — a verb name and a parenthesized, comma-separated argument list: remember(K., current(K., list_attributes(K). The first argument is the dotted K. slot path (or a name literal or a claim, per the verb). Read verbs bind by assignment (cur = current(K.).
Path slots: K (kind), K. (kind + subject), K. (full cell), K. (kind + attribute). Each segment is either a bare name written literally — remember(Greeting., which is what the examples throughout use — or a string-typed parameter or binding in scope, which is how a script is parameterized over the slot it writes. The two forms compile to the same path; a bare name is not a variable reference and needs nothing declared. A K. cell may also be written as an instance’s field, s., for every verb that takes one, and a K. subject as the instance itself, s, for caveats and list_attributes (§8.3).
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, and like every verb’s word they are soft keywords recognized as a verb only immediately before (, so the same spelling stays a usable ordinary identifier elsewhere.
A verb takes only the keyword arguments its section names, such as known_at= on a read. A keyword it does not take is refused naming the ones it does, or saying it takes its arguments by position. Its keyword arguments come after its positional ones, as in any Python call. One it takes, written before them, is refused saying they come first, and a call writes one: a second, after another, is refused saying so. It takes as many positional arguments as its section writes, and one more is refused at that argument, saying how many the verb takes. So are one more to a fold (§5.8), to len, isinstance or issubclass, and to a literal factory such as datetime("…"). Where its section writes a path, the path is the whole argument, and an argument that goes on past it as an expression, as current(K. does, is refused at the argument, saying so. So is an argument there that is no path, as in current("s"), saying what a path there wants. A call short of an argument it takes, as remember(K. is, is refused at its ), saying how many it takes at least. So is one to a hypothesis, a fold (§5.8), len, sorted, isinstance or issubclass.
5.1 Write verbs (statement position)
remember(K.s.a, v, src)
Records value v at cell K. attributed to src. Permissive. The value is a scalar literal (string / int / bool) and is stored with its type — the claim reads back through .value at the type it was written (§4.1, value), and query verbs match it type-aware (subjects, verify, agreement). An optional pair of trailing datetime bounds gives a validity window — the fact is valid only between them (both inclusive); either bound may be null for an open side. Omit the pair for an always-valid fact. A windowed write is selected by an as-of read (§5.4) whose time falls in the window — that is how it is read back, and on the traversal verbs how it is found at all. The same source restating the same value over a different window that overlaps the one it gave is refused as a conflicting write — use update to move the window. A window that does not overlap it is a second claim, for another period.
remember(K.s.color, "orange", "op1") # string
remember(K.s.count, 42, "op1") # int — reads back as int
remember(K.s.rate, "3.0", "src", datetime("2019-01-01T00:00:00Z"), datetime("2021-01-01T00:00:00Z")) # valid 2019–2021
remember(K.s.plan, "active", "ops", datetime("2024-01-01T00:00:00Z"), null) # valid 2024 onward, open endThe source src is a label: any non-empty text naming where the value came from — a citation, a register, a URL — read back exactly as written. Two texts that differ in any character are two sources, so "2026 finance act" and "2026-finance-act" are not one. A few characters are reserved and may not appear in a source. An empty or reserved source written as a literal does not compile; one arriving from data is a refusal you can catch per row, and it names the character it met by its code point.
remember(Rate.vat.pct, 25, "2026 finance act")
remember(Rate.food.pct, 15, "https://lovdata.no/lov/2009-06-19-58")A kind whose name differs from a kind the store already holds only in the case of a letter A to Z is refused, naming the kind held, and nothing is written: with Cat held, remember(cat. is refused and teaches Cat. Letters beyond A to Z are not compared this way, so Øy and øy are two kinds. A read of such a name is not refused; it finds nothing. The writer may also state facts about the claim itself with the one keyword argument, qualifiers= — a dict from name to value, after every positional argument: how the value was obtained, how sure the writer is, what it was measured with. Each name follows the rule a path segment does; each value is a string, int, bool, real, datetime, duration, blob, scientific or complex; at most 16. They are the writer’s own account, read back as .qualifiers (§11.2) on every read that returns the claim, and the store acts on none of them. The same source restating the same value with different qualifiers is refused as a conflicting write — use update to revise them.
remember(Reading.r1.temp, 21, "lab", qualifiers={"method": "observed", "confidence": 0.8})
remember(Reading.r1.temp, 23, "forecast", datetime("2026-10-01T00:00:00Z"), null,
qualifiers={"method": "predicted"})update(K.s.a, v, src)
Supersedes the claim src holds at the cell; establishes one on an empty cell. Accepts the same optional trailing datetime validity window as remember, and the same qualifiers= — which is how a source revises what it said about its claim. Restating exactly what src already holds there — the same value, qualifiers and window — changes nothing. A windowed update supersedes only the claims of src whose windows overlap its own: the same value over a different, overlapping window is a revision, and a claim for a period the new window does not reach stands, so one source records a value that changed over time by writing each period with update.
update(K.s.mood, "playful", "op2")
update(Rate.vat.pct, 25, "finance act", datetime("2024-01-01T00:00:00Z"), datetime("2024-12-31T00:00:00Z"))
update(Rate.vat.pct, 27, "finance act", datetime("2025-01-01T00:00:00Z"), null) # 2024 still reads 25Choosing between them. Neither verb is a fallback for the other. remember asserts something new; update says revise my earlier reading, and because it also establishes a value on an empty cell it works the first time as well as the tenth. A producer meant to run more than once — a loader, a CI step, anything pointed at data that changes — means update, and should ask for it by name. remember being Permissive means a re-assert succeeds rather than refusing; it does not make the two interchangeable, because what is lost is a reader of the program being able to tell which was meant.
Supersession is per SOURCE, for both verbs. Revising your own claim leaves every other source’s claim at that cell standing, so a re-run never quietly settles a disagreement the store is holding — see prefer (§5.1) for the verb that records a judgment between sources. To replace another source’s value rather than stand beside it, remove that claim with forget(K..
A write establishes the kind. Neither verb requires the kind to exist beforehand: writing K. when nothing named K has been recorded stores the claim and brings K into being. It is the complement of forget(K. below, which removes the last subject and takes the kind with it — a kind stands for exactly as long as something is recorded under it.
Declaring a class (§8) fixes the schema a kind’s fields are checked against; it is not permission to write, and the two directions are deliberately not symmetric. Within a declared class, writing a field the class does not declare is a type_error (§8.3). A write to a kind no class declares is accepted. Knowing which of those you are relying on is worth settling before you build on it: a producer that writes its own bookkeeping under an undeclared kind is using the second, and will keep working; a typo’d field name inside a declared class is the first, and will be refused.
A module-qualified path and an unqualified one name different cells. m. and K. are two cells, not one, and a value written through one is not read back through the other. The qualified path is open only to a kind m declares with class (§9.6).
forget(K.s.a) / forget(K.s) / forget(K.s.a, v, src) / forget(K.s.a, datetime("…")) / forget(<address>, recorded_before=datetime("…")) / forget(<claim>)
Permanently removes data. Hard and irreversible — the value is physically reclaimed, nothing is kept in history that a read can reach, and nothing in this language puts it back.
One qualifier, and it is not a language feature: while a checkpoint stands, a removed value is kept aside where no read reaches it, so the restore tool in the MCP Reference can bring it back — a value that was in force as a new claim, an older version into the history where it stood; a re-assertion, not a rewind. It needs a read-write-delete key. With no checkpoint standing at the moment of removal, nothing is kept and nothing can be reinstated.
Path-scoped: forget(K. reclaims every value ever recorded at the cell, not just the current one, and forget(K. removes the whole subject, after which its kind is gone.
Claim-scoped: forget(K. removes the single claim recording value v from source src, leaving every other claim at that cell untouched — the way to drop one side of a disagreement without discarding the other. Both v and src are required, because a value alone does not name a claim: two sources may record the same value at one cell.
forget(Project.dke.license, "MIT", "registry_one")v is matched by value and by type, and both halves matter because a cell can hold the same text in two types. 5 and "5" are different claims, so forget(K. does not reach an integer claim — write the value the way you wrote it, and forget(K. reaches it. Every type you can write, you can name: string, int, bool, real, datetime, duration, blob, scientific and complex.
remember(Reading.probe.n, 5, "sensor") # an int claim
forget(Reading.probe.n, "5", "sensor") # refused - no text claim there
forget(Reading.probe.n, 5, "sensor") # removes itType equality here is exact. == compares int and real numerically, so 1 == 1.0 is True and a query reaches both — but a removal names a specific claim and takes it permanently, so an int literal does not reach a stored real. Write the real and it does.
Naming a claim the cell does not hold is a refuse, not a silent no-op; call get(K. to see the values and sources actually recorded there. Where the value and source match and only the type differs, the refusal says so and names the type it found — a read renders 5 and "5" identically, so that is the one place the difference is visible. To see a stored value’s type yourself, match over a claim’s .value (§7.8).
By claim: forget(c) removes the one claim c — a claim a read returned, or its .claim_id — and nothing else. It is how you choose between two claims that agree on value and source and differ only in their periods, which the claim-scoped form cannot tell apart: it removes the first.
remember(Rate.eu.vat, 25, "tax", datetime("2020-01-01T00:00:00Z"), datetime("2020-12-31T00:00:00Z"))
remember(Rate.eu.vat, 25, "tax", datetime("2022-01-01T00:00:00Z"), datetime("2022-12-31T00:00:00Z"))
h = get(Rate.eu.vat, datetime("2022-06-01T00:00:00Z"))
forget(h[0]) # the 2022 claim; 2020 staysThe claim is looked for in the store as it stands. One removed since it was read, or written inside a branch that rolled back, is refused, and so is a .claim_id two claims share: nothing is removed, and neither is chosen. A superseded claim is removed like any other. The claim is the whole address, so nothing follows it — write forget(c), not forget(c..
Time-scoped: forget(K. removes the values at that cell whose validity window covers the instant named, and leaves the rest. It addresses by when a value was true rather than by what it says, so it is the way to drop a period you no longer want to keep without knowing what was recorded in it. The same instant you would pass to current(K. to read what was in force selects what is removed.
forget(Sensor.probe.reading, datetime("2021-06-01T00:00:00Z")) # the values in force thenA time-scoped call takes a K. path only. An instant narrows which values at one cell are named, and a subject holds many cells with many windows rather than one window to narrow; forget(K. already removes a whole subject and needs no instant to do it.
An instant no value covers removes nothing and succeeds — unlike the claim-scoped form, which refuses. The difference is what is being named: a claim either exists or it does not, so naming one that is absent is a mistake worth reporting, while a moment nothing was true at is an ordinary answer.
Range-scoped: forget(<address>, recorded_before=datetime("…")) trims history. It removes the older versions that fall before the instant, and never a value in force — whatever an address currently says survives every range, including one whose instant is far in the future. That is the difference between this form and every other one above: the others can empty what they name, and this one cannot.
Three widths. The range is the only form that takes more than a cell, and that is the point of it — trimming a year of history one cell at a time is not a thing anyone can do:
| Address | Trims the history of… |
|---|---|
forget(K. |
one cell |
forget(K. |
every cell of one subject |
forget(K, <range>) |
every cell of every subject of the kind |
Each answers a different number over the same store, and the safety property is the same predicate at all three, so none of them can reach a value in force. A kind-wide range cannot empty anything.
forget(Sensor.probe.reading, recorded_before=datetime("2026-01-01T00:00:00Z"))
forget(Sensor.probe, recorded_before=datetime("2026-01-01T00:00:00Z"))
forget(Sensor, recorded_before=datetime("2026-01-01T00:00:00Z"))K is an address only with a range. A bare forget(K) is refused at compile time and told about this form: emptying a whole kind is not something to reach by leaving a path short. To remove one subject entire, name it — forget(K..
Four spellings, choosing an axis and a boundary:
| Keyword | Removes the older versions… |
|---|---|
recorded_before=datetime("…") |
written before that instant |
recorded_at_or_before=datetime("…") |
written before it, or exactly at it |
valid_before=datetime("…") |
that stopped being true before that instant |
valid_at_or_before=datetime("…") |
that stopped being true before it, or exactly at it |
forget(Sensor.probe.reading, recorded_before=datetime("2026-01-01T00:00:00Z"))The two axes answer different questions and select different versions, so choosing between them is the substance of the call rather than a detail. Ask recorded_* when the reason is age — this was written years ago and I no longer need it. Ask valid_* when the reason is subject matter — this stopped being true before we cared. A version whose validity window was never closed has no valid_* answer at all and no valid_* range reaches it, which is why a valid_before= call can report removing nothing where recorded_before= would remove several. That is not a failure; it is the two axes disagreeing, which they are entitled to do.
There is deliberately no after form. It would name the versions newer than the instant, which includes the value in force — so it would undo part of a cell’s history rather than trim its tail, and the safety property above would not hold. Removing a current value is what forget(K. and the claim-scoped form are for, and both say so at the call site.
Removing nothing is a success, not a refusal: a cell that has only ever held one value has no older versions to trim, and every range over it reports zero. Use would_forget (below) when the difference matters before you act — and at the wider addresses, prefer to.
The two-argument forms are told apart by the type of the second argument. A datetime is the time-scoped form; anything else begins the claim-scoped one and its src is still required.
Removal cascades into facts derived from the target rather than refusing while they exist; a derived conclusion that some other standing rule still supports is re-derived and stays. Needs a read-write-delete key.
forget(K.s.mood) # the cell and its full history
forget(K.s) # the whole subject; its kind is gone
forget(K.s.mood, datetime("2024-01-01T00:00:00Z")) # only what was true at that instantThis verb takes no confirmation, and that is deliberate. The tool surface gates forget_module behind confirm: true and leaves forget_rule and forget_constraint ungated, sorting them by whether what they destroy can be brought back: a module’s data cannot, a rule’s conclusions can, since recompiling the module reinstates them, and a constraint destroys nothing but its own declaration. That sorting has no answer for forget, because here it depends on the ARGUMENT rather than on the verb. Three cases, and only the last is beyond recall:
- a rule conclusion comes straight back — the standing rule re-derives it on the next read;
- a fact a module’s top level wrote comes back on the next
importof that module, because importing runs the top level again; - a fact you wrote from your own data does not come back from anything in the store, and takes its derived conclusions with it.
So a blanket confirmation would add a step to two cases that do not need one, and a caller typing it for the third has already decided. What guards an address you cannot afford to lose is pin — it refuses forget(K. and forget(K. while it stands — and it has to be set in advance, which is the honest shape of the guarantee: this surface offers no undo after the fact.
assert(K.s.a, src)
Asserts the value at K. is known to be absent per src. No value.
assert(K.s.chip_id, "vet")prefer(K.s.a, src)
At a location where sources disagree, states that you go by src. Records your judgment beside the claims: every claim stays, nothing is discarded, and the location stays in disagreement. Reads are unchanged — get(K. still returns every claim; each carries .preferred, True on the one you act on. Standing rules are unchanged too: a preference marks the claim you go by, and does not change what any rule derives. src must have recorded a value at K.: a location with no value, or a source that recorded none there, refuses as not_recorded. Preferring the source already preferred refuses as already_in_effect while that preference still covers every source at the location; once another source disagrees there, stating it again is a new judgment over the new set. Naming a different source replaces the judgment.
prefer(Sensor.r1.temp, "registry_two")unprefer(K.s.a)
Withdraws the preference at K.. Removes the judgment only — every claim at the location survives, as does the disagreement. Where no preference is recorded it refuses: as already_in_effect when the location holds a value, as not_recorded when it holds nothing at all.
unprefer(Sensor.r1.temp)pin(K.s.a)
Protects the address. While it is pinned, forget(K. is refused, and so is forget(K. — removing the subject would take the pinned cell with it — and so is removing one claim there, by value and source or by the claim itself. The address must hold a value: pinning one that holds none refuses as not_recorded, and pinning an address already pinned refuses as already_in_effect — catch that one to make sure of a pin.
pin(K.s.color)unpin(K.s.a)
Lifts the pin, so the address can be removed again — any pin that stands, whatever the address holds now. Where nothing is pinned it refuses: as already_in_effect when the address holds a value, as not_recorded when it holds nothing. list_pinned() reads what is currently protected, naming each address the way pin and unpin take it — a kind a module declares qualified by the module — so an address it lists can be written back into unpin.
unpin(K.s.color)5.2 Read verbs (expression position)
current(K.s.a) → active_claim
The single live claim, or the empty case. Consume with match or .value. An optional datetime reads the value as of another time — past or future: current(K.. Refuses where no single claim is the answer — a location holding two live values, whether they were recorded there or a rule concluded both. Catch it, or read the set with get.
cur = current(K.s.color)get(K.s.a) → claim_list
The full claim history. Iterable. An optional datetime reads the claims valid as of another time — past or future: get(K..
hist = get(K.s.mood)caveats(K.s) → claim_list
The claims at subject K. that are in disagreement — the same question conflicts() asks store-wide, narrowed to one subject. An instance is a subject: caveats(s) asks it of the instance s holds (§8.3).
cav = caveats(K.s)conflicts() → claim_list
Every claim currently in disagreement, store-wide. (No slot argument.) Two claims disagree when they stand at one location with different values. Who recorded them does not enter into it — a rule that concludes on both sides of a disagreement is one producer, and the location it concludes at is in disagreement like any other.
confs = conflicts()Answers which locations are in disagreement. Disagreement is a state the store holds, not an error: both claims remain and reads keep working. The service never chooses between the claims — you say which source you go by with prefer, and that judgment leaves the disagreement standing. A standing rule reading a location in disagreement derives from each value that satisfies its condition, so the disagreement travels into the conclusion instead of stopping at the premise; why on a conclusion shows the claims it was drawn from.
How it clears depends on what disagrees. Between claims you wrote, forget one of them, or have one source update its claim to the value the other holds. Between conclusions those do not reach: a conclusion is in force exactly while its condition holds (§8.8), so forget withdraws it and the rule records it again, and update adds a claim of your own beside the two already standing. Two rules that both hold and conclude different values for one field is that case, and both conclusions carry the same derived provenance, so there is no source to prefer between them either. It clears when the rules stop both concluding — narrow one rule’s condition, or remove one of them with forget_rule.
list_values(K.a) / list_values(K.a, datetime("…")) → claim_list
Every value claim for attribute K. across subjects. An optional datetime lists the claims valid as of that time instead of now (§5.4).
vals = list_values(K.color)
then = list_values(K.color, datetime("2027-06-01T00:00:00Z"))subjects(K.a, v) → subject_set
Subjects of kind K whose attribute a currently equals v. The value v is a scalar literal (string / int / bool) and the match is type-aware: subjects(K. matches the int 42, not the text "42". Numbers are one family, so an int literal also matches a stored real of the same number — subjects(K. finds a 5.0 — and a bool is the number it is, so subjects(K. finds a stored True. It is the number that is compared, never its spelling.
hits = subjects(K.color, "orange") # text match
warm = subjects(K.temp, 42) # int match — not the text "42"
five = subjects(K.amt, 5) # finds a stored 5 and a stored 5.0list_subjects(K) / list_subjects(K.a) → subject_set
Every subject of kind K, or only those carrying a claim for attribute a. The two differ whenever a subject is reachable through an attribute its siblings lack — that subject is in the first and not the second. Either form takes an optional trailing datetime, listing the subjects a traversal would find at that time rather than now (§5.4).
everyone = list_subjects(K) # every subject of the kind
coloured = list_subjects(K.color) # only those with a colour
then = list_subjects(K, datetime("2027-06-01T00:00:00Z"))list_attributes(K) / list_attributes(K.s) → string_list
Attribute names for a kind, or for one subject. The two answer different questions and only the second takes an as-of time (§5.4): K gives the attribute names recorded for the kind, which describes the store’s shape and already covers every time; K. reads one subject’s claims, which are what a validity window applies to. An instance is a subject, so list_attributes(s) lists the attributes of the instance s holds, and takes the as-of time the K. form takes (§8.3).
attrs = list_attributes(K) # the kind's attribute names
mine = list_attributes(K.s) # this subject's, now
then = list_attributes(K.s, datetime("2027-06-01T00:00:00Z")) # …and at that timeA read of a kind that is really a missing import. A read naming a kind the store does not know refuses. current, get, subjects, the list_* reads and forget say instead that an import is missing when it is: when the name asked for is itself a stored module, and when some stored module declares a class of that name. Each refusal names the module and the import that reaches it. Both are gated on the kind being absent, so an enumeration that is legitimately empty still answers empty.
list_categories() → string_list
Every kind (category) known to the store. A kind a module declares is named in the qualified form <module>.<Kind> — the form the read verbs take, given an import of that module (§9); a kind no module declares is named as written. The name a listing gives you is a name the language accepts. Takes no as-of time, for the same reason list_attributes(K) does not — a kind is recorded rather than valid, so the answer already covers every time.
cats = list_categories() # e.g. ["Loose", "shop.Widget"]5.3 Provenance and reasoning
why(K.s.a) → proof_tree
The derivation of the current value. The returned proof_tree is an iterable finite collection of the claims that participated in the derivation: .length, for … in, and [i] yield those claims, each an active_claim carrying .value / .source / .trust / .created_at / .path (§4.1, §11.2). .root still yields the top active_claim — the queried claim. A participating claim may stand at a different location than the one you asked about — that is what a premise is — so read .path rather than assuming. A premise that tested an absence — absent, not_asserted, a none(...) — matched no claim, so it is not an element; the elements are the conclusion and the claims premises (below) lists. 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. A known_absent premise read a recorded absence, which is a claim, and is one.
A derivation can run deeper than a proof is rendered. .truncated is True when the answer stops short of the whole derivation, and False when what you have is all of it. Check it before treating a proof as complete.
pt = why(K.s.color)
n = pt.length # how many claims participated
r = pt.root # the queried claim
first = pt[0] # a participating claim → active_claim
for c in pt: # iterate the derivation's claims
print("via " + str(c.source))why(K. — the derivation as it stood then. An optional trailing datetime (§5.4) answers with the premises that were in force at that instant rather than now. A conclusion rests on claims that carry validity windows (§5.1), so it can hold at one time and not another; the anchored form is what reads back the derivation that used to hold.
This is how you account for an answer that changed. Ask the same cell at two instants, and the difference between the two trees names the premise that moved:
before = why(Order.o1.review, datetime("2024-03-01T00:00:00Z"))
after = why(Order.o1.review, datetime("2024-09-01T00:00:00Z"))
for c in after:
print("now rests on " + str(c.path) + " = " + str(c.value))
for c in before:
print("then rested on " + str(c.path) + " = " + str(c.value))An anchored derivation may be absent where the bare one is not, and the reverse. Absence here is the empty discriminator (§4.1), not a zero-length proof_tree — so .length, [i] and for … in are for an answer you have established is a proof, with match as for a read: case proof_tree: for a derivation, case empty: for none (§7.8). 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 is the verb that explains the absence.
why_not(K.s.a) → diagnosis
Why a conclusion is not there. why needs a claim to explain; when a read comes back empty the question is a different one, and this answers it — naming the condition that blocked the conclusion, at the cell you can act on.
It separates cases that every other verb renders identically. An empty read looks the same whether a condition was never recorded, was recorded with a validity window that does not cover the time you asked about, was recorded and fails its test, was recorded as absent with assert, was recorded with a value that has no order under <, <=, > or >= (a blob, or a recorded absence), is compared under one of those with a value that has no order, is required to be absent and is present, or is recorded by sources that disagree. A condition comparing two of your fields, such as o., is stated with both, as you wrote it. Those need opposite fixes, so the answer says which one you have — recording a fact you already hold is not a weaker version of the right move, it is the wrong one.
When no standing rule concludes the cell at all, that is its own answer — your model has no path to the conclusion, which is a different problem from an unmet condition and usually a more useful thing to learn.
When a rule has every condition it needs and still concludes nothing, the answer names that rule and the reason, which is one of three. A fold it computes cannot take the values it reads — a string where it sums; count takes any value. A comparison it concludes cannot rank the values it reads — a value with no order, or two values of different types. Or the arithmetic it computes has no answer for them — a divisor of zero, a value its operator does not take, or a result its type cannot hold — and the answer shows that operation over the values it read, such as 6 / 0, with the fields they came from. None of those is a fact to record, and waiting draws nothing: the move is changing those values. what_needs says the same of that rule, before the conditions it names as recorded.
A condition is named at its cell as your rule reads it, with its value spelled the way you write one — True, "5", blob("00FF"). A subject or field the rule leaves open is named by your rule’s own name for it, in backticks: E[`e`]. is a condition any E can meet, the rule’s e, and not a subject called e. Where the rule reads a condition’s subject out of a field your sources disagree on, the condition is named once for each subject they give, each at its own cell, and what_needs does the same.
A condition whose subject the rule leaves open names no one cell, so it is answered for its whole kind. It is recorded and failing when some E holds that field and none meets the test, and nothing is recorded only when no E holds it. When each of the rule’s conditions on that subject is met by some subject, and no one subject meets them all, the answer says so, and names the conditions together. That is one finding, and its fix is on one subject rather than a missing fact. It names no subject, and it is the same whichever order you wrote the conditions in. A condition that needs such a field absent is answered the same way: some E holds it.
The cell you ask about is checked before any of that. If it holds a claim whose validity window does not cover the time asked about, you are told so and given the value — the fact is there and out of force, and what it needs is a wider window or an as-of the window covers, not another write. Whatever the rules have to say follows it, so you get both. When the conclusion does hold, you are told so and pointed at why.
The result is an iterable finite collection of lines: printing it shows the whole explanation, iterating it takes one line at a time. It is a read and writes nothing, and it takes no as_of — a diagnosis is about the store as it stands.
This verb exists only in English. A unit compiled under another language cannot call it — the English spelling is not a verb there — and the remedy is the unit’s language tag rather than an edit to the program (§12).
d = why_not(Device.b.online)
print(d) # rule `device_is_online` needs Device[b].checks > 0,
# and nothing is recorded there
win = why_not(Device.w.online)
print(win) # rule `device_is_online` needs Device[w].checks > 0,
# and Device[w].checks is recorded as 2 but not in
# force at the time asked about
n = d.length
for line in d:
print(line)A rule over a subject it leaves open, where fern is thirsty and in the north bed, and cactus is in the south bed and not thirsty:
bed = why_not(Bed.south.water_on)
print(bed) # rule `open_valve` needs Plant[`p`].thirsty == True
# and Plant[`p`].bed == "south" together: each is met
# on its own, but no one `p` meets all of the rule's
# conditions at oncewhat_needs(K.s.a) → diagnosis
What would make a conclusion hold. Where why_not names what is blocking now, this names the facts that would unblock it — walking through conditions your standing rules can themselves derive, and stopping at the ones only you can supply. Each line says which it is, so the answer is a list of things to do rather than a description of the problem. A condition you already recorded, whose validity window does not cover the time asked about, is named as that rather than as a fact to record — what it needs is a wider window or a different as-of, not another write. So is a condition whose value you recorded and which does not meet the test, has no order under <, <=, > or >=, is compared under one of those with a value that has no order, or is recorded by sources that disagree: each is named as recorded, with what is wrong with it, and only a condition with nothing recorded is a fact to record. A condition recorded as absent with assert is named as that, since the rule needs a value there and the absence you recorded is not one. A condition whose subject the rule leaves open is answered for its kind, as why_not answers it, and a condition met on its own that no one subject meets together with the rule’s other conditions is named as that.
A condition that needs a field absent, such as absent(c. or a none(...), is the other way round. When the field holds a value it is named as present, with the move that clears it. Where you recorded the value, that is removing it or narrowing its window. Where a rule concludes it, the move goes through that rule, since a conclusion forget removes is drawn again while its premises hold, and why shows what it rests on. It is never a fact to record, and the rules that would conclude it are not walked into, since what they need would make it hold. A condition that needs a field absent and has it absent is listed as held.
The walk is bounded by the shape of your rule set: each rule is entered once per question, so a rule set that refers to itself in a circle reports the circle rather than following it. Like why_not it is a read, takes no as_of, and returns a diagnosis.
Like why_not, this verb exists only in English: a unit compiled under another language cannot call it, and the remedy is the unit’s language tag rather than an edit to the program (§12).
d = what_needs(Device.b.online)
print(d) # via rule `device_is_online`:
# have Device[b].power
# need Device[b].checks > 0 (record this)
win = what_needs(Device.w.online)
print(win) # via rule `device_is_online`:
# have Device[w].power
# need Device[w].checks > 0 (recorded, but not in
# force at the time asked about)Over the same plants:
bed = what_needs(Bed.south.water_on)
print(bed) # via rule `open_valve`:
# need Plant[`p`].thirsty == True (met on its own,
# but not together with the rule's other conditions)
# need Plant[`p`].bed == "south" (met on its own,
# but not together with the rule's other conditions)A rule that needs c. and c. absent, where c1 has blocked recorded, and another rule concludes jammed for c2:
one = what_needs(Crate.c1.ok)
print(one) # via rule `ok_when_clear`:
# have Crate[c1].x
# need Crate[c1].blocked to be absent (it is present
# — remove it, or narrow its window)
# have Crate[c1].jammed absent
two = what_needs(Crate.c2.ok)
print(two) # via rule `ok_when_clear`:
# have Crate[c2].x
# have Crate[c2].blocked absent
# need Crate[c2].jammed to be absent (it is present,
# and a rule concludes it — `why` shows what it
# rests on)verify(K.s.a, v) / verify(K.s.a, v, src) → verify_result
Checks whether K. currently holds v (optionally per src). The value v is a scalar literal (string / int / bool) and the check is type-aware: .match is True only when the stored value and its type agree with v — except that int, real and bool are one numeric family, so a stored 5.0 matches the literal 5 and a stored True the literal 1. Fields .match (bool), .actual, .expected (string).
v = verify(K.s.color, "orange")
if v.match:
print("confirmed")agreement(K.s.a, v) → int
Returns how many distinct sources recorded v at K. — the quantitative sibling of verify (verify asks is v present?, agreement asks how many sources say v?). Type-aware, like verify. A count of corroboration, not a judgment of truth: the language reports how many sources recorded the value and never ranks, certifies, or assumes the independence of those sources — which ones to trust is the caller’s judgment (read them by name with get). Counts every source, so it takes no source argument (a type_error); an unrecorded value reports 0.
orange = agreement(K.s.color, "orange") # e.g. 2would_forget(<address>, recorded_before=datetime("…")) / would_forget(<claim>) → int
Returns how many older versions the matching forget range would remove — and removes none of them. Ask what a removal costs before paying for it.
It takes the same four keywords as the range form of forget (recorded_before / recorded_at_or_before / valid_before / valid_at_or_before) and the same three addresses — K., K., K — so every removal you can write has a preview you can write. With an address, one of the keywords is required: there is no bare would_forget(K., because previewing an unbounded removal only ever answers everything here, which needs no instrument and would make the one call worth checking look as though it had been checked.
n = would_forget(Sensor.probe.reading, recorded_before=datetime("2026-01-01T00:00:00Z"))
if n > 100:
print("that would drop a lot of history")
else:
forget(Sensor.probe.reading, recorded_before=datetime("2026-01-01T00:00:00Z"))It is a read, so a read-only key can call it: finding out whether you want to remove something should not require holding the ability to remove it. It records nothing — a preview leaves no trace in the removal record, which stays a record of what was actually destroyed rather than of what was considered.
The number the preview gives is the number the removal reclaims, at whichever width you asked — the two read the same selection rather than agreeing by convention, so a preview taken at K and a removal run at K cannot disagree.
would_forget(c) previews forget(c), and takes no keyword: it names one claim rather than a range. It returns 1 while the store holds the claim c, and refuses as forget(c) would when it does not.
0 means the range names nothing, which is the ordinary answer at a cell with no history to trim, and it is worth distinguishing from a valid_* range that finds nothing because the versions carry no closed validity window (see forget above). Asking the recorded_* question as well is how you tell those two apart.
writers(K.s.a, v) → int
Returns how many distinct store keys wrote v at K. — agreement on the other provenance axis. agreement counts what was cited; writers counts who wrote. The difference matters: three sources cited by one key is corroboration one party assembled, while one source written by three keys is three parties independently recording the same thing. Type-aware and judgment-free like agreement, and it takes no source argument (a type_error). A claim written outside an authenticated call has no key and is not counted — so writers can be lower than agreement at the same cell, and reports 0 where every claim was written unauthenticated.
a = agreement(K.s.color, "orange") # 2 — two sources cited
w = writers(K.s.color, "orange") # 1 — but one key wrote bothdependents(<claim>) → claim_list
The claims concluded directly from a claim — one step down its downstream in the dependency graph. The operand is a claim read from get/current, or an element of an earlier dependents (bind it to a name first), or a claim’s .claim_id (§11.2), which names the claim whatever has been written since it was read. A claim the store no longer holds is refused as a refuse, whether passed as the claim or as its .claim_id: a conclusion withdrawn since it was read, or one a known_at= read worked out again. So is a .claim_id two claims share; a string that is not a .claim_id is an error. Each element is the dependent claim itself — its value, its trust, and .derived_by naming the rule — so it can be walked a further step with dependents or back with premises. A base fact with no rules firing on it has no dependents (an empty claim_list). With transitive=True it answers every distance at once: the claims concluded from the claim, from those, and so on down, each listed once, in name order. It answers in full on every store, one where two conclusions each rest on the other included. The keyword takes a bool and is given at most once; transitive=False is the one-step read.
base = get(K.s.a)
for c in base:
deps = dependents(c)
for d in deps:
print("derived cell = " + str(d.path)) # .path is the dependent's K.s.a
below = dependents(c, transitive=True) # everything resting on cpremises(<claim>) → claim_list
The claims a conclusion was drawn from — one step up, the mirror of dependents: the claims its rule read, or the fields a computed field reads. The operand is a claim read from get/current, or an element of a proof_tree or of dependents (bind it to a name first), or a claim’s .claim_id, refused as dependents refuses it, a claim the store no longer holds included. A claim someone recorded has no premises (an empty claim_list). A conclusion whose validity window does not cover the present still names its premises, read at a time inside its window; where two rules conclude different values at one cell, each claim answers its own. The claim’s .derived_by names the rule that drew the conclusion (§11.2).
t = current(Order.o1.total) # total: int = price * qty
print(t.derived_by) # Order.total
ps = premises(t)
print(str([p.path for p in ps])) # ['Order.o1.price', 'Order.o1.qty']5.4 Reading as of a time
Six read verbs accept an optional trailing datetime (§4.1) naming the time the read is answered as of:
| verb | anchored form | what it answers at that time |
|---|---|---|
current |
current(K. |
the single value active then |
get |
get(K. |
the claims valid then |
why |
why(K. |
the derivation as it stood then (§5.3) |
list_subjects |
list_subjects(K, datetime("…")) / list_subjects(K. |
the subjects a traversal finds |
list_values |
list_values(K. |
the values recorded at the attribute |
list_attributes |
list_attributes(K. |
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 (§5.1) is in play — a fact written without one is valid at every time, and reads the same either way.
The time may be future as well as past. A commitment written for later — a release date, a scheduled rate change, a contract that starts next quarter — is not part of the present, so nothing at the present reports it. The anchor is what reads it back, and on the three enumerations it is what finds it at all: without one, a store can hold a fact that no traversal reaches.
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")
list_subjects(Release, when) # ["v2"]
list_attributes(Release.v2, when) # ["ships"]
list_values(Release.ships, when) # the claim
current(Release.v2.ships, when) # "2027-03-01"list_attributes(K) and list_categories() take no as-of time. The first gives the attribute names recorded for a kind, 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. This is why list_attributes takes one in its K. form and not in its K form — the first reads a subject’s claims, the second a kind’s vocabulary. Offering a time where none is accepted is a compile error naming the form that takes one.
Reading as of what the store knew. The same six reads take one keyword argument, known_at=, a datetime on the other time axis: not when a fact was true of the world, but what the store had been told by then. A claim written later is not among the answer. A claim revised or replaced later 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 would over a disagreement now.
remember(Price.widget.eur, 10, "list") # written in January
update(Price.widget.eur, 12, "list") # written in March
feb = datetime("2026-02-01T00:00:00Z")
current(Price.widget.eur, known_at=feb) # 10 — what the store held then
current(Price.widget.eur) # 12The two times are independent and may be given together: current(K. is the value the store, as of T2, held to be in force at T1. Without known_at= a read answers over everything the store holds now, and a known_at= later than every write answers the same way.
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. That includes a conclusion withdrawn since, and leaves 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. A known_at= read refuses catchably in three cases:
| reason | when | what to do |
|---|---|---|
derived_field |
the answer rests on a rule whose text at that moment the store does not keep: a rule changed after that moment, or a rule whose earlier text was never recorded | read it without known_at=, or as of a moment after the change; a rule of the second kind takes part once it is compiled again, unchanged, for a read as of any moment from then on |
not_recorded |
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 | read it without known_at=, or read another address at that time |
limit_exceeded |
the answer rests on more of the store than a read as of an earlier moment may work over; the same read over the same data refuses the same way every time | read it without known_at= |
known_at= is a keyword, never a position, and is given at most once. Only these six reads accept it — on any other verb, forget included, it is a compile error — and its value must be a datetime.
5.5 Result-type introspection
list_result_types() → result_type_set
The result types a program can hold — these nine, in this order:
rts = list_result_types()
# active_claim, claim_list, diagnosis, empty, proof_tree,
# result_type_set, string_list, subject_set, verify_resultEight are also type names you may write in an annotation. empty is the ninth and is not one — it is the sentinel case of an active_claim (§4.1) — and it is listed because a program can hold it, which is what this verb asks.
These names are canonical. They are the same strings .kind gives you (§11.2) and the same case labels a match compares against.
The order is ascending by canonical name.
This is not the set of result. discriminators the wire envelope can carry. That set is a different one: it has no diagnosis and no verify_result, and it does carry handle_result, change_event_list and program_output, which no program binds.
5.6 Script-data reads (in-language)
Read-only, so they run inside a program.
list_scripts() → string_list
Bare names of every stored script.
names = list_scripts()5.7 Wire-only verbs (not callable in a program)
Callable at the dke. wire surface but rejected in a program body — they mutate the script set or answer administrative provenance. Each row is an MCP tool call, with the tool’s own argument names. The table is a selection: the tools that change or describe your modules and rules. MCP Reference §2 lists every tool the wire serves.
| Tool call | What it does |
|---|---|
compile(name, source) |
provide + store + run a module; its defs become callable by name (§7.11) |
compile(name) |
reload a stored module by name — re-run its body (§7.11) |
run(source) |
execute instructions WITHOUT storing anything — “try it” to compile’s “keep it” (MCP Reference §4.2). A program reads, writes, and calls compiled scripts; a definition (def / class / @rule / computed field) belongs to compile. To call stored code: import <module> then <module>.<name>(...) |
list(target = scripts) |
list signatures (also runs in-language, as list_scripts()) |
forget(name) |
remove a script (idempotent); name is module. |
info(name) |
compile provenance; name is module. |
list(target = rules) |
list the names of the standing rules you wrote with @rule, each with its kind when it has one — authored-only, so a computed field is not listed (§8.8) |
list_modules() |
list stored modules — name, script count, created-at (MCP Reference §9.4) |
info_module(name) |
a module’s record — init language, its module. names, the classes it declares, its version if it has one, updated-at, created-at; name is BARE (MCP Reference §9.4) |
forget_module(name, confirm = true) |
delete a whole module + its scripts, kinds, and data; needs confirm; refuses on a live cross-module reference; name is BARE (MCP Reference §9.4) |
forget_rule(name) |
remove ONE standing rule and withdraw what it derived, leaving the rest of its module in place; name is the module. name from list(target = rules) |
checkup() |
report your store against the standing rules over it — which premise cells that need a value hold nothing, which of your kinds no rule reads; counts every standing rule in force, computed fields included (§8.8) |
The in-language import <module> statement (§7.11) loads a stored module inside a program body and so is not listed here.
compile and run are the two ways to submit a source, and they differ in one respect: compile stores the compilation (the module’s defs become callable by name, its rules stand); an inline run executes it once and stores nothing — data it remembers persists, but no script is left for later instructions to call or for list to report. Neither needs a def: bare top-level statements are a complete program, which is the shortest form for a one-off question (such a program takes no arguments — a def is what gives it parameters). Any number of procedures run (they may call one another; the last top-level def is the entry); a source that defines standing rules (a @rule or a computed field) cannot take effect in a run and is refused — such a source is compiled to activate its rules. Both report what the module body did — its output and its store effects — since both run it, and compile additionally states what it installed. → §9.3, MCP Reference §4.1, §4.2
5.8 Aggregate query functions
A read-only fold over a whole attribute — one attribute across every subject of a kind — returning a value (§4.1). Bind it to a name on its own line, then use it, or return it whole (return avg(Reading.); an aggregate may not be a bare argument to another call.
n = count(Reading.value) # single-attribute fold
p90 = percentile(Reading.value, 90) # ranked fold, rank 0–100
slope_xy = slope(Point.x, Point.y) # two-attribute fold (both attributes of one kind)
mean = avg(Reading.value) # bind first, then use `mean`- Single-attribute
fn(K.—attr) count,sum,product;avg,median,midpoint,mode,geomean,harmmean,rms;min,max,span,variance,stdev,iqr,mad,cv;skewness,kurtosis,entropy. - Identity / positional
fn(K.—attr) distinct(how many distinct values, counting equal numbers once as a Pythonsetdoes:1,1.0andTrueare one value),argmin/argmax(which subject holds the min / max value),first/last(the earliest- / latest-recorded current value). - Ranked
percentile(K.— theattr, <int>) <int>-th percentile (rank 0–100). The rank is an integer literal or the name of anint, resolved at runtime; outside 0–100 it is refused, at compile time for a literal and at runtime for a name. An expression is bound to a name first. - Two-attribute
fn(K.(both attributes of one kind) —x, K. y) covariance,correlation,slope,intercept,rsquared.
Anything after a single-attribute fold’s attribute is refused at the fold’s name, which gives its form, and so is a two-attribute fold’s second operand that is not an attribute, such as x + 1. A keyword argument is refused at the keyword, since an aggregate query takes none; key= and default= on min and max, and start= on sum, are named as what those names take over a list.
Which definition each name denotes. Some of these names mean a different number in different systems, so the choice is stated rather than left to the reader: variance and stdev are the population figures (divide by n); skewness is the population g1; kurtosis is excess kurtosis (0 for a normal distribution); mad is the median absolute deviation about the median; entropy is in nats; percentile and iqr interpolate linearly between the two nearest ranks; and covariance is the population covariance, where Python’s statistics. is the sample one.
stdev is worth a second look if you come from CPython. There, statistics. 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 answers 2.0 where statistics. answers 2.138090 — both correct for their own definition, which is why the name alone does not tell you which you have.
Rounding over real values. A fold has no order to add its values in, so it does not add them as a loop over a list does. sum is the exactly rounded sum, Python’s math. over reals, with an int entering as itself; avg is the exact mean rounded once, and rms the exact root mean square rounded once, where statistics. and math. round along the way and can differ in the last place. product, geomean and harmmean are their exact values rounded once too, in no order: over ten values of 0.1, geomean is 0.1, where statistics. gives 0.10000000000000002, and sum is 1.0, as sum([0.1] * 10) is. variance, stdev, cv, skewness, kurtosis and the two-attribute folds are their exact values rounded once as well: variance and stdev answer what statistics. and statistics. give. min, max, median, midpoint and span select a member or combine two exactly, and percentile, iqr and mad are their exact values rounded once as well: neither the rank nor the interpolation rounds, where the same formulas in reals round at each step. entropy is its exact value rounded once too, where a Python computation can differ in the last place (Python Specification §7.1.1). Where the Python computation passes the largest real on the way and the fold’s value does not, the fold answers its value: over 1e308 and 1.5e308, avg yields 1.25e+308, where statistics. raises. A value past the largest real yields inf or -inf. A fold that yields a duration or a datetime is its exact value rounded once to a nanosecond, half away from zero: over PT0S and PT15638400.000000001S, avg, median and stdev are each P90DT12H0.000000001S.
The result’s class follows the attribute (a numeric attribute yields a number; a datetime / duration attribute a datetime / duration) — except distinct (always an int) and argmin / argmax (always a string, the subject); first / last follow the attribute. Over values holding a real, a fold that computes yields a real, whole or not (sum over 0.5, 1.5 is 2.0). Over int and bool values alone, sum, avg, span, midpoint, variance and product yield an int when the result is whole (avg over 1, 3 is 2), and stdev, iqr, percentile, mad, cv, geomean, harmmean, rms, skewness, kurtosis, the two-attribute folds and an even-count median yield a real (stdev over 1, 3 is 1.0). A bool attribute is numeric: a fold that computes yields a number (sum over True, True, False is 2), and min, max, mode, first and last yield the stored value (max is True), the first recorded of equal values, or for last the last. Destructure with match or use it wherever a value is expressible. Fold names are soft keywords — recognised only right before (, so they stay ordinary identifiers elsewhere. An aggregate over an attribute with no data is a refuse, with the aggregate_empty discriminator, and so is a fold whose formula has no value: geomean and harmmean over a zero or a negative value, cv where the mean is zero, slope and intercept where the first attribute’s values are all equal, and correlation and rsquared where either attribute’s are. Python Specification §7.1.1 gives each fold’s formula. A value-comparing fold (sum, avg, min, max, argmin, argmax, …) over an attribute whose values are not numbers (a text attribute, or one mixing numbers with non-numeric values) is likewise a refuse — the fold declines rather than treating a non-number as zero. So is one over an attribute mixing two of the numbers, duration, datetime and scientific (a duration beside an int), as Python’s sum and max raise. The folds that don’t 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 that ground (a complex has no order, so the folds that compare by value, sum among them, refuse one), and an attribute mixing them folds too, each chosen value 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.
5.9 Worlds
A world is a named alternative to the store, 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 in a world and reads it inside a with world(...) block (§7.18), and a later program can enter the same world again, so one alternative can be built up and read over as many runs as it takes. A world’s name is a string and follows the rule a branch’s name does (§7.10).
create_world(name) (statement)
Creates the world, holding nothing of its own yet. A world of that name that already exists is a refuse (already_in_effect).
list_worlds() → string_list
The worlds that exist, in name order. It answers anywhere, inside a world block too.
compare_worlds(a) / compare_worlds(a, b) → claim_list
The claims world a holds that the store does not hold, or that world b does not hold, in name order, each an active_claim with the fields every claim carries. A claim of a is left out when the other side 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 rule’s conclusion is compared like any other claim, so where two worlds’ facts differ, the conclusions drawn from them are listed too. Everything the store holds is held in every world, so compare_worlds(a) is what a adds to the store, and asking in both directions answers every difference.
d = compare_worlds("wet", "dry")
for c in d:
print(c.path, c.value, c.source)merge_world(name) (statement)
Makes the world’s writes the store’s own, and removes the world. Where the store and the world 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. Each keeps, as its .created_at, the time it was written in the world.
A value written in the world at a field one of the store’s rules concludes, where the rule does not yield to a recorded value (§8.8), makes the merge a refuse (asserted_field) naming the value and the rule. Nothing is merged, and the world keeps everything written in it. Forget the value inside the world, or forget the rule, and merge again; or discard the world. Where the store already holds a conclusion at that address, forget the world’s value by its value and source, forget(K.: forgetting the whole address would reach the store’s conclusion too, which a world cannot remove.
discard_world(name) (statement)
Removes the world and everything written in it. It is a removal, so it needs a key with the delete grant, as forget does.
Keeping a world current. What the store gains reaches a world when the world is entered, compared or merged, and 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.
What a world cannot change. A world adds to the store and never changes what the store held before it. An update inside a world revises a claim the store holds as it would outside (§7.10), and what the store’s rules concluded from that claim no longer reads in the world; the store still holds both. A fact the world writes does the same to what the store’s rules concluded from that fact’s absence. Inside a world, forgetting a claim or a rule the store holds is a refuse (immutable_in_branch), and so is prefer or unprefer at a field the store has judged. A field’s judgment is held in one place at a time, so prefer at a field another world has judged is a refuse (still_referenced) until that world is merged or discarded.
Where the verbs are reached. create_world, merge_world, discard_world and compare_worlds are refused inside a world or branch block. Naming a world that does not exist is a refuse (not_recorded), and so is a name that is not one identifier.
6. Operators
DKE Python uses word-form logical operators (and / or / not), the comparisons == != < > <= >=, the arithmetic / concatenation operators + - * / %, the bitwise operators & | ^ << >> ~ on integers (§6.3), and the set operators | & ^ (§6.10).
An operator given operands it does not take is a type error. Where CPython refuses the same operands, each of a class it has, the refusal is CPython’s sentence, naming CPython’s classes: "a" < 1 is refused with '<' not supported between instances of 'str' and 'int', [1] + 5 with can only concatenate list (not "int") to list, and x += "a" over an int with unsupported operand type(s) for +=: 'int' and 'str'. Where an operand is a value read from a claim, a datetime, a duration or an instance, the refusal names the operand types the operator takes. A range is named range, as CPython names it.
6.1 Comparison
== != over any two values. Values of different types are unequal, as in Python — "7" == 7 is False and "7" != 7 is True, and nothing is converted to be compared. Numbers compare by value across int, real and bool, or an element of a list mixing them (§6.7): 1 == 1.0 is True, and a bool is the int it is, so True == 1 is True, matching the ordered comparisons and Python. A value compares with any value as the class it holds (§4.1), and two discriminators compare .kind. Two constructed lists compare element-wise at any depth — [1] == [1.0] is True, [1] == ["a"] is False, and an empty list compares with any list (§6.7). The same rule holds wherever a value is looked for: in, .index, .count, .remove, a dict key read, set inclusion and is (below). Two operands are refused: a class instance, and an answer read from the store (a claim, a proof) against anything but its own type. < > <= >= over numeric operands (int, real, bool or scientific, or an element of a list mixing them, §6.7; mixed allowed — compared numerically) or two strings. A scientific orders by its magnitude, the real nearest the number it writes, against any number or another scientific, as a rule premise orders one (§8.8): scientific("2.0e4") > 100 is True. Its figures take no part, so scientific("6.022e23") and scientific("6.0220e23") are neither < nor > each other, while == still tells them apart, and tells scientific("1.0e0") from 1. A complex 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). It has no order: < > <= >= with a complex on either side is a type error in Python’s words ('<' not supported between instances of 'complex' and 'int'), and so are sorted, min and max over one. Order by a part or by the magnitude instead: sorted(zs, key=abs). A string comparison is lexicographic by Unicode codepoint, so "apple" < "banana" and (uppercase first) "Z" < "a", matching Python; a shorter string that is a prefix orders first ("a" < "ab"). A string compared against a number is a type error. < > <= >= also order two datetimes (chronologically — the earlier instant first) or two durations (by length — the shorter first). Both compare by value, not spelling: duration("PT90M") < duration("PT1H30M") is False, and so is >, since the two are the same length; a datetime written with fractional seconds orders against one written without them by the instants they denote. A datetime compared against a duration, a number, or a string is a type error. datetime and duration also COMPUTE together: datetime + duration (either order) and datetime - duration shift an instant, datetime - datetime gives the length between two instants, and duration + duration / duration - duration add and subtract lengths. A duration also scales: duration * <number> (either order) stretches it (duration("PT1H") * 2.5 is PT2H30M) and duration / <number> splits it (duration("PT1H") / 4 is PT15M). Dividing a duration by another duration asks how many times one fits into the other and gives a plain real — duration("PT3H") / duration("PT45M") is 4.0, not a length — while % gives the length left over (duration("PT3H10M") % duration("PT45M") is PT10M). Shifts respect the calendar (datetime("2024-02-28T12:00:00Z") + duration("P1D") is 2024-02-29T12:00:00Z). Addition, subtraction, % and scaling by an int are exact to the nanosecond; scaling by a real rounds to the nearest nanosecond, halves away from zero. Subtracting a later instant from an earlier one yields a negative duration, which renders and reads back with a leading - (duration("-PT2H")). Two datetimes have no sum, duration - datetime is meaningless, duration * duration is an area rather than a time, <number> / duration is a frequency rather than a time, and //, ** are undefined over temporal values; a span past ~±292 years, an instant outside years 0001..9999, or a division or remainder by a zero-length duration, is a catchable refuse. < > <= >= also order two lists of an orderable element type or two tuples with orderable elements, of any lengths — element-wise lexicographic: the first unequal pair decides, and a prefix orders first ([1, 2] < [1, 2, 3], (1, 2) < (1, 3), (1,) < (1, 2)). An orderable element is a number, a scientific, a string, a datetime, a duration, or a nested list/tuple whose own elements are orderable, so the rule recurses all the way down ([[1], [2]] < [[1], [3]], [[2]] < [[10]] — element 2 < 10, not a string compare). Comparisons chain: a < b < c is (a < b) and (b < c) — each adjacent pair compared, the results conjoined, with b evaluated only once.
== / != also compare two tuples of any lengths (tuples of different lengths are unequal, and so are two whose positions hold different types, as (1,) == ("a",); equal tuples agree position by position, §6.8) and two sets (element-wise and order-independent — two sets are equal when they hold the same elements, and an int, a real and a bool of equal value are the same element, so {1, 2.0} == {1.0, 2} is True, §6.10). < > <= >= on two sets compare them by inclusion (§6.10), and so do they on a dict’s keys or items view beside a set or another such view (§6.9). Two dicts also compare with == / !=, order-independent — equal when they hold the same key→value mapping regardless of insertion order (numbers of either class by value, so {1: "a"} == {1.0: "a"} is True, a value of another type is unequal, and {} == {"a": 1} is False, §6.9).
Infinity and NaN. inf and -inf order beyond every other number. A NaN compares as IEEE 754 has it: math. is False and every ordered comparison with one is False. DKE Python has one NaN, and a collection finds it by value, where Python finds a NaN only when it is the very object: float("nan") in [math. is True, {math. is 1, len({math. is 1, and [float("nan")] == [float("nan")] is True (Python: False, KeyError, 2, False). Sorting puts a NaN after inf, so sorted([3.0, math. is [1.0, 3.0, nan] (Python: [3.0, nan, 1.0]), and min and max answer nan when any operand is one, as IEEE 754’s minimum and maximum do (Python Specification §8.1).
Identity: is / is not. a is b is True when a and b are one list, set or dict. A collection is shared (§7.17), so a name bound to another’s list, a parameter, an element read and a loop variable each hold the collection itself, while a copy, a conversion, a comprehension and a literal each build a new one: after b = a, a is b is True and a is a. is False. is not is the negation. 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 nothing 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 is there is a compile error that names == or !=. Over any other value, a tuple or a class instance, the error names the three kinds is takes. x is None and x is not None have no == rewrite either, since there is no None here — a rule asks whether a field has no value with absent(...) (§8.8), and over None the error names no comparison at all. The word itself is reserved, as it is in Python, so is = 3 is a parse error.
6.1a Membership
in / not in over (string, string) → bool — substring containment: sub in s is True when sub occurs anywhere in s (the empty string is in every string) — or element containment: x in xs over a constructed list<T> (§6.7), x in s over a set<T> (§6.10), k in d over a dict<K, V> (testing its keys, §6.9) and k in d. the same way, x in d. and (k, v) in d. over a dict’s views (§6.9), and x in t over a tuple, each True when an element == the item. A number is found among numbers of either class by value, as 1 == 1.0 compares: 1 in [1.0] is True, and so is 2.5 in (1, 2.5). A dict finds a numeric key the same way (§6.9). An item of another type is not found — "a" in [1, 2] is False — and a record tuple is searched for any of its fields: "alice" in ("alice", 30) is True. Membership does not test a store-derived collection — to iterate one, use the for <var> in <collection>: statement (§7), whose in is the loop keyword, not this operator.
if "@" in address: ...
if 4 in [3, 1, 4]: ...
if "red" in tags: ... # set membership
if "ana" in ages: ... # dict key membership6.2 Logical
and, or, not over any value that has a truth value — False / 0 / 0.0 / 0j / "" / empty constructed collections are falsy, everything else truthy. The eight store-derived read-result types have none — active_claim, claim_list, proof_tree, diagnosis, subject_set, string_list, verify_result, result_type_set — and are refused in all four condition positions: an if / elif, a comprehension if filter, the condition of a if c else b, and an operand of and / or / not. This diverges from Python on purpose. An empty store read and a read that found nothing to tell you are different answers, and one False would say both; use .length to ask about emptiness and for … in to walk. and / or are short-circuit and operand-return — a and b is a when a is falsy else b; a or b is a when a is truthy else b — so name or "anon" returns the string. Their result type joins the operands (identical → that type; int/real → real; a complex and a number → complex; two other value scalars → value), and the operand returned keeps its class, so 1 or 2.5 is 1; a non-unifiable pair is a type_error. not always yields a bool. not binds looser than comparison, so not a == b is not (a == b).
6.2a Conditional expression (ternary)
a if c else b yields a when c is truthy (any value that has a truth value, §6.2), else b. The two branches must share a type, which is the result type, be two class instances, which join under their common base as a list’s elements do, be a scalar beside a value or two scalars that are not both numbers, which join to a value holding their classes, or be two numbers: an int/real pair is typed real, and the branch selected is returned unconverted, so 2.5 if False else 1 is 1. A complex and a number join the same way, to complex: 2j if False else 1 is 1. Evaluation is short-circuit — only the selected branch runs, so an untaken branch that would refuse is never reached (safe if d == 0 else 1 / d). A name holding a value that the condition tests is read in the first branch at what the test holding leaves it, and in the second at what its failing leaves (§8.11). It binds lower than every operator and is right-associative.
grade = "pass" if score >= 60 else "fail"6.3 Arithmetic
+ - * / // % ** over numeric operands (int, real or bool — a bool computes as the int it is, so True + True is 2); prefix - negates an int (-True is -1), and prefix + takes the same operands and changes no number (+True is 1). The numeric operators promote: int × int stays int, and any real operand yields a real: the exact value of the operation rounded once, so an int past 2**53 is not rounded first (9007199254740993 + 0.5 is 9007199254740994.0, where Python answers 9007199254740992.0), as a base or an exponent of ** too ((-1.0000000000000002) ** 9007199254740993 is -7.38905609893065, where Python answers 7.389056098930649). Nothing but arithmetic and float(x) converts: a value given to a declared real, and a selection of two numbers, keep the class they have. So a real field written with 1 + 0.0 holds 1.0, and written with a -> real script’s return 1 holds 1; the two are equal. One exception and one detail: - / is true division — it always yields a real, so int / int promotes (7 / 2 is 3.5, 8 / 2 is 4.0). The floored integer quotient is // (7 // 2 is 3, -7 // 2 is -4 — floor rounds toward −∞). - % and // are the floor pair — both take the sign of the divisor (-7 % 2 is 1, -7.5 % 2 is 0.5), so a == (a // b) * b + (a % b) holds at either operand class. A remainder of zero takes the divisor’s sign too, which only a real can show: 6.0 % -2 is -0.0.
Complex arithmetic. +, -, *, / and ** take a complex beside another complex or beside a number and answer a complex, what CPython answers for the same operands: (3 + 4j) * 2 is (6+8j), (3 + 4j) / 2 is (1.5+2j), 1 / (3 + 4j) is (0.12-0.16j), 1j ** 2 is (-1+0j). The prefix - negates both parts, so -4j renders (-0-4j). // and % do not take a complex, and neither do divmod, round, int and float; read .real or .imag for a part. A division by zero with a finite dividend refuses at run time in Python’s words, complex division by zero. A part may be infinite or a NaN, and computes as Infinity and NaN (below) says.
** is power, right-associative, binding tighter than the prefix - (-2 ** 2 is -4, 2 ** 3 ** 2 is 512): int ** non-negative int is an int (2 ** 10 is 1024); a real operand yields a real; an int base with a negative exponent is a real when the exponent is written as a literal (2 ** -1 is 0.5), and otherwise refuses at run time with a teaching hint — write the base as a real (2.0 ** n), or the exponent as a literal. Reals compute to IEEE-754-double precision and render in canonical decimal form. A power with a real operand and a complex’s abs are correctly rounded, and each part of a product, quotient or power over a complex with every part finite is its exact value rounded once: the answer is the same wherever it is computed. Python takes these functions from its platform, which rounds them correctly almost always; where it does not, Python’s last digit differs from DKE’s (Python Specification §8.3). Division / modulo by zero of finite operands and integer overflow refuse at runtime; a real result past the range is inf or -inf. Two ** shapes refuse for reasons that are not overflow: a zero base under a negative exponent (0 ** -1) is a division by zero and says so, and a negative base under a fractional exponent is a complex number, which the real a power of two numbers is typed as cannot hold. Two spellings compute it. Written with both operands as literals the power is typed complex: (-8) ** 0.5 is 2.8284271247461903j, its real part exactly zero, where Python’s is 1.7319121124709868e-16. And a complex operand makes any power a complex, so complex(x) ** 0.5 answers for a negative x. A zero raised to a negative or a complex power refuses in Python’s words, 0.0 to a negative or complex power; a base that is not zero never does. A complex power’s part past the range is inf or -inf, where Python raises OverflowError: complex exponentiation. A negative base under an integral exponent ((-8.0) ** 3.0 is -512.0) and a zero base under a non-negative one (0 ** 0 is 1) are ordinary.
Infinity and NaN. A real, and a part of a complex, may be inf, -inf or nan, and every operation takes one and answers IEEE 754’s value, as Python’s operators do: math. is nan, 1 / math. is 0.0, and an operation with a NaN operand is nan (save math., which 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 only when the operands are finite. DKE Python departs from Python where Python raises instead, and at three corners where Python’s answer breaks its operator’s rule:
| Expression | DKE Python | Python 3.11 |
|---|---|---|
10.0 ** 400, round(1.7976931348623157e308, -308) |
inf |
OverflowError |
math., math., math., divmod(math. |
inf, inf, nan, (inf, nan) |
ZeroDivisionError |
math., -1 % math., -1 // math. |
inf, nan, -0.0 |
nan, inf, -1.0 |
complex(1e200, 1e200) * complex(1e200, 1e200), complex(1e308, 1e308) / complex(2, 2) |
infj, (5e+307+0j) |
(nan+infj), (inf+0j) |
complex(1e200, 1e200) ** 2, complex(1e300, 0) ** 2 |
infj, (inf+0j) |
(nan+nanj), OverflowError |
complex(math., complex(-1, 0) ** 1e308 |
(inf+nanj), (1+0j) |
ZeroDivisionError |
(0.1+0.2j) * (3-4j), (1 + 1j) ** 101 |
(1.1+0.2j), (-1125899906842624-1125899906842624j) |
(1.1+0.20000000000000007j), (-1125899906842634.5-1125899906842629.5j) |
Each part of a complex product, quotient or power of finite operands (a power’s base not zero) is its exact value rounded once, inf only where that value is past the range; Python rounds at every step, and loses some parts to an intermediate. A part whose exact value is zero takes the sign Python gives it where Python answers a zero there, and +0 otherwise. A quotient by a complex zero answers as C99 Annex G says. Comparisons, collections, min, max and sorting treat a NaN as §6.1 says, and the math functions as §6.14 says (Python Specification §8.3).
* also does sequence repetition: a string, a constructed list or a constructed tuple repeated by an int (either order) — "ab" * 3 is "ababab", "-" * 20 a rule, [0] * 4 is [0, 0, 0, 0], [1, 2] * 3 is [1, 2, 1, 2, 1, 2], (1, 2) * 2 is (1, 2, 1, 2). A non-positive count yields the empty sequence ("x" * 0 and (1, 2) * 0 are "" and ()); the count must be an int. A tuple’s type is its position list, so its count must be written as a literal and the result may name at most 65536 positions; with a count the run supplies, repeat a list — whose length is not in its type — and convert: tuple([1, 2] * n). One rule covers that and the negative literal exponent above, and the Specification states it once: a static type may depend on an operand’s value only where the program writes that value as a literal. → Python Specification §8.3
Bitwise operators — on two integers, a bool counting as the int it is, &, | and ^ are bitwise and, or and exclusive or, << and >> shift left and right by a count of bits, and the prefix ~ inverts every bit, as in Python: 12 & 10 is 8, 12 | 10 is 14, 12 ^ 10 is 6, 1 << 4 is 16, -5 >> 1 is -3 and ~5 is -6. Two bools given &, | or ^ answer a bool (True ^ True is False); ~ and the shifts answer an int. They bind between comparison and + / -, in Python’s order — | loosest, then ^, & and the shifts — so i & 4 == 4 is (i & 4) == 4, and ~ binds as the prefix - does. &=, |=, ^=, <<= and >>= assign the result. A negative shift count refuses in Python’s words, negative shift count (value_error), and a left shift whose result leaves the int range refuses as any integer overflow does. @ and @= read, of *’s level, and no value here answers them: each is a type error naming its operands, unsupported operand type(s) for @: 'int' and 'int'. → Python Specification §8.3
abs(x) — the built-in absolute value: abs(-5) is 5, abs(-3.5) is 3.5; it keeps the argument’s numeric type and takes exactly one number (a non-number, or a different argument count, is a type error). The abs of a complex is its magnitude, a real: abs(3 + 4j) is 5.0.
round(x[, n]) — half-to-even (“banker’s”) rounding: round(0.5) and round(2.5) are 0 and 2, round(0.125, 2) is 0.12. The optional digit count n is an int (negatives allowed: round(35, -1) is 40). Result type: round(int, …) → int; round(real) (no n) → int; round(real, n) → real (a real even at n = 0, so round(2.5, 0) is 2.0). Rounding follows the stored double, so round(2.675, 2) is 2.67 (matching CPython).
sum(iterable) / min(…) / max(…) — reduce an in-memory iterable, distinct from the same-named store-aggregate folds sum(K. / … (the argument shape selects: a bare K. path → fold; anything else → this reduce). sum adds numeric elements (all-int → int, empty → 0, any real → real, complex elements → complex, so sum([1j, 2j]) is 3j; over a list of both classes, §6.7, the class the additions give — 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: sum([0.1] * 10) is 1.0, where Python 3.11 answers 0.9999999999999999, and an int enters as the integer it is. It is refused as arithmetic_overflow only when that rounded value, or an all-int total, is out of range, and a zero sum is -0.0 only when the start and every element are. A sum holding a complex is, in each part, that part’s exact sum rounded once: sum([0.1j] * 10) is 1j, where Python 3.11, adding left to right, answers 0.9999999999999999j. A start value, sum(xs, 10) or sum(xs, start=10), is added first, as in Python: sum([1, 2], 10) is 13, an empty iterable answers it, and a real one makes the sum real. A start that is not a number takes what + adds to it: a list or tuple start concatenates (sum([[1], [2]], []) is [1, 2], and a tuple sum has a runtime length), a duration start adds lengths and a datetime start is shifted by each duration. A start + cannot keep adding the elements to is refused, and so is a string start, as Python refuses it. min/max take one iterable or two-plus scalar values → the extreme in sorted’s order (numbers by value, a scientific by its magnitude, strings lexicographically, a datetime chronologically, a duration by length; the first of two equal elements); elements/values must share an orderable type (numeric, scientific, string, datetime, duration, or a nested list/tuple of those — the comparison recurses), and a scientific shares one with any number. An int and a real compare by value and the extreme keeps its class: min([1, 2.5]) is 1. min/ max of a runtime-empty iterable is a catchable refuse (ValueError), unless a keyword-only default=<value> is supplied — min([], default=0) is 0 — which is returned only when the iterable is empty and whose type must match the element type (min(<iterable>, default=…); default= is invalid with multiple positional values).
key=<name> on min, max and sorted — compares the elements by what the named function returns for each: max(words, key=len) is the longest word, sorted(words, key=len) is shortest first. The name is a builtin taking one argument (len, abs, ord, str, …), a method of a built-in type (key=str., over elements of that type), or a script — declared in the unit, imported by from, or key=m. — called once per element, in order. Only the keys are compared, so only they must be orderable: max([{"a": 1}, {}], key=len) is {'a': 1}. Equal keys keep their order: min/max return the first of them, and sorted is stable both ways, so sorted(["bb", "a", "cc"], key=len, reverse=True) is ['bb', 'cc', 'a']. key= goes with default= or reverse= in either order. It takes a name and nothing else — a lambda, a method of a value such as d., a call or an expression is a parse_error (write a def that computes the key and name it), key=None is too (leave key= out), and sum and the store fold min(K. take no key=. DKE Python has no lambda because a lambda is a function held as a value, and no type holds a function.
divmod(a, b) — the floored quotient and the remainder as one pair, equal to (a // b, a % b): divmod(7, 2) is (3, 1) and divmod(-7, 2) is (-4, 1) (the quotient floors toward negative infinity, the remainder takes the divisor’s sign, and q * b + r == a always holds). Both operands must be numeric, and the pair promotes together — tuple<int, int> for two ints, tuple<real, real> when either is real, never a mixed pair: divmod(7.5, 2) is (3.0, 1.5). An element of a list of both classes (§6.7) gives a pair whose one class is the element’s: divmod([1, 2.5][1], 2) is (1.0, 0.5). A zero divisor, and the most-negative int over -1, are catchable refuses.
reversed(xs) — an iterator<T> (§6.12) over the elements in the reverse of the order for walks them, so a dict contributes its keys, a set its sorted elements and a dict view what it views, backwards: "". is "cba", and list(reversed(xs)) is a reversed copy. An iterator is not an argument, as in CPython, since an iterator reads only forwards.
sorted(xs[, key=<name>][, reverse=<bool>]) / any(xs) / all(xs) — sorted returns a new list<T> in ascending order (descending with reverse=True; a stable sort, key= and reverse= keyword-only, a key as above); without a key the elements must be orderable — numeric, scientific, string, datetime, duration, or a nested list/tuple of those (the sort recurses lexicographically, so sorted([[10], [2], [1, 3]]) is [[1, 3], [2], [10]]). ints and reals sort by value and keep their classes (sorted([3, 2.5]) is [2.5, 3]). A keyword sorted or a list’s sort does not take is a type error in Python’s words, named for sort() as Python names it ('x' is an invalid keyword argument for sort()). any/all fold truthiness (§6.2) to a bool — any([]) is False, all([]) is True.
format(value, spec) / repr(value) — format renders a value per the Python format mini-language, with the empty spec if it is given none (format(3.14159, ". → "3.14", format(42, "05") → "00042", format(255, "x") → "ff", format(1234.5, ". → "1.2e+03", format(65, "c") → "A"): [[fill]align][sign][z][#][0][width] [grouping][.. The fill is any one character and align is <, >, ^ or =, which pads between a number’s sign and its digits (format(-5, "*=6") → "-****5"). An int or bool takes d x X o b c n and every type a real takes; a real takes e E f F g G n %, a string s. A precision counts decimals under e, f and %, significant digits under g, n and with no type, and characters on a string; an int takes none. A bool formats as its int once any spec is written. + and a space write a positive number’s sign (format(5, "+") → "+5"), z drops a negative zero’s, # writes a prefix or keeps a point (format(5, "#b") → "0b101"), and , and _ group digits (format(1234567, ",") → "1,234,567"), each as in Python. A type the value does not take is a catchable refuse; so are =, a sign, z or # on a string, and c of a number that is no character’s code point. 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. and __format__ called by name, and an empty one answers str of the value. A complex formats as in Python: format(3 + 4j, ". is "3.00+4.00j", a precision alone counts significant digits in each part (format(3.14159 + 2j, ". is "(3.14+2j)"), it takes e E f F g G n, it is right-aligned by default, and zero padding or = is a catchable refuse. repr(x) gives the repr text (a string keeps its quotes, and a character Python would not print, such as a no-break space, is escaped: repr("a\xa0b") is "'a\\xa0b'"). Both power f-string {v:spec} / {v!r} (§6.4a).
ord(s) / chr(i) — the Unicode codepoint round-trip. ord(s) returns the integer codepoint of a one-character string (ord("A") is 65, ord("æ") is 230); a string of any other length is a catchable refuse. chr(i) returns the one-character string for a codepoint i in range(0x110000) (chr(97) is "a", chr(937) is "Ω"); an i outside that range — or a lone surrogate (U+D800..U+DFFF), which has no UTF-8 encoding — is a catchable refuse.
bin(n) / hex(n) / oct(n) / ascii(x) — text for a value, as Python writes it. bin, hex and oct write an int in base 2, 16 or 8 behind 0b, 0x or 0o, lowercase and unpadded: hex(255) is "0xff", oct(0) is "0o0", and a negative number is signed, bin(-5) being "-0b101". int(s, base) reads each back. ascii(x) is repr(x) with every character past U+007F escaped, so ascii("é") is "'\\xe9'"; with nothing to escape it equals repr. Each takes exactly one argument, and bin, hex and oct an int or a bool.
pow(base, exp[, mod]) — with two arguments, base ** exp itself: pow(2, 10) is 1024 and pow(2, -1) is 0.5, and it refuses as ** does. With a third, modular exponentiation over ints, computed below the modulus so any exponent finishes: pow(2, 10, 1000) is 24, pow(3, -1, 7) is 5 (a negative exponent raises the base’s inverse), and the answer takes the modulus’s sign, so pow(2, 3, -5) is -2. A zero modulus, and a base with no inverse, are catchable refuses (value_error). A non-integer in the three-argument form is a type error in Python’s words: a real is refused, pow() 3rd argument not allowed unless all arguments are integers, a complex complex modulo, and anything else names the three classes, unsupported operand type(s) for ** or pow(): 'int', 'str', 'int'.
Type conversions. str(x) renders any value to text (total, the +/print rendering); int(x) and float(x) convert a string / int / real / bool, or an element of a list of both number classes (§6.7), to an int / real. int(3.9) truncates toward zero (3), int(" -7 ") is -7, and _ separates digits as it does in a literal (int("1_000") is 1000, float("1_000.5") is 1000.5); a string that is not a valid number (int("abc"), int("3.5")) or an out-of-range value is a catchable refuse (Python ValueError). A decimal digit of any script reads as its value and a Unicode space pads, as in Python: int("٥5") is 55. With no argument each conversion is the zero of its type, as in Python: int() is 0, float() is 0.0, str() is "" and bool() is False. int(s, base) parses a string in an explicit radix: base is 2..36 (or 0 to auto-detect from a 0x/0o/0b prefix), a 0x/0o/0b prefix is accepted when it matches base, an optional leading sign and _ digit separators are allowed, and a digit outside the base, an out-of-range base, 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, as in Python. complex(re, im) builds a complex from two numbers, each keeping the sign of a zero, and complex(x) builds one from a single number, from a complex, or from a string holding the text a complex prints as (complex("3+4j"), complex("-2.5j")); with no argument it is 0j. Either argument of the two-argument form may itself be a complex, computed as in Python, so complex(1j, 2j) prints (-2+1j). A string that is not a complex is a catchable refuse (Python ValueError); a part spelled inf or nan, or past the range, is what float reads, as in Python: complex("inf") is (inf+0j). Arguments by name: an argument Python lets a caller name may be named here too, in any order after the positional ones — 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). Each argument is evaluated where it is written, whichever position it fills, so pow(exp=xs. pops the exponent first, as in Python. An omitted real is 0, so complex(imag=2) is 2j. The names given to dict are keys, each the string written in any language: dict(a=1) is {"a": 1}, and dict({"a": 1}, a=2, b=3) writes them over a positional argument’s entries, giving {"a": 2, "b": 3}. int’s text is positional only, as in Python, and a built-in function that takes no argument by name refuses one in Python’s words and says what to do instead: abs(x=1) is a type error beginning abs() takes no keyword arguments. A method’s names are listed with the method (§6.6, §6.9). Collection conversions: list(x) collects a finite collection’s elements (a dict view what it views, a string its characters, a tuple its elements — list((1,2,3)) → [1, 2, 3], the element type what a for over the tuple binds, §6.8) into a list; set(x) collects them into a deduped (1 and 1.0 once), sorted set (elements must be hashable — a scalar or a tuple of hashables); dict(x) builds a dict<K, V> from another dict — a new dict holding its entries — or from any other collection of pairs, read as update reads one: a list<tuple<K, V>>, a tuple of them, an iterator<tuple<K, V>> or a dict_items<K, V> — dict(zip(keys, vals)); tuple(x) collects the elements into a variadic homogeneous tuple tuple<T, ...> (runtime length, indexed by a runtime int, unpacked as a list is), and accepts a fixed tuple too — which is how a fixed tuple is widened to a variadic one. Called with no argument, each gives the empty collection, as in Python: list() → [], dict() → {}, tuple() → (), set() → set(). bool(x) returns the truthiness (§6.2) of any value as a bool (bool([]) → False, bool("x") → True).
6.4 Concatenation
+ joins two strings into a new string. A string + a non-string operand is a type error (as in Python, which raises TypeError) — convert the non-string side with str(…) ("count = " + str(xs.); there is no implicit string coercion. An f-string applies str(…) to each field for you (§6.4a). With two list operands + concatenates into a new list: [1, 2] + [3, 4] is [1, 2, 3, 4]. xs += ys, xs. and xs. change xs in place to the same concatenation (§7.17), and xs += c takes any collection, as extend does: xs = [1] then xs += (2, 3) leaves [1, 2, 3]. The result element type follows the list-literal rule (§6.7): same type → list<T>, differing scalars → list<value> ([1] + [3.5], holding numbers), an empty operand takes the other side ([] + [1]); a non-scalar mismatch is a type error. A list plus a non-list, non-string operand is a type error.
With two tuple operands + concatenates into a new immutable tuple: (1, 2) + (3,) is (1, 2, 3). A tuple is heterogeneous, so the result keeps each operand’s positions and their types rather than settling on one element type — tuple<int, string> + tuple<bool> is tuple<int, string, bool>, and a constant index into the result reads the position it names (after t = (1, "a") + (5,), t[2] is an int). The empty tuple takes the other side (() + (1,)). A tuple plus a non-tuple operand is a type error. When one side is a tuple whose length is only known at run time, the result is too, so it needs a single element type covering both sides — the same unification list concatenation uses, and element types that do not unify are a type error.
print("count = " + str(xs.length))
print("items = " + str(xs))
combined = xs + [99] # concatenate two lists
pair = (1, "a") + (True,) # concatenate two tuplesEither quote — a string opens with " or ' and ends at the next of the same, so 'a' and "a" are one string and the other quote is ordinary content: "it's", 'say "hi"'. A prefix u or U changes nothing. A printed collection of strings, ['a', 'b'], reads back as written.
Adjacent literals — two or more string literals separated only by whitespace are joined at compile time, with no +, so a long constant can span lines without a run-time concatenation: "one " "two" is "one two" and "a" "b" "c" is "abc". An f-string joins the run too, in either order and any mixture (f"a" "b", "a" f"{x}", f"{x}" f"{y}"), which is the idiomatic way to write a long interpolated message across lines — and the whole run is one interpolated string, costing what the single f-string it replaces costs. A typed value (datetime("…"), duration("…"), blob("…")) does not join: concatenation is for strings. Render one with str(…) and use +, or interpolate it.
Triple-quoted literals — a string delimited by """ runs until the next """, so it may span multiple lines (each newline is a literal \n) and hold lone "/"" characters without escaping: """line one … line two""". The escapes are the same as a string in one quote and the value is an ordinary string. ''' is the same with the other quote, and an f-string may be triple-quoted too.
Raw strings — an r or R prefix makes a backslash an ordinary character: it stays in the value with the character after it, so r"a\n" is three characters and r"C:\path" is the path as written. A backslash still keeps the next " from ending the literal, so a raw string cannot end in one backslash. A raw string may be triple-quoted, joins an adjacent run like any string, and combines with f as rf"…" or fr"…", where a brace still opens a field (Python Specification §3.5.1).
Escapes — a backslash inside a string begins an escape: \\ a backslash, \" a double quote, \' a single quote, \n line feed, \t tab, \r carriage return, \a bell, \b backspace, \f form feed, \v vertical tab. Three take hex digits and are written into the value as UTF-8: \xNN is a codepoint up to U+00FF from exactly two digits — so "\x85" is the same one-character string as "\u0085", as in CPython — \uNNNN one up to U+FFFF, and \UNNNNNNNN a codepoint up to U+10FFFF — the only escape that reaches above the BMP. One more names its codepoint rather than numbering it — \N{name}, below. One to three octal digits name a codepoint, so "\101" is "A", and a backslash at the end of a line continues the string on the next line. Anything else after a backslash stays in the value with the backslash, as in CPython: "\d" is two characters, the same string as "\\d". Two values are refused rather than encoded: a surrogate (U+D800..U+DFFF, written either way), and any escape that would produce a NUL — \0, \x00 and \u0000 alike — so a string never carries an interior NUL.
Named-character escape — \N{name} is the codepoint with that Unicode character name, matched case-insensitively, across every script: "\N{BULLET}" is "•", "\N{GREEK SMALL LETTER ALPHA}" is "α", and "\N{CJK UNIFIED IDEOGRAPH-4E00}" is "一". A name Unicode does not assign is a parse error.
6.4a Formatted strings (f-strings)
An f-string — a string literal with an f (or F) prefix — writes a value into text directly: each { … } field holds an expression that is evaluated and written in place. f"a{x}b" has the same value as "a" + str(x) + "b", so a field is rendered the way + renders that value (§6.4) and an f-string is always a string (even a lone field: f"{n}" is "42", not the int 42). A field holds any expression — a bound variable, arithmetic, a method call, an index or slice; a store read is bound to a name first (§7.2), as with +. Write {{ or }} for a literal brace. A field may carry a conversion and/or a format spec: {x!r} uses repr(x), {x!s} uses str(x); {x:spec} applies the format mini-language — f"{x:., f"{n:>5}", f"{s:*^8}" — and they combine ({x!r:>10}). A field may end in a self-documenting =: f"{x=}" writes the field’s source text up to the = (whitespace preserved) followed by the value, defaulting to repr — f"{x=}" is "x=5", f"{ x = }" is " x = 5", f"{s=}" is "s='hi'" — and a :spec/!conv overrides that default (f"{x=:. is "x=5.00"). A trailing ==/>=/<=/!= is a comparison, not the debug =. A spec may hold fields of its own, each replaced by its value before the spec applies: f"{x:>{w}}" pads x to the width w holds. A field may hold a string in the quote that does not enclose the f-string, as f"{d['k']}" does. Python’s two older ways to format a string read as Python’s do: "{}". and "{a}". (§6.6), and "%.. A % with a string on its left takes the next element of a tuple on its right, or the one value that is not, and %(key)d looks its key up in a dict or a list. It knows s r a d i u o x X e E f F g G c, the flags, a width, a precision, * and %%. A fault in either refuses as the program runs, in Python’s words.
print(f"count = {xs.length}")
name = "Merlin"
print(f"hi {name}, first char {name[0]}, upper {name.upper()}")6.5 Field access, indexing, and slicing
x. (see §11) composes on any computed value — a method result, an index, or a slice — not only a bare name (text., xs[0]., hist[-1].); a store read is still bound to a name first (§7.2). The builtin len(x) is an alias for x. (a string’s character count, a blob’s byte count, or a collection’s element count). xs[i] (zero-based; index is int; out-of-range refuses). A string may also be indexed to read a single character (s[i] → a one-character string; a negative index counts from the end, so s[-1] is the last character) and sliced for a substring — s[a:b], with either bound optional (s[a:], s[:b], s[:]) and negative bounds counting from the end (s[-3:], s[:-1]), plus an optional step s[a:b:step] (a negative step reverses, so s[::-1] is s reversed and s[::2] is every other character). Slicing is total: it never refuses; bounds are clamped and an inverted range yields "" — the sole exception is a step of 0, a catchable refuse. A constructed list<T> indexes and slices by the same rules (xs[i], negative from-end permitted; xs[a:b:c] yields a new list<T>); from-end (negative) positions apply to a string and a constructed list, while a store-derived collection index is non-negative. Positions count characters (a string) or elements (a list). A tuple slices too — t[a:b] yields a tuple, the sub-tuple of the selected positions when its bounds are constant integers, and a variadic tuple of what a computed index reads when one is computed (§6.8); list(t[a:b]) converts the result to a list. An index of another type than int is refused at compile, in Python’s words over a string, a list or a tuple: list indices must be integers or slices, not str for [1]["a"].
A blob indexes, slices and joins as Python’s bytes do, over its bytes: b[i] is the byte at i as an int from 0 to 255, a negative i counting from the end; b[a:b:c] is the blob of the bytes selected, by the rules of a string’s slice, a negative step walking from the high end and a step of 0 a catchable refusal; and b1 + b2 is the blob of the bytes of b1 and then those of b2. A position past either end refuses with index out of range (index_out_of_bounds), and an index of another type than int is refused at compile, byte indices must be integers or slices, not str. Each is one operation, however long the blob, and builds no cells (Python Specification §3.5.8).
b = blob("000102030405FF")
print(b[1:4]) # 010203
print(b[::-2]) # FF040200
print(b[0], b[-1]) # 0 255
print(b[:2] + b[5:]) # 000105FFA slice is also a value. slice(stop) or slice(start, stop[, step]) builds one. Each argument is an int, a bool or None, and None or a bound left out is open, so slice(5) is slice(None, 5, None), which is also how it prints. xs[s] selects what xs[a:b:c] with the same bounds selects, from a string, a list or a tuple, a fixed-length tuple reading it as a computed bound (§6.8). On a list, xs[s] = c, xs[s] op= c and del xs[s] change what the bracketed slice would. s., s. and s. read the bounds back as value, an open one being the null class, which prints null where CPython prints None. s. answers the (start, stop, step) that xs[s] selects from a sequence of length n, refusing a negative n (value_error) before a zero step (slice_step_zero). Two slices are == when their bounds are. A slice has no order and no .length, is always true, is not hashable (so it is no set element and no dict key), and is never stored.
6.6 String methods
A string carries these built-in methods (all character-counting, all total except split, the justify family, and index/rindex, noted below): - s. → int / s. → int — the first / last position of sub, or -1; the optional start[, end] window searches s[start:end] and the index is into the whole s. - s. → int / s. → int — like find / rfind, but a missing sub refuses (catchable, §10) instead of returning -1. - s. → int — the number of non-overlapping occurrences of sub in the window (an empty sub yields the window’s length + 1). - s. → bool / s. → bool — whether the window of s begins / ends with the argument (an empty argument yields True). The argument may instead be a tuple of strings — True if the window matches any of them (an empty tuple yields False). - s. → string — a copy of s with each non-overlapping occurrence of old replaced by new, at most count times when given (an empty old inserts new in every gap, as in CPython: "abc". → -a-b-c-). - s. → string / s. → string — the full Unicode case mapping. A character may expand to more than one ("straße". is "STRASSE", "file". is "FILE", "İ". is "i̇"), so a case transform can change the length (which .length reflects). lower() also applies the Greek word-final rule ("ΟΔΟΣ". is "οδος", final ς). - s. → string — first character titlecased, the rest lowercased. - s. → string — the first character of each word titlecased, the rest lowercased (a word is a run of cased characters; any non-letter, including an apostrophe, starts a new word — so "they're". is "They'Re"). - s. → string — each uppercase character lowercased and vice versa. (These four case transforms use the full Unicode mapping above, including the Greek word-final rule where they lowercase.) - s. → string — aggressive, context-free folding for caseless matching: like lower but more so ("STRAßE". is "strasse", "fi" folds to "fi"), and it does not apply the Greek word-final rule, so "ΟΔΟΣ". is "οδοσ" (a medial σ, unlike .lower()’s "οδος"). Use a. for caseless equality. - s. → string — left-pad with '0' to width characters, keeping a leading +/- sign first; width ≤ s. is a no-op. - s. / s. / s. → string — justify s in a field of width characters, padding right / left / both with fill (a single character, default space). width ≤ s. is a no-op; a fill that is not exactly one character refuses (catchable). - s. / s. / s. → string — s with characters removed from both ends / the left / the right. With no argument they remove whitespace; with a chars argument they remove any character that occurs in the chars set. - s. / s. → string — s with a leading prefix / trailing suffix removed if present, else s unchanged (an empty suffix is a no-op). - s. → string — each tab replaced by spaces to the next tab stop (multiple of tabsize, default 8); the column counts characters and resets after \n/\r. tabsize ≤ 0 removes tabs. tabsize may be named: s.. - s. → list<string> — the substrings of s split at each occurrence of sep (empty pieces kept); an optional maxsplit caps the splits (≤ maxsplit + 1 pieces). With no argument, s splits on runs of whitespace, dropping empty pieces. An explicit empty separator is a catchable refuse (§10). Both arguments may be named, s., and naming maxsplit alone bounds a whitespace split, keeping the rest as one piece: " a b c ". is ['a', 'b c ']. - s. → list<string> — like split, but a bounded maxsplit takes its splits from the right ("a,b,c". is ['a,b', 'c']); with no maxsplit the result equals split. It takes the same names, and " a b c ". is [' a b', 'c']. - sep. → string — the strings xs yields, concatenated with sep between adjacent ones: xs is a string (its characters), or a list, tuple, set, dict (its keys) or iterator of strings, and an empty one yields "". "-". is "a-b-c". Elements of any other type are a type error, in Python’s words: "a". is refused with sequence item 0: expected str instance, int found, and "a". with can only join an iterable. - s. / s. → tuple<string, string, string> — split s once at the first / last occurrence of sep into (before, sep, after); when sep is absent, partition yields (s, "", "") and rpartition yields ("", "", s). An explicit empty separator is a catchable refuse (§10). - s. → 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 from code points, as str. makes one (§11), finds a code point as any dict does, so 97.0 is the key 97. A list, a tuple or a string is indexed by it, and a character past its end stays. A table of any other type, a value of any other type, a code point outside range(0x110000) or a surrogate is a catchable refuse (§10), when a character reaches it. - s. → string — s read as a template, as Python reads it: each {} takes the next positional argument, {0} the one at that position and {a} the keyword. After that, .x reads a field and [w] an item, !r, !s and !a convert, and :spec is what format(value, spec) takes (§6.4a). {{ and }} are a brace. .x on an instance refuses as the program runs, since the store is not read in the middle of an expression: bind the field to a name and pass that. A fault refuses in Python’s words: key_not_found for a name no argument holds, index_out_of_bounds for a position past the last, type_error or value_error for the rest. - s. → string — as format, every name looked up in m. - s. → list<string> — the lines of s, split at each of CPython’s eleven line boundaries: \n, \r\n, \r, the vertical tab, the form feed, the three C0 separators, U+0085, U+2028 and U+2029 — with the trailing-newline empty line dropped ("" yields []). With a true keepends, given by position or by name, each line keeps the boundary that ends it: "a\r\nb". is ['a\r\n', 'b']. - Arguments by name: the names above are the only ones a method takes, as in Python, besides a list’s sort (key=, reverse=) and a dict’s update, whose names are keys. Any other method refuses a name in Python’s words, naming the receiver’s Python class: s. is a type error beginning str.. - Arguments of a type a method does not take are a type error in Python’s words, which differ by method as CPython’s do: "abc". is refused with must be str, not int, "abc". with replace() argument 2 must be str, not int, "abc". with must be str or None, not int and "abc". with 'str' object cannot be interpreted as an integer. A search’s bounds are a slice’s: "abc". is refused with slice indices must be integers or None or have an __index__ method, and a list’s or a tuple’s index bounds with slice indices must be integers or have an __index__ method. A list’s insert position is an integer: xs. 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 which type the method takes. - The character-class predicates — each () → bool, testing every character of s against its Unicode class: s., s., s., s., s., s., s., s., s., s., s., s.. All but isascii and isprintable are False on the empty string (the whole-string rule is “non-empty and all characters match”); "". and "". are True. isupper/islower require at least one cased character and no character of the opposite case; istitle tests the titlecase pattern. They classify by full Unicode, so "blåbær". and "ÆØÅ". are True. isprintable is False for a control, a format character, any separator but the space, and a private-use or unassigned codepoint. isidentifier asks what Python asks of a name, and reserves no keyword: "class". is True.
domain = email.strip()[email.find("@") + 1:]
label = raw.replace("_", " ").upper()
paid = invoice.endswith("PAID")
tidy = title.strip("*_ ")
parts = "a,b,c".split(",") # ['a', 'b', 'c']
line = " ".join(["a", "b", "c"]) # "a b c"
addr = "user@host".partition("@") # ('user', '@', 'host')
rows = "one\ntwo".splitlines() # ['one', 'two']
digits = code.isdigit() # True if code is all digits
at = path.index("/") # position of "/", or refuses if absent6.7 Constructed lists and comprehensions
A list literal [e1, e2, …] builds a constructed list<T> (shared, bounded, existing only while the program runs and never stored). Elements are inferred to a common type: homogeneous → list<T>, mixed scalars → list<value>, empty [] → list<unknown>; lists nest. The empty list is compatible with any list<T> — wherever two list types must agree it takes the concrete side’s type: [1, 2] if c else [], a re-binding (xs = [] then xs = [1]), an annotated binding (xs: list<int> = [], and d: dict<string, int> = {} likewise), a nested sibling ([[1, 2], []] is list<list<int>>), ","., and [] == xs. The empty set set([]) and the empty dict {} are compatible the same way with any set and any dict, so the accumulator seen = set([]) then seen |= {w} binds a set<string> when w is a string. A constructed list is indexed, sliced, measured (.length), iterated (for … in), tested for membership (x in xs), searched (xs. → the first index of v, within an optional codepoint window and refusing if absent; xs. → how many times v occurs), compared (== / !=) and copied (xs. → a new list holding the same elements); it renders in print and +. A list is shared: every name holding it holds the one list, and a statement that changes it — xs., xs[i] = v, del xs[i] and the rest of §7.17 — changes it in place, so every one of them sees the change.
A list mixing only ints and reals — [1, 2.5] — holds numbers. Its type is still list<value> and each element keeps its own class ([1, 2.5][0] is 1), but arithmetic, ordering, membership, sum / min / max / sorted, abs / round / divmod and int / float take its elements as numbers: min([1, 2.5]) is 1, sum([1, 2.5]) is 3.5 and [1, 2.5][1] + 1 is 3.5, as in Python. So do a set, a dict’s values and a tuple’s positions that mix the two classes, and whatever is read from them — an index, a loop or comprehension variable, a generator expression, + of two such lists. A result is a real where a real decides it ([1, 2.5][0] + 0.5, and /), and otherwise again a value holding a number, which satisfies no int or real annotation or parameter: convert it with int(…) or float(…). Anything that could bring a non-number in removes the property — a conditional expression or or choosing between [1, 2.5] and ["a", 1], a re-binding, a nested literal. A bool is a number here, the int it is, as in Python: sum([True, 2]) is 3, True in [1, 2.5] is True, and max([True, 0]) is True, the element itself. It is an int by class too, as in Python: [True, 2] is a list<int>, a parameter, annotation or return declared int takes a bool, so do an index, a bound and a repetition count, and a case int arm over a value takes True — write case bool above it to tell the two apart. It keeps its class, so x: int = True prints True. A list<bool> is still not a list<int>, since a list is shared. Python Specification §6.4 and §8.1 state the rule.
A list mixing a scientific with numbers — [scientific("2.0e0"), 1] — holds magnitudes: its elements order by magnitude, as a scientific does (§6.1), so sorted, min and max take it and sorted([scientific("2.0e0"), 1, 2.5]) is [1, 2.0e0, 2.5]. They compute nothing, so arithmetic and sum refuse them. The same holds for a set, a dict’s keys or values and a tuple’s positions, for whatever is read from them, and for min or max of a scientific and a number. Python Specification §5.6 states the rule.
A comprehension [expr for x in xs if cond] builds a new list by bounded iteration: it binds each element of the finite source xs to x, keeps those the optional cond (truth-tested) admits, and collects expr. The loop variable is comprehension-local (it may shadow an outer name and does not leak). Over an empty source whose element type nothing supplied, a comprehension is empty, and what reads x is not checked (§7.7): [x. is list<unknown>. A clause may name a target list of two or more names, or one name and a trailing comma ([k for k, v in d.), which destructures each element the way a for statement does (§7.7): the element must be a tuple of that arity or a list. Nested targets ([c for a, (b, c) in xs]) and one optional starred target ([r for a, *r in xs]) are taken here as they are everywhere else, the star making the arity a minimum. A clause’s targets are names — an element target goes in a for statement, which has a body to write it in. Multiple for clauses form the flat cartesian product ([a + b for a in xs for b in ys]) — the first clause is the outermost loop, and a later clause’s source may reference earlier loop variables ([y for row in grid for y in row]); a clause may carry several if filters (applied conjunctively, each asked only for its truth value). A comprehension whose result is itself a comprehension also nests.
nums = [3, 1, 4, 1, 5]
evens = [n for n in nums if n % 2 == 0] # [4]
caps = [w.upper() for w in "a,b".split(",")] # ['A', 'B']
grid = [[x for x in nums] for r in caps] # list<list<int>>
pairs = [a + b for a in [1, 2] for b in [10, 20]] # [11, 21, 12, 22]
flat = [y for row in [[1, 2], [3, 4]] for y in row] # [1, 2, 3, 4]A generator expression is a comprehension written in a call’s parentheses instead of brackets, sum(x for x in xs). It is accepted as the whole argument of a call that reads every element before it returns: sum, min, max, any, all, sorted, list, tuple, set, dict, a string’s join, a set’s union, intersection, difference, symmetric_difference, issubset, issuperset and isdisjoint, a list’s extend, a set’s update, difference_update, intersection_update and symmetric_difference_update, and a dict’s update. There it means the bracketed comprehension, except that any and all stop at the element that decides, and a set’s update and difference_update and a dict’s update read it one element at a time (§7.17), as Python’s do. Beside another argument it is written in its own parentheses, sorted((x for x in xs), reverse=True). Written bare beside another argument, in a call of any kind, it is a grammar error at its start, as in Python, so print(1, x for x in xs) is refused at column 10. In its own parentheses right after a *, in a call or a display, it is read the same way, since the * reads every element first: print(*(x for x in xs)), [*(x for x in xs)]. Anywhere else it would be a generator object, which DKE Python does not have: bound to a name, returned, printed, or passed to len, enumerate, zip, a method not listed here or a script, it is refused with a message naming [… for … in …].
nums = [3, 1, 4, 1, 5]
total = sum(n * n for n in nums) # 52
big = any(n > 3 for n in nums) # True
ordered = sorted((n % 3 for n in nums), reverse=True) # [2, 1, 1, 1, 0]6.8 Tuples
A tuple literal (e1, e2, …) builds a constructed tuple<T1, …, TN> — a fixed-length, heterogeneous sequence in which each position keeps its own type (immutable, bounded, never stored). A comma makes the tuple: (e) is a grouped expression, (e,) a one-element tuple, (a, b) a two-element tuple, () the empty tuple; tuples nest (((1, 2), 3) is a tuple<tuple<int, int>, int>). After return and on the right of an assignment the parentheses may be left off, as in CPython: return 1, 2 returns (1, 2), and x = 1, binds (1,). A tuple indexed by a compile-time constant integer reads that position’s own type — t[0] the first position’s, t[-1] the last’s. A computed index reads what a for over the tuple binds, below, and is a compile-time error only over a tuple holding a collection beside a value of another type (every position stays reachable by a literal index); an index past either end refuses when it runs, catchably, as Python’s IndexError does. It is measured with .length and compared with == / != against any value — (1, 2) == (1,) is False, and so is (1,) == ("a",). A tuple is written after for … in when its positions share one type, are all scalars, which the loop variable takes as a value (§6.7, §7.7), or are all class instances, which it takes at their common base (§8.10); a tuple holding a collection beside a value of another type has no single element type to bind, so index it instead. It renders in print and + as (e1, e2, …), a one-element tuple as (e,). Two tuples concatenate with + (§6.4), the result keeping every position’s own type. A tuple is searched (t. → the first position of v, within an optional window and refusing if absent; t. → how many positions hold v) wherever v in t is allowed, comparing v with every position as == does — (1, "a"). is 1, and a missing value refuses as tuple.. A fixed tuple whose positions all agree with T is a tuple<T, ...>, a tuple being immutable, so it fills one wherever that variadic type is declared — as a value, inside a collection literal written at the 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 being shared.
point = (3, 4) # tuple<int, int>
x = point[0] # 3 — constant index
at = point.index(4) # 1
mixed = (1, "ana", True) # tuple<int, string, bool>
same = (3, 4) == point # True
wide = point + (5,) # tuple<int, int, int>6.9 Dicts
A dict literal {k1: v1, k2: v2} builds a constructed dict<K, V> — a mapping from hashable keys (a scalar or a tuple of hashables) to values, kept in insertion order (shared as a list is, bounded, never stored). Keys are drawn from a scalar value class (string / int / bool / datetime / duration / real / blob / scientific / complex), a value, or a tuple of hashables, and are homogeneous (a mix of scalar value classes gives a value-typed key part, and any other mix, such as a tuple beside a string, is a type error naming the two types, in a literal or written by d[k] = v, since a value holds a scalar); values join as a list’s elements do: a mix of scalar value classes gives a value-typed value part, holding numbers when all are ints and reals (§6.7), collections whose elements meet are one type ({"a": [1], "b": []} is a dict<string, list<int>>), and instances of several subclasses take their common base (§8.10). The empty dict is {}. When a key is written twice the entry keeps its first position but takes the last value ({"x": 1, "x": 9} is {'x': 9}). A key matches by value, so an int and a real of equal value are one key, as in Python — {1: "a", 1.0: "b"} is {1: 'b'}, and a bool and the int it is are one key too, {True: "a", 1: "b"} is {True: 'b'} — and the keys of a dict of int and real keys hold numbers in the §6.7 sense (sum({1: "a", 2.5: "b"}) is 3.5). A **d entry gives every entry d holds at that place, so {**d, "a": 0} keeps 0 for "a", and d | e is the same merge as {**d, **e}. A list, a set or a dict cannot be a key: writing one is told unhashable type: 'list', and so is looking one up, as the program runs, whatever the dict holds ([1] in d, d.). A dict is: - read by key — d[k] returns V; the key is compared with the keys as == does (a number finds a key of either class, {1: "a"}[1.0] is 'a'), and a key absent at run time, or of another type, is a catchable refuse (§10), never a silent default; - tested — k in d / k not in d over its keys (§6.1a); - measured — .length, its entry count; - walked — for k in d: binds each key in insertion order (§7.7); - viewed — d. → dict_keys<K>, d. → dict_values<V>, and d. → dict_items<K, V>, each a view that reads the dict in insertion order as it stands whenever the view is read (below); - read defensively — d. returns the value for key, or default when the key is absent. A default is required (there is no None for a 1-argument form); the result type is the join of V and the default’s type; - copied — d. or dict(d) → a new dict holding the same entries; - changed — d[k] = v, d[k] += v, del d[k], d. and d. are statements that change d in place (§7.17). update also takes names, each written as a string key: d. sets d["a"], and d. applies e first.
It renders in print and + as {k1: v1, k2: v2} in insertion order. Two dicts compare with == / != order-independently — equal when they hold the same key→value mapping regardless of insertion order ({"a": 1, "b": 2} == {"b": 2, "a": 1} is True); its key and value types must meet, and an empty dict unifies with any dict.
ages = {"ana": 30, "bo": 25}
ages["ana"] # 30
"bo" in ages # True
names = ages.keys() # dict_keys(['ana', 'bo'])
pairs = ages.items() # dict_items([('ana', 30), ('bo', 25)])Views. A view follows its dict, as Python’s does: after names = ages. and ages["cy"] = 41, names holds "cy". It is walked by for and a comprehension, read by the conversions (list(v), set(v), dict(ages.) and by every builtin that reads a collection, has a length (len, .length) and a truth value, and takes in. It has no position: names[0] is a type_error in Python’s words, 'dict_keys' object is not subscriptable, naming list(...). A keys or items view is set-like — it equals a set holding its elements, takes & | - ^ beside any collection on either side, as Python’s does, and < <= > >= beside a set or another such view, answering a set: {"a": 1}. is {'a'}. A number beside a view is a type_error in Python’s words, 'int' object is not iterable. Of the set methods it has isdisjoint alone. A values view equals only itself. A view prints as Python’s does and is not a key or set element. An annotation writes one dict_keys<K>, dict_values<V> or dict_items<K, V>.
6.10 Sets
A set literal {a, b, c} (no colons) builds a constructed set<T> — a collection of unique hashable elements — a scalar, a value, or a tuple of hashables (shared as a list is, bounded, never stored). A { … } with a colon is a dict (§6.9), so — because the empty braces {} are the empty dict — the empty set has no literal: write set(), as in Python, or set([]); either renders set(). Elements are homogeneous (a mix of scalar value classes gives a set<value>, holding numbers when all are ints, reals and bools, §6.7, and any other mix, such as {1, (1, 2)}, is a type error naming the two types, as a list’s is); duplicates are removed at construction, and an int, a real and a bool of equal value are duplicates, as in Python, the first written kept ({1.0, 1} is {1.0}, {True, 1} is {True}). A set renders and is walked (for x in s:) in a deterministic sorted order — a defined refinement of Python, whose set order is unspecified. It is the order sorted(s) gives: numbers rank by value, an int and a real together, so {2.5, 1, 3} renders {1, 2.5, 3} and a loop over {2, 1.5} meets 1.5 first. A set is: - tested — x in s / x not in s (§6.1a); - measured — .length; - combined — s | t (union), s & t (intersection), s - t (difference) and s ^ t (symmetric difference), or the methods s., s., s. and s., each returning a new sorted set, where union, intersection and difference take any number of collections and with none answer a copy of s; - compared — == / !=, element-wise and order-independent; s <= t (subset), s < t (proper subset), s >= t (superset) and s > t (proper superset); and the methods s., s. and s.; - copied — s. → a new set holding the same elements; - changed — s., s., s., s., s., s., s. and s. are statements that change s in place (§7.17). update, difference_update and intersection_update take any number of collections, and with none change nothing.
An operator takes two sets, or a set and a dict’s keys or items view, which also takes any collection (§6.9). A method takes any collection set(t) would — a set, list, tuple, string, dict (its keys) or generator expression — as Python’s do. An int, a real and a bool of equal value are one element to every relation ({1} <= {1.0} is True). A relation that only compares takes any element type, since an element of another type equals nothing: {1} <= {"a"} and {1}. are False, {1}. is True, and {1} - {"a"}, {1} & {"a"} and .difference and .intersection keep s’s element type. |, ^, .union and .symmetric_difference keep elements of both, joined as a list literal’s are: a set of ints joined with a set of strings is a set<value>, as Python builds a mixed set, and one joined with a set of tuples is a type error, since a value holds no tuple. Joined in place, by |=, ^=, .update or .add, a set<int> that would hold a string is a type error, as a name keeps its type. On two integers |, & and ^ are bitwise (§6.3), and | on two dicts merges them (§6.9), d |= e changing d in place, e being anything update reads, so d |= [("b", 2)] too. On anything else but sets they are type errors.
s |= t, s &= t, s -= t and s ^= t change s in place to the new set, as xs += ys changes a list, so another name holding the set sees the change, as in Python (§7.17). Inclusion is a partial order ({1} < {2} and {2} < {1} are both False), so a list of sets is not sortable, and min and max do not take sets.
It renders in print and + as {e1, e2, …} in sorted order.
tags = {"red", "green", "red", "blue"} # {'blue', 'green', 'red'} (unique, sorted)
"red" in tags # True
warm = {"red", "orange"}
shared = tags.intersection(warm) # {'red'}
both = tags.union(warm) # {'blue', 'green', 'orange', 'red'}
cool = tags - warm # {'blue', 'green'}
inside = shared <= warm # True
apart = warm.isdisjoint(["blue", "gray"]) # True6.11 Dict and set comprehensions
Alongside the list comprehension (§6.7), a set comprehension {expr for x in xs if cond} builds a new set<T> — deduping and sorting, with expr hashable (a scalar or a tuple of hashables) — and a dict comprehension {key: value for x in xs if cond} builds a new dict<K, V> in insertion order, last value winning on a repeated key and key hashable on the same terms. Both iterate a finite source, bind each element to the comprehension-local x (or to a target list, §6.7), keep those the optional cond admits, and collect the result — the same bounded iteration as a list comprehension, so both terminate and store nothing. A brace form with a : in its head is a dict comprehension; without one, a set comprehension.
squares = {n: n * n for n in [1, 2, 3]} # {1: 1, 2: 4, 3: 9}
parity = {n % 2 for n in [1, 2, 3, 4]} # {0, 1} (deduped, sorted)6.12 zip, enumerate, iter, next and range
Two builtin functions pair finite collections into tuples, each returning an iterator, as CPython’s do: - zip(a, b, …) → iterator<tuple<…>> — pairs elements position by position, up to the shortest input (zip(["a", "b"], [1, 2, 3]) gives two tuples), and zip() gives none. zip(a, b, strict=True) pairs them the same way and refuses, in CPython’s words, if the inputs differ in length: zip() argument 2 is shorter than argument 1. The refusal is a value_error, and the pairs read before it have been given. - enumerate(xs [, start]) → iterator<tuple<int, T>> — numbers each element, from 0 by default or from an optional int start (positional or the start= keyword; may be negative).
Each argument is any finite collection (a dict contributes its keys, a set its sorted elements, a dict view what it views) or an iterator. reversed (§6.3) returns one too.
iter(x) → iterator<T> makes one over any finite collection, read as for reads it, and answers an iterator with itself, so iter(e) is e. It prints as CPython’s class for it: <list_iterator object> over a list, <tuple_iterator object>, <set_iterator object>, <dict_keyiterator object> over a dict, and over a string <str_ascii_iterator object>, or <str_iterator object> when a character is not ASCII. It takes exactly one argument; CPython’s two-argument form calls a function, and no value here is one. An iterator is not a key or set element, where CPython hashes one by its identity: a key here is held by its value, and an iterator is equal only to itself (Python Specification §5.1). any, all, in, a set’s intersection, issuperset and isdisjoint stop reading an iterator once the answer is known, as CPython’s do, and leave the rest.
next(e [, default]) reads the next element of the iterator e. When none is left, next(e, default) answers default, which is evaluated before the read, and next(e) is a refuse with discriminator stop_iteration where CPython raises StopIteration. The default and the elements join as two elements of one list display do (§6.7), so a default of another scalar class gives a value (§4.1), and one that cannot join, such as a list beside an int, is a type_error. next(xs) over a list is a type_error in CPython’s words, 'list' object is not an iterator. next takes no keyword argument, and next(e) may stand alone as a statement, which skips an element (§7.4).
An iterator is read once. for, a comprehension, list, tuple, set, dict, sorted, sum, min, max, any, all, join, extend, update, zip, enumerate, next and unpacking each take the elements they read, so a second read finds only what the first left. A for reads one per pass, so break leaves the rest, and x in e reads up to the first match. Every name holding an iterator holds the same one. An iterator never reads past the length its source had when it was made; a dict or set that changes size refuses the next read in CPython’s words, dictionary changed size during iteration. A set’s update and difference_update and a dict’s update read one element at a time, so one over the collection they change refuses as soon as its size changes (§7.17).
It has no length and no position: len(e), e[0] and e[1:] are type_errors that name list(...), which reads it into a list with both. It is always truthy, equal only to itself, and prints as its class without CPython’s address — <zip object>, <enumerate object>, <list_reverseiterator object>. Printing it reads nothing. An annotation writes it iterator<T>.
paired = zip(["ana", "bo"], [90, 85])
print(paired) # <zip object>
print(list(paired)) # [('ana', 90), ('bo', 85)]
print(list(paired)) # []
print(list(enumerate(["ana", "bo"], 1))) # [(1, 'ana'), (2, 'bo')]
e = iter([1, 2])
print(next(e), next(e)) # 1 2
print(next(e, "done")) # done
for name, score in zip(names, scores): # tuple unpacking (§7.7)
print(name + " scored " + str(score))range(stop), range(start, stop) and range(start, stop, step) → range: the integers from start (default 0) up to but not including stop, step apart (default 1); a negative step counts down. It is how a counting loop gets its numbers. Every argument is an int, and there are one to three of them. A zero step is a refuse with CPython’s text, range() arg 3 must not be zero (discriminator value_error).
list(range(4)) # [0, 1, 2, 3]
list(range(2, 7)) # [2, 3, 4, 5, 6]
list(range(10, 0, -3)) # [10, 7, 4, 1]
list(range(5, 2)) # []
for i in range(3):
print(i)A range is CPython’s: it holds its three bounds, never its integers, so a for walks it and len, an index, in, .index and .count are answered without building it. It prints as range(0, 3), equals a range denoting the same integers and nothing else, and a slice of it is a range. list(range(n)) builds its integers, and they count toward a service’s budget on how much one run builds (§10), as [0] * n’s do; so does what a run adds to a collection it holds. sum, min and max read a range from its bounds, and a generator of ints one element at a time, building nothing: sum(range(10 ** 6)) is 499999500000. A generator’s elements from its first that is not an int are held. any and all hold nothing of a generator, and stop at the element that decides.
6.13 Spreading a collection
A * before an element of a list, a tuple or a set spreads a collection into it, as in Python: its elements stand in that place, in the order 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)
for x in *a, 3: # 1, 2, 3
print(x)Anything for walks may be spread (§7.7): a list, a set, a dict (its keys), a dict view, a string, a tuple and an iterator, which the spread reads to its end. Anything else is a compile-time type_error that names the spread, where Python raises Value after * must be an iterable, not int as the program runs. The elements and spreads are evaluated left to right, each once, and the result is a new collection: ys = [*xs] is a copy, and a change to ys is not seen through xs. In a subscript a spread makes the index a tuple, as a comma does: d[*k] is d[(*k,)].
The type is the one the elements would give written out. A list is a list<T> and a set a set<T>, with T joined as a list literal’s elements join (§6.7). A tuple whose spreads are all of tuples written with a fixed number of positions keeps every position’s type, so (0, *(1, "a")) is a tuple<int, int, string>. One that spreads anything else has no length in its type, so it is a tuple<T, ...>: (*xs, 3) with xs a list<int> is a tuple<int, ...>.
A starred value alone is no tuple: y = *a, return *a and for x in *a: are each a parse_error at the *, “can’t use starred expression here”, as in Python, and y = *a, is the tuple. As a comprehension’s element, [*x for x in xs], it is a parse_error, “iterable unpacking cannot be used in comprehension”. A * before an argument of print spreads the collection across print’s arguments (§7.13); so does one before an argument of a script’s call, §7.4, and one before a method’s argument, §8.4; before an argument of a verb it is not taken. One before an argument of a builtin spreads the collection across the call’s arguments, which then answers what it answers written out: max(*xs), zip(*rows), range(*bounds). A count the builtin does not take is refused in Python’s words: at compile for a tuple of fixed length or a list display, and as a refuse with discriminator type_error as the program runs for any other. A builtin whose result’s type would depend on the count is a type_error, as max(*xs) is for a list of lists. A keyword argument beside the * binds as Python binds it, written before the * or after it: int(*["ff"], base=16), round(*xs, ndigits=1), sorted(*xs, reverse=True), max(*xs, default=0), zip(*rows, strict=True). A count that does not fit the keyword is refused the same way, in Python’s words, and a name the builtin does not take is a type_error. One before an argument of a built-in method spreads the same way, "-"., "abc"., and through a type’s name what it gives first may be the receiver, str.. A count a built-in method does not take, spread or written out, is refused in Python’s words, str., replace expected at least 2 arguments, got 1: at compile where the count is written or fixed, and as a refuse with discriminator type_error as the program runs for any other. A name beside the * binds as written out, once the count is known: "a b c"., d., xs., which sorts where ys gives nothing and otherwise raises sort() takes no positional arguments, in Python’s words. One before an argument of a call into math, cmath, vector or struct spreads the collection across the call’s arguments, as in a script’s call: given p = [1.0, 1.0], math. answers what math. answers. A count of arguments the function does not take is then refused as the program runs, with type_error, and math. places an a or b given by name at the count the * gives: with xs holding one element, math. compares it with 1.0. A ** in a dict is not taken either; to merge two dicts, copy one and update it (§6.9).
6.14 math and cmath
import math
import cmathPython’s math and cmath, provided by the language: an import of either reads nothing from your store (§9.4), and math.<name> is available from the import to the end of the unit (§9.3). from math import sqrt, pi binds the names it lists, and leaves math itself unbound, as in Python.
| Name | Answers |
|---|---|
math. |
the square root, a real |
math. |
e to the power x |
math., math. |
the natural logarithm; with base, the logarithm of x to that base, rounded once: math. is 3.0 |
math., math. |
the logarithm to base 10, and to base 2 |
math., math. |
the error function, and its complement 1 - erf(x) |
math., math. |
the gamma function, and the natural logarithm of its magnitude: math. is 24.0 |
math., math., math. |
of an angle in radians |
math., math., math. |
the angle in radians whose sine, cosine or tangent is x: asin and atan from -π/ to π/, acos from 0 to π |
math. |
the angle of the point (x, y), from -π to π |
math. |
the distance from the origin to the point with these coordinates, any number of them |
math., math. |
the angle x from radians to degrees, and from degrees to radians |
math. |
a bool: whether a and b are within rel_tol of the larger in magnitude, or within abs_tol |
math., math., math. |
an int: the greatest integer no larger than x, the least no smaller, and the one nearest zero |
math., math., math., math., math., math. |
the hyperbolic functions and their inverses |
math., math., math., math., math. |
the cube root; 2 to the power x; e to the power x less 1; the natural logarithm of 1 plus x; and x to the power y, always a real |
math., math., math., math., math., math. |
the magnitude; x with the sign of y; the remainder with the sign of x; the remainder from the nearest whole quotient; the next real after x towards y; the gap to the next real |
math., math., math. |
(fraction, whole), a tuple<real, real>; (mantissa, exponent), a tuple<real, int>; and x times 2 to the power i, an int |
math., math., math., math., math., math. |
an int, of ints: math. is 0 and math. is 1; perm with one argument takes k as n; a real argument is a type_error |
math., math., math. |
the exact sum of the elements, a real; the product of start and the elements, an int, a real or a complex; and the distance between two points of one length, a real |
math., math., math. |
the nearest reals to π, e and 2π |
math., math. |
the positive infinity and the NaN |
math., math., math. |
a bool: whether x is an infinity, a NaN, or neither; an int and a bool are finite |
| Name | Answers |
|---|---|
cmath. |
e to the power z, a complex |
cmath. |
the principal square root, a complex |
cmath. |
the argument of z, a real from -π to π |
cmath. |
(abs(z), phase(z)), a tuple<real, real> |
cmath. |
the complex of modulus r and argument phi |
cmath., cmath., cmath. |
a bool: whether a part of z is an infinity, whether a part is a NaN, and whether both parts are finite |
cmath., cmath., cmath., cmath., cmath. |
as in math |
cmath., cmath. |
the complex with a zero real part and an imaginary part that is inf, and nan |
A math function takes an int, a real or a bool; a cmath function takes a complex too, except rect, whose two arguments are real. Anything else is a type_error; prod takes a complex too. Only isclose and prod take keyword arguments, prod’s start by name only, and hypot, gcd and lcm take any number of arguments, none included. A wrong number of arguments is a type_error in Python’s words, math. or math.. A constant called (math.), a function used as a value (f = math.), a name the namespace does not have, a namespace used without its import, and a call on a line of its own are each a parse_error.
import math
import cmath
print(math.sqrt(2)) # 1.4142135623730951
print(math.log(8, 2)) # 3.0
print(math.log(1000, 10)) # 3.0
print(math.erf(1)) # 0.8427007929497149
print(math.gamma(5)) # 24.0
print(math.atan2(1, 1) == math.pi / 4) # True
print(math.degrees(math.asin(0.5))) # 30.000000000000004
print(math.hypot(3, 4, 12)) # 13.0
print(cmath.exp(1j * math.pi / 2)) # (6.123233995736766e-17+1j)
print(cmath.sqrt(-4)) # 2j
print(cmath.polar(3 + 4j)) # (5.0, 0.9272952180016122)Each function follows Python’s algorithm with every power, logarithm, exponential, trigonometric step, inverse and modulus correctly rounded, as ** is (§6.3), so an answer is the same wherever it is computed and departs from Python’s only where Python’s platform rounds one of those steps wrongly (Python Specification §8.16). degrees and radians are the exact product rounded once, where Python’s rounds twice, so a last digit may differ from Python’s: math. is -50465.9885268683, where Python answers -50465.98852686831. math. is rounded once too, where Python’s divides two rounded logarithms: math. is 3.0, where Python answers 2.9999999999999996. An int past 2**53 is taken as the int it is, where Python rounds it to a real first: math. is 945130030.1318684, where Python answers 945130030.1318682, and math. is 3.0, where Python answers 2.0. hypot of any number of coordinates is correctly rounded, and dist and hypot of one point never differ. fsum, prod and dist are the exact value rounded once, where Python’s round at every step: math. is 1e+308, where Python raises OverflowError; math. is 0.0, where Python answers nan; and math. is 4.199999999999999, where Python answers 4.2. prod takes numbers, where Python’s multiplies whatever * takes. cmath., cmath. and cmath. give each part, real and imaginary, as its exact value rounded once, where Python’s rounds the parts it composes and again: cmath. is 1.5681835703495033, where Python answers 1.5681835703495035. A call refuses where Python raises, in Python’s words: an argument outside the domain (math., math., math., math., math., or a pole of the gamma function, math., math., math.) with value_error, math domain error; an int answer past what an int holds (math.) with arithmetic_overflow, integer overflow: `math.; an infinity or a NaN to math., math. or math. with arithmetic_overflow, cannot convert float infinity to integer, or value_error, cannot convert float NaN to integer; math. with division_by_zero, float division by zero; and a negative tolerance to isclose with value_error, tolerances must be non-negative. A result too small to represent is the nearest one that is: math. is 0.0. Two departures from Python: a result past the largest real is inf or -inf (math., math., math., cmath. is (inf+0j)), where Python raises OverflowError; and an infinite argument with no value is nan (math., math., math., math.), where Python raises ValueError. There is one NaN, so math. is 1.0, where Python answers -1.0 (Python Specification §8.16).
A constant costs nothing, as the literal it names does; each call costs one operation.
6.15 vector
import vectorA namespace the language provides, imported and reserved as math is (§6.14). Python has no module of this name. Its two functions apply a matrix to a list, and add the squares of a list, along axes, each in one call.
| Name | Answers |
|---|---|
vector. |
xs with the square matrix m applied along the axes axes |
vector. |
the sum of the squared parts of the elements of xs, for each pattern of the axes axes |
xs is a list of 2ⁿ numbers, and axis a is bit a of an index into it, so axis 0 is the lowest bit. axes names k different axes, and m is 2ᵏ rows of 2ᵏ numbers. The first axis named is the high bit of a row or column of m, so along [a, b] row 2 of m is the element whose bit a is 1 and whose bit b is 0, and along [1, 0] the rows of a 4 × 4 matrix follow the order of a four-element list. For each setting of the bits the axes do not name, the 2ᵏ elements the named bits reach are replaced by m times them: the element at row r becomes m[r][0] * x0 + m[r][1] * x1 + …, where xc is the element at column c. Each element keeps the class that arithmetic gives it, as in Python; one of ints is that int, and any other, its operands finite, is in each part the exact sum of the exact products rounded once, whatever order they come in. The answer is that of a Python function stated in full (Python Specification §8.17).
import vector
print(vector.apply([4.0, 8.0], [[0.75, 0.25], [0.25, 0.75]], [0])) # [5.0, 7.0]
print(vector.apply([1, 2, 3, 4], [[1, 0], [1, 1]], [0])) # [1, 3, 3, 7]
print(vector.apply([1, 2, 3, 4], [[1, 0], [1, 1]], [1])) # [1, 2, 4, 6]
upto = [[1, 0, 0, 0], [1, 1, 0, 0], [1, 1, 1, 0], [1, 1, 1, 1]]
print(vector.apply([10, 20, 30, 40], upto, [1, 0])) # [10, 30, 60, 100]
print(vector.apply([10, 20, 30, 40], upto, [0, 1])) # [10, 60, 40, 100]
print(vector.apply([1.0, 2.0], [[1, 0], [1j, 1]], [0])) # [1.0, (2+1j)]An argument that is not a list of numbers, a matrix that is not a list of lists of numbers, and axes that are not a list of integers are each a type_error. A call refuses with value_error where the length of xs is not a power of two, an axis is outside 0 to n − 1 or named twice, or m is not 2ᵏ rows of 2ᵏ; and with arithmetic_overflow where a product or sum passes the range of its class, as * and + do (§6.3). A call costs one operation, whatever the length of xs.
vector. takes the same xs and the same axes, and answers a list of 2ᵏ reals: one sum for each pattern of the bits the axes name, listed with the first axis named as the high bit of the pattern, as the rows of m are. The sum for a pattern is the exact sum of the exact squares of z. and z. of every element z = complex(x) whose bits on the axes make that pattern, rounded once, so it does not depend on the order the elements are in. Naming no axis makes one sum of every element. The answer is that of a Python function stated in full (Python Specification §8.17).
import vector
print(vector.sumsq([3, 4], [])) # [25.0]
print(vector.sumsq([1, 2, 3, 4], [0])) # [10.0, 20.0]
print(vector.sumsq([1, 2, 3, 4], [1])) # [5.0, 25.0]
print(vector.sumsq([1, 2, 3, 4], [1, 0])) # [1.0, 4.0, 9.0, 16.0]
print(vector.sumsq([1, 2, 3, 4], [0, 1])) # [1.0, 9.0, 4.0, 16.0]
print(vector.sumsq([1.0 + 2.0j, True, -0.0, 0.0], [1])) # [6.0, 0.0]A vector that is not a list of numbers and axes that are not a list of integers are each a type_error. A call refuses with value_error where the length of xs is not a power of two or an axis is outside 0 to n − 1 or named twice. A square, or a sum of squares, past the range of a real is inf, where Python’s fsum may raise OverflowError, and an element with a NaN part makes its sum nan: vector. is [inf, 1.0]. A call costs one operation, whatever the length of xs, and builds the 2ᵏ sums alone.
6.16 struct
import structPython’s module of that name, provided by the language, imported and reserved as math is (§6.14). It lays numbers out as the bytes of a blob, and reads them back, by a format string, as Python’s does.
| Name | Answers |
|---|---|
struct. |
a blob holding the values, laid out as the format describes |
struct. |
a tuple of the values the format reads from the blob buffer |
struct. |
how many bytes the format lays out |
A format opens with an optional @ (native sizes, each value aligned, the default), = or < (little-endian, no padding), or > or ! (big-endian), and then has codes, each after an optional count. The integers are b, h, i, l and q, of 1, 2, 4, 4 and 8 bytes, signed, with their unsigned twins B, H, I, L and Q; l and L are 8 bytes in native layout, and n, N and P are 8-byte integers that only native layout has. ? is a bool, e, f and d are reals of 2, 4 and 8 bytes, and F and D are complex numbers of two f or two d. x is a pad byte, c a blob of one byte, and s and p one blob whose length is the count. Native layout is Python’s on a 64-bit x86 Linux host, wherever a program runs, so a format lays out the same bytes everywhere.
import struct
b = struct.pack("<hd", -2, 0.5)
print(b) # FEFF000000000000E03F
print(len(b), struct.calcsize("@hd")) # 10 16
n, x = struct.unpack("<hd", b)
print(n, x) # -2 0.5
print(struct.unpack(">I", blob("0000FF01"))) # (65281,)
zs = [1j, complex(-0.0, 2.0)]
kept = struct.pack(f"<{len(zs)}D", *zs)
print(list(struct.unpack(f"<{len(kept) // 16}D", kept))) # [1j, (-0+2j)]unpack takes its tuple’s types from a format written in the call: a string literal gives each position the type of its code, so struct. is a tuple<int, real>, and an f-string whose holes are integers and whose codes all read one type gives a tuple of that type, as f"<{n}D" gives a tuple<complex, ...>. Any other format gives a tuple<value, ...>.
A call refuses where Python raises, in Python’s words: with value_error for a format it cannot read, a count of values or a length of blob the format does not lay out, a value its code does not pack, or an integer outside its code’s range, and with arithmetic_overflow for a finite real too large for e or f. A DKE int is at most 2⁶³ − 1, so unpack refuses an unsigned integer above that with arithmetic_overflow. An infinity or a NaN packs and unpacks as in Python, and there is one NaN: pack writes it as the quiet NaN with its sign bit clear (7ff8000000000000 for d), where Python writes the bits of the NaN it was given. A call costs one operation, however many values it packs or reads, and a * argument one more (§6.13). The rules in full are in Python Specification §8.18.
6.17 Special names on a built-in value
A special name is one that begins and ends with two underscores. A value holds the special names its Python class holds, and one called by name answers as the spelling it stands for, at that spelling’s cost. A reflected name takes its operands the other way round.
xs = [3, 1]
print(xs.__len__()) # 2
print((2).__rsub__(10)) # 8
print(xs.__contains__(3)) # True
xs.__setitem__(0, 5) # xs[0] = 5
xs.__iadd__([7]) # xs += [7]
print(xs) # [5, 1, 7]A name the class does not hold, a count of arguments its method does not take, and a keyword argument are type_errors in Python’s words. So is __hash__ on a list, a dict or a set, which is None in Python: 'NoneType' object is not callable. Where Python answers something no value here holds, the call is a type_error saying so: NotImplemented, which an operator’s special name answers for an operand its class does not take; __class__, a class; a special name read with no call, a method; and the machinery behind a value, such as __sizeof__. A special name that changes its receiver, __setitem__, __delitem__ or an in-place name such as __iadd__, is a statement, as append is (§7.17), and read as a value it is a type_error. A * and a ** before any dict are read as at any method (§7.4): the names are merged and refused in Python’s words, wrapper __len__() takes no keyword arguments, before the count is, as the program runs. Beside a ** whose keys are known only as the program runs, a call whose answer no value here holds, and a change read as a value, are refused as the program runs too, after the names, and a change is made first; a value bound from one is a type_error where the unit is compiled. The rules in full are in Python Specification §8.20.
7. Statements
Every statement is one logical line, or a :-headed block with an indented suite. A ; ends a simple statement too, so a = 1; b = 2; is two, and a block’s simple statements may stand on its header’s line, as if a: b = 1; c = 2 and def f(): pass do. A compound statement stands on a line of its own, and never after a ;. Indent with spaces only. A line is indented deeper only as the first line of a block, below a header that ends in :. A line indented deeper anywhere else is a parse_error, “unexpected indent”.
An expression alone is a statement: it is evaluated and its value dropped, so x + 1, xs[0] and s. run, and a type or a name the expression gets wrong is refused as it is in a binding. ... alone does nothing, as pass does. An annotation with no value, x: int, binds nothing: a read of x needs a binding below it.
A list written inside ( ), [ ] or { } may end with one trailing comma, and it changes nothing: the arguments of any call, the parameters of a def or a method, a class’s base, and the elements of a literal or a pattern. f(1, 2,) is f(1, 2), and a call written one argument per line may end every line with a comma. A comma never stands alone or doubled: f(,) and [1,,2] are refused. In a subscript, a comma makes the index a tuple, as in Python: d[1, 2] is d[(1, 2)], and d[1,] is d[(1,)].
7.1 Declaration
def audit(K: string, s: string):
<suite>
def doubled(n: int) -> int: # value script: returns an int
return n * 2The def keyword, name, typed parameters (name: type), an optional -> type return type, :, indented body. A type is a scalar / read-result type (§5.5) or a parameterized collection — list<T>, set<T>, dict<K, V>, tuple<T, ...>, nestable (dict<string, list<int>>) — so a collection crosses a function boundary as an argument or return value (def total(xs: list<int>) -> int:, def doubled(xs: list<int>) -> list<int>:). A parameter may carry a default (def f(x: int, y: int = 1)) — a constant expression whose type matches the parameter; a defaulted parameter may not precede a required one. Two declarations in a unit must not share a name, whatever their kinds: a def, a @rule, a @constraint and a class share one name space, and a second declaration of a name is refused with the kind that holds it. A call may target a declaration above or below it in the unit, as long as no call leads back to its caller (compile-before-call). A module’s own statement runs where it stands, so one that reaches a def below it is told name 'g' is not defined when the unit compiles.
A parameter may be variadic — written *name: T — collecting the surplus positional arguments into a tuple<T, ...> (possibly empty): def total(label: string, *nums: int) -> int: accepts total("a", 1, 2, 3) with nums = (1, 2, 3). There may be at most one, its element type is required, it takes no default, and it cannot be passed by keyword; inside the body it is an ordinary tuple (nums[0], len(nums), for n in nums, sum(nums)). There is no **kwargs: a signature names every keyword a call may pass, so a misspelt keyword is refused where it is written. Use explicit named parameters or a dict parameter.
A / makes the parameters before it positional-only, and a bare * or a variadic makes those 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). Passing a by name or c by position is a type_error naming the parameter and its marker. A keyword-only parameter with no default may follow one with a default.
A script with no -> type whose body holds no return <expr> is a void script (no value); one written def name(...) -> type: is a value script — it produces a value of type and MUST return a value on every path (§7.12). With no -> type, a body holding a return <expr> is a value script of the type its returns give. Only a value script may be called as an expression (§7.4). A value script that hands on a store fold’s result declares -> value, as a value method does (§8.5), and its caller narrows it with match.
7.2 Assignment (bind a read)
hist = get(K.s.color)A name is bound by bare assignment, name = <expr>. The right side must not be a statement-only verb (no value). Augmented assignment x += e (and -= *= /= %= //= **=) is accepted on a bare name; x <op>= e is equivalent to x = x <op> e. It does not apply to a field (obj. is rejected): a field is a provenance-carrying claim, not a mutable cell, and a compound assignment names no source — update a field by reading it, then writing the new value back with remember(obj.. An annotated assignment x: <type> = <expr> gives the binding its type explicitly (n: int = 5, xs: list<int> = [1, 2, 3]); the annotation must agree with the initializer, and a bare x: <type> with no value is rejected (write x: <type> = <value>). An element write xs[i] = v or d[k] = v, and d[k] += v, changes the collection in place (§7.17); its value may be a bare comma-list, so ts[0] = 3, 4 writes a tuple. An element or a slice may also be one target of several, in a target list, a chained assignment or a for header, written the same way. Chained assignment a = b = <expr> binds every target to one value (evaluated once), left to right: a = xs[0] = 5 binds a and writes xs[0], and xs[a] = a = 1 writes xs at the a held before the statement. Unpacking a, b = <expr> destructures a tuple or a list into its targets, each a name, an element or a slice. A tuple must hold exactly the target count, written parenthesized ((1, 2)) or as a bare comma-list (b, a). A list, a variadic tuple<T, ...> such as a *args parameter, a string, a set, a dict (its keys), a dict’s view or an iterator gives each target its element type, and its length is checked when the statement runs, so k, v = line., x, y = args and a, b = "xy" work and a mismatch is a catchable refusal in CPython’s words — not enough values to unpack (expected 2, got 1) or too many values to unpack (expected 2), discriminator value_error. Anything else, a number included, does not unpack. The whole right side is evaluated before any target binds, so a, b = b, a swaps (§7.7), and so does xs[i], xs[j] = xs[j], xs[i]; the targets are then assigned left to right, so i, xs[i] = 1, 5 writes xs[1]. A target may itself be a parenthesized target list, so nested unpacking (a, b), c = ((1, 2), 3) (or a, (b, c) = …) destructures a tuple or list of tuples or lists, each level arity-checked independently. One target may be starred (a, *rest = (1, 2, 3) → rest = [2, 3], always a list): it absorbs the surplus elements (which must share one type), the RHS needing at least the fixed-target count. A parenthesized group may hold its own star, so a, (b, *c) = 1, (2, 3, 4) gives c = [3, 4]. The star may stand before a group or an element as well: a, *(b, c) = 1, 2, 3 unpacks what it absorbs, binding b = 2 and c = 3, and with xs = [[0]], a, *xs[0] = 1, 2 writes the list [2] to xs[0]. A target list may be written in brackets too, [a, b] = t, where one target is a list of one, [a] = t, and none is a list of none, [] = t, which asks that t hold nothing; a chained assignment’s target may be a target list, t = a, b = 1, 2. An element target may begin with a value no name holds, f(xs)[0] = v, and changes the collection that value is, so a script that returns the list it was given hands back the caller’s. A field is never a target: written as one, in any of these positions, it is refused in favour of remember(...), and so is an annotation on anything but a name, xs[0]: int = 1.
7.3 What a value prints as
print([3, 1, 2]) # [3, 1, 2]
names = list_subjects(Cat)
print(names) # 2 entries [felix, tom]
proof = why(Cat.tom.old)
print(proof) # proof_tree(2)
age = current(Cat.felix.age)
print(age) # <empty>print accepts any expressible value and writes it as its text form, the one str(x) gives. A boolean is written True / False. A constructed list<T> renders [e1, e2, …]; a tuple renders (e1, e2, …); a dict renders {k1: v1, k2: v2} in insertion order; a set renders {e1, e2, …} in sorted order (an empty set, set()) — each element rendered recursively. A read is bound to a name on its own line and the name printed (§7.2), as above.
What a read gives back is written too, each type its own way, and this is the whole list (Python Specification §8.13.1):
| Value | Written as |
|---|---|
claim_list, subject_set, string_list, result_type_set |
a counted preview, 2 entries [e1, e2] (1 entry [e1] for one, 0 entries [] for none): at most 8 elements, then , +<k> more |
proof_tree |
its type name and size, proof_tree(2) |
diagnosis |
the explanation itself, which may span several lines |
active_claim |
its value alone |
verify_result |
its type name and verdict, verify_result match=True, with conflicted appended when the cell holds competing values |
| a read that found nothing | <empty> |
These are for reading, not for parsing: to take a value out of a read, iterate the collection or read the field. The call itself, with several arguments, sep, end and a * spread, is §7.13.
7.4 Script call (invocation)
audit_subject(K, s) # statement: run for effects, discard any value
total = summed(a, b) # value-call: bind what a value script returns
total = summed(*pair) # a spread: each element of `pair` an argumentA plain call of a stored script: a script is called like any function. The call is fixed when the calling program is compiled and runs the body the callee had then (§9.6); a script cannot call itself, directly or through another. Arguments bind by position, or by keyword (greet("World", greeting="Hi")); a keyword may not precede a positional argument, and an omitted argument uses the parameter’s default (§7.1). A call that does not fit is a type_error in Python’s words and in Python’s order: summed() got an unexpected keyword argument 'z', summed() takes 2 positional arguments but 3 were given, or summed() missing 1 required positional argument: 'b'.
A positional argument written *xs spreads xs: each element is a positional argument of its own, in its place, and a variadic parameter collects what is left over. A spread may follow a keyword argument, as in Python: summed(b=2, *xs) fills a from xs. What may be spread is what for walks, and a tuple (§6.13). A tuple written with a fixed number of positions has its length in its type, so a call spreading only such tuples is checked as written: summed(*(1, 2, 3)), given two parameters, is a type_error. Any other spread has a length known only as the program runs, so the call is checked then, once every argument is evaluated, and one that does not fit is a refuse with discriminator type_error, in Python’s words: summed() takes 2 positional arguments but 3 were given, summed() missing 1 required positional argument: 'b', or summed() got multiple values for argument 'b' when a keyword names a parameter the spread reaches. Each element must suit every parameter it could fill, and so must each argument written after the spread.
An argument written **d gives each entry of the dict d as a keyword argument, as in Python: summed(**{"a": 1, "b": 2}) is summed(a=1, b=2). Its keys are known only as the program runs, so the call is checked then, and one that does not fit is a refuse with discriminator type_error, in Python’s words: summed() got an unexpected keyword argument 'z', or __main__.. An operand that is no dict is a type_error.
print, a builtin, a built-in type’s method, sorted, a list’s sort and a math function take a ** before a dict display whose keys are all literals, its keys given as keyword arguments: int("10", **{"base": 2}) is 2, and xs. sorts in reverse. A display that gives nothing gives the call nothing, and a key given twice in one display keeps its later value, as in Python. A call it does not fit is a type_error where the unit is compiled, in Python’s words and order: a keyword two arguments give (print() got multiple values for keyword argument 'sep', and '1' for print(**{1: 2}, **{True: 3}), a key that is no string being one key as a dict’s are equal), then a key that is no string (keywords must be strings), then a name the callee does not take ('zz' is an invalid keyword argument for print(), len() takes no keyword arguments). Any other operand after a ** there, such as a name, is read when the call runs at print (§7.13), at a builtin that takes no name, such as len, abs, list or isinstance, and at int, round, pow, complex, str, zip, enumerate, sum, sorted, min, max and dict, at a built-in value’s method, a change among them, and at a function of math, cmath, vector or struct, and not yet anywhere else. At a builtin that takes no name it is read after the positional arguments, and a mapping that gives nothing leaves the call as written, so len(xs, **opts) is len(xs) while opts is empty. Any name it gives is a catchable refuse with discriminator type_error, len() takes no keyword arguments, after a name given twice (len() got multiple values for keyword argument 'x', or '1' where two mappings give the key 1) or, once every mapping is merged, a key that is no string (keywords must be strings), and a display written beside such a **, whose key is no string, is merged with it as the call runs: print(**{1: 2}, **n, **k) with n and k both holding "x" is print() got multiple values for keyword argument 'x'. Two displays written out that give one name are refused as the call runs too where a ** the program reads is merged before the second giving, in the order Python merges: print(**n, **{"x": 1}, **{"z": 1}, **{"z": 2}) with n holding "x" is print() got multiple values for keyword argument 'x', and the refusal is catchable; with every mapping written out it is refused where the unit is compiled. A count it does not take is refused only where the mapping gives no name, as the program runs: with opts empty, len(xs, 2, **opts) is len() takes exactly one argument (2 given). So it is at a built-in value’s method and a function of a built-in namespace that take no name, s. and math., and without such a ** the count is a type_error where the unit is compiled. So is an argument it does not take, of a type or a value it refuses: with opts holding x, len(5, **opts) is len() takes no keyword arguments and s. is str., and with opts empty each is refused in the words it has without such a **, object of type 'int' has no len(), as the program runs. So is a change’s, such as s., and a dict’s get or setdefault of a key alone, which has no None to answer, once the names are merged. So is a change that would give its receiver a second type: after s = {1}, s. is set. with opts holding x, and with opts empty the refusal a name’s one type gives, as the program runs; the statement refuses either way, so the receiver keeps its type. Beside a * whose length is known only as the program runs, it is read where what is called takes no name, a builtin, a function, a built-in value’s method or a change, after every argument and before the count the * gives: with opts holding x, len(*xs, **opts) is len() takes no keyword arguments whatever xs holds. So it is at int, round, pow, complex, str, zip, enumerate, sum and dict, which take names, over the count the * gives and in the order below: with xs empty and opts holding base, int(*xs, **opts) is int() missing string argument. So it is at sorted, min and max, which count the * before they read a name, as Python’s do: over two elements sorted(*xs, **opts) is sorted expected 1 argument, got 2 whatever opts holds. So it is at a built-in value’s method that takes names, split and its kin, and at math., over the count the * gives. So it is at a list’s sort, which counts what the * gives as arguments by position and refuses any, sort() takes no positional arguments, and at a dict’s update, which writes what the * gives and then the names, as keys. So it is through a type’s name, where the receiver is the first thing the * gives, and the names are merged before the receiver is taken, as Python merges them: with xs empty and opts holding a key that is no string, list. and str. are keywords must be strings, and otherwise unbound method list. and unbound method str.; where xs gives a value of another type first, descriptor 'upper' for 'str' objects doesn't apply to a 'int' object. So it is where nothing types what the * gives, as with xs = [].
At int, round, pow, complex, str, zip, enumerate, sum, sorted, min and max the names it gives are the call’s, as if written: int(s, **opts) with opts holding base is int(s, base=...). A name the callee does not take is a catchable refuse with discriminator type_error, 'x' is an invalid keyword argument for int(), after too many names in all (int() takes at most 2 arguments (3 given)) and after a name the call lacks or is given by name and by position, as in Python. Any other refusal is the one the call written out meets, as the program runs. enumerate reads the names where they stand, as Python’s does: with two arguments in all a name must be start, and with one it must be iterable, so enumerate(**opts) with opts holding start alone is refused 'start' is an invalid keyword argument for enumerate(), as enumerate(start=1) is. sorted, min and max count the names apart from the positional arguments, and sorted is refused under sort’s name, as Python’s is. A key a mapping gives is never a function, since a dict holds none, so the call refuses it, 'int' object is not callable, once there is an element to call it on, and over none is the call without it. dict takes every name as a key, after its positional argument’s entries. A call whose type would turn on the names 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 no 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). At a built-in value’s method the names are the method’s, as if written: split, rsplit, splitlines and expandtabs take theirs as Python’s do, and any other method refuses any name it is given, str.. So does a method that changes its receiver: 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 refuses any name before the collection changes, list., and a count it does not take only where the mapping gives no name. Read as a value and bound to no name, such a change is made, and only then is the value refused, as the program runs: with opts empty, print(xs. appends 2 and is refused. A name written beside the ** is merged with the names it gives, so that with opts holding x, xs. is list.. Through a type’s name, a receiver written of another type is refused once the names are merged, before the method reads a name: str. 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. Without such a ** each is a type_error where the unit is compiled. Through its type a call given no positional argument, only such a **, merges the names and is then refused for its missing receiver: str. is unbound method str., after keywords must be strings or a name given twice. A name written beside such a ** at sorted, min, max or a list’s sort that the callee does not take waits for the merge too: with opts holding x, sorted(xs, x=1, **opts) is sorted() got multiple values for keyword argument 'x', and with opts empty 'x' is an invalid keyword argument for sort(). A list’s sort whose receiver is written, with a typed * after it, counts what the * gives before it names one: xs. and list. are sort() takes no positional arguments where ys gives a value and 'x' is an invalid keyword argument for sort() where it gives none, as the program runs; where nothing types ys the second is refused where the unit is compiled. A positional argument written beside the * is counted with what the * gives, before any name: xs. is sort() takes at most 2 arguments (3 given) where ys gives one value, as the program runs, and where nothing types ys it is refused where the unit is compiled. A key= beside the * is applied only where the sort runs: xs. over a list of ints is sort() takes no positional arguments where ys gives one value, and object of type 'int' has no len() where it gives none, each as the program runs and catchable; where nothing types ys, and where there is no *, a key the elements refuse is refused where the unit is compiled. A list’s sort 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: list. sorts ys where xs gives nothing, list. with xs giving nothing is unbound method list., and over a tuple t, list. is descriptor 'sort' for 'list' objects doesn't apply to a 'tuple' object. At a function of a built-in namespace the names are merged under the function as Python’s call names it, math. or _struct.: math. takes its names as int does above, named alone, isclose() missing required argument 'b' (pos 2), and any other function refuses any name, math., and math. before it reads a key, log() takes no keyword arguments. A call of slice refuses any name before it reads a key too, slice() takes no keyword arguments. The three methods a type answers itself, dict., str. and float., read a ** the same way, named with their type: with opts holding x, dict. is dict., and with opts empty dict.. A value bound from such a call, 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 name bound by bare assignment takes its value’s type and such a call has none: v = list. 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), v = abs("a", **opts) is a type_error as v = abs("a") is, since abs answers the type it is given, and over xs = [], v = list. 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, as above, and so is a value bound from a call that fixes a type, such as str., len(5, **opts) or s..
A value script (§7.1) may be called in expression position — its call yields what the script returns, and composes anywhere an expression may: y = f(x), return f(x), print(f(x)), f(a) + g(b), or nested f(g(x)) (evaluated inner-first, left-to-right). A void script called as a value is a type_error — call it as a statement instead. A call names what it calls: (…) after a value — a literal, or what an index, a call or a .name answers, xs[0](1) or f()(1) — calls that value, and is a type_error in Python’s words, 'int' object is not callable, since no value holds a function. The name of a def, a method or a module read with no call is a type_error saying so.
A builtin called as a statement, where no script has its name, runs and its value is dropped, as any expression alone is: len(xs) does nothing you can see, and next(e) (§6.12) reads an element and discards it, since the read changes e.
7.5 Verb-call statement
A write verb (§5.1) in statement position:
remember(K.s.color, "orange", "op1")7.6 if
if <cond>:
<suite>
elif <cond>:
<suite>
else:
<suite><cond> may be any value — it is tested for truthiness (§6.2), so if xs: / if s: / if n: branch on non-empty / non-zero as in Python; each elif and the else are optional. An elif is equivalent to an else: containing a nested if. A name first bound in both arms of an if/else, or in every arm of a chain ending in else, is readable after it; one bound in some arms only is not, and an arm ending in return, raise, break or continue need not bind it (Python Specification §6.1).
7.7 for … in
for c in hist:
<suite>Iterates one of the six finite collections — claim_list, string_list, subject_set, result_type_set, proof_tree and diagnosis (§4.1) — a constructed list<T> (§6.7), a range(n) (§6.12), a dict (binding each key in insertion order, §6.9) or one of its views, or a set (in sorted order, §6.10), or a string, binding each character (a one-character string, by Unicode character), or a tuple whose elements all have one type (a variadic tuple always, a fixed one when its positions share a type). Written without its brackets, as in Python, it is the same tuple: for x in 1, 2: iterates (1, 2). A fixed tuple with differing position types, and any other iterand, is a type_error. An empty [] or {} whose element type no context supplied (§6.7) iterates too, zero times, as in Python: for x in []: compiles, and the code that reads x is not type-checked, since it never runs; the rest of the body is. If an enclosing loop could give that list, set or dict elements on a later pass (xs = xs + [1] in the same outer loop), the program is refused: declare the type where it starts, xs: list<int> = [], seen: set<int> = set([]) or d: dict<string, int> = {}. The loop variable stays bound after the loop and in its else, holding the last element (an empty loop leaves it as it was), as in Python. So a for over a name that is already a variable must keep its type. After x = 1, the loop for x in ["a"]: would leave a string in an int variable, and is refused with a type_error that names it. → Python Specification §6.1, §6.3 The collection is snapshotted at entry. A comprehension (§6.7, §6.11) is the same bounded iteration in expression position. Bounded iteration is what gives DKE Python its termination guarantee.
A target list unpacks each element (a tuple or a list) into named variables — for i, x in enumerate(xs):, for a, b in zip(xs, ys):, for k, v in d. — each element must be a tuple of the target arity, or a list or a variadic tuple, whose length is checked as the element is reached. A trailing comma ends the list, so for a, b, in xs: is for a, b in xs: and for a, in xs: binds a to each one-element tuple’s item. A target, alone or in a list, may be an element or a slice: for xs[0] in ys: writes xs[0] with each element before the body runs. Nested targets (for a, (b, c) in xs:) and one optional starred target (for a, *rest in xs:) are taken here as they are in an assignment, the star making the target arity a minimum. A name first bound in the body is not readable after the loop, which may run zero times. Like the single loop variable, the targets stay bound after the loop, holding the last element’s values.
Inside a for body, break stops the loop and continue skips to the next element (each resolves to the innermost enclosing loop; both may sit inside a nested if/match/try). Either outside a loop is a compile-time error, and a break/continue must be inside a loop in its own def, not merely called from one. Both only shorten the (bounded) iteration, so termination still holds.
for c in claims:
if c.value == "":
continue # skip blanks
if c.value == stop:
break # stop at a sentinel
print(c.value)A for may end with an else: clause: it runs once after the loop completes, but not if a break left the loop. An empty collection (zero iterations) still runs it. The loop variable is still bound in the else, holding the last element, as it is after any loop (above).
for c in hist:
print(c.value)
else:
print("no more history") # runs unless a break fired above7.8 match (discriminator dispatch)
match cur:
case active_claim:
print("value = " + str(cur.value))
case empty:
print("absent")The scrutinee must be a discriminator (active_claim, proof_tree, verify_result). Arms exhaustively cover the discriminator’s cases. case _: is the catch-all case default: is, and may stand in any match.
(A match over a value Python has is Python’s, each arm a test and none required, §7.8; over an instance the compiler follows it is also the class match, §8.6.)
A match whose scrutinee is a value (a claim’s .value, §4.1) is a value match: the case labels are the value classes int, bool, string, datetime, duration, real, null, blob, scientific and complex. The write-producible classes (int, bool, string, datetime, duration, real, blob, scientific, complex) may bind the typed content with a named as <var> arm; the one read-only class, null, is a bare arm only — attaching as <var> to it is a type_error:
match cur.value:
case int as n: print("next = " + str((n + 1))) # n : int
case string as s: print("text = " + str(s))
case duration as u: print("span = " + str(u)) # u : duration
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 null: print("an absent value") # bare — no `as`
case default: print("other class")Arms cover every class or provide case default:; a bound as <var> is typed at the arm’s class and scoped to that arm. A bool is an int, so a case int arm takes one and covers bool, and a case bool below an unguarded case int is refused as an arm that could never run — put case bool first. case int():, Python’s class-pattern spelling, tests what case int: tests, and case str(): and case float(): are the string and real arms. When every arm naming a class writes the parentheses, and none is a bare label, the match is Python’s instead: each arm a test and none required, and case int(n): binds the value to n, as a capture binds it. Beside a bare label, a pattern inside the parentheses is not taken — bind the content with as. In an arm naming classes, a scrutinee written as a name is read at their class (§8.11), and in an arm below them at what their tests failing leave it. A label that is no class, such as null or default, takes no parentheses.
for e in [1, "a"]:
match e:
case int():
print(e + 1) # e : int
case str():
print(e.upper()) # e : stringAlternatives in one arm. A case label may name two or more classes separated by |, and the arm runs when the value is any one of them:
match cur.value:
case int | real | scientific: print("a number")
case string: print("text")
case default: print("something else")case int | real: and two arms case int: / case real: with the same body mean the same thing; the alternation is a spelling, not a widening. An alternation takes no as <var> — the alternatives bind different types, so there is no single type for the name — and default may not appear in one, since it already covers everything not named. There is no AND-pattern: a value has exactly one class.
Guards. An arm may carry a condition it must also satisfy:
match cur.value:
case int as n if n > 100: print("big")
case int as n if n > 0: print("positive")
case int: print("zero or negative")
case default: print("not an int")Arms are tried in order and the first whose class matches and whose guard holds runs; a false guard falls through to the next arm. The as binding is in scope in the guard, which is what makes if n > 0 possible. The condition is anything with a truth value, the same rule if uses. The body reads a name its guard narrows at the narrower class (§8.11).
Two things follow, and both are compile-time errors when broken. A guarded arm does not count towards exhaustiveness — its guard may fail — so a match with no case default: must name every class in an arm with no guard; that is what guarantees an exhaustive match always runs exactly one arm. And a class may be named by several arms only while every earlier one is guarded: an unguarded case int: runs for every int, so nothing after it can. case default: may not carry a guard at all. There is no AND-pattern: a value has exactly one class.
Matching on a value. A case label can be a value pattern — Python’s own — and then the match compares rather than dispatches:
match n:
case 0: print("none")
case 1 | 2: print("a couple")
case k if k < 0: print("negative " + str(k))
case _: print("several")A literal — a number (negative included), a text, True or False — matches when the scrutinee == it, so case 1: matches 1.0. Literals joined by | match any of them. _ matches anything and binds nothing; a name that is not a class label matches anything and binds the value to that name, and as <var> after any pattern does the same. A capture and an as over a value pattern bind like ordinary assignments — made before the guard runs, kept when the guard is false, and §7.8’s scoping note applies to them, not the arm-local rule the class arms use.
A match over a value Python has — a number, a text, a collection, a slice, a view, an iterator or an instance — takes value patterns, shape patterns and class patterns, and needs no catch-all: when no arm matches, none runs and execution continues after the match, exactly as in Python. An arm that can never be taken compiles and never runs. case int():, case Site(): and case geo. test the class, as isinstance does, at the top of an arm or inside another pattern (case [int(), Site() as s]:, case 1 | str():), and a guard and a body read the value as that class. A type that reads as itself, int, bool, float, str, list, tuple, dict or set, may hold one pattern over the same value (case int(n):); past that, and for anything inside another class’s parentheses, the test raises Python’s type_error once the class has held (int() accepts 1 positional sub-pattern (2 given)). Over a value, literals stand beside the class arms and are tried where they are written — a match naming a class is still exhaustive, and one naming none is not. A catch-all — _, an unguarded capture, or case default: beside a value pattern — must be the last arm, since nothing after it could run.
case True: takes only a bool, by identity, as in Python (case 1: over a bool does take True, since 1 == True).
What is refused, and why: a class named with no () where the compiler does not follow an instance to it (case int: over an int), which Python reads as a capture — write case int():; a keyword in a class pattern (case Site(size=1):), which would read a field while the match dispatches — read it inside the arm; a typed value (datetime("…"), duration("…"), blob("…"), scientific("…")), which reads as a class pattern, and a value class takes no sub-patterns; a capture in a match that dispatches on classes, where a misspelt class must not silently take everything; and mixing a class and a value in one arm (case 1 | int:). case 1 | n: is refused as Python refuses it — a capture cannot be one alternative — and case _ | 1: because nothing follows _. An as _ is a syntax error, since _ binds nothing, and a pattern binding one name twice is refused, as case [a, a]: is.
Matching on shape. A case label can be a shape instead of a class — a list pattern or a dict pattern — and then the match is matching the structure of a collection rather than the class of a stored value:
match parts:
case []: print("nothing")
case [only]: print("just " + only)
case [first, *rest]: print(first + " and " + str(rest.length) + " more")
case default: print("unreachable, but required")A name in a pattern binds whatever is at that position, and _ matches it and binds nothing; a literal tests for equality and binds nothing; a nested [...] or {...} matches a shape at that position. Alternatives separated by | can stand at a position, as in case [1 | 2, b]:, and are tried in order. Each binds the same names, so case [[1, a] | [2, a, 0]]: binds a from whichever matched. as name after a pattern at a position binds what it matched, as in case [[1, y] as z]:. Alternatives and as stand at the top of a case as well, around a shape: case [1] | [2]:, case 1 | [b, 2]:, case [1, _] as z:. A *name collects the remaining elements and must be the last element. Without a * the pattern matches a list of exactly that length; with one, of at least the fixed length.
A dict pattern names keys, each a literal — a text, a number, True or False — and each key once:
match record:
case {"id": i, "kind": "sensor"}: print("sensor " + i)
case {"id": i}: print("something " + i)
case default: print("no id")It matches when every key it names is present, and ignores keys it does not name — it is a subset test. As with a list pattern, i binds the value and the literal "sensor" tests it.
A shape arm matches a sequence or a mapping and nothing else: over any other value it compiles and never matches, and as one alternative of several it is left out, the arm standing on the others: over x = 1, case 1 | [2]: is taken. The thing matched on may be any expression, evaluated once, and a shape match needs no catch-all. Shape arms take guards (case [a, b] if a > b:) with the pattern’s bindings in scope, and may stand beside value and class patterns in one match.
An ordinary assignment inside an arm is not arm-scoped, and the contrast is worth stating because one match has both rules in it. The as <var> binding of a CLASS arm exists only inside its arm (a value pattern’s capture and as are assignments, above); a name you assign in an arm belongs to the enclosing scope, and is readable after the match when it was bound before it or when every arm assigns it and the match has a catch-all (Python Specification §6.1), which is how a match computes a value for the code below it. Reading the arm binding after the match is a type_error, name 'n' is not defined; reading the assigned name is ordinary.
label = "none"
match cur.value:
case int as n: label = "int " + str(n) # `n` lives only here
case default: label = "other"
print("after: " + label) # `label` is readable; `n` is not7.9 try
try:
<suite>
except refuse as r:
<suite>
except engine_error as e:
<suite>
else:
<suite> # runs only if the body raised nothing
finally:
<suite> # always runs, lastPython’s own except … [as …]: shape. Both handlers are optional, and so is each handler’s as <var> binding — except refuse: catches without naming the exception (omit as when the body does not use the refuse/engine_error info). A refused statement does not bind and transfers control to except refuse; an engine error to except engine_error, which 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. With no matching handler, the outcome propagates. Handlers may be written in either order, and a category may be named more than once — the first handler naming the raised category runs, as in Python, and a later handler for that same category never runs. The program still compiles, and compile and run each answer with a note naming that handler and the line of the one that runs, after the result and as a notes entry the MCP Reference describes. An optional else (allowed only alongside an except) runs when the body raised nothing; a refuse in the else is not caught here. An optional finally runs on every exit path — clean, caught, propagating, or a break/continue leaving the block — and runs last; if the finally itself raises or breaks, that supersedes. A try needs at least one except or a finally. A name first bound in the body and in every handler is readable after the try, the else reads what the body bound, and a name the finally binds is readable after it; the finally itself reads none of the body’s names, since it also runs when the body did not complete (Python Specification §6.1).
try:
remember(Reading.probe.taken_at, "not a timestamp", "sensor")
except refuse as r:
print(r.reason)
if r.teaching_hint != "":
print(r.teaching_hint) # empty when there is no suggestion
except engine_error as e:
print(e.reason)Report .reason; add .teaching_hint when it is non-empty, since it says how to correct the call. Branch on .discriminator (or an engine error’s .code) only where the program must act differently — see §4.2.
7.10 branch / commit / rollback
with branch("experiment"):
remember(K.s.color, "tigret", "audit")
if ok:
commit
else:
rollbackA with branch("<name>"): block groups writes transactionally: clean exit commits; an uncaught refuse / engine error rolls back. Leaving the block early by return, break or continue is a clean exit and commits, as reaching the end does — the line is between leaving and failing, not between leaving early and leaving at the end. To abandon the writes, say rollback. Bare commit / rollback end the branch explicitly — the branch is over at that statement, so a write in the block’s remaining lines is an ordinary, immediately durable one. Ordinary invocation is not transactional.
An update inside the block revises a claim its source recorded before the branch as it would outside one, and a remember after it is judged against the new claim:
remember(Plant.basil.min_moisture, 40, "care-guide")
with branch("bonus"):
update(Plant.basil.min_moisture, 40, "care-guide", null, datetime("2026-10-31T23:59:59Z"))
remember(Plant.basil.min_moisture, 25, "care-guide", datetime("2026-11-01T00:00:00Z"), null)
# October reads 40 and January reads 25, in the block and after itWhat the store’s rules concluded from the claim from before the branch no longer reads inside the block either, as after a commit, while a conclusion the rules still reach from what the block reads does. A fact the block writes does the same to what the rules concluded from that fact’s absence. The claim from before the branch is not changed, and neither is what was concluded from it, so a rollback leaves the store exactly as it was. → §5.1
A branch’s name begins with a letter or _ and continues with letters, digits, _ or -; it holds no . or space and cannot be empty. Any other name is a refuse naming the character at fault, before the block runs. A branch may not take the name of a world that exists, and cannot open inside a world block (§7.18).
7.11 import (module)
import catalog # in a program body: load a stored moduleA DKE Python file is a module (§9); import <module> loads one: it runs the module’s body once and makes its names available under the module’s namespace. import is a reserved word, as it is in Python, and begins the import statement at statement head. A module may import another module; an import cycle is rejected when the module loads. Loading a module already loaded reconciles it: only the cells whose values changed are rewritten, so re-import is safe to repeat. → §9.1, §9.3, §9.5
A module’s names are namespaced. A kind a module declares with class is addressed under the module name — catalog.. The module-qualified construction module. is an instance of it, and you read a cell through the instance’s field, as with any class:
p = catalog.Product("widget").price # read catalog's Product widget, field price
print("price = " + str(p.value))You can also bind the instance first. After w = catalog., w. reads the same cell, and w can be passed to a catalog script that takes a Product. Two modules may each declare a Product without collision (catalog., store., distinct). A script a module declares with def is addressed the same way — module. — and that qualified name is how the service surface names it: the name info and forget take, the name list reports, and the name instructions call once the module is imported (warehouse., store., distinct). A script’s own body still calls its siblings unqualified; the qualifier is the address, used from outside at the service surface. → §9.6
An import also brings the module’s kinds into scope unqualified, so after import catalog a bare Product means catalog.:
import catalog
def restock():
remember(Product.widget.price, "9", "ops") # catalog's ProductA kind the unit declares itself wins over an imported one of the same name, and if two imported modules declare the same kind the program is refused — name the module to say which you mean (catalog.). That rule covers a kind in a path. A class is constructed only through its module: after import catalog, a bare Product("widget") is refused, just as a bare name is after import catalog in Python. → §9.6
Provide a module from the wire. compile with a source stores a module by name and runs it (§5.7). Every module in a store arrived that way. The bare import <module> is the in-language form written in a program. → §9.3, §9.4
Data at module scope. A write verb (§5.1) written at a module’s top level — not inside a def — is a data declaration: it runs when the module is compiled or imported. A module that only declares a schema, installs a rule, or writes data defines no callable script and is loaded for its body’s effect. → §9.2, §9.7
# in a module body, at top level:
remember(Rate.vat.pct, 25, "2026-finance-act") # runs when compiled or imported7.12 return
def classify(n: int) -> string:
if n > 100:
return "big"
else:
return "small" # every path returns → value script (§7.1)
def find(xs: claim_list, stop: string):
for c in xs:
if c.value == stop:
return # bare early exit → any script
print(c.value)A bare return ends the current script early and is valid in a script returning no value; outside a script it is refused, as Python refuses it. A return <expr> yields a value and makes the script a value script (§7.1); the expression’s type must match the declared -> type (an int satisfies -> real), and with none, the returns give the type, the wider of two where one fills the other. A value script must return on every path — a return in its suite, or an if/ (or exhaustive match) where each branch returns; a for alone never satisfies it. Two returns of types neither of which fills the other, or a value script that can fall off its end, are a type_error. A return exits past any enclosing for/try (running a try’s finally first) and never runs a for … else.
7.13 print
print("hi", who) # one line: "hi <who>"
print(1, 2, 3) # "1 2 3"
print("a", "b", sep=", ") # "a, b"
print("x", end="!") # "x!"
print(*[1, 2], 3, sep="-") # "1-2-3"
print(1, 2, sep="-" * 2) # "1--2"
print("x", flush=True) # "x"
print("a", "b", **{"sep": "-"}) # "a-b"
opts = {"sep": "-", "end": "!"}
print("a", "b", **opts) # "a-b!"
print() # an empty lineWrites one output line. Each argument is written as its text form (§7.3) and the pieces are joined by sep (default a single space), then end (default a newline) is appended. An argument written with * spreads a collection, and each of its elements is an argument of its own (§6.13). sep and end are keyword arguments, each any expression that gives a string, read after the positional arguments in the order written. None written as the whole of either is its default, and one that gives anything else is a catchable refuse when the line is composed, “sep must be None or a string, not int”. flush is read and changes nothing, and file may be given None alone, its default. A keyword print does not take is a type error in Python’s words ('x' is an invalid keyword argument for print()), the first one written, followed by the ones it takes. A ** before a dict display gives these keywords as its keys (§7.4). A ** before any other dict, such as a name that holds one, gives them as the line is composed, merged where it was written, as Python merges a call’s keywords: a name given twice (print() got multiple values for keyword argument 'sep') is refused as it is met, and a key that is no string (keywords must be strings) once every mapping is merged, then the first name print does not take, written or given, in Python’s words alone, then the values, sep before end. Each is a catchable refuse with discriminator type_error, and a file it gives is refused in the words a written one is given. An operand that is no dict is a type error where the unit is compiled (print() argument after ** must be a mapping, not list). Output is line-oriented — each print writes exactly one line — so a trailing newline in end is that line, and end="" reads the same as the default (there is no continuing a line).
7.14 pass
if x > 0:
print("positive")
else:
pass # do nothingA no-op statement: it does nothing and produces no output. Use it where a block would otherwise be empty.
7.15 raise
def withdraw(amount: int):
if amount <= 0:
raise ValueError("amount must be positive")
remember(Account.main.pending, amount, "teller")
try:
withdraw(x)
except refuse as r:
print("blocked: " + str(r.reason)) # "blocked: amount must be positive"
print("kind: " + str(r.discriminator)) # "kind: ValueError"Raises a recoverable error — a refuse — that an enclosing try catches with except refuse as r:. Name one of the built-in exceptions (ValueError, KeyError, TypeError, RuntimeError, …); the message becomes r. and the exception name r.. Several arguments are held as Python holds them, and r. is the text of their tuple. Every exception name behaves the same — there are no custom exception types. A raise never falls through, so a branch that ends in one satisfies a value script’s “return on every path”.
raise alone raises again what is being handled, in an except or in a finally with one pending, keeping its category, reason and discriminator; with nothing being handled it is RuntimeError: No active exception to reraise. raise e raises what a handler’s e holds. raise X from e and raise X from None read as Python’s do; nothing here reads a cause back, so a cause changes nothing a program can observe, and one that is neither is a type_error, exception causes must derive from BaseException.
7.16 assert
def withdraw(amount: int):
assert amount > 0, "amount must be positive"
remember(Account.main.pending, amount, "teller")
assert x == 5 # passes silently, execution continues
try:
assert x > 100, "too small"
except refuse as r:
print("caught: " + str(r.reason)) # "caught: too small"Python’s assertion. assert cond evaluates the bool cond and, when it is false, raises an AssertionError — a refuse, equivalent to if not cond: raise AssertionError. assert cond, msg records msg on r. (evaluated only on failure); a bare assert cond gives an empty reason. A passing assertion produces no output and execution continues. A condition may begin with a bracket: assert (x) and assert (x), "m" are this statement. It is distinct from the absence-assertion verb assert(K. (§5.1), which records an explicit-absent claim, and which is read where the bracket ends the statement and holds a dotted name and then a comma. So assert (x., a tuple that never fails in Python, is the verb here.
7.17 Changing a collection
xs = [3, 1]
xs.append(2) # xs is [3, 1, 2]
xs.sort() # [1, 2, 3]
xs[0] = 10 # [10, 2, 3]
del xs[-1] # [10, 2]
xs[1:] = [7, 8, 9] # [10, 7, 8, 9]
del xs[::2] # [7, 9]
tags = {"red"}
tags.add("blue") # {'blue', 'red'}
ages = {"ana": 30}
ages["bo"] = 25 # {'ana': 30, 'bo': 25}
ages["ana"] += 1 # {'ana': 31, 'bo': 25}
del ages["bo"] # {'ana': 31}A list, a set and a dict are shared: b = a binds b to the list a holds, not to a copy, and a script called with a list receives that list. A statement that changes one changes it in place, so every name, parameter and container holding it sees the change, as in Python. Each answers nothing and is a statement of its own — ys = xs. is an error — save pop, setdefault and popitem, which answer a value too (below). On a list: xs., xs. (the elements of a list, tuple, set, string or dict), xs. (its position clamped, a negative one counting from the end), xs. (the first equal element), xs. with sorted’s optional key= and reverse=, xs., xs., xs[i] = v, del xs[i], a slice write xs[i:j] = c (any collection, more or fewer elements; with a step, exactly as many as the slice selects), del xs[i:j], xs += c (any collection, as extend) and xs *= n. On a set: s., s., s., s., s., s., s., s., and |=, &=, -= and ^=. On a dict: d[k] = v (a key already held keeps its place), d. (another dict, or (key, value) pairs, then any names as string keys: d.), d. and del d[k]. An element or slice write evaluates its value before its target, and an augmented one, xs[i] += v or d[k] += v, its index or key once, as Python does. Each of these changes the collection its target reaches, so it works on an element, or on what a method answers, as on a name: m[i][j] = v, d[k]., del m[i][j:] — the chain before the last step is read once. It works on a value no name holds as well, read once, f(xs)., [xs][0]. or del f(xs)[0], and through parentheses, which are the name or element they hold: (xs). and del (xs)[0] change xs. A list of targets in parentheses is that list, so del (a, b) is refused as del a, b is. A target that is no name, attribute, element or slice, such as del f() or del xs[0] + 1, is a parse_error at that target, naming what it is, as in Python.
a = [1]
b = a
a.append(2) # b is [1, 2] as well
row = [0]
grid = [row, row]
row.append(1) # grid is [[0, 1], [0, 1]]
r = grid[0]
r.append(2) # grid is [[0, 1, 2], [0, 1, 2]]
grid[0][0] = 5 # grid is [[5, 1, 2], [5, 1, 2]]
groups = {"a": []}
groups["a"].append(1) # {'a': [1]}
c = a.copy() # a new list: c.append(3) leaves a as it isA literal, a comprehension, .copy(), list(c), set(c), dict(c), sorted(c), a slice read and the operators +, *, |, &, - and ^ build a new collection, shallow as Python’s is: its elements are the ones its operands hold. To change a collection and keep the original, copy it first — xs., s., d. or dict(d). x = e binds x to what e answers and changes nothing x held before; += on a number, a string or a tuple binds a new value, as in Python.
The changed collection keeps its type, so xs. on a list<int> is a type error naming the type change, as xs += ["a"] is. A collection first bound empty takes its element type from the first statement that supplies one, for every name holding it: after a = [], b = a, a., b is a list<int>, and b. is refused as b sharing its element type with a. A .get() default supplies a dict’s value type the same way: counts = {} filled by counts[w] = counts. is a dict<string, int>. An operator supplies it too: counts[w] += 1 under if w in counts: makes counts a dict<string, int> (* and % supply nothing), and a read nothing supplies a type for is refused naming the annotation, d: dict<string, int> = {}. An empty collection written into a collection of collections takes their type: grid. on a list<list<int>> and d[k] = [] on a dict<string, list<int>> are accepted. A collection built from one still empty (b = a., b = list(a)) shares its element type too, since a loop could build it after the source holds elements — Python would let the two differ. A collection cannot hold itself: a. is a type error. A slice write takes any collection, typed as extend’s argument is, and a change through an element keeps the element’s type: m[0]. on a list<list<int>> is refused naming m[…].
Changing and answering. pop, setdefault and popitem change the collection and answer a value, so each is an expression as well as a statement and works wherever an expression may stand. xs. removes and answers a list’s last element, and xs. the one at i, a negative i counting from the end. s. removes and answers a set’s first element in its sorted order, where Python’s choice follows its hash table. d. removes k and answers its value, and d. answers the default when k is absent. d. answers the value at k, writing the default there first when k is absent — DKE Python has no None, so the default is required. d. removes the last entry and answers it as a (key, value) tuple. A default supplies an empty dict’s value type as a .get() default does, so setdefault groups by key. As a statement the answer is discarded; within an expression the call runs where Python evaluates it.
xs = [3, 1, 2]
last = xs.pop() # 2; xs is [3, 1]
first = xs.pop(0) # 3; xs is [1]
ages = {"ana": 31, "bo": 25}
bo = ages.pop("bo") # 25; ages is {'ana': 31}
cy = ages.pop("cy", 0) # 0; ages is unchanged
groups = {}
groups.setdefault("a", []).append(1) # {'a': [1]}
k, v = ages.popitem() # k is 'ana', v is 31; ages is {}A change the collection cannot make is a catchable refuse (§10) in Python’s words: xs. of an absent value is list. (discriminator value_not_in_list); s. of an absent element and del d[k] of an absent key give the repr of the missing value (key_not_found); a list index outside the list on a write or del is list assignment index out of range (index_out_of_bounds); a slice with a step, written with a different number of elements than it selects, is attempt to assign sequence of size N to extended slice of size M (value_error); a step of 0 is slice step cannot be zero (slice_step_zero); xs. on an empty list is pop from empty list and xs. outside the list pop index out of range (index_out_of_bounds); and s. on an empty set is 'pop from an empty set', d. of an absent key with no default the repr of the key, and d. on an empty dict 'popitem(): dictionary is empty' (key_not_found).
A loop over what its body changes. A for reads a list as it stands at each step, as Python does, so removing elements ends it sooner — but it never runs more steps than the list had when it began, so appending to the list it walks stops there, where Python would go on (the bound is what termination rests on). A list’s extend over the list itself, by an iterator or a generator expression, stops at the same bound. A for over a dict or set walks what it held at the start, and a change of its size is a catchable refuse at the next step in Python’s words — dictionary changed size during iteration or Set changed size during iteration (changed_during_iteration). Writing a value to a key already held is not a change of size. A for over a dict view (§6.9) reads the dict at each step, taking the element at the position it has reached, so it binds a value written during the loop; removing a key it has passed makes it pass over one, where Python’s does not.
One element at a time. 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 before reading the next, as Python’s do. One that reads the collection being changed finds it changed and is refused at the read after its size changes, in the same words; what was written before stays, so after s = {1, 2} a caught s. leaves {1, 2, 11}. A dict’s pair of the wrong length is refused naming its position, after the pairs before it are written. intersection_update and symmetric_difference_update read theirs whole first, as Python’s do.
Not statements. del x would unbind a name, which stays bound to the end of its script. A string and a tuple take none of these; rebuild them from slices.
7.18 world
create_world("wet")
with world("wet"):
remember(Field.north.rain_mm, 180, "forecast")
r = current(Field.north.rain_mm)
print(r.value)A with world(<name>): block runs its body inside the world named (§5.9). The name is a string expression, read once before the body. Entering brings the world up to date with the store; the body reads the store and the world together, and every write in it lands in the world. Leaving the body by any path — its end, return, break, continue, or an uncaught refuse or engine error — leaves the world, and the statements after the block run outside it. Nothing is committed or rolled back on the way out: what the body wrote stays in the world until 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 through a script called from inside one, each is a refuse. world is a contextual keyword, recognised only after with, and an ordinary identifier elsewhere.
7.19 Named expressions (:=)
xs = [1, 2, 3]
if (n := len(xs)) > 2:
print(n)
print(a := 1, a)
print([y for x in xs if (y := x * 2) > 2], y)name := value binds the name when it is evaluated and answers the value, in parentheses, as an element or an argument, and as the whole condition of an if, an elif or a case’s guard, or the whole subject of a match, as in Python. Anywhere else, as in y = a := 1, it is a parse_error at the :=. It binds as an assignment does, so the name keeps its one type, and what is evaluated before it reads the old value. In a comprehension it binds in the scope that holds the comprehension. It is a binding, so it costs no operation, and its value cannot be a read of the store (§7.2).
8. Classes, methods, and reasoning
Classes group typed fields with the methods over them and give a typed handle — an instance — for one named member (§8.1–8.6). A class may extend another, inheriting its fields and methods (§8.10), and isinstance tests an instance’s class (§8.11). The section then covers the derivation features — computed fields (§8.7), standing rules (§8.8), and hypothesis (§8.9) — where the engine records or supposes values on your behalf. The class surface adds no new verbs, and termination is preserved throughout. class and return are keywords.
8.1 Class declaration
class Sensor:
temp: string # typed field (primitive)
def record(self, v: string, src: string): # void method (explicit self)
remember(self.temp, v, src)
def label(self) -> string: # value method
return "sensor"Written class <Name>:. Fields are name: type with a primitive type; names unique within the class. Declaring a class stores nothing. A class may name a single base class it extends — class Dog(Animal): — inheriting the base’s members (§8.10). A class exists from the line that declares it, as in Python: a line that runs as the module runs and names one below it is refused, name 'K' is not defined. The body of a def or a method runs when called, so it may name a class declared anywhere in the unit, and a signature may name one in a string, def f(x: "K"):. A module statement reaching such a body above the class is refused in the same words.
8.2 Instance construction and binding
s = Sensor(room)Sensor(room) names one member of Sensor identified by the string room; bound by assignment, its type is the class name. Construction records nothing. Reading a never-written field yields empty.
A class declared by a unit you compiled earlier is constructed the same way wherever that unit’s scripts are called by their bare names (§7.4). Its fields, its methods, a parameter annotated with it and isinstance all work as they do for a class your own unit declares. Declaring a class that extends it is refused: a subclass needs the class’s body, which only the unit that declared it has. A class, script or import of the same name in your unit takes the name, and a class declared again replaces the earlier one for the units compiled after it.
8.3 Field read and write
reading = s.temp # read → active_claim or empty
remember(s.temp, "21", "op1") # write value + provenance sourceA read has type active_claim; consume with match or .value. Accessing a field the class does not declare is a type_error, as is calling a method it does not declare. Inside a method the explicit receiver self (the first parameter) names the instance (self.).
Every verb that takes a K. cell takes a field written this way as that field’s cell, the one remember writes, through any name that holds an instance: current(s., get(s. and why(s. read it, and so do verify, agreement, writers, pin, unpin, prefer, unprefer, why_not, what_needs, assert, forget and would_forget, and a hypothesis takes it as each field it supposes and as the field it reads (§8.9). 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 and list_attributes — takes the instance itself as that subject, caveats(s), through any name that holds one. A verb that takes a class and one of its fields — list_values, list_subjects, subjects or a fold — is given the class, list_values(Sensor.. An instance’s field at either is a type_error.
A field is reached through an instance variable: a name bound by construction, a parameter of the class’s type, a call to a script declared to return the class, the loop variable of a for over a list of instances or a tuple of them, an element taken from such a list by index, a second name for one of these, a conditional expression over two instances of classes with a common base, or a binding annotated with the class. A list of instances is a display of them, a name bound to one, a list<Sensor> or *args: Sensor parameter, a call to a script declared to return list<Sensor>, a comprehension that collects its own loop variable over one, a slice of one, or two joined with +. An instance in a dict or a tuple whose type is declared, or in a dict or tuple display that holds one, is reached the same way: by key, by a literal index, by unpacking, and through values() and items(). Through a name holding an instance from anywhere else, such as the value d. returns, a verb reaches the field, as above, but reading the field itself through it is a type_error, as is a field read straight off such an expression (xs.); declare the name with the class first:
class Sensor:
temp: string
def readings(rooms: dict<string, Sensor>):
x = rooms.get("hall", Sensor("none"))
item: Sensor = x
reading = item.tempA method called through such a name or straight off such an expression, and a class match over the name (§8.6), are refused the same way, naming the binding to write first. A field or a method may also be taken straight after a call or an index that reaches an instance this way, with no name written between them: reading = rooms["hall"]., sensors()[0].. The call or the index is evaluated once, where the expression is.
8.4 Void method
s.record("21", "op1") # statement position; self is supplied by the receiverdef name(self, params): — no return type and no return <expr>; performs actions; called as a statement. The self argument is supplied by the receiver, not written at the call.
A method’s arguments bind as a script’s do (§7.1): by position or by keyword, an omitted one takes its default, and a parameter list may hold / and a bare *.
A method’s call takes a spread as a script’s does (§7.4): s.. Python counts the receiver among the positional arguments, and so does a refusal: Box.. It names the method by the class whose body holds its def, as Python does, so a method Sub inherits from Box is Box.. Called on a class another module declares, it is named by that class instead.
8.5 Value method
name = s.label() # expression positiondef name(self, params) -> type: whose body is what a value script’s may be, any statements with a return of that type on every path; called in expression position. With no -> type, a body holding a return <expr> gives the method its type, as a def’s does. The return type is any a def takes, a collection included (-> list<string>). A method returning a field read declares -> active_claim, or nothing; one returning a store fold declares -> value, which its caller narrows with match, as its own body may. A method may call one written below it. Resolved from the receiver’s class — the actual class when the receiver is held at a base type (§8.10); compile-before-call; no recursion.
Called as a statement, s. runs and its value is discarded, as a value script’s is.
8.6 Class match (dispatch by class)
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 may dispatch over class names. Inside case <Class>: the instance is treated as that class; a case matches that class or any subclass, first match winning (§8.10). An arm names its class as an annotation does, so a class a module declares is case geo., or case Site: after from geo import Site. Written so, the arm set is closed and exhaustive — cover every class the instance could be, or provide a catch-all, case default: or case _:, which must be the last arm — and a non-exhaustive one is a parse_error. case Site():, Python’s spelling, is the same arm and is a test, as §7.8 reads one over any value: a match with an arm written so runs no arm when none holds, and takes | between classes.
8.7 Computed fields
A class field declared with an initializer the engine derives from the class’s other fields and keeps current as they change. The customer writes only the inputs; the derived field reads back like any field, with a derived provenance in place of a source.
class Order:
price: int
qty: int
total: int = price * qty # int: an arithmetic expression (+ - * %) over earlier fields
big: bool = total > 1000 # bool: one ordered comparison of an earlier fieldAn int field is arithmetic (+ - * %) over earlier fields; a real field may also use /; a complex field is arithmetic (+ - * /) over earlier fields, numbers and imaginary literals, and derives what a program computes for the same expression (§6.3); a bool field is one ordered comparison (< <= > >=), and holds neither True nor False for a subject whose input it cannot rank — a blob, which has no order, or a value of another type, such as a string recorded in an int field; why_not there says so. A complex number has no order either, and a bool field that compares a complex field is refused where it is declared. A field whose expression divides by zero holds no value for that subject. Every reference is to a field declared earlier, so a class’s computed fields form no cycle among themselves. One that would close a cycle with what the store already holds, a computed field an earlier compile installed or a rule, is refused at the compile that closes it, naming the field, when the cycle passes through a field that applies an operator (total: int = price * qty); a cycle of copies and comparisons settles and is accepted. Writing an input re-derives every field built on it. why over a computed field lists the inputs it was derived from (§5.3).
A computed field is never written. remember, update or a supposition there refuses, naming the inputs to write instead, and current goes on reading the derived value. forget there is never refused. Installing a class whose computed field already holds recorded values refuses as well, naming them: forget them, or give the computed field a name of its own.
8.8 Standing rules
A named top-level declaration — a sibling of class / def, not a class member — that derives a field automatically: its conclusion is in force exactly while its condition holds, so a derived field always reflects the current data. The @rule decorator marks the def as a rule; absent names one of its three negative premises, inside a rule block only.
@rule
def needs_review():
for o in Order:
if o.total > 1000 and absent(o.shipment):
o.review = Truefor <var> in <Kind> (, <var> in <Kind>)*— the name the rule gives each subject, and the kind whose fields it reads under that name. The premises say which subjects a name ranges over. A name whose fields a comparison, a join or another ranging premise reads ranges over the subjects recorded with those fields; a name reached only through a join or a relation ranges over the subjects the join or the relation names, whether or not anything of its kind is recorded for them. So infor e in E, x in N, y in N: if e., ana == x and e. b == y: Link. x. y = True EnamingpandwconcludesLink.though nop. w Nnamedwis recorded; addingand y.limitsname == y. name yto theNs recorded with aname. Names must be distinct. The bindings are a conjunction and carry no order; a nestedforis not accepted, because nesting would read as a walk and a rule states a condition over the whole store.if <premise> (and <premise>)*— every premise must hold:
| premise | example | holds when |
|---|---|---|
| comparison | o. |
the field stands in that order (< <= > >=) to the bound. A numeric bound may be negative (r.) and need not be whole (o.) — what is compared is the number, not the way either side was written; a quoted bound (o.) orders the field alphabetically instead. A bound in scientific notation (o.) is numeric and orders by magnitude. The two readings never mix, so a field carrying both text and numbers never answers by accident. |
| equality | o. |
the field holds exactly that value — 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 two literals carry == and != and not < <= > >=. Over numbers it reads the number: o. holds for 5.0, and not for the text "5". A complex number and a number compare by value, as a program’s == does: z == 1+0j holds for a field holding 1, and z == 1 for one holding 1+0j. |
| inequality | o. |
the field holds some recorded value other than the given one. A field with no value holds neither == nor != — comparing against nothing is not a comparison. Ask that with absent. |
| join | c. |
the field holds another subject the rule binds. The right side is one of the for names, not a value: that is how a rule relates two subjects to each other. == also ranges the name on its right, over the subjects the field names, so the conclusion may be about one of them; != does not. The right side is a subject; to compare against a second field, write that field — the next row. |
| field comparison | o. |
the two fields’ stored values stand in that order — numerically between numbers, chronologically between instants, by length between durations, alphabetically between texts — or are equal / unequal under == / !=, as a program’s == decides, so 1+0j equals 1. Values of two different classes stand in no order, so a duration against a number does not hold. Both fields must have a value: a subject with no deadline has nothing to be late against and is not concluded about. The two may belong to the same subject or to different subjects the same for binds (c.). One token separates this from a join — a bare subject on the right is a join, a subject followed by a field is this. A specific subject (Order.) cannot be named on the right; a rule ranges over the subjects it binds, and current(...) is what asks about one |
| relation | Reach. |
the three-segment path holds that value, with bound names in its last two slots. It ranges both names over the pairs it holds, and a bound name in the field position asks for every cell of that subject rather than requiring one to be known. |
| absence | absent(o. |
the subject has no value recorded for that field — missing information, not a field recorded as false or empty. The stricter of the two cell-negatives: a value a rule concluded makes it false. So does a known absence recorded with assert(...), which is information rather than its lack: known_absent asks about that. |
| not asserted | not_asserted(b. |
nobody has recorded a value for that field. Narrower than absent: it reads only what people wrote, so a value a rule concluded leaves it holding. Shape-only — there is no form taking a value, because did a person record THIS value is answered by reading the field. A known absence recorded with assert(...) was recorded by a person, so it makes this false too. Ranges no subject, as absent does not. |
| known absence | known_absent(p. |
a source recorded the field as known to be absent, with assert(...). The positive form beside the two negatives, and the three tell a field’s states apart: absent holds when it has no value at all, not_asserted when nobody recorded one, 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 ranges the subject. Beside source(p. it holds only where the vet recorded the absence. |
| nonexistence | none(c in Call: c. |
no subject of that kind satisfies every premise inside. Where absent asks about one cell of one subject, this asks about a whole set. The inner premises are the ordinary premise grammar and may name ONE of the outer rule’s subject variables — c. is what ties the set to the subject being concluded about — and it is answered for each subject that variable takes, whether or not the rule concludes about it: in a rule over for r in Req, u in User:, none(f in Flag: f. asks about each owner u. One naming two of them is refused. It holds vacuously for a subject the set never mentions: a function with no callees at all satisfies “none of its callees is tainted”. That is also why it is enough on its own to say which subjects a rule applies to, where an absent is not. A none(...) inside another is refused; conclude the inner one into a field with its own rule and quantify over that. |
| source | source(r. |
the value came from that source — the text a write gave as its source, quoted. It is about where the value came from, not what it is: a field three sources disagree on holds it through the value that source recorded, and every other premise on the field, and a copy of it, reads that same value. != holds through a value some other source recorded, and does not say the named source did not record it. A value a rule concluded has no source a person gave it and satisfies neither. The right side may instead be a subject the rule binds (source(r., == only): it holds when the value’s source is that subject’s name, and ranges the subject over the ones the store holds of its kind. |
| trust | trust(s. |
the value was concluded by a rule ("derived") or recorded by a person ("asserted") — the two words .trust reads back, English on every surface (§12). != selects the other. A field holds both only where a rule yields to a recorded value, so this is what tells the defaults from the values recorded over them. |
| text test | r. |
the field’s text begins with the part; t. holds when it ends with it, and "admin" in a. when it holds it anywhere. The part is a quoted string or a field of a subject the rule binds (r., s.). 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 never does, whatever its written form contains. One part per test, with no window; a negated test (not in) is not a premise. It ranges the subject, as a comparison does. |
- premises that name the same field through the same name read one value of it. A field that two disagreeing sources each hold a value in (§5.2) satisfies
r.only if one of its values lies in the band:t > 30 and r. t < 40 50and20do not, though each meets one bound. A join names the subject that the value the other premises accepted refers to, a source or trust test is about where that value came from, and a fold over the field folds only the values the premises accepted.known_absentaccepts the recorded absence, so the premises beside it are about that claim. - each of
absent,not_assertedandknown_absentalso takes a pair, written as a relation premise writes one:absent(Gap., both names bound by thex. y) for, asks about the cell ofGapat the subjectxand the fieldynames, as the one-field form asks about one cell, and a value recorded there later withdraws what its absence concluded.absentandnot_assertedover a pair range neither name;known_absentranges both. The field is whichever oneynames, so a rule testingabsent(Gap.may not conclude any field ofx. y) Gap; over the same pair,not_asserted(Gap.is the default that yields.x. y) not_asserted(...)is the one negative a rule may point at its own conclusion, and that is what it is for: a rule states a default and stops concluding the moment a person records something else. This is the idiom for “everything of this kind, unless…”, and it needs one thing a reader reaching for it usually leaves out: a positive premise to range the subjects. A negative says which subjects a rule does not apply to and never which ones it does, soif not_asserted(b.alone is rejected — pair it with a premise that names the population, asflies) b.does below.species == "avian"
@rule
def birds_fly():
for b in Bird:
if b.species == "avian" and not_asserted(b.flies):
b.flies = TrueRecording b. for one bird stops the rule concluding about that bird — its premise stopped holding, so no conflict is created and nothing has to be ordered. Removing the record returns the default. absent(b. there would be self-cancelling and is refused, with a message naming not_asserted(...). - a rule using none(...) records an intermediate field on the subject the none(...) is answered for — the outer subject variable its inner premises name, or the subject the rule concludes about when they name none — named after the RULE (<rule>__none<N>, numbered from 1 in the order the none(...) premises are written), so two rules concluding one field never share one. A rule is_safe concluding f. records f.; a rule open_req concluding r. over none(f in Flag: f. records u. on each user. In a module, a field recorded on a kind the module does not declare puts the module’s name first: audit__is_safe__none1. It is an ordinary derived field — it appears in list, in current, in 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 for exactly the subjects the none(...) excludes. Reading it is a supported way to see which subjects the quantifier found. - the conclusion <cell> = <literal> is recorded with a derived provenance; the customer never writes it directly — a write there refuses, as it does at a computed field (§8.7), unless the rule yields: its condition includes not_asserted(...) on the very field it concludes, so a recorded value wins and may be written. A pair conclusion Pref. yields at every pair when its condition includes not_asserted(Pref. over the same two names. A rule that concludes a field at every subject while also reading it (p. over one class) leaves no value that could be written there for it to read, and its refusal says so: give the conclusion a field of its own, as p. does, or let the rule yield. A rule that does not yield is refused if its field already holds recorded values, naming them; forget them, or conclude at another field. Merging a world whose recorded values sit at such a field is refused the same way (§5.9). The literal may be a negative integer (r.), a number in scientific notation (s.), a complex number (s.) or a blob (d.); the field then reads back as that kind. A cell is either <name>.<field>, a field of a subject the for clause bound, or the three-segment <Kind>.<subject>.<field> path used everywhere else in the language — except that its last two segments may be bound names, which is what records a fact about a pair. Every name in a conclusion must be bound by the for clause. A join or a relation premise binds no name; it ranges one, deciding which subjects the name stands for, and a relation premise ranges both of its names, so either may be the subject of the conclusion: Back. records Reach. backwards. - the conclusion may instead be a fold over the subjects the rule ranges over, <cell> = <fold>(<name>) or <cell> = <fold>(<name>.<field>) — c., c.. See Folding the subjects below. - the conclusion may instead copy a value the rule reads: a field, <cell> = <name>.<field> — d. — or the value a relation holds for a pair, <cell> = <Kind>.<name>.<name> — r., whose two names are bound as a relation premise’s are. The value recorded is the one read there, of the same kind, once for each value the field holds, and valid while that value is. Reading the field makes its presence a premise and ranges its subject (both names, for a relation), so a default that copies needs no comparison: if not_asserted(s.. A path naming a specific subject is not copied, and a conclusion cannot copy what it concludes (x., Km.), which would be its own only source. Nor can it copy or fold a field the rule tests with known_absent: a recorded absence holds no value to carry, and a rule that concludes the absence writes assert(<cell>) instead. The same field of another subject is how a value travels along a link (x. with x.). - the conclusion may instead write a text from values the rule reads, with an f-string: <cell> = f"…{<name>.<field>}…" — d.. Each field between braces is what a copy’s right side is, a field of a bound subject or the value a relation holds for a pair, and is read as a copy reads it: 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. The text is the one a program’s f-string prints over the same values, and it is a string whatever it reads as (f"{n. over 4 records "4"). A field holds a string or an int; where it holds a value of another type no text is drawn for that subject, and why_not on the concluded field names the field and the value. A field is written plain: a conversion ({d.), a format ({d.), a debug field ({d.), a subject alone ({d}) and any other expression are refused when the rule is defined. A text longer than 65,536 bytes is not drawn. A conclusion cannot write a text from the field it concludes (x.), nor from a field the rule tests with known_absent, and a rule that closes a cycle through a written text is refused. An f-string with no field is a string literal. - the conclusion may instead record an absence, assert(<cell>) — assert(p.: the field is recorded as known to be absent, as a program records it with assert(K. (§5.1). It takes no source, because the rule is the source, and a second argument is refused. known_absent(...) holds over it, absent(...) does not, and not_asserted(...) does, since no person recorded it. Like any conclusion, it goes when the condition stops holding.
Folding the subjects. 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)A customer with four orders gets 4, one with a single order gets 1.
Not an aggregate query. §5.8 folds a whole attribute across every subject of a kind and answers once: count(Order. is how many orders exist, where count(o) here is how many are this customer’s.
Which folds may appear. Any single-operand fold of §5.8 — every family there but ranked and two-attribute, whose extra argument a conclusion has nowhere to put. Each keeps its §5.8 meaning, the identity and positional folds included: argmax(o. records the subject at this group’s maximum, distinct(o. a count of distinct values, and distinct(o) the number of distinct subjects.
What is folded, and what count counts. The folded name must be one the for binds. A subject missing the folded field adds nothing to a fold over it and still counts toward count(o), since naming a field makes its presence a premise.
A fold ranges over the premises’ MATCHES, and a location holding more than one value in force matches once per value (§5.2) — two orders, one of which two sources agree belongs to this customer, make count(o) answer 3. Each fold follows the claims of the field IT reads, so the two shapes move independently: a second claim on the JOINED field raises count(o) and leaves sum(o. alone, and only a second claim on the AMOUNT adds that amount again. A premise on the folded field decides which of its values count: with o. in the condition, an amount recorded as 5 by one source and 15 by another folds as 15.
A derived field is an ordinary premise, so if c. in a second rule is how “more than three orders is a regular” gets written.
What a conclusion may write — a class does not constrain it. A rule concludes into a path, and paths are open. Three consequences, all of which surprise a reader who has met class first:
- A rule may conclude about a kind no
classdeclares. Nothing has to be declared before a rule writes it. - A rule may conclude into a field its class does not declare. Declaring
class Order: total: intdoes not closeOrderagainst a rule concludingo..review - The field’s declared type does not constrain the concluded value. A rule concluding
s.overmean = avg(r. v) intreadings records1.5into a field declaredint, and nothing refuses. This third one is not a difference between the two surfaces — it holds on the instance surface too, as the paragraph below says.
That is not a contradiction of §8.3, which makes an undeclared field a type_error — it is the same rule seen from the other side. §8.3 governs instance access: o = Order("o1") binds an instance, and o. is refused because a class is a closed schema for the instance surface. A rule’s for o in Order binds a path, not an instance, and writing a path is open. A class is a schema you get checked against where you construct one, and a vocabulary you are not confined to where you reason.
Which half of the schema closes. §8.3 closes the instance surface over field names and not over the class of a written value. o. is a type_error; remember(o. into a field declared int is not, on either write spelling, and the value reads back as the string it was written. So a typo’d field name inside a declared class is refused and a wrong-classed value in the same statement is not. The declared type governs a computed field’s initializer (§8.7), where a non-numeric operand is refused with teaching, and it constrains no write, by any spelling. Nothing is unsound here: .value is the union type, so arithmetic on it is refused (§6.3) and a caller destructures with match.
A conclusion may be False as readily as True — o. records the bool False, which is a recorded value and not an absence. The absent premise in the table above is what tells those two apart.
A rule that reads the same relation it writes applies to its own results:
@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 = TrueThe first records every direct edge; the second joins a reach already known to a further edge and records the longer one. It settles on everything the graph connects rather than on a fixed number of steps. A name used twice in one cell agrees with itself, so Reach. asks which subjects reach themselves:
@rule
def in_cycle():
for x in Fn:
if Reach.x.x == True:
x.cyclic = TrueEvery name a rule binds must be ranged by a comparison, a join, or a relation premise — a name mentioned only in an absence test says which subjects nowhere — and a rule must not cancel itself out; either is rejected when the rule is defined. It ranges over finitely many subjects; termination is preserved. A derived field reads back like any other (current / get).
At the wire surface, list(target = rules) names the standing rules you wrote with @rule — names only; keep your own source for what each rule does. It is authored-only: a computed field is a standing rule too, and it is not listed, because it has no name you gave it. So an empty listing means you wrote no @rule, not that nothing is reasoning — a class declaring area: int = w * h and no @rule lists nothing here and reports rules=1 from checkup, which counts every standing rule in force. The two verbs answer different questions and the difference between them is the computed fields.
forget_rule(name) removes ONE standing rule and reclaims what it derived, leaving the rest of its module in place; name is the qualified module. name list(target = rules) prints. A conclusion another standing rule still supports is re-derived and stays — only this rule’s support is withdrawn — and the facts the rule read are untouched. Wire-only; needs a read-write-delete key.
checkup answers the question a silent rule leaves open. A rule that never fired and a rule that fired and found nothing return the same empty result, and when the reasoning came from a module you did not write, nothing else tells you which happened. It reports unmet <Kind>.<field> for a cell some rule or constraint needs a value at that holds nothing (a premise asking for an absence, such as absent(...), is met by an empty cell, so it is never reported) — no-such-kind when nothing of that shape exists, no-values when the kind is there and the field is empty, and not-in-force when a value is recorded at the field but its validity window does not cover the time asked about, which is a fact you already hold rather than one to write — unread <Kind> for a kind of yours no rule or constraint reads, violated <name> <n> <subject> … for a standing constraint (§8.12) that does not hold, near A ~ B when those two lists contain names a letter or two apart, and a counts line. A near line is usually a mis-spelled kind name: writing facts under a kind spelled slightly differently from the one the reasoning reads produces both halves, and the pair names the mistake in a way neither half does alone. Findings name cells rather than rules — what an unmet cell needs is your data. A violated line is the exception that proves it: a constraint is a named invariant you declared, so the constraint is the right noun, and the subjects beside it are what make the finding actionable. The count is exact; at most five subjects are listed.
The counts line’s rules= covers every standing rule in force: the rules you wrote with @rule and the computed fields your classes declare. A computed field is counted 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) is authored-only, so rules= is at least as large as that listing, and the difference between the two is exactly the computed fields. A class declaring area: int = w * h and one @rule lists one name and counts two.
Wire-only; takes no arguments and changes nothing.
A rule’s kind. @rule(kind="…") says what kind of relation a rule states, one of five: "definition", "causal", "empirical", "policy" or "heuristic". It is for the reader and changes nothing the rule concludes: the same rule with and without a kind concludes the same claims. A conclusion reads it as .rule_kind (§11.2), list(target = rules) shows it after the rule’s name (shop.), and so does info_module. The five words are the same in every language; the keyword 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="policy")
def needs_review():
for o in Order:
if o.total > 1000:
o.review = True8.9 Hypothesis
suppose(<field>, <value>, …, <field>) reads what the last field would be if each named field were set to the value beside it — letting standing rules and computed fields run under the suppositions — then discards everything. Nothing persists. Returns a value; bind it and read it, or concatenate it into a print.
would = suppose(Order.o1.total, 2000, Order.o1.review) # true, if a rule sets review over 1000Give a <field>, <value> pair per supposed fact. The field to read is always the last argument, alone — so a rule needing two facts at once can be asked about:
would = suppose(Order.o1.total, 2000,
Order.o1.shipment, "sent",
Order.o1.review)Suppositions apply left to right: two naming the same field leave the later one standing, exactly as a later write wins over an earlier one. A hypothesis takes 1 to 16 of them — a cost bound rather than a semantic one, since the work grows faster than the count; over the limit is a compile error naming the number. If the read field has no value under the suppositions, suppose produces no value — a catchable refuse (guard it with try / except). Safe for preview, comparison, and what-if exploration; termination preserved.
suppose_absent(<field>, …, <field>) asks the opposite: what the last field would be if the other fields’ facts did not exist. Same isolation — the suppositions and everything that follows from them are discarded, and nothing is removed. It takes 1 to 16 fields to suppose away, applied left to right, and the read is the last argument — so suppose_absent(a, b, c) supposes away a and b, and reads c.
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")Returns an active claim that may be empty, matched exactly like current — not a value. The two hypothesis verbs differ here on purpose: suppose asks what a value would be, so a missing answer is an edge case and arrives as a catchable refuse; suppose_absent asks what would survive, and “nothing” is the answer it exists to give, so it arrives as an ordinary empty read.
A single hypothesis supposes facts into place or away, not both; mixing the two in one call is not available.
Every field either verb names is a K. cell written as any verb’s is (§5): a segment held by a string name in scope is that name’s value, and an instance’s field, s., is that instance’s cell (§8.3). A field that is no path, or goes on past one as an expression, as in suppose(Order., is refused at it, as a verb’s path argument is.
Use it before forget (§5.1), which cannot be undone. dependents tells you what points at a fact; this tells you what the answers become without it — while the fact is still there to keep. Termination is preserved.
8.10 Inheritance
A class may extend one base class, named in parentheses; it inherits the base’s fields and methods and may add its own or override a method:
class Animal:
sound: string
def speak(self):
remember(self.sound, "generic", "op1")
class Dog(Animal): # single inheritance (one base)
breed: string
def speak(self):
super().speak() # run the base's version, then extend it
remember(self.breed, "collie", "op1")Single inheritance only (class C(A, B): is an error), and class A(): with empty parentheses names no base, the same class as class A:. super().<name>(...) runs the base’s version on the same instance; when the method returns a value, the call is that value (return "z" + super().). super() names the base of the class declaring the method, also in a subclass inheriting it. A subclass instance is accepted wherever the base is expected — a base-typed parameter, a binding, a list element, or the value a script or method declared -> Base returns — and a class that neither is the named one nor descends from it is refused where the name is written, when the program compiles rather than when it runs. A list written with several subclasses takes their nearest common base as its element type, so [Dog(d), Cat(c)] is a list<Animal>. A list written where a type is declared for it (a binding, an argument, a return, a default) takes that type, so [Dog(d)] passes where a list<Animal> is named. A dict written with values of several subclasses takes their common base as its value type in the same way ({"d": Dog(d), "c": Cat(c)} is a dict<string, Animal>), and so does a display written inside a display, at any depth ([[Dog(d)], [Cat(c)]] is a list<list<Animal>>). Instances of classes that share no base may be held together, 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. A list a name already holds does not widen, since the list is one object its holders share (§6.7). A method call through a base-typed reference runs the actual class’s method (for a in animals: a. runs each element’s own version); value methods dispatch the same way. Where the method of the instance’s class returns nothing, the call runs it and reading its value is then refused as the line runs (method `B.), since CPython’s answer is None; no except takes the refusal. So does a read in a method a subclass inherits, and in a script written above the class. A call whose arguments the method of the instance’s class does not take is refused as it reaches that class, in the words of CPython’s TypeError (B.), and except refuse takes it. A read no class can answer, and a call no class’s method takes, are refused when the program compiles. Through a base-typed reference you may use any member the base declares; a subclass-only member needs isinstance (§8.11) or a class match (§8.6) first. The class set is closed and finite, so dispatch is bounded and termination preserved.
8.11 isinstance
isinstance(x, C) → bool: true when x’s class is C or a subclass of C. Inside the taken branch it narrows x to C, exposing that class’s members; the test must be the whole condition, the narrowing ends with the branch, and a tuple or union narrows to the nearest class its alternatives share (sharing none, or naming a type, narrows nothing). The second argument may instead name a type as the language spells it, or str / float for string / real, and then tests any value on what it holds when the call runs: isinstance(1, int) and isinstance(True, int) are True, and a tuple or | union of classes and types is true when any is — isinstance(x, (int, real)), isinstance(x, int | real). A value no class holds is False for a class, isinstance(1, Dog), and every value is an object. A name holding a value is narrowed by a test against types, to the tested class: in the taken branch, after the test in an and, after a comprehension’s if, in a conditional’s first branch, and in the body of a match arm whose guard makes it. int beside bool narrows to int, either beside real to a value holding a number, and an or of two tests to what they join to. A field read such as c. is not narrowed. A value built from scalars holds their classes, [1, "a"] an int or a string, 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 claim’s value may be any class, so a failed test leaves it a value, as does the else of an and. A branch no value can take, such as one isinstance(v, list) guards, is never run and not read, and an operator meeting a value of known classes runs where it takes each: x * 2 over an int, a real or a string.
if isinstance(d, Dog):
print("a dog")issubclass(A, B) → bool: true when the class A is B or a subclass of it, at any depth, so issubclass(Dog, Dog) holds as issubclass(Dog, Animal) does. Either argument may name a type as isinstance’s second does; a type is a subclass only of itself and of object, except that issubclass(bool, int) is True, and a class is a subclass of no other type but object, and a type of no class. The second argument may be a tuple or a | union, true when any one is. A class is not a value, so each argument is a name written at the call, the first naming one class or type, and the answer is fixed where the call is written. To ask about an instance, use isinstance.
8.12 Standing constraints
@constraint declares an invariant the store is watched against. It concludes nothing, writes nothing, and never fires:
@constraint
def stock_never_negative():
for p in Product:
if p.stock < 0:
violatedIt reads like a rule with violated where the conclusion would be, and that is what it is: the same for and if, and no head. violated is a statement rather than a value, and a reserved word only in that position.
Ask for it with checkup (§8.8), which reports one violated line per constraint that does not hold, naming the breaching subjects. It is never checked 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 saying so.
Why not just write a rule? A rule that concludes p. records a fact ABOUT the store INSIDE the store, and the next rule reads it as an observation. A constraint has no conclusion to record, so there is nothing for a later rule to pick up. That is the whole difference.
A constraint binds exactly one subject variable, and its premises are a comparison against a literal (p., p.) or an absence (absent(o.), joined by and. Premises on one field hold through one value of it, as a rule’s do. 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:
@constraint
def every_order_has_a_customer():
for o in Order:
if absent(o.customer):
violatedBoth limits have one cause: a rule is answered by the reasoning, and a constraint is answered by re-reading your store, so it can ask what a read can ask. To watch something outside that, conclude it into a field with a @rule and write the constraint over that field.
A constraint shares the unit’s one name space with scripts, rules and classes, and list(target = rules) does not list one — it derives nothing, so a caller enumerating rules to reason about what your store concludes would be misled by it.
Removing one. forget_constraint(name) drops a single constraint, addressed by the qualified name checkup prints. Nothing is reclaimed — a constraint concludes nothing, so the data it watched is untouched, including the subjects that breached it. Stopping the watch is not fixing the breach. forget_module removes a module’s constraints along with the rest of it; forget_constraint drops one invariant and leaves everything else the module brought.
9. Modules
A .dpy source file is a module — the unit you submit, store and import. Modules introduce no new types (§4) and no new verbs (§5): a module body is written in the language already described here, and compiling or importing a module is the operation that runs that body.
9.1 A .dpy file is a module
A module’s top level is its body — a sequence of declarations and statements, read top to bottom. A body may contain, in any order:
- declarations —
def(a callable script, §7),class(a kind schema, including computed fields, §8), a@rule-markeddef(a standing rule, §8); - data declarations — the write verbs
remember/update/forget/assert, each naming its source as its provenance, written at module scope (§9.2); - statements — anything legal inside a
defbody (§7): boundedfor/if/match/try, reads bound by assignment,print, andcommit/rollback; importstatements (§9.3).
The body runs once, top to bottom, in source order, when the module is compiled or imported. A def becomes callable afterward; a class installs its schema and any computed fields; a @rule-marked def installs a standing rule; a data declaration performs its write; an ordinary statement runs.
A plain call is one of those statements, and it is how a module demonstrates itself: define a script and then call it, so importing the module both installs the script and runs it once.
def greet():
remember(Greeting.g1.text, "hello", "setup")
print("greet ran")
greet()Importing that module reports the script installed and the body’s write and print as having happened. The script stays callable afterward: the top-level call is one run, not the only one.
A module with no def is valid — one that only installs a schema, defines a rule, or writes data declares no callable script and is loaded for its body’s effect. A single-def source is a one-declaration module.
A module may state its version. A body that assigns __version__ once, at its top level, to a plain string states that string as the module’s version:
__version__ = "1.3.1"A compile given no version records it, and info_module reports it; a version the compile is given wins over it (MCP Reference §4.1). A body that binds __version__ more than once, anywhere in it, or to anything but one plain string, states no version. The line is otherwise an ordinary assignment, run with the rest of the body.
Termination is unchanged. Every construct legal at module scope is already bounded — there is no while, for iterates only finite collections, and a script cannot recurse — so a module body always terminates, for the same reason a def body does. Module scope introduces no new iteration and no new recursion.
A body that fails keeps nothing. When a failure nothing catches stops the body — a refusal, or an operation that cannot be carried out, such as an import of a module that is not stored (§9.4) — the compile or import that ran it keeps none of the body’s work. A compile installs none of the module’s declarations, and the writes the body made before the failure are discarded. The store is as it was, so a corrected source compiles with nothing to forget first. What the body printed before the failure is still reported, because it ran.
9.2 Data declarations at module scope
A write statement written at module scope — not inside a def — is a data declaration: an assertion of what is, or for forget a removal, applied when the module is compiled or imported.
remember(Rate.vat.pct, 25, "finance-act-2026")
update(Rate.vat.pct, 24, "finance-act-2027")
forget(Flag.legacy.enabled)A data declaration is an ordinary write (§5) that happens to sit at module scope; it obeys every rule a write inside a def obeys, save the one a re-run changes (§9.5).
9.3 import
import <module>Loads a module already stored under <module>: it makes that module’s names available (§9.6) and runs its body once (§9.1). <module> resolves along the search path of §9.4. This is the one module operation written in a program body — a module may import another module. import is a reserved word, and begins the statement at statement head.
Because loading runs the module body, what that body prints and writes belongs to the run that imported it: the imported body’s output and store effects join the importing program’s transcript at the point of the import, and count in its verdict.
An import has two effects, and they follow two different rules:
- Its names are in scope from the
importto the end of the unit, whatever block theimportsits in and whether or not that block ever runs. Position decides, not the block: a name used above itsimportis refused when the program is compiled, and one used below it resolves even when theimportsits in anifthat never runs, or in another script of the same unit. - Its body runs when control reaches the
import, and each time it does — never, for animportin a branch not taken; once for each pass, for one in a loop (each re-run reconciles, §9.5).
So if <condition>: followed by an import m does not make m optional: m. resolves on every path, while what the body of m writes lands only on the path that reached the import. This differs from Python, where an import inside a function binds a name local to that function.
Loading order is an acyclic dependency order — a module is loaded before a module that imports it — and an import cycle, a module that imports itself directly or through another, is rejected when the module is loaded.
A module your store does not hold. An import naming one fails when control reaches it, with no module named <module> at the import’s line, and what ran before it stands. A program that also uses that module — calls one of its scripts, constructs one of its classes or names one as a parameter type or in isinstance, or uses a name from bound — is refused before it runs instead, at that import and in the same words, since nothing below the use can be checked without the module. So is a program with a type_error anywhere below a top-level import of such a module: control always reaches that import, so nothing below it could run. A grammar error is reported where it is, above a use of the module or below it (§10). Any other parse_error below a use that names one of the module’s classes is not reached, since that use is refused as it is read. A script missing from a module your store does hold is refused at the call: module 'm' has no attribute 'f'.
from <module> import <name>, <name>
from <module> import (<name>,
<name>)Loads <module> exactly as import <module> does, including running its body each time control reaches the statement, and binds each named script or class, so a later area(2, 3) means geo. and compiles to the same program. Each name must be one the module declares; one it does not is refused at the import, used or not. Only the names are bound, as in Python: geo. needs an import geo of its own. 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. Keep both with import <module> and the qualified name. Naming it in an annotation, as in item: Site = b, uses it and binds nothing. A message that names a bound script or class names it as you bound it, Site rather than geo., so a form it offers compiles as written. A keyword argument named like an imported name is the callee’s parameter, and a class field or method named like one belongs to its class; neither is affected. import a, b loads each module in turn. import geo as g binds g to the module, and from geo import area as ar binds ar to the name, each as if the module’s name had been written; neither binds geo, as in Python. A name an import bound is refused if it is bound again to something else. There is no from m import * and no relative import. Each is refused naming the form to write. from __future__ import <feature> loads nothing and binds nothing: Python’s features are accepted and change nothing here. Below the top of a unit, or naming a feature Python does not know, it is refused in Python’s words.
9.4 Name resolution and shadowing
import <module> resolves <module> against the modules stored in your store, each put there by an earlier compile (§5.7). There is no second place to look and no fall-through: a name that is not in your store is not found. The exceptions are the language’s own modules — math and cmath (§6.14), vector (§6.15) and struct (§6.16) — which are never looked up in your store; no module you compile may take any of those names.
This is what makes a published module’s name a default rather than a reserved word. A module we publish, such as code, is a file you compiled into your own store, so import code resolves to whatever you compiled under that name — the copy you took from us, or your own edit of it, or something unrelated.
9.5 Re-submitting reconciles
Compiling a module whose name is already stored — or importing an already-loaded module — re-runs its body, and the re-run is a reconciliation:
- a data declaration whose value changed since the last run replaces the claim it recorded before — its own claim and no other: a value another source holds at that cell stays beside it, as any second source’s does;
- a data declaration whose value is unchanged leaves the cell as it is — an identical write is not recorded again;
- a
classor rule install is likewise a no-op when nothing changed; - the body writes each cell once per source and period. A second
rememberorupdateof one cell from one source, over a window that overlaps the first, is refused as a conflicting write (conflicting_write) naming the cell: replayed on every import, it would revise the cell again each time. Write the final value once. One source writing a cell for periods that do not overlap states two values, and adefthe body calls writes as anydefdoes; - a
def, rule, computed field or constraint that an earlier compile installed and the text no longer declares is removed, and a body the text no longer has stops running onimport. The answer names each thing it removed. Removing a rule or a computed field withdraws what it concluded, asforget_ruledoes. Removing needs aread-write-deletekey, asforget_ruledoes: under aread-writekey a compile that would remove any of these is refused withscope_forbidden, names them, and changes nothing. A body the text drops is not one of them, and stops running under either key.
Reconciliation leaves the store reflecting the module’s current text, rewriting only the cells whose values changed, and is safe to repeat. So the way to stop using one of a module’s rules is to delete it from your copy of the file and compile again, and the way to bring it back is to restore the line and compile again.
Data is never removed by a re-import. A fact the module’s body wrote, or one written under a class the text no longer declares, stays where it is and stays readable as module.. Facts are the one thing compiling again cannot bring back, so removing them takes a verb that says so: forget, or forget_module for the whole module. This is the one place import differs from a language whose import runs a module body only once: a DKE Python module is durable store state, not process memory, so re-running re-applies it.
Re-importing a changed module reaches the module, and not what was compiled against it. A changed rule or computed field applies at once, since both belong to the module. A changed def replaces the module’s own script, but a script compiled earlier that calls it keeps the body it was compiled with (§9.6). So one re-import can move a module’s rules while the callers of its scripts stay where they were — a threshold visibly changes in the same run where an already-compiled caller still answers from the old script. The import’s answer names each such script and the module script whose old body it keeps. Compile or import that caller again to move it onto the new version.
9.6 Namespacing
A module is a namespace. A name it introduces — a kind declared with class, a script declared with def — is addressed under the module name with a dot: code., code.. Two modules may each declare a Function without collision; they are code. and finance., distinct. Within a module’s own body, its own names are reachable unqualified.
An import brings the imported module’s kinds into scope unqualified. After import code, a bare Call in that unit means code.. A bare kind name resolves in this order:
- a kind the unit declares itself wins — a unit’s own
class Callis never redirected to an imported module’sCall; local beats imported; - otherwise, the one imported module that declares it;
- if two or more imported modules declare it and the unit declares none, the program is refused, with a
type_errornaming the candidates — import order states dependency, not precedence, so the ambiguity is reported rather than resolved by position; - otherwise the name is unchanged — a kind no imported module declares is an ordinary global kind. That holds where a module the unit does not import declares a kind of the same name: the bare name is a kind of its own, beside the module’s. A write to it is accepted, and
compileandrunanswer it with a note naming the module and theimportthat reaches its kind, once per kind, at the first write. A read is not noted.
The qualified form is always available and always unambiguous, so it is both the way to reach a specific module’s kind past a local declaration of the same name and the fix for case 3.
The qualified form names only a kind the module declares with class. Where code declares no class Pk, code. is refused with a type_error, whether it is written to or read: Pk is a global kind (case 4), its bare name reaches it, and a rule code installs over Pk reads it there.
The module-qualified construction is the module name, the kind and the subject in parentheses. It is an instance of the module’s kind, and you read a cell through its field:
a = code.Function("f1").fanin # code's Function f1, field fanin
print("fanin = " + str(a.value))This reads like any field read (§8) and its result is an active_claim. A field code’s Function does not declare is refused, read or written, as one your own class does not declare is (§8.3). The parenthesised subject is what distinguishes the construction from an ordinary dotted path (§7). Because each module’s names are addressed under its own module name, two modules that both declare a Function never read or write each other’s cells.
The construction is also a value on its own. f = code. binds the instance, and then: - f. reads the same cell; - remember(f. writes it; - print(f) shows <code.; - f can be passed to one of code’s scripts whose parameter is a Function; - a method of Function can be called on it, and isinstance(f, code. tests it.
A parameter of your own may be annotated code.. It accepts an instance of code’s Function or of a subclass code declares, and a method called on it runs the method of the instance’s own class, as for a class your unit declares. code. is a type wherever a class of your own is one, so an annotated binding (f: code.), a collection’s element (list<code.) and a return type (-> code.) may name it too.
A module’s class is constructed only through its module. After import code, a bare Function("f1") is refused with a type_error that names code., just as a bare name is not in scope after import code in Python. The unqualified-kind rule above covers a kind in a path, not a construction. If you compiled a script of your own named Function earlier, a bare Function(…) still calls it.
A script a module declares with def is likewise addressed module. — the name a program calls after importing its module, the name info and forget take, and the name list reports. Two modules may each declare a restock without collision.
An imported module’s scripts are callable from your own code under that same qualified name:
import code
def review(fn: string):
who = code.callers_of(fn) # a value call
print(str(who.length) + " caller(s)")
reach = code.impact_of(fn) # its result is an ordinary list
for r in reach:
print(" reaches " + r)
code.log_review(fn) # a void call, as a statementSuch a call obeys the same rules as any other (§7): arguments are checked against the callee’s declared parameters, a value call must name a value script, and the callee must already be stored — which an imported module’s scripts are, since importing it stored them.
code. and code. open with the same words. What tells them apart is what code declares. Function is a class, so code. is a construction. callers_of is a script, so code. is a call.
A call is resolved when the calling module is compiled, and the callee’s body at that moment is the body the call runs. Importing a newer version of the callee’s module afterwards does not change a caller that was already compiled — it changes what module. runs when called directly, and what the next compile of the caller picks up. Import the calling module again to move it onto the new version.
9.7 Published modules
A published module is an ordinary module — classes, rules and queries in one .dpy file — that we have written and published for you to use instead of writing the same declarations again. You download the file and compile it into your store, once. Nothing about it is privileged: it is the same kind of file you may write yourself, it resolves the same way (§9.4), and once compiled your store cannot tell the two apart.
Three things follow, and each is a consequence of the file being yours.
- You can read it before you run it. It is source in this language. What it declares is what its lines say; its published documentation — for the modules we publish, the Library Reference — is a lookup over that source, not a substitute for it.
- Its interface is enumerable once compiled.
list_modulesreports it,info_modulereports the scripts it defines, the classes it declares and the version its__version__line states (§9.1), when it has one,listreports its scripts with their signatures, andlist(target = rules)its standing rules by name. This is a guarantee, not an artefact: a script that could not be discovered could not be called. A module’s SOURCE is not stored — the store keeps the compiled form, and there is no readback verb for any module — so the file you downloaded is the copy to keep. - Removal and editing are ordinary.
forget_ruleremoves one of its rules andforget_moduleremoves the whole module; nothing reinstates either. To reason without one of its rules, remove the rule, or delete the line and compile again (§9.5).
10. Errors
Two static diagnostic kinds; runtime adds two catchable outcomes.
| Category | Stage | Recoverable in source? |
|---|---|---|
parse (parse_error) |
static — token structure | no |
type (type_error) |
static — semantic rule | no |
refuse |
runtime — verb call | yes, via except refuse as |
engine_error |
runtime — service layer | yes, via except engine_error as |
Common triggers:
- parse — tab in indentation, a bad dedent or an indent where no block opens; unterminated string; a closing bracket that closes none, or one of another kind; a bracket never closed; reserved word (
class,return, …) as an identifier; an empty module (whitespace and comments only). - type — a name nothing binds; a wire-only script verb in a program body; a non-exhaustive
match; aforover a non-collection; a type mismatch; a non-existent field. refuse— the store rejects a well-formed request: out-of-surface slot, wrong slot count, index out of bounds, division / modulo by zero, integer overflow,commitoutside a branch. Also a service budget on how much one run may build — see below.engine_error— a well-formed request the service could not carry out.
A grammar error is answered first. A grammar error is a ), : or line break that is missing, or an expression that is not there. So is one of Python’s own keywords written where a name or a value goes, as in y = def: DKE Python teaches it as a reserved word, and answers it where Python does. So is one written after a ., as in xs. or remember(K.: such a word names no field, method or path segment, and a kind, subject or attribute spelled as one is reached through a name that holds it, k = "class", then K.. So are two values side by side inside brackets, as in [1 2]: DKE Python answers them where the first begins and names the comma that may be missing, as Python does. Python 3.11 answers some pairs at the second value instead, among them one whose first value begins with a name it mistakes for the soft keyword case or match, such as c or ma, and DKE Python answers those where Python does. A grammar error is placed where Python places it, which is sometimes a token further on: x = a.-b is answered at the -, after a . no name follows; a not no in follows is answered at the token after it; f(a. is answered at the a, as an argument that assigns to an expression, and so is if x = 1: at the x and [1, (a)=1] at the a, wherever Python reads a named expression; a := is answered where Python answers it, at the := in y = a := 1 and inside its value in y = (a := 1 +), and where its target begins when that is no name, as in y = (a.; a second = after a keyword argument’s value, as in f(a=b=1), is answered at that =; a dict key with no : after it, as in {1: 2, a., is answered at the key’s last character, and one whose first operand does not parse, as in {1: 2, (3 +)}, at its grammar error; a positional argument after a keyword argument, as in f(a=1, b, c=2), is answered at the call’s ), once Python has read the rest of the call, and so it is in a verb’s call, a builtin’s and a class’s bases; and a return annotation that ends before its :, as in def f() -> int + -:, is answered where the expression Python reads ends, as the : that is missing. Python reads a whole program before it runs any of it and answers a SyntaxError wherever it stands, and so does DKE Python: a program refused for something else, such as isinstance(x, 5), is answered with a grammar error on a later line instead. A refusal made as the source is read, such as a bytes literal, does not hide a grammar error, even one later on its own line. Below a refusal inside a @rule or @constraint, and later on the line of a refusal inside a value such as isinstance(x, 5), a grammar error is reached. Inside a @rule, the rule’s own grammar decides what a grammar error is, so a conclusion that is not a value is one. A grammar error is reached later on the line of a refusal in a header, such as a type annotation, a parameter or a method’s name, in a decorator or in a lambda, and later in a rule’s line than its refusal. A starred argument DKE Python refuses is read past as Python reads it, so a grammar error later in its bracket is reached too, as in f(*a, 1 +) (an element’s * is a spread, §6.13, and is read as one, and so is the * before an argument of print, of a script’s call, §7.4, or of a method’s, §8.4); a starred value alone in parentheses, as in y = (*a), is itself Python’s grammar error at the star. So is one later in a verb call refused for a keyword argument or a second argument it does not take, as in pin(K., and so is one after an argument a verb or a builtin refuses for any other reason, as in isinstance(1, 5, 1 +), or after an argument more than a call takes, as in len(x, c, 1 +), or in or after a keyword argument to an aggregate query, which takes none, as in avg(K., or in or after a verb’s path argument that goes on past its path, as in current(K., or that is no path, as in current("s", 1 +), and so is one in or after a hypothesis’s field that is either. So is one after or below a call short of an argument it takes, as in remember(K. above z = 1 +. A grammar error is reached inside the brackets after any refused value, as in datetime("bad")[1 +], and in the arguments of a method call refused at its name, as in s.. A statement refused at its first word is read as Python reads it, its body included, as in while True: x +. So is a class inside a block or a class body, with its bases and its body. What a decorator refused at its name decorates is read as Python reads it, unless it reads as a rule or a constraint, whose lines may be a rule’s grammar under a misspelled @rule. A parameter list or a lambda’s parameters Python would not read are answered where Python answers them, as in def f(**kw, y: int = 1 +):, answered at the y after **kw. An import refused past its first word, as at the dot of import m., is read on as Python reads an import, so that line is answered at the o. A malformed escape, or a character such as $ or ? that no token begins with, is answered where it stands, so a grammar error above it comes first. A string the source never closes, a malformed number, or a character outside ASCII is answered before any grammar error.
A keyword argument named twice in one call, as in f(a=1, a=2), is refused at its second name, in Python’s words: “keyword argument repeated: a”. Python finds it only once it has read the whole program, so a grammar error anywhere comes first. A verb’s keyword named twice, as known_at= in current, is refused at the same place in words of its own.
What Python’s compiler refuses, a program its grammar reads, is refused here at the place Python answers and in its words: 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, and others. Its order holds too. A grammar error anywhere comes first, every other refusal comes after, and of two it would both make the one it reaches first is answered, which need not be the one written first. __debug__ reads as True, and nothing may bind it or delete it.
A Python builtin DKE Python does not have is a type error that names it as one and says why. It is not taken for a script you have yet to declare, and declaring a script of that name would not give you the builtin. Each is in one of four families:
| Family | Builtins | Why |
|---|---|---|
| Left out on purpose | aiter anext bytearray bytes callable classmethod eval exec filter globals id locals map property staticmethod vars |
each belongs to a Python feature this language leaves out: asynchronous iteration (aiter, anext); bytes, which are a blob here (bytes, bytearray); functions held as values (callable, filter, map — a comprehension filters and maps); method decorators (classmethod, property, staticmethod); running text as code (eval, exec); namespaces held as values (globals, locals, vars); and object identity (id) |
| Out of reach | breakpoint delattr dir getattr hasattr hash input memoryview object open setattr super type |
each reaches the interpreter or the machine, and a program here reads and writes a store |
| Needs a value this language does not hold | frozenset |
a set that cannot change |
| Undecided | none | no builtin is undecided |
super is in the table only outside a method. Inside one, super() reaches the base class (§8.10).
A service budget is not catchable. A service may limit how much data a single run builds. This is separate from the language’s own guarantees: bounded iteration bounds how LONG a program runs, not how much it HOLDS at once, so a program can terminate as promised and still build more than the service will hold. Such a limit is reported as a refuse but except refuse does not catch it — a handler able to swallow a resource limit could retry inside a bounded loop and defeat it. Build fewer values in one run, or split the work across runs.
11. Accessors and field tables
11.1 Universal accessors
<expr>.— for any discriminator and forkind → string proof_tree.<expr>.— for anylength → int string(its character count — Unicode characters, not bytes) or finite collection (its element count).
11.2 Field access by type
| Type | Field | Field type |
|---|---|---|
active_claim |
value |
value (union; §4.1) |
active_claim |
source |
string |
active_claim |
path |
string (cell path K., or m. for a kind module m declares; the cell a dependents/premises/conflicts claim lives at) |
active_claim |
claim_id |
string — the claim’s name: its cell path, # and a code of twelve hexadecimal digits (Cat.), made from what the claim shows (its value and type, where it came from, its window, its qualifiers and created_at). The same claim carries the same claim_id on every read, and writing, revising or removing another claim leaves it unchanged. A claim recorded anew has one of its own: the new claim of a revision, a conclusion a rule draws again, a value in force the restore tool brings back (an older version it puts back keeps its own). Two claims at one cell that agree on all of those fields share one. dependents, premises, forget and would_forget take it in place of the claim (§5.3) |
active_claim |
created_at |
string (RFC 3339 UTC µs; empty when the time the claim was recorded is not kept, §5.4). Orders claims, does not measure elapsed time — compare it, never subtract it; use duration to record how long something took. Claims recorded in the same microsecond share a stamp and tie under sorting |
active_claim |
trust |
string — how the claim came to hold its value: asserted (someone recorded it) or derived (a rule concluded it). Engine-established, never caller-asserted: citing "derived" as a source still reads asserted |
active_claim |
preferred |
bool (a recorded judgment says act on this one) |
active_claim |
maintained |
bool — 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. True for every asserted claim and every derivation still being kept current. Not a conflict and not a preference — nothing disagrees, the value is simply not being kept up, and every read still serves it. Established at read rather than recorded on the claim, so it appears as soon as the rule stops keeping the value current and clears on its own; read it alongside the value it qualifies rather than carrying it forward |
active_claim |
written_by |
string (the store key that wrote the claim; empty when no key wrote it — either the write was not authenticated, or a rule concluded the claim and there was no write. trust tells those apart; why accounts for a derived claim as a key accounts for a written one). Observed, never asserted — contrast source, which the writer states |
active_claim |
derived_by |
string — what concluded a derived claim: a rule by the name list(target = rules) lists it under (heat., or warm for a rule compiled on its own), a computed field by its class and field qualified the same way (heat.). Empty on a claim someone recorded. premises returns what it was concluded from (§5.3) |
active_claim |
rule_kind |
string — the kind of relation the concluding rule states, when its author said ("causal" for @rule(kind="causal"), §8.8). Empty on a claim someone recorded, on a computed field’s conclusion, and on a conclusion of a rule written without one |
active_claim |
valid_from |
value (union; §4.1) — the start of the claim’s validity window: the world-time it says something about, as distinct from when it was recorded. The null class when the claim has no start bound, so read it by matching, never by comparing against a sentinel instant. A conclusion carries the overlap of its premises’ windows |
active_claim |
valid_to |
value (union; §4.1) — the end of that window, null when the claim has no stated end. Only the datetime and null classes ever arise for either field, but a match still needs a case default: arm: exhaustiveness is checked against the whole union |
active_claim |
qualifiers |
dict<string, value> — the facts the writer stated about this claim when it wrote it (qualifiers= on remember / update), each value keeping its class, names in name order; {} when it stated none. Stated, never established: the store keeps them with the claim and acts on none — contrast trust, which the store establishes |
proof_tree |
root |
active_claim |
proof_tree |
truncated |
bool (True when the proof stops short of the whole derivation) |
verify_result |
match |
bool |
verify_result |
actual |
string (the value that stands; empty at a location that is unwritten or in disagreement — conflicted tells you which) |
verify_result |
expected |
string |
verify_result |
conflicted |
bool (the location holds competing values, so there is no single actual; match still answers over all of them. A source-scoped verify(K. is never marked) |
complex |
real |
real — the real part |
complex |
imag |
real — the imaginary part |
int |
real |
int — the number itself |
int |
imag |
int — 0 |
int |
numerator |
int — the number itself |
int |
denominator |
int — 1 |
bool |
real |
int — 1 for True, 0 for False, the int it is |
bool |
imag |
int — 0 |
bool |
numerator |
int — 1 for True, 0 for False, the int it is |
bool |
denominator |
int — 1 |
real |
real |
real — the number itself |
real |
imag |
real — 0.0 |
slice |
start |
value — the first bound as written: an int, or the null class when it is open |
slice |
stop |
value — the second bound, the same way |
slice |
step |
value — the step, the same way |
range |
start |
int — the first integer it would give, whether or not it gives any |
range |
stop |
int — the bound it stops before |
range |
step |
int — the distance between two of its integers, never 0 |
refuse_info |
reason |
string — what was refused, written for a reader and in the script’s own language. Report this |
refuse_info |
teaching_hint |
string — how to correct the call; empty when there is no suggestion. An aid to a human reader, never a second reason and never something to branch on |
refuse_info |
discriminator |
string — a short tag naming which refusal this is, for a program that must branch rather than report. The tag set is not part of this contract (§4.2), with two exceptions: a refusal from your own raise <ExcType>(…) carries the exception name you wrote, and a refusal the store raises carries its published refuse_reason code |
engine_error_info |
reason |
string — what failed, written for a reader |
engine_error_info |
code |
string — a short tag naming which failure this is, in the role discriminator plays for a refusal. The tag set is not part of this contract (§4.2) |
The last two types are the handler bindings of §4.2 — a program cannot write either name as a type, and they are listed here under the names .kind reports.
11.3 Indexing element types
| Value | [int] yields |
|---|---|
claim_list |
active_claim |
proof_tree |
active_claim |
diagnosis |
string — one report line |
string_list |
string |
subject_set |
string |
result_type_set |
string |
string |
string (the one character at that position) |
list<T> |
T (negative from-end index permitted) |
tuple<T1, …, TN> |
the indexed position’s type (constant index only) |
dict<K, V> |
V (indexed by a key of type K, not an int) |
A set does not index: s[i] and s[a:b] are refused before the run, as Python refuses them ('set' object is not subscriptable). Walk it with for, in sorted order, or test an element with in.
A string also supports slicing — s[a:b] (either bound optional) yields the substring, s[a:b:step] adds a step (a negative step reverses: s[::-1]), and negative indices/bounds count from the end (s[-1], s[-3:]). Total and character-indexed; a step of 0 is a catchable refuse.
11.4 String methods
| Method | Result |
|---|---|
s. |
int — first character position of sub, -1 if absent |
s. |
int — number of non-overlapping occurrences (empty sub = length + 1) |
s. / s. |
bool — whether s begins / ends with the argument (a string, or a tuple of strings → any of them) |
s. |
string — copy with all occurrences of old replaced (empty old inserts new in every gap) |
s. / s. |
string — full Unicode case mapping (may expand, e.g. ß→SS, so length can change; lower applies the Greek word-final Σ→ς rule) |
s. / s. / s. |
string — remove characters from both ends / left / right (whitespace, or the chars set) |
s. / s. |
tuple<string, string, string> — split once at the first / last sep into (before, sep, after) |
s. |
string — each character whose code point table holds replaced by what it holds for it |
s. |
list<string> — the lines of s, split at each of CPython’s eleven line boundaries, U+2028 and the separator \x1e among them; a true keepends keeps each line’s boundary |
All are total (never refuse) and count characters, except partition / rpartition, which refuse on an explicit empty separator, and translate, which refuses a code point no character has.
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.
| Call | Returns |
|---|---|
str. |
dict<int, int> — each character of x to the character at its place in y, as code points. Strings of different lengths refuse with value_error |
str. |
dict<int, V> — the dict d, each string key as its code point. A string key not one character long refuses with value_error, and a key that is no string, int or bool with type_error |
dict. |
dict<K, V> — each element of xs to the one value v |
float. |
real — what s spells in hexadecimal. A string that spells nothing refuses with value_error, a value too large with arithmetic_overflow, and inf or nan with conversion_error |
Python answers str. and dict. with None among the values, which has no spelling here, so each is told what to write instead. A value of the type reaches each of them too, as in Python: "a"..
A method through its type’s name. T. is x., its first argument the receiver, as in Python: str. is "a"., and list. is xs.. T is str, int, bool, float, complex, list, dict, set or tuple. No receiver, a receiver of another type and a name the type does not hold are each a type_error in Python’s words. A name the program binds is no longer that type.
12. Keywords
The keyword surface localises per natural-language surface, and the surface is chosen per call (MCP Reference §8.1, which names the tag for each). This Reference describes the English one, where the structural keywords are English: def class return match case if else for in try except as branch commit rollback and or not print True False None null. Python’s own keywords and null are reserved, as they are in Python, and name nothing, including those this language has no statement for, such as async, await, global, lambda, nonlocal, while and yield. None is spelled so on every surface, being Python’s word for a value this language does not have rather than one it translates. Verb names (remember, current, get, …) localise correspondingly (§5), and on this surface they are the English spellings this document uses throughout. A word of this language’s own that Python keeps no keyword of is an ordinary name where its form does not stand: a verb’s word is the verb only right before its (, and match, case, branch, commit and rollback name what they do only where those forms stand, so get = 1 and def f(match: int): read as they do in Python.
Nine words have an English spelling and no other. Two are the diagnostic verbs. A unit written on another surface cannot call them, and writing the English spelling does not reach them either — on that surface the word is not a verb at all. The compiler says so and names the remedy, which is the unit’s language tag rather than an edit to the program: Compile this unit as eng to use it. The other two are the words a trust(...) premise compares with (§8.8), "asserted" and "derived". They are the values .trust reads back, which are English on every surface, so a premise written on any surface compares with them as they are spelled here. The next two are real and imag, the names complex(...) takes its two parts by (§6.3). They are also the .real and .imag attributes (§11.2), which read the same on every surface. The last three are maketrans, fromkeys and fromhex, the methods a type itself answers (§11), spelled as Python spells them, as a string’s methods are. Everything else in the keyword surface localises: the except handler kinds, print’s keyword arguments, the method receiver, the compound verb heads, and every other keyword argument a built-in function takes.
English on every surface. why_not what_needs asserted derived real imag maketrans fromkeys fromhex
world (§7.18) is a contextual keyword — a block only right after with, an ordinary identifier elsewhere. Type names (string, int, …) are likewise contextual, not reserved — but avoid all of these as identifiers.
A name is read as Python reads it, in Unicode’s Normalization Form KC, so every spelling of one form is one name: x (U+FF58) is the name x, and e followed by U+0301 is é. A keyword is compared as it is written, so if = 1 binds the name if. A string is not read: a variable holding "x" addresses x, where the path K. addresses K.. A name that keeps a combining mark from U+0300 to U+036F with no letter to compose into may name a local, a parameter or an alias, and nothing a store holds.
13. Quick reference card
DECLARE def name(K: string, n: int): # `def`; name: type params
ASSIGN hist = get(K.s.a) # bare assignment
PRINT print("x = " + str(n)) # string|int|bool|discriminator
WRITE remember(K.s.a, v, src) update(K.s.a, v, src) forget(K.s.a)
assert(K.s.a, src) pin(K.s.a) forget(K.s)
unpin(K.s.a)
READ current(K.s.a) -> active_claim get(K.s.a) -> claim_list
why(K.s.a) -> proof_tree verify(K.s.a, v[, src]) -> verify_result
why_not(K.s.a) -> diagnosis what_needs(K.s.a) -> diagnosis
subjects(K.a, v) -> subject_set list_attributes(K) -> string_list
caveats(K.s) conflicts() list_values(K.a) list_subjects(K[.a])
LISTS list_categories() list_pinned() list_scripts() list_result_types()
SCRIPT list_scripts() -> string_list
TYPES string int bool | active_claim proof_tree verify_result empty (discriminators)
claim_list proof_tree diagnosis string_list subject_set result_type_set (collections)
OPS == != < > <= >= (int cmp) and or not (bool) + - * / % (int)
+ (string concat) .field [i] | & - ^ <= < (set)
COLLS [1, 2, 3] list (1, "a") tuple {"k": 1} dict {1, 2} set # constructed, never stored
xs[i] xs[a:b] x in xs .length [e for x in xs if c] zip(a, b) enumerate(xs) range(a, b)
iter(xs) next(e) next(e, default) # one-pass iterator
sum(xs) min(xs) max(xs) min(a, b, c) # reduce (vs sum(K.attr) store fold)
sorted(xs) sorted(xs, reverse=True) any(xs) all(xs) bool(x) tuple(xs)
d[k] d.get(k, x) k in d d.keys() d.values() d.items() s.union(t) s.intersection(t)
s.issubset(t) s.issuperset(t) s.isdisjoint(t) set([]) c.copy() dict(d)
CHANGE xs.append(v) xs.extend(ys) xs.insert(i, v) xs.remove(v) xs.sort() xs.reverse()
s.add(x) s.discard(x) s.update(t) d.update(e) c.clear()
xs[i] = v d[k] = v d[k] += v del xs[i] del d[k] # each changes it in place (§7.17)
IF if c: … / else: …
FOR for c in list: … # finite collections only (list / dict-keys / set)
MATCH match d: case active_claim: … / case empty: …
match n: case 1 | 2: … / case k if k > 2: … / case _: … # value patterns
TRY try: … / except refuse as r: … / except engine_error as e: …
BRANCH with branch("b"): … commit / rollback
WORLD create_world(n) with world(n): … compare_worlds(a[, b]) -> claim_list
merge_world(n) discard_world(n) list_worlds() -> string_list
CALL name(args) # plain call
CLASS class Sensor:
temp: string
def record(self, v: string, src: string): remember(self.temp, v, src)
def label(self) -> string: return "sensor"
INSTANCE s = Sensor(room) remember(s.temp, "21", "op1") s.record("22", "op1")
reading = s.temp name = s.label()
match s: case Sensor: … / case default: …
ERRORS parse type (static) refuse / engine_error (runtime, catchable)
HELP --help [header|types|verbs|statements|scripts]14. Python tooling and type checking
DKE Python is a Python-syntax surface: a standard Python parser accepts .dpy, so formatters (black), linters (ruff / flake8), language servers, and syntax highlighting work on .dpy files as they stand.
For type checkers (pyright / mypy), which additionally want every name in scope, DKE ships a stub, dke., declaring the builtin surface — the verbs, the value factories, the type names, the @rule decorator, and the branch context manager, none of which the source imports. A checked copy that opens with the one-line prelude from dke import * then type-checks; the service compiles the original source unchanged.
The core surface (verbs, slot paths, values, control flow, rules, aggregates, transactions) checks clean. Two DKE constructs mean something different to a Python checker than to the DKE service — class construction (Sensor(room)) and the case <type> as <var>: type match — so relax the codes they trigger (the dke. header lists the exact settings). The DKE service remains the authority on types.
15. The knowledge model
DKE keeps apart several things a store usually runs together. This chapter names each distinction and the verbs that make it, and points to the section where each verb is described. It adds nothing those sections do not say.
| the distinction | what you write or read | where |
|---|---|---|
| unknown is not false | A cell nobody wrote reads empty from current. A cell recorded as known to be absent, with assert(K., reads back as a claim whose value is null. A rule records one with assert(p. as its conclusion. False is an ordinary value. A rule tells the three apart: absent(...) holds for the cell nobody wrote, known_absent(...) for the recorded absence, and a comparison for the value. not_asserted(...) holds where no person wrote, whatever a rule concluded, and why_not says which empty case a read met. |
§5.1 assert, §5.2 current, §5.3 why_not, §8.8 |
| disagreement is not uncertainty | Two sources that record different values at one cell are both held, and neither replaces the other. conflicts() and caveats(K. find such cells, agreement counts the sources behind a value and writers the keys that wrote it. None of them grades a source: which to believe is your judgment. |
§5.2, §5.3 |
| a preference is not a truth | prefer(K. records which source you go by. Every claim stays, the cell stays in disagreement, and no rule concludes differently; the claim you go by reads .preferred. |
§5.1 prefer, §11.2 |
| revision is not contradiction | A source revising its own reading writes update, and get still returns what it replaced. Another source saying something else is a disagreement, above. |
§5.1 update, §5.2 get |
| an alternative is not the store | with world("w"): writes into a named world that the store does not see, kept across runs until merge_world adopts it or discard_world drops it. compare_worlds(a, b) returns the claims one holds that the other does not, conclusions included. |
§5.9, §7.18 |
| a hypothesis is not a fact | suppose(...) reads what a field would be if other fields held the values you give, with standing rules running under them, and keeps nothing. suppose_absent(...) asks the same with facts taken away. Writes inside a branch can still be rolled back. |
§8.9, §7.10 |
| concluded is not recorded | .trust reads asserted on a claim someone recorded and derived on one a rule concluded, and nobody can write it. .derived_by names the rule, premises(c) returns the claims it concluded from, why returns every claim that took part in the derivation, and .rule_kind the kind of relation its author said the rule states. |
§5.3, §8.8, §11.2 |
| how it is known travels with the claim | A writer can state how it knows, as qualifiers={"method": "observed", "confidence": 0.8}, and every read returns them as .qualifiers. They are the writer’s statement: the store keeps them and acts on none. |
§5.1 remember, §11.2 |
| when it holds is not when it was known | A validity window says when a fact holds, and an as-of datetime reads along it. known_at= reads the other axis: what the store held at a given moment. |
§5.1 remember, §5.4 |
| what rests on what | dependents(c) returns the claims concluded from c, and dependents(c, transitive=True) everything resting on it at any distance. would_forget(...) counts what a range removal would take before you make it. |
§5.3 |
| a claim keeps its name | .claim_id names a claim on every read that returns it, and other writes leave it unchanged, so a program can keep the id and hand it to dependents or premises later. A claim recorded anew, such as a conclusion drawn again, has an id of its own. |
§5.3, §11.2 |
Companion documents: the Python Tutorial, which teaches this language in task order, and the MCP Reference, which specifies the transport that carries a program to the service.
Revision history
13.4.0 · rev 171 (2026-11-15) — §9.1: a module may state its version: a body that assigns
__version__once, at its top level, to a plain string states that string, which acompilegiven noversionrecords andinfo_modulereports, aversionthe compile is given winning over it; a second binding anywhere, or anything but one plain string, states none. §9.7 saysinfo_modulereports it. An addition, so the API is 1.5.13.3.7 · rev 170 (2026-11-15) — §6.12:
sum,minandmaxread a range from its bounds and a generator ofints one element at a time, building nothing, andanyandallhold nothing of a generator. Text only, so the API stays 1.4.13.3.6 · rev 169 (2026-11-15) — §5.8: an aggregate with no value refuses with
aggregate_empty, over no data and where a fold’s formula has none, which the section lists, pointing to the Specification for every fold’s formula. Text only, so the API stays 1.4.13.3.5 · rev 168 (2026-11-15) — §6.14: a
mathfunction given anintpast 2^53 takes it as theintit is, where it rounded theintto arealfirst:math.isfmod(9007199254740993, 10.0) 3.0, where it answered2.0. A correction, so the API stays 1.4.13.3.4 · rev 167 (2026-11-15) — §6.3:
**with anintpast 2^53 as its base or its exponent is the exact power rounded once, where theintwas rounded first:(-1.0000000000000002) ** 9007199254740993is-7.38905609893065, where it answered7.389056098930649. A correction, so the API stays 1.4.13.3.3 · rev 166 (2026-11-15) — §6.15: an element of
vector.that is not anapply int, its operands finite, is in each part the exact sum of the exact products rounded once, whatever order they come in, where it was the products added left to right and rounded at each step. A correction, so the API stays 1.4.13.3.2 · rev 165 (2026-11-15) — §6.15: each sum
vector.answers is the exact sum of the exact squares rounded once, where it wassumsq math.over squares each already rounded, a unit away on about one sum in six. A correction, so the API stays 1.4.fsum 13.3.1 · rev 164 (2026-11-15) — §6.3: each part of a complex product, quotient or power of finite operands, and of a
sumholding acomplex, is its exact value rounded once, where Python rounds at every step:(1 + 1j) ** 101is(-1125899906842624-1125899906842624j),sum([0.1j] * 10)is1j,(-8) ** 0.5is2.8284271247461903j, andcomplex(-1, 0) ** 1e308is(1+0j), where it answered(nan+nanj). A correction, so the API stays 1.4.13.3.0 · rev 163 (2026-11-14) — §4.1, §6.12 and §11.2:
rangeis a type of its own, held as its three bounds and never built; aforwalks it, and it answerslen, an index, a slice,==,in,.index,.count,.start/.stop/.stepand itsrepras CPython’s does. Where it printed as a list it printsrange(0, 3). An addition, so the API is 1.4.13.2.9 · rev 162 (2026-11-14) — §5.1: a kind whose name differs from a held kind only in the case of a letter A to Z is refused at write, naming the kind held, where the write was accepted and read back under the held kind. A correction, so the API stays 1.3.
13.2.8 · rev 161 (2026-11-14) — §5.8: a fold that yields a
durationor adatetimeis its exact value rounded once to a nanosecond, half away from zero, where past about 104 days it could answer a nanosecond or two off. A correction, so the API stays 1.3.13.2.7 · rev 160 (2026-11-14) — §5.8:
medianandmidpointoverdatetimes halve the gap between two instants exactly, where past about 104 days they could answer a nanosecond early. A correction, so the API stays 1.3.13.2.6 · rev 159 (2026-11-14) — §5.8:
entropyis its exact value rounded once, where it was computed in an order of its own. §6.3 no longer counts it among the answers computed through correctly rounded steps. A correction, so the API stays 1.3.13.2.5 · rev 158 (2026-11-14) — §5.8:
percentile,iqr,mad,median,midpointandspanare their exact values rounded once, everyintentering as itself, wherepercentile,iqrandmadrounded each step. A correction, so the API stays 1.3.13.2.4 · rev 157 (2026-11-14) — §5.8:
variance,stdev,cv,skewness,kurtosisand the two-attribute folds are their exact values rounded once, where they were computed in an order of their own;varianceandstdevanswer whatstatistics.andpvariance statistics.give. A correction, so the API stays 1.3.pstdev 13.2.3 · rev 156 (2026-11-13) — §5.8:
product,geomeanandharmmeanare their exact values rounded once, wheregeomeanwas computed asstatistics.computes it and the other two in an order of their own. §6.3:geometric_mean geomeanno longer computes through logarithms. A correction, so the API stays 1.3.13.2.2 · rev 155 (2026-11-13) — §5.8:
avgandrmsare their exact values rounded once, where they were computed asstatistics.and its root over the squares, and anfmean intenters a fold’s sum as itself. A correction, so the API stays 1.3.13.2.1 · rev 154 (2026-11-13) — §6.3: an operation with an
intand arealis the exact value rounded once, where theintwas converted first and anintpast 2^53 rounded twice. A correction, so the API stays 1.3.13.2.0 · rev 151 (2026-11-13) — §8.8: the text test joins the premises a rule may write:
r.,path. startswith("/ srv/") t.andname. endswith(". test") "admin" in a., against a quoted string or a field of a subject the rule binds, code point by code point, one part per test and never negated. An addition, so the Version line states API 1.3.role 13.1.1 · rev 150 (2026-11-13) — The Version line states API 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. Nothing else changed.
13.1.0 · rev 149 (2026-11-13) — Rules: a conclusion may write a text from values the rule reads, with an f-string whose fields are each a field of a bound subject or the value a relation holds for a pair:
d.. The text is aline = f"{d. title} cites Version {d. cites}." stringwhatever it reads as and follows its premises as any conclusion does. A field holds astringor anint; where it holds another type no text is drawn, andwhy_notnames 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, as are a text written from the field it concludes and a rule that closes a cycle through a written text. A text longer than 65,536 bytes is not drawn. The Version line states API 1.1.13.0.0 · rev 148 (2026-11-12) — The Reference states a version of its own again, first on the Version line and followed by the revision it last changed in; the number continues the one it last stated, 12.0, rather than starting over. The opening of this history says so, and from this revision each entry opens with the version it made (Python Specification §11.1). Nothing else changed.
rev 147 (2026-11-12) — The API version is
MAJOR., and the Version line states API 1.0. A first-line declaration’sMINOR python.is the API’s MAJOR, valid at 1.0 and every later 1.N.1 rev 146 (2026-11-12) — Aggregate queries: a fold whose Python computation passes the largest
realon the way answers its value where that value is areal(over1e308and1.5e308,avgyields1.25e+308), andinfor-infwhere the value itself is past it.rev 142 (2026-11-12) — A
real, and a part of acomplex, may beinf,-infornan(§4):1e400isinf,math.,inf math.,nan math.,isinf math.,isnan math.,isfinite cmath.,inf cmath.,nan cmath.,infj cmath.,nanj cmath.,isinf cmath.andisnan cmath.are added, andisfinite float()andcomplex()read the three words (§6.14). A result past the largestrealisinfor-infwhere it refused, an operation with an infinite or NaN operand answers IEEE 754’s value, and each departure from Python is stated: §6.3 Infinity and NaN, §6.1 Infinity and NaN (one NaN, found by value, sorted afterinf,nanfromminandmax), §6.14 (themathandcmathfunctions) and thestructandvectorsections. A cell holds every real exactly,-0.0included.rev 141 (2026-11-11) — §6.14:
mathgains thirty names, with Python 3.11’s signatures and words:gcd,lcm,comb,perm,factorial,isqrt,prod,sinh,cosh,tanh,asinh,acosh,atanh,cbrt,exp2,expm1,log1p,pow,fabs,copysign,fmod,remainder,trunc,modf,frexp,ldexp,fsum,dist,nextafterandulp. Each answers the exact value rounded once;fsum,prodanddisttake every operand exactly, somath.isfsum([1e308, 1e308, -1e308]) 1e+308where Python raises, andmath.of three or more coordinates is the same exact distance ashypot math.. Andist intanswer past what anintholds is refused as any suchintis.rev 140 (2026-11-11) — §7.4: 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: withnholding"x",print(**n, **{"x": 1}, **{"z": 1}, **{"z": 2})isprint() got multiple values for keyword argument 'x', where it named'z', refused where the unit was compiled. 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) — §7.4: 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: withnandkboth holding"x",print(**{1: 2}, **n, **k)andlen(xs, **n, **{1: 2}, **k)areprint() got multiple values for keyword argument 'x'wherever the display stands, andkeywords must be stringsanswers only where no name is given twice. It waskeywords must be stringswhere the unit is compiled.rev 138 (2026-11-11) — §7.4: a list’s
sortgiven a*and akey=the elements refuse,xs.over a list ofsort(*ys, key=len) ints, counts its arguments before it applies the key, as Python does: withysgiving one value it issort() takes no positional arguments, where it wasobject of type 'int' has no len(), refused where the unit was compiled; withysgiving none it is that refusal of the key, as the program runs and only where the sort has an element to apply it to. Each is catchable. A*nothing types, and a sort with no*, keep the key’s refusal where the unit is compiled, in the same words.rev 137 (2026-11-11) — §7.4: a list’s
sortgiven a*and a positional argument written beside it,xs., counts every argument before it refuses the positional one, as Python does: withsort(*ys, 5, x=1) ysgiving one value it issort() takes at most 2 arguments (3 given), where it wassort() takes no positional arguments, refused where the unit was compiled; it is decided as the program runs and is catchable. A*nothing types keeps its refusal where the unit is compiled, in the same words.rev 136 (2026-11-11) — §7.4: a list’s
sortwhose receiver is written,xs.orsort(*ys, x=1) list., counts what a typedsort(xs, *ys, x=1) *gives before it names a keyword it does not take, as Python does: withysgiving one value it issort() 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, catchable and without the hint. A*` nothing types keeps its refusal where the unit is compiled, in the same words.rev 135 (2026-11-11) — §7.4: 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: withmandnboth holding1,print(**m, **n),len(xs, **m, **n),sorted(xs, **m, **n),s.and a script’ssplit(**m, **n) f(**m, **n)areprint() got multiple values for keyword argument '1', where they werekeywords must be strings. Keys are equal as a dict’s are (1,1.0andTrueare one) and the later is named asstrwrites it. Written as displays,print(**{1: 2}, **{1: 3})is refused where the unit is compiled, in the same words.rev 134 (2026-11-10) — §3: a source may say what it is written in on its first line,
# dke: edition=python., with either version left out to mean the one the service serves. A declared name supplies a parameter the call leaves out, and a parameter naming another value is refused, as is a version the service does not serve.1 language=eng. 1 rev 130 (2026-11-10) — §8.12: a constraint premise that orders a string field against a quoted bound is judged, alone or with
absent(), and is reported violated exactly when a rule with the same premise would conclude. §5.3: theproof_treeawhyyields is the derivation alone, and a cell’s earlier, superseded values are no longer elements of it;getreads them. §7.9:except engine_errortakes every run-time failure the wire reports asengine_error, including a value narrowed to a class it did not hold and a read of a method that returns nothing; a spent budget, a stopped run and an add-only refusal remain uncatchable. §6.3: a collection takes no format spec, and an empty one answersstrof the value.rev 128 (2026-11-09) —
cmath.,exp cmath.andrect cmath.give each part, real and imaginary, as its exact value rounded once, sosqrt cmath.isexp(complex(1.3, 2.7)). imag 1.5681835703495033where Python answers1.5681835703495035.rev 127 (2026-11-09) —
sumover numbers holding arealis the exact sum rounded once, sosum([0.1] * 10)is1.0where Python 3.11 answers0.9999999999999999, and anintenters as the integer it is; it is refused only when its rounded value, or an all-inttotal, is out of range. The store fold’ssumand the list’s agree.rev 122 (2026-11-08) — §6.14:
mathhaslog10,log2,erf,erfc,gammaandlgamma;math.is rounded once, solog(x, base) math.islog(1000, 10) 3.0where Python answers2.9999999999999996.rev 120 (2026-11-08) — §6.14:
mathhasasin,acos,atan,hypot,degreesandradians;degrees,radiansandhypotof two coordinates are correctly rounded where Python’s algorithm rounds twice, with an example where the last digit differs from Python’s.rev 112 (2026-11-06) — §6.4, §6.5: a blob indexes, slices and joins as Python’s
bytesdo, with a program showing a cut, a reversal, a byte and a join.rev 111 (2026-11-06) — §6.15:
vector.adds along axes, assumsq(xs, axes) vector.carries a matrix along them: oneapply math.of the squared parts of the elements for each pattern of the named axes, a list of 2ᵏ reals, in one call.fsum rev 110 (2026-11-05) — §7.4:
dict.,fromkeys str.andmaketrans float., the three methods a type answers itself, read afromhex **as every callee that takes no name does: withoptsholdingx,dict.isfromkeys(xs, 0, **opts) dict., with a key that is no stringfromkeys() takes no keyword arguments keywords must be strings, and withoptsempty the call as written. It was a parse_error that**spreads a collection the call does not read.rev 109 (2026-11-05) — §7.4: 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:s.afteradd("a", **opts) s = {1}isset.withadd() takes no keyword arguments optsholdingx, and withoptsempty the refusal a name’s one type gives, as the program runs. It was that refusal at compile whateveroptsheld.rev 108 (2026-11-05) — §7.4:
slicecalled beside a**refuses any name before it reads a key, asmath.does:log slice(1, **m)withmholding a key that is no string isslice() takes no keyword arguments. It waskeywords must be strings.rev 107 (2026-11-05) — §7.4: beside a
**whose keys are known only as the program runs, every mapping is merged before a key that is no string is refused, so a name given twice is refused first (print() got multiple values for keyword argument 'x'), and a name written beside such a**thatsorted,min,maxor a list’ssortdoes not take waits for the merge:sorted(xs, x=1, **opts)issorted() got multiple values for keyword argument 'x'whereoptsholdsx. It waskeywords must be stringsand atype_errorwhere the unit is compiled.rev 106 (2026-11-05) — §7.4: a method reached through its type’s name given only such a
**, with no positional argument, merges the names and is then refused for its missing receiver, as the program runs:str.isupper(**opts) unbound method str.. It was a parse_error.upper() needs an argument rev 105 (2026-11-05) — §7.4: a list’s
sortthrough 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.sortssort(*xs, ys) yswherexsgives nothing. A list written of another type is refused for its type before what follows it is counted or named: over a tuplet,list.issort(t, 1) descriptor 'sort' for 'list' objects doesn't apply to a 'tuple' object, where it wassort() takes no positional arguments.rev 104 (2026-11-05) — §7.4: beside a
**whose keys are known only as the program runs, an argument a callee that takes no name does not take is refused as the program runs, after the names, in the words the call has without such a**: withoptsholdingx,s.iscenter(1.5, **opts) str., and withcenter() takes no keyword arguments optsempty'float' object cannot be interpreted as an integer. So is a change’s, and a dict’sgetorsetdefaultof a key alone. Each was atype_errorwhere 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, and stays atype_errorwhere the unit is compiled where nothing types what it answers,v = abs("a", **opts).rev 103 (2026-11-05) — §6.17, §7.4: a special name beside a
*or a**before any dict is read as at any method, its names refused in Python’s words before its count, as the program runs; beside a*it was refused in words that were not Python’s, and beside a**whose keys are known only as the program runs it was a parse_error. Beside such a**, a change read as a value and bound to no name is made and then refused as the program runs, after its names, and so is a special name whose answer no value here holds; each was atype_errorwhere the unit is compiled, and stays one where the value is bound.rev 102 (2026-11-04) — §7.4: beside a
**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 atype_errorwhere the unit is compiled, in the call’s own words, whatever the mapping gives:v = list.ispop((1, 2), **opts) descriptor 'pop' for 'list' objects doesn't apply to a 'tuple' object. 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) — §7.4: beside a
**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: withoptsempty,len(xs, 2, **opts)islen() takes exactly one argument (2 given). It was atype_errorwhere the unit was compiled, and stays one without such a**.rev 100 (2026-11-04) — §7.4: beside a
**whose keys are known only as the program runs, a name written at a change is merged with the names it gives, and through a type’s name a receiver written of another type is refused once they are merged, as the program runs. Each was atype_errorwhere the unit is compiled, whatever the mapping held, and still is without such a**.rev 99 (2026-11-04) — §7.4: through a type’s name, where the receiver is the first thing a
*gives, every method merges the names a**gives before it takes the receiver, aslist.did, and refuses a receiver of another type after them, as the program runs; so it does where nothing types what thesort *gives. It took the receiver first.rev 98 (2026-11-04) — §7.4: a
**before any dict beside a*whose length is known only as the program runs is read atlist.where the list is the first thing thesort *gives, the names merged before the list is taken. It was a parse_error.rev 97 (2026-11-04) — §7.4: a
**before any dict beside a*whose length is known only as the program runs is read at a list’ssortand a dict’supdate. It was refused, and stays a parse_error atlist.where the list is the first thing thesort *gives.rev 96 (2026-11-04) — §6.13:
math.places anisclose aorbgiven by name beside a*at the count the*gives, where it took them by name only in a call with no*. §7.4: 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 and atmath.. It was a parse_error, and stays one at a list’sisclose sort.rev 95 (2026-11-04) — §7.4: a
**before any dict beside a*whose length is known only as the program runs is read atsorted,minandmax, which count the*before they read a name. It was a parse_error, and stays one atmath.and a method that takes names.isclose rev 94 (2026-11-04) — §7.4: a
**before any dict 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,sumanddict, over the count the*gives. It was a parse_error, and stays one atsorted,min,max,math.and a method that takes names.isclose rev 93 (2026-11-04) — §7.4: 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, before the count the*gives. It was a parse_error, and stays one where what is called takes names.rev 92 (2026-11-04) — §7.4: a
**before any dict, such as a name that holds one, at a function ofmath,cmath,vectororstructgives it its names as Python’s does:math.takes its names, and any other function refuses any name. It was a parse_error.isclose rev 91 (2026-11-04) — §7.4: a
**before any dict, such as a name that holds one, at a method that changes a collection gives it its names as Python’s does: a list’ssorttakeskeyandreverse, a dict’supdatetakes every name as a key, and any other change refuses any name before the collection changes. It was a parse_error. A call that no names its mapping could give would let run is refused as the program runs, in the words of what the mapping gives; it was a type_error where the unit is compiled.rev 90 (2026-11-04) — §7.4: a
**before any dict, such as a name that holds one, at a built-in value’s method called as a value gives the method its names as if written:split,rsplit,splitlinesandexpandtabstake theirs as Python’s do, and any other method refuses any name,str.. It was a parse_error.upper() takes no keyword arguments rev 89 (2026-11-04) — §7.4: a
**before any dict, such as a name that holds one, atsorted,minandmaxgives the call its names as if written, counted apart from the positional arguments, andsortedis refused undersort’s name, as Python’s is. Akeythe mapping gives is refused'int' object is not callableonce there is an element to call it on. It was a parse_error.rev 88 (2026-11-04) — §7.4: a
**before any dict, such as a name that holds one, atenumerateandsumgives the call its names as if written, andenumeratereads them where they stand, as Python’s does, so astartgiven alone is'start' is an invalid keyword argument for enumerate(). It was a parse_error. A call that no names its mapping could give would let run is refused where the unit is compiled.rev 87 (2026-11-04) — §7.4: a
**before any dict, such as a name that holds one, atint,round,pow,complex,strandzipgives the call its names as if written, and atdictits keys; a name the callee does not take, too many in all, and any refusal the call written out meets are catchabletype_errors in Python’s words and order, and a call whose type would turn on the names given is refused where the unit is compiled. It was a parse_error.rev 86 (2026-11-04) — §7.4: a
**before any dict, such as a name that holds one, at a builtin that takes no name (len,abs,list,isinstanceand the rest) is read after the positional arguments; a mapping that gives nothing leaves the call as written, and any name it gives is a catchabletype_errorin Python’s words. It was a parse_error.rev 85 (2026-11-04) — §7.3, §7.4, §7.13: §7.3 is titled What a value prints as and states how every value is written, a read’s included: a counted preview such as
2 entries [felix, tom],proof_tree(2), a diagnosis’s own text, an active claim’s value,verify_result match=Trueand<empty>. It said such a value could not be printed directly. §7.13 points to it for each argument’s text. §7.13: a**before any dict, such as a name that holds one, givesprintits keys as keywords when the line is composed, merged and checked as Python does, each fault a catchabletype_error; it was a parse_error, and §7.4 saysprintis the exception.rev 83 (2026-11-03) — §6.14: a wrong number of arguments to a
mathorcmathfunction is a type error in Python’s words,math., and no longer a parse_error.sqrt() takes exactly one argument (2 given) rev 82 (2026-11-03) — §6.3, §7.13: a keyword
print,sortedor a list’ssortdoes not take is a type error in Python’s words,'x' is an invalid keyword argument for print(), named forsort()fromsortedas Python names it.rev 81 (2026-11-03) — §6.13: a name beside a
*of a length known only as the program runs binds as written out at a built-in method, adict’supdateand a list’ssort:"a b c".,split(*ys, maxsplit=1) d.,update(*ys, k=1) xs..sort(*ys, reverse=True) rev 79 (2026-11-03) — §7.4: a
**before a dict display whose keys are all literals atprint, a builtin, a built-in type’s method,sorted, a list’ssortand amathfunction. §7.13:print’sfilemay be givenNone.rev 78 (2026-11-03) — §8.10: a read through a family of classes runs as Python’s does until it reaches a class whose method returns nothing, in a method a subclass inherits and in a script written above the class too. A call whose arguments the method of the instance’s class does not take is refused as it reaches that class, in the words of Python’s
TypeError. A read no class can answer, and a call no class takes, stay refused when the program compiles.rev 77 (2026-11-03) — §6.3:
powwith a modulus given an operand that is not an integer is refused in Python’s words. §6.4, §7.17:xs += con a list takes any collection, asextenddoes. §6.14:math.andfloor math..ceil rev 76 (2026-11-03) — §6.17: a special name a built-in value holds, called by name, answers as the spelling it stands for, and where Python’s answer is one no value here holds, the call is a type_error saying so.
rev 75 (2026-11-03) — §5.8: a fold over
realvalues is exactly rounded where it adds (sumismath.,fsum avgstatistics.,fmean rmsandgeomeanlikewise), andcovarianceis the population figure. §6.3:sumover a list adds left to right, as CPython 3.11’s does. §6.12: an iterator is not a key or a set element, and why.rev 74 (2026-11-03) — §6.6, §7.8: an argument of a type a string’s method does not take, a list’s or a tuple’s
indexbound and a list’sinsertposition are refused in CPython’s sentence, where the argument is of a class CPython has, andjoinnames the first position that holds no string; a shape arm’s alternative that can never match is left out, the arm standing on the others.rev 73 (2026-11-03) — §6, §6.9, §6.10: an operator given operands it does not take is refused in CPython’s sentence, naming CPython’s classes, where CPython refuses the same operands; a set, or a dict’s keys, that would hold a tuple beside a scalar is refused naming the two types, as a list is.
rev 72 (2026-11-03) — §6.7, §6.12, §7.17: a set’s
updateanddifference_update, and a dict’supdate, read an iterator or a generator expression one element at a time, as in Python, so one over the collection being changed is refused at its next read and what was written before stays; a list’sextendover the list itself stops at the length it began with.rev 71 (2026-11-02) — §6.9, §6.10: a union across scalar element types is a
set<value>, as in Python, and a keys or items view takes any collection beside&,|,-and^.rev 70 (2026-11-02) — §8.10: 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 it.
rev 69 (2026-11-02) — §8.11: a
valuebuilt 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 theelse, in a negated test’s branch, in a conditional’s second branch, after the test in anor, and in amatcharm below arms naming classes. A branch novaluecan take is never run and not read, and an operator meeting avalueof known classes runs where it takes each. §6.2a: two scalars that are not both numbers join to avalueholding their classes, and the second branch reads a tested name at what the test failing leaves it. §7.8: an arm below arms naming classes reads the scrutinee at what their tests failing leave it.rev 68 (2026-11-02) — §4.1, §8.11: a name holding a
valueis narrowed by anisinstancetest, in the branch it takes, after it in anand, after a comprehension’sif, in a conditional’s first branch and in the body of amatcharm whose guard makes it. §7.8: amatchover avaluewhose class arms all write Python’s parentheses is Python’s, every arm a test and none required, andcase int(n):and a capture bind the value; a guard narrows its arm’s body. §6.2a: a scalar beside avaluejoins to thevalue.rev 67 (2026-11-02) — §4.1, §6.8: a tuple of scalars, or of class instances, iterates and is indexed at a computed position as a list of its elements is; only one holding a collection beside a value of another type is not. §6.3: so it converts, and
dict(x)reads any collection of pairs asupdatedoes. §6.5: a tuple sliced at a computed bound, and an index of the wrong type told in Python’s words.rev 66 (2026-11-02) — §7.8: a
matchover a value Python has is Python’s: every arm a test and none needed, class patterns at an arm’s head or inside another pattern, and a type that reads itself holding one. §8.6: a class match with()is a test.rev 65 (2026-11-02) — §7.1, §7.12: a script with no
-> typewhose body holds areturn <expr>is a value script of the type itsreturns give. §7.4: the name of adef, a method or a module read with no call is told no value holds it. §8.4, §8.5: so a method’s.rev 64 (2026-11-02) — §7.8, §9.3, §10: a name nothing binds, and a script a held module does not have, are told in Python’s words.
rev 63 (2026-11-02) — §7.4: a call that does not fit is told in Python’s words and order; a call of a value is a
type_errorin Python’s words, and no longer a parse_error. §8.4: a method’s refusal counts its receiver wherever it is told. §8.11: a class tests any value, and every value is anobject. 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. 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 this document’s version, the revision it last changed in and the current revision; from rev 148 each entry opens with the version that revision gave this document. The history starts at rev 1. What a revision promises is stated in the Python Specification, its section 11.1.rev 62 (2026-11-01) — §8.1: a class exists from the line that declares it, and a body or a string in a signature may name one declared below.
rev 61 (2026-11-01) — §7.1: a declaration may call one above or below it, and a module’s own statement reaching a
defbelow it is toldname 'g' is not defined. §8.5: a value method’s body holds any statement, and a method may call one written below it. §8.10: a method call through classes sharing no base runs the instance’s own.rev 60 (2026-11-01) — §6.9: a key no dict could hold is told in Python’s words, written or looked up. §6.12: what stops reading an iterator once the answer is known.
rev 59 (2026-11-01) — §6.13: a
*at a built-in method’s call, and a count a built-in method does not take in Python’s words. §6.10, §7.17: a set’sunion,intersection,differenceandupdatetake any number of collections, withdifference_update,intersection_updateandsymmetric_difference_updatebeside them. §6.7: a generator expression right after a*.rev 58 (2026-11-01) — §6.4a: a format spec’s sign,
z,#and grouping format as Python’s do. §11.4: a method through its type’s name. §6.9: a dict’s|=reads whatupdatereads. §7.4: a**at a script’s call.rev 57 (2026-11-01) — §6.4a, §6.6: a string’s
.format,.format_mapand%format as Python’s do, andtranslatetakes a list, a tuple or a string as its table. §6.3:@and@=read and are refused naming their operands. §6.9, §6.10: a dict’s**,|and|=merge. §7.15:raiseholds several arguments. §11: a type’s own three methods are reached through a value.rev 56 (2026-11-01) — §7.19: a named expression (
:=) reads. §7.13:print’ssepandendare any expression that gives a string, andflushis read. §7.16: a condition ofassertmay begin with a bracket.rev 55 (2026-11-01) — §6.4a, §6.6, §11.4, §12:
s., and what a type itself answers:translate(table) str.,maketrans dict.andfromkeys float.. A string has nofromhex .format_map. Three more words are English on every surface.rev 53 (2026-11-01) — §4.1, §11.2: a
realtakesis_integerandhex, anintor abooltakesbit_lengthandbit_count, and each takesas_integer_ratio. Anintand aboolhave.numeratorand.denominator.rev 51 (2026-11-01) — §6.7: a comprehension’s
iffilters are each asked only for a truth value.rev 50 (2026-10-31) — A
string, aset, adict(its keys) and a dict’s view unpack into a target list.rev 48 (2026-10-31) —
key=takes a method of a built-in type,key=str., over elements of that type.lower rev 47 (2026-10-31) — §12: a name is read as Python reads it, in Normalization Form KC, a keyword is compared as written, a string is not read, and a name that keeps a combining mark may name a local and nothing a store holds.
rev 46 (2026-10-31) — §7.12: a
returnoutside a script is refused where it ended the program. §9.3:from __future__ importis read as Python reads it. §10: what Python’s compiler refuses is answered at its place and in its order.rev 45 (2026-10-31) — §6.13: a keyword argument beside a builtin’s
*binds as Python binds it, where it was refused.rev 43 (2026-10-31) — §6.13: a
*before a builtin’s argument spreads the collection.zip()gives nothing, andformat(value)takes the empty spec.rev 42 (2026-10-31) — §7.1: 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) — §7.1: a parameter list reads a
/and a bare*as Python’s do, and a parameter may follow a variadic. §7.15:raisealone,raise eandfromread as Python’s do. §8.4: a method’s arguments bind as a script’s do.rev 40 (2026-10-31) — §4.1: Python’s type spellings are read, and
-> Nonesays what no annotation says. §6.3: prefix+. §6.4: octal escapes, a backslash at a line’s end, and any other escape keeps its backslash. §7, §7.4: an expression alone is a statement, and so is a builtin called alone, whose value is dropped;...and an annotation with no value do nothing. §9.3:import a, bandas. §10: an import refused past its first word is read on as Python reads one.rev 39 (2026-10-31) — §5, §12: a verb’s word,
match,case,branch,commitandrollbackare names where their form does not stand, and only Python’s keywords andnullare reserved. §6.4, §6.4a: a string opens with either quote, a prefixuorUchanges nothing, an f-string may be triple-quoted, and a field may hold a string in the other quote. §7: a;ends a simple statement, and a block’s simple statements may stand on its header’s line.rev 37 (2026-10-29) — §4.1: each typed value is written as its factory call, and only so. §4.2, §5.1, §5.4, §5.9, §9.5: refusal codes are snake_case,
conflicting_write,not_recorded,already_in_effect,derived_fieldandscope_forbiddenamong them. §6.4: a typed value does not join an adjacent string. §7.8: a typed value in acasereads as a class pattern.rev 36 (2026-10-29) — §7.11, §9.3:
importis a reserved word. §12: every keyword Python reserves is reserved,del,from,importandyieldamong them.rev 35 (2026-10-29) — §6.1:
isis a reserved word. §12: Python’s own keywords are reserved, as they are in Python, bardel,from,importandyield.rev 33 (2026-10-23) — §3: the natural language is set by the
languageparameter alone. §4.1: each value class that has a prefix spelling states it beside the factory call it equals. A name is bound by bare assignment; a plain call runs the body its callee had when the calling program was compiled; a constructed value exists only while the program runs and is never stored. Theknown_at=table’sDerivedFieldcondition names the rule whose text at that moment the store does not keep. The quick card follows the same wording.rev 32 (2026-10-21) — §8.8 says the three absence premises take a pair, and that a pair conclusion yields at every pair under
not_assertedover the same names.rev 31 (2026-10-19) — §9.7 names the Library Reference at its new address.
rev 30 (2026-10-17) — §4.1 says a
realcomputes under all seven arithmetic operators,+ - * / // % **, and acomplexunder+ - * / **, as §6.3 states.rev 29 (2026-10-17) — §9.5 says that removing what a recompile no longer declares needs a
read-write-deletekey, and that under aread-writekey such a compile is refused, names what it would remove and changes nothing.rev 26 (2026-10-17) — The header states Version, Status, Audience and the Markdown copy, one field to a line. Citations of the MCP Reference and the Python Specification name them so and link to the section they cite; §3 links MCP Reference §8.1 for the accepted languages. §5.7 says its table is a selection and points to MCP Reference §2 for every tool. §7.11 cites §9 for modules.
writersandwritten_by(§11.2) name store keys. Passages that had been struck through are removed, and the page states only what holds.rev 21 (2026-10-14) — This section records revisions 13 to 20 as one entry. Nothing else changed.
rev 13 to rev 20 (2026-10-13 to 2026-10-14) — Operators and Modules: new sections and examples, 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.
rev 4 (2026-10-11) — The MIT grant, in the opening section, is spelled licence. Cross-references take the published documents’ short names, the ones the footer uses — Python Tutorial, Python Specification, MCP Reference, Modules Reference. Nothing else changed.
rev 3 (2026-10-11) — The revision is the surface’s: one number shared by every published document, the web pages and the service wire. This section says so, and the Version line moves from rev 1 to it. Nothing else changed.
rev 1 (2026-10-11) — The Version line states the document’s revision and this section is added. Nothing else changed.