DKE Python — Tutorial

Status: draft · Normative status: informative. This tutorial teaches DKE Python by example. Where it and the Language Specification differ, the Specification governs; the Language Reference is the exhaustive descriptive companion. Section pointers like (§7.1) refer to the Specification.

This tutorial assumes you know Python. It does not re-teach def, for, match, or string building — you have those already. It teaches what those familiar shapes do in DKE, the vocabulary that has no Python equivalent, and the handful of places DKE deliberately departs from Python. The syntax is Python’s; the semantics are a knowledge engine’s.

Every complete example below runs exactly as shown: each is a program body that defines a procedure you can import and invoke. The shorter fenced snippets are fragments shown for illustration only.


Chapter 1 — Coming from Python

DKE Python is a small language for recording facts and reading them back — you tell the DKE service to remember a claim, then ask for it later with its provenance and its type intact. It talks to the DKE service at dke.langsyn.net.

The one shift to make up front is where your data lives. A Python program keeps state in variables and objects that vanish when the function returns; a DKE Python program keeps state in the store, as claims. A claim is a value at an addressed cell, stamped with the source that asserted it — and claims persist, carry their own type, remember their history, and can be reasoned over long after your program has finished. Local variables still exist, but they only hold what you are about to write or have just read. The durable state is always the store.

Here is the Python you already know, translated into DKE habits:

In Python you… In DKE Python you…
keep state in variables and objects record claims in the store — persistent, typed, and stamped with a source
mutate a value in place supersede it — the store is append-only, so the prior value stays as history
return a value from a function let data flow through the store; procedures don’t return (class methods do)
loop with while, recursion, or an unbounded for iterate a finite collection with for … in — every program is guaranteed to terminate
lean on truthiness (if items:) give if a real bool
chain a < b < c write a < b and b < c — comparisons don’t chain
divide int / intfloat int / int is integer division; a real operand promotes the result to real
catch a ZeroDivisionError a divide-by-zero refuses — a catchable outcome, not an exception
match on structural patterns match on a result’s kind, a value’s class, or an instance’s class

Keep that table in the corner of your eye; the chapters ahead earn every row. And one capability has no Python row at all: the engine reasons. You can declare facts the engine derives and maintains on its own — recompute-on-change fields, standing rules, and what-if hypotheses. That is where DKE Python stops resembling a scripting language and starts resembling a knowledge engine you program. It is Chapter 8, and it is the reason the language exists.

Your first program

A DKE Python source file is just the program body — there is no header line; it begins with your first declaration or statement. The keywords are English, and that is the only surface offered.

A source has at least one procedure, introduced with def. A procedure records and reads facts and prints lines with log (DKE’s print). Here is a complete first program:

# hello-store.dpy — record one fact, read it straight back.
def hello_store():
    remember(Greeting.hello.text, "world", "op1")
    cur = current(Greeting.hello.text)
    match cur:
        case active_claim:
            log("greeting = " + cur.value + " (per " + cur.source + ")")
        case empty:
            log("nothing recorded yet")

Three things are happening, and only one of them looks like Python:

  • remember(Greeting.hello.text, "world", "op1") records a claim: at the cell named Greeting.hello.text (a kind Greeting, a subject hello, an attribute text), the value "world", attributed to the source "op1". No Python assignment does this — the fact goes into the store, not into a variable.
  • current(Greeting.hello.text) reads back the live claim at that cell.
  • match cur: inspects the result. Reading a cell gives you either an active_claim or empty, and a match must handle both (§6.2). DKE’s match dispatches on a result’s kind — not on Python’s structural patterns — but the case syntax is the one you know. Inside the active_claim arm, cur.value and cur.source are available.

Importing and invoking

A DKE Python program runs at the DKE service in two wire steps (§11). First you import the source — the service typechecks it and stores each procedure under its name (§11.1):

import hello_store from "<source>"

Then you invoke a stored procedure by name (§11.2):

invoke hello_store()

The service runs it and returns a transcript — a header with a verdict tallying what the run did, then the lines the program logged and the facts it recorded, in execution order:

OK script hello_store(): 1 claim written, 1 log
  remember Greeting.hello.text = "world"
  log greeting = world (per op1)

There are no free-standing statements: all work lives inside a def, and a program runs by invoking one. This is the whole shape of the language — the rest of this tutorial fills in the values you can record, the questions you can ask, the reasoning the engine can do on your behalf, and the control flow that ties it all together.


Chapter 2 — Claims, cells, and provenance

Chapter 1 recorded a claim with remember and read it back with current. Those two verbs are the heart of DKE programming, so before anything else, meet the model they work over and the fuller cast around them (§7.1).

Every fact in the store is a claim: a value at a cell, from a source. A cell is addressed by a three-part path K.s.a — a kind K, a subject s, and an attribute a (Greeting.hello.text, Cat.felix.color). You never declare a kind or a subject up front; naming a cell brings it into being. Where a Python object has fields you assign, a DKE subject has attributes you claim — and the difference that matters is the third argument every write carries.

Writing

  • remember(K.s.a, v, src) — record a value from a source. A different value from a different source stands alongside as a co-active alternative; to revise a value from the same source, use update.
  • update(K.s.a, v, src) — revise the value from that source, superseding the prior one (and establishing a value on an empty cell).
  • forget(K.s.a) — drop the claims at a cell (path-scoped: K.s.a or K.s).
  • assert(K.s.a, src) — record an absence — that there is deliberately no value — with no value argument.
  • pin(K.s.a) — mark a cell (a flag you can later enumerate with list pinned); pinning does not protect the cell from forget.

Every write takes a source as its last argument (§7.3): provenance is not optional. A claim always knows where it came from. This is the habit with no Python equivalent — you do not just store "final", you store that op1 said "final" — and it is what makes a store auditable rather than merely stateful.

This example revises a value, marks a cell, removes another, and records a deliberate absence:

# revise-remove-mark.dpy — supersede, pin, forget, and assert absence.
def revise_remove_mark():
    remember(Widget.w1.state, "draft", "op1")
    update(Widget.w1.state, "final", "op1")     # revise (same source supersedes)
    pin(Widget.w1.state)                          # mark the cell
    remember(Widget.w1.note, "temp", "op1")
    forget(Widget.w1.note)                        # drop that cell's claims
    assert(Widget.w1.price, "op1")               # record a deliberate absence
    log("revised, marked, removed, and asserted")

Notice there is no deletion in the Python sense. update supersedes rather than overwrites, and even forget and restore (Chapter 11) leave the ledger intact — the store is append-only, so the prior state survives as history you can walk.

Reading

  • current(K.s.a) gives the live active_claim (or empty).
  • get(K.s.a) gives the full history as a claim_list you can iterate.

Recording twice and reading the history shows how claims accumulate:

# history-walk.dpy — record, supersede, then walk the history.
def history_walk():
    remember(Cat.felix.color, "grey", "op1")
    update(Cat.felix.color, "orange", "op1")     # same source supersedes
    cur = current(Cat.felix.color)
    match cur:
        case active_claim:
            log("current colour = " + cur.value + " (per " + cur.source + ")")
        case empty:
            log("no colour")
    hist = get(Cat.felix.color)
    log("history depth = " + hist.length)

Fields on a claim

An active_claim exposes typed fields (§5.2): .value (the typed value), .source (who recorded it), .created_at (when, as an RFC 3339 UTC string), and .trust. A claim_list and any collection answer .length, and any value or discriminator answers .kind. Only ever read .value/.source on a claim you know is active — inside a case active_claim: arm, or right after a write. This is why reads come back as a match-able result rather than a bare value: there is no None to trip over, because empty is a case you are made to handle.


Chapter 3 — Typed values

A recorded value carries its own type, and you get that exact type back when you read it (§5.6). DKE Python has eight value classes: you can write seven of them, and null you can only detect on read.

Writing typed values

The write literals are:

  • string"orange", with the escapes \\ \" \n \t \r \xNN \uNNNN (§3.5.1).
  • int42, and also 0xDE_AD, 0o17, 0b1010, 1_000_000 (§3.5.2).
  • boolTrue or False (§3.5.3).
  • datetimedatetime("2026-01-02T03:04:05Z"), an RFC 3339 UTC instant; the Z is required and 1–9 fractional-second digits are allowed (§3.5.5).
  • durationduration("PT1H30M"), an ISO 8601 span (P[nD][T[nH][nM][nS]], no calendar years or months) (§3.5.6).
  • real3.5, a fractional number (a digit on each side of the point; _ separators allowed, 3 alone stays an int) (§3.5.7).
  • blobblob("48656C6C6F"), an opaque byte string as an even count of hex digits — or the same bytes as base64, blob_b64("SGVsbG8=") (both read back as canonical hex) (§3.5.8).

The type is chosen by the literal you write — 42 records an int, "42" records a string, and the two never collide on read:

# values-write.dpy — three cells, three different value classes.
def values_write():
    remember(Widget.w1.size, 42, "op1")          # int
    remember(Widget.w1.ready, True, "op1")        # bool
    remember(Widget.w1.label, "42", "op1")        # string (not the int 42)
    n = current(Widget.w1.size)
    match n:
        case active_claim:
            log("size = " + n.value)
        case empty:
            log("no size")

datetime and duration read back in a canonical form — duration("PT90M") comes back as PT1H30M, and a datetime keeps its instant:

# datetime-round-trip.dpy — a datetime value keeps its type across write/read.
def datetime_round_trip():
    remember(Event.launch.at, datetime("2026-01-02T03:04:05Z"), "ops")
    cur = current(Event.launch.at)
    match cur.value:
        case datetime as d:
            log("launch at " + d)
        case default:
            log("not a datetime")
# duration-round-trip.dpy — a duration keeps its type and reads back canonical.
def duration_round_trip():
    remember(Task.build.budget, duration("PT90M"), "ops")
    cur = current(Task.build.budget)
    match cur.value:
        case duration as u:
            log("budget = " + u)               # prints PT1H30M
        case default:
            log("not a duration")

real and blob round-trip the same way. A real reads back in a canonical decimal form (3.50 comes back as 3.5), and a blob reads back as canonical uppercase hex:

# real-blob-round-trip.dpy — a real and a blob keep their types across write/read.
def real_blob_round_trip():
    remember(Sensor.a.reading, 3.5, "ops")
    remember(Doc.d1.payload, blob("48656C6C6F"), "ops")
    r = current(Sensor.a.reading)
    match r.value:
        case real as rv:
            log("reading = " + rv)             # prints 3.5
        case default:
            log("not a real")
    b = current(Doc.d1.payload)
    match b.value:
        case blob as bv:
            log("payload = " + bv)             # prints 48656C6C6F
        case default:
            log("not a blob")

The value union

When you read a claim’s .value, its type is one of eight classes (§5.6):

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

You discriminate them with a match over .value. The seven writable classes can bind the typed value with as; the one detect-only class, null, is matched bare — it has no literal and you never write it (an absent value is asserted with the assert verb, Chapter 2), but you can still recognise it on read:

# value-classes.dpy — every value class discriminated on read.
def value_classes():
    remember(Sensor.room.count, 41, "op1")
    c = current(Sensor.room.count)
    match c.value:
        case int as n:      log("next = " + (n + 1))     # n : int, arithmetic OK
        case bool as b:     log("flag = " + b)
        case string as s:   log("text = " + s)
        case datetime as d: log("time = " + d)
        case duration as u: log("span = " + u)
        case real as r:     log("half = " + (r / 2))      # r : real, arithmetic OK
        case blob as b2:    log("bytes = " + b2)          # b2 : blob
        case null:          log("an absent value")        # detect-only, no binding

A match over .value must either name all eight classes or end with a case default: (§6.2). Because the recorded value here is an int, the int arm runs — and because every arm is type-checked, case int as n: can safely do integer arithmetic on n. Numbers compute with + - * / and the ordered comparisons < > <= >=, over int and real alike. Two rules differ from Python and are worth pinning now: int / int is integer division (not Python 3’s float — a real operand promotes the result to real, so r / 2 above is a real), and % is integer-only. And where Python raises, DKE refuses: dividing by zero, or a result out of range, is a catchable refuse (§8.3) rather than a garbage number or an exception.

The type system at a glance

The value classes above are what you store. They are part of a larger, closed type vocabulary — 17 types in all (§5.1), with no subtyping and no user-defined types. Most you never write down; you meet them as the shapes verbs hand back. They fall into a few families:

Family Types You meet them as
Primitives string, int, bool literals, .length, comparison results
Value value, datetime, duration, real, blob a claim’s .value union (§5.6) and its four named value-class types (null is a further class of the union, not a standalone type)
Discriminators active_claim (with its empty case), verify_result results of current and verify — destructured with match
Finite collections claim_list, proof_tree, subject_set, string_list, result_type_set results of get, why, subjects, and the list_* verbs — iterated with for … in
Opaque token handle the result of checkpoint

Two accessors work across every family: .length gives an int for any string or collection, and .kind gives the type’s name as a string for any discriminator or a proof_tree. You meet each of these types as the tutorial goes — the collections in Chapters 5 and 11, the discriminators throughout, and the handle in Chapter 11.


Chapter 4 — Recording and reading across time

The claims in Chapter 2 were all about now. But knowledge in DKE is not only current — it is dated. A write may carry a validity window: a from- and a to-datetime bracketing the span the value was true (§7.1). A read may carry an as-of datetime, asking for the value the store held at that moment rather than now. Together they let one cell hold a value that changed over time, and let you read it at any past instant — a bitemporal history you get for free, without bookkeeping of your own.

Two non-overlapping windows model a rate that changed, and two as-of reads select the value in force at each date. A single source may not assert two current values at one cell, so each window is recorded from its own source:

# reading-across-time.dpy — a value that changed over time, read as of a past date.
def reading_across_time():
    remember(Rate.usd.pct, "3.0", "y2019", datetime("2019-01-01T00:00:00Z"), datetime("2021-01-01T00:00:00Z"))
    remember(Rate.usd.pct, "5.0", "y2021", datetime("2021-01-01T00:00:00Z"), datetime("2023-01-01T00:00:00Z"))
    early = current(Rate.usd.pct, datetime("2020-06-01T00:00:00Z"))
    log("rate as of 2020 = " + early.value)       # 3.0 — inside the first window
    later = current(Rate.usd.pct, datetime("2022-06-01T00:00:00Z"))
    log("rate as of 2022 = " + later.value)        # 5.0 — inside the second

The write signature is remember(K.s.a, v, src, t"from", t"to") and the timed read is current(K.s.a, t"as-of"); both time arguments are optional — omit the window to record a value with no stated end, and omit the as-of time to read the present. get takes an as-of time the same way. A datetime is an RFC 3339 UTC instant with the required Z suffix (§3.5.5).


Chapter 5 — Asking questions

Beyond reading a single cell, you can ask the store questions (§7.1–7.2).

verify checks whether a cell holds a particular value and gives you a verify_result with a .match boolean:

# confirm-value.dpy — verify a cell against an expected value.
def confirm_value():
    remember(Cat.felix.color, "orange", "op1")
    v = verify(Cat.felix.color, "orange")
    if v.match:
        log("confirmed: colour is orange")
    else:
        log("mismatch: expected orange, got " + v.actual)

subjects finds every subject of a kind whose attribute holds a given value, and returns a subject_set you can size or iterate. It is type-discriminating (§7.2) — searching for the int 42 never matches the string "42":

# find-subjects.dpy — which cats are orange?
def find_subjects():
    remember(Cat.felix.color, "orange", "op1")
    remember(Cat.tom.color, "orange", "op1")
    remember(Cat.mittens.color, "black", "op1")
    orange_cats = subjects(Cat.color, "orange")
    log("orange cats = " + orange_cats.length)

The list_* family enumerates structure — attributes of a kind, subjects, values at an attribute, and more:

# list-structure.dpy — enumerate what a kind knows.
def list_structure():
    remember(Cat.felix.color, "orange", "op1")
    remember(Cat.felix.age, 3, "op1")
    attrs = list_attributes(Cat)
    log("Cat has " + attrs.length + " attribute(s)")

These list_* heads (list_attributes, list_values, list_subjects, …) are soft keywords — recognised as verbs only right before (, so the same words are free to be ordinary identifiers elsewhere (§7.1). The full enumeration family walks a kind’s subjects, values, and categories:

# enumerate-store.dpy — walk the store's structure.
def enumerate_store():
    remember(Cat.felix.color, "orange", "op1")
    remember(Cat.tom.color, "grey", "op1")
    subs = list_subjects(Cat.color)     # subject_set — subjects with a colour
    vals = list_values(Cat.color)       # claim_list — the values recorded there
    cats = list_categories()            # string_list — the kinds in the store
    log("subjects = " + subs.length + ", values = " + vals.length + ", kinds = " + cats.length)

Two verbs inspect disagreement. caveats(K.s) returns the cautionary claims on a subject, and conflicts() returns the claims that stand in disagreement — the co-active alternatives from different sources you met in Chapter 2:

# inspect-disagreement.dpy — caveats on a subject, conflicts across the store.
def inspect_disagreement():
    remember(Cat.felix.color, "orange", "op1")
    remember(Cat.felix.color, "grey", "op2")   # a second source disagrees
    cv = caveats(Cat.felix)
    cf = conflicts()
    log("caveats = " + cv.length + ", conflicting claims = " + cf.length)

The store also keeps a small registry you can read back — the checkpoints and pins you have set, the result types the language can produce, and the names of the scripts you have stored:

# read-registry.dpy — enumerate checkpoints, pins, result types, and scripts.
def helper():
    remember(Cat.felix.color, "orange", "op1")

def read_registry():
    remember(Cat.felix.color, "orange", "op1")
    pin(Cat.felix.color)
    checkpoint("cp_one")
    cps  = list_checkpoints()       # string_list — live checkpoint names
    pins = list_pinned()            # string_list — pinned cell paths
    rts  = list_result_types()      # result_type_set — the types verbs can return
    scr  = list_scripts()           # string_list — stored script names
    log("checkpoints = " + cps.length + ", pinned = " + pins.length)
    log("result types = " + rts.length + ", scripts = " + scr.length)

Summary statistics over a column

Beyond reading single cells, you can summarise a whole column — the values of one attribute across every subject of a kind. An aggregate query folds a column into one summary value (§7.1.1). It is read-only: it never changes the store.

# summary-stats.dpy — read-only summary statistics over a column.
def summary_stats():
    remember(Reading.r1.value, 10, "sensor")
    remember(Reading.r2.value, 20, "sensor")
    remember(Reading.r3.value, 30, "sensor")
    n = count(Reading.value)
    mean = avg(Reading.value)
    hi = max(Reading.value)
    log("readings = " + n + ", mean = " + mean + ", max = " + hi)   # 3, 20, 30

The single-column folds include count, sum, avg, min, max, median, variance, and stdev; two-column folds relate two attributes of one kind — slope(Point.x, Point.y), correlation(Point.x, Point.y). Each returns a value whose class follows the column (a numeric column yields a number), so bind it and render or match it.

Two rules matter. First, bind an aggregate to a name on its own line, then use that name — an aggregate may not be written as a bare argument to another call (n = count(Reading.value) then log("readings = " + n), never log("readings = " + count(Reading.value))). Second, a numeric fold refuses rather than guess (§10.3): an aggregate over a column with no data refuses, and so does a numeric fold (sum, avg, min, max, stdev, …) over a column whose values are not numbers — it declines rather than treating a non-number as zero. Aggregate over numeric data you know exists, or guard the call. (count counts any class.) The fold names are soft keywords, recognised as an aggregate only right before (, so count, sum, and the rest stay ordinary identifiers elsewhere.

For reference, here is the full verb surface (§7.1) — each verb’s result type and where it appears in this tutorial:

Verb Result Chapter
remember / update / forget / assert / pin (void) 1, 2
current active_claim 1
get claim_list 2
verify verify_result 5
subjects / list_subjects subject_set 5
list_attributes / list_categories / list_checkpoints / list_pinned / list_scripts string_list 5
list_values / caveats / conflicts / dependents / diff claim_list 5, 11
list_result_types result_type_set 5
checkpoint handle 11
restore (void) 11
why proof_tree 12

Chapter 6 — Procedures

Every program so far has been a single def. A source file may declare several, and this is how you structure a program — not with imports and modules, but with procedures that compile in order and share their work through the store.

Procedures compile in declaration order, and a later one may call an earlier one — this is compile-before-call (§6.8). A call is expanded inline at compile time, so there is no recursion and calls never form a cycle (§9.7): the same termination guarantee that governs loops governs calls.

In-language, you call a procedure as a plain callname(args), with no invoke keyword (that spelling belongs to the wire surface, §11.2):

# compose-scripts.dpy — two procedures; the second calls the first.
def seed_colour():
    remember(Cat.felix.color, "orange", "op1")

def audit_colour():
    seed_colour()                        # calls the earlier declaration
    cur = current(Cat.felix.color)
    match cur:
        case active_claim:
            log("audited colour = " + cur.value)
        case empty:
            log("no colour to audit")

When a file declares more than one procedure, invoking it runs the last one (here audit_colour), which is the natural entry point. Here is the departure a Python programmer feels most: procedures share their work through the store, not through return values. seed_colour records a claim; audit_colour reads it back. There is no value-returning procedure call — data flows through the store (§8.8). (Class methods, in Chapter 7, are the one exception: they do return values.)

Procedures may also take typed parameters (def name(room: string):), supplied positionally when invoked. Parameter types are string, int, or bool.


Chapter 7 — Classes

A class groups typed fields and the methods that operate on them (§14). Here is the mapping to keep in mind: class instances are subjects, and reading or writing a field is reading or writing a claim — so a class is a convenient, Python-shaped surface over the very same store you have used all along. Nothing new happens underneath; the cells just get names organised by class.

A class declaration takes no marker — just class Name: with typed fields and def methods. A void method (def m(self, …):) is called as a statement; a value method (def m(self, …) -> T: ending in return) is called in an expression:

# classes.dpy — a class with a typed field, a void method, and a value
# method; a def binds an instance and calls both.
class Sensor:
    temp: string
    def record(self, v: string, src: string):
        remember(self.temp, v, src)    # field write
    def label(self) -> string:
        return "sensor"                # value method

def demo():
    s = Sensor("room-a")               # instance binding
    s.record("21", "op1")              # void method call
    name = s.label()                   # value method call
    reading = s.temp                   # field read
    match reading:
        case active_claim:
            log(name + " = " + reading.value + " per " + reading.source)
        case empty:
            log(name + ": no reading")

Note the field write remember(self.temp, v, src) — a value and its source, exactly like remember. Provenance does not disappear because you wrapped the cell in a class. A never-written field reads back as empty.

You can dispatch on an instance’s class with a class match (§14.5). It is resolved from the instance’s declared class at compile time, and must cover that class or provide case default::

# class-match.dpy — dispatch on an instance's class.
class Sensor:
    temp: string
class Actuator:
    state: string

def configure():
    s = Sensor("room-a")
    match s:
        case Sensor:
            remember(s.temp, "21", "op1")
            log("configured a sensor")
        case Actuator:
            remember(s.state, "on", "op1")
            log("configured an actuator")

A class field can also be computed — derived and kept current by the engine rather than written by you. That is the first form of the engine’s reasoning, and it opens the next chapter.


Chapter 8 — The engine reasons

This is the chapter with no Python analogue. Until now you have recorded facts and read them back. Now you declare facts the engine derives and maintains for you — you state a relationship once, and the engine keeps the conclusion current as its inputs change, on its own, forever after. It comes in three escalating forms: a field derived within one class, a named rule that ranges over the whole store, and a hypothesis that reasons without committing anything.

Computed fields — derivation within a class

A class field may be computed: declared with an initializer the engine derives from the class’s other fields and keeps current as they change (§14.7). You write only the inputs; the derived field is maintained automatically and reads back with a derived provenance in place of a source:

# computed-fields.dpy — fields the engine derives and maintains.
class Invoice:
    rate: int
    hours: int
    total: int = rate * hours        # a computed value
    large: bool = total > 1000        # a computed flag, built on the one above

def computed_fields():
    remember(Invoice.i1.rate, 150, "billing")
    remember(Invoice.i1.hours, 8, "billing")
    t = current(Invoice.i1.total)
    log("total = " + t.value)                    # 1200 (derived: rate × hours)
    big = current(Invoice.i1.large)
    log("large = " + big.value)                  # true  (derived: total > 1000)

An int field is an arithmetic expression (+ - * %) over earlier fields; a real field may also use /; a bool field is one ordered comparison of an earlier field. Every reference points at a field declared earlier, so a class’s computed fields never form a cycle — and writing an input re-derives every field built on it. You will see the derivation itself in Chapter 12, where why over a computed field lists the inputs it was built from.

Standing rules — derivation across the store

A computed field derives one field of one class from that class’s own fields. A standing rule is the same idea made standalone and named: it states a condition over the stored data and a conclusion to record whenever that condition holds. Once defined, the engine keeps the conclusion current on its own — recording it whenever the condition becomes true, and withdrawing it when the condition no longer holds (§15).

A rule is a def marked with the @rule decorator, named like any script, then a for clause naming the rows it ranges over, an if clause of one or more premises joined by and, and an indented conclusion. A premise is either a comparison of a field against a number (o.total > 1000) or an absenceabsent(o.shipment), which holds when the row has no value recorded for that field:

# standing-rule.dpy — a named rule the engine applies and withdraws on its own.
@rule
def needs_review():
    for o in Order:
        if o.total > 1000 and absent(o.shipment):
            o.review = True

def standing_rule():
    remember(Order.o1.total, 1500, "sales")
    r = current(Order.o1.review)
    log("review before shipment = " + r.value)       # true — the rule fired
    remember(Order.o1.shipment, "sent", "logistics")
    after = current(Order.o1.review)
    match after:
        case active_claim:
            log("review after shipment = " + after.value)
        case empty:
            log("review withdrawn after shipment")     # this arm runs

Invoking it shows the rule firing, then withdrawing its own conclusion once the absent(o.shipment) premise stops holding:

OK script standing_rule(): 2 claims written, 2 logs
  remember Order.o1.total = "1500"
  log review before shipment = true
  remember Order.o1.shipment = "sent"
  log review withdrawn after shipment

The derived review field reads back like any other field, with a derived provenance; its value always reflects the rules currently in force. The for/if here look like the control flow of Chapter 9, but read them declaratively: a rule does not loop, it holds. Two things keep a rule usable, each checked when you define it (§15.5): a rule must name which rows it applies to — at least one comparison, not an absence alone — and its outcome must settle, so a rule set that would cancel itself out is rejected. A rule ranges over finitely many rows and its premises are tested, not iterated, so the termination guarantee of Chapter 13 still holds.

Hypotheses — reasoning without committing

A hypothesis reads what a value would be under a supposed fact, changing nothing (§16). suppose(<field>, <value>, <field>) sets the first field, lets your rules and computed fields run as if it were true, reads the second field, and then discards the supposition — the store is left exactly as it was:

# what-if.dpy — read a value under a supposed fact, committing nothing.
@rule
def needs_review():
    for o in Order:
        if o.total > 1000:
            o.review = True

def what_if():
    would = suppose(Order.o1.total, 2000, Order.o1.review)
    log("if total were 2000, review would be = " + would)   # true
    after = current(Order.o1.review)
    match after:
        case active_claim:
            log("persisted?! " + after.value)
        case empty:
            log("nothing persisted")                          # this arm runs
OK script what_if(): 2 logs
  log if total were 2000, review would be = true
  log nothing persisted

The result is a value — bind it and read it, or concatenate it into a log (it renders true here). A hypothesis supposes exactly one fact; if the read field has no value under the supposition, suppose produces no value — a catchable refuse (guard it with try / except, Chapter 9, or read a field a rule is known to derive). Because nothing it touches persists, a hypothesis is safe for preview, comparison, and what-if exploration.


Chapter 9 — Control flow as constraints

You have been using DKE Python’s control flow all along — match since Chapter 1, if since Chapter 5, for … in inside rules in Chapter 8. It is the control flow you already know from Python: if/else, for … in, match, and try/except (§9.2). What earns a chapter is not what these constructs do — you have that — but what DKE deliberately takes away.

There is no while, no recursion, and no unbounded loop; for … in ranges only over a finite collection the store hands back, never an open-ended counter. That is not a limitation to work around — it is the mechanism by which every DKE Python program is guaranteed to terminate (§9.5), bought by refusing the shapes that could loop forever. The rest of this chapter is the specific guardrails.

if / else

The condition must be a bool — DKE does not use Python’s truthiness. An if over a collection or a claim is a type error; write the comparison out (if items.length > 0:). You saw a real one in if v.match: in Chapter 5.

for … in

You may only iterate a finite collection — a claim_list, string_list, subject_set, result_type_set, or a proof_tree (§6.3). The collection is snapshotted when the loop begins:

# iterate-history.dpy — walk every claim in a cell's history.
def iterate_history():
    remember(Cat.felix.color, "grey", "op1")
    remember(Cat.felix.color, "orange", "op2")     # co-active alternative
    hist = get(Cat.felix.color)
    log("walking " + hist.length + " claim(s)")
    for c in hist:
        log("  recorded by " + c.source)

match

match dispatches three ways — over a read result (active_claim vs empty, as in Chapter 1), over a .value’s class (Chapter 3), and over an instance’s class (Chapter 7). It never dispatches on structural shape, as Python’s does. Each match must be exhaustive, or end in case default: (§6.2).

try / except

A recoverable failure is a refuse (§9.3); a fault from the service layer is an engine_error (§9.4). You catch them in that fixed order, binding the details:

# guard-try.dpy — catch a refuse and an engine error.
def guard_try():
    try:
        remember(Cat.felix.color, "orange", "op1")
        log("write succeeded")
    except refuse as r:
        log("refused: " + r.reason)
    except engine_error as e:
        log("engine error: " + e.reason)

A last departure to remember: comparisons do not chain (§4.6). Write a < b and b < c, never a < b < c.


Chapter 10 — Transactions

Ordinary invocation is not transactional: if a later write fails, earlier writes still stand (§7.5). When you need all-or-nothing, open a branch (§9.2.5):

# try-paint.dpy — an experimental write, committed only if it holds.
def try_paint():
    with branch("experiment"):
        try:
            remember(Cat.felix.color, "tigret", "audit")
            after = current(Cat.felix.color)
            if after.value == "tigret":
                commit
            else:
                rollback
        except refuse as r:
            log("paint refused: " + r.reason)
            rollback

A branch runs its suite and commits on normal completion; an uncaught failure rolls it back and re-raises. Inside a branch, commit and rollback finish it explicitly (§9.2.6). Branches do not nest, and only the store is rolled back — your variable bindings are never undone (§6.1). This is the one place the append-only store behaves transactionally: a rolled-back branch leaves no trace, where an ordinary sequence of writes leaves each write as history.


Chapter 11 — Snapshots and change tracking

A checkpoint captures a named restore point and hands you a handle — the opaque token type from Chapter 3. Its name must be identifier-shaped (letters, digits, underscores; no hyphens). With a handle you can ask what has changed since that point and what depends on it:

# track-changes.dpy — checkpoint a point, then see what changed since.
def track_changes():
    remember(Cat.felix.color, "orange", "op1")
    h = checkpoint("pre_edit")              # a handle — a named restore point
    log("checkpoint set: " + h.name)
    update(Cat.felix.color, "grey", "op1")   # a later edit
    changed = diff(Cat.felix.color, h)       # claims at this cell changed since h
    deps = dependents(h)                     # claims that depend on the checkpoint
    log("changed since checkpoint = " + changed.length)
    log("dependents = " + deps.length)
  • checkpoint("<name>")handle — captures the restore point; .name reads its name back.
  • diff(K.s.a, h)claim_list — the claims at a cell that changed since h.
  • dependents(h)claim_list — the claims that depend on the checkpoint’s state.

The companion verb restore(<handle>) reverts the store to the point the checkpoint captured — every cell edited after the checkpoint returns to its checkpoint-era value:

# restore-demo.dpy — edit past a checkpoint, then roll the view back.
def restore_demo():
    remember(Cat.felix.color, "orange", "op1")
    h = checkpoint("pre_edit")
    update(Cat.felix.color, "grey", "op1")       # a later edit
    before = current(Cat.felix.color)
    match before:
        case active_claim:
            log("before restore = " + before.value)   # grey
        case empty:
            log("no color")
    restore(h)                                    # roll back to the checkpoint
    after = current(Cat.felix.color)
    match after:
        case active_claim:
            log("after restore  = " + after.value)     # orange
        case empty:
            log("no color")

Invoking restore_demo() shows the round-trip — after restore reads back orange, the checkpoint-era value the reversion restored:

OK script restore_demo(): 2 claims written, 2 logs
  remember Cat.felix.color = "orange"
  update Cat.felix.color = "grey"
  log before restore = grey
  log after restore  = orange

restore reverts the current view, not the ledger: the store is append-only, so it records the reversion as new claims rather than erasing history. current returns to "orange", while get(Cat.felix.color) — the full history — keeps growing, because the round-trip (orangegreyorange) is preserved as an audit trail. Restoring an unknown handle is refused. Full behaviour is in the Language Reference.


Chapter 12 — Provenance with why

Every claim knows where it came from, and why shows you the whole derivation that produced a value (§7.1). It returns a proof_tree — a finite, iterable collection of the claims that participated, with .length, indexing, for … in, and a .root (the claim you asked about):

# show-why.dpy — inspect the provenance of a recorded value.
def show_why():
    remember(Cat.felix.color, "orange", "op1")
    pt = why(Cat.felix.color)
    log("proof has " + pt.length + " claim(s)")
    r = pt.root
    log("root recorded per " + r.source)
    for c in pt:
        log("  participating claim per " + c.source)

A proof_tree (like any collection) must not be handed straight to log (§8.7) — read a claim’s string field, or build a string with +, as above.

When the value you ask about is derived — a computed field or a fact a standing rule recorded (Chapter 8, the reasoner) — why shows the claims it was built from, not just the value itself. This is where the engine’s reasoning becomes auditable: a derived fact carries the inputs that justify it:

# why-derived.dpy — why over a derived value lists the claims it was built from.
class Order:
    price: int
    qty: int
    total: int = price * qty

def why_derived():
    remember(Order.o1.price, 300, "sales")
    remember(Order.o1.qty, 5, "sales")
    pt = why(Order.o1.total)                       # total is derived: price × qty
    log("proof has " + pt.length + " claim(s)")     # 3 — the value and its two inputs
    root = pt.root
    log("derived value = " + root.value)            # 1500

Chapter 13 — Errors and guarantees

Four things can go wrong, each with a distinct class (§10):

  • parse_error — the source is not well-formed (a syntax slip). Caught at compile time.
  • type_error — the source parses but is not well-typed (a non-bool if condition, a chained comparison, an as binding on a detect-only value class). Also caught at compile time.
  • refuse — a runtime request the service declines (for example rollback outside a branch, an aggregate over a column with no data, or a hypothesis whose supposed read has no value). Refuses are control flow — you catch them with except refuse (§9.3). Where Python would raise, DKE hands you a refuse.
  • engine_error — a fault from the service layer, caught with except engine_error (§9.4).

Because a refuse is ordinary control flow, you can turn a declined request into a logged line rather than a crash:

# catch-refuse.dpy — a refuse is control flow, not a crash.
def catch_refuse():
    try:
        rollback                        # refuses: there is no branch here
        log("this line is skipped")
    except refuse as r:
        log("caught a refuse: " + r.reason)

Two guarantees hold for every DKE Python program (§9.5–9.6), and they are the deepest departure from Python — not a feature you invoke but a property you cannot opt out of:

  • Termination. With no recursion and only bounded iteration over finite collections, every program finishes. There is no infinite loop to write.
  • Determinism. The same program over the same store produces the same result shape every time.

Python gives you the freedom to loop forever and to depend on run-to-run variation; DKE trades both away on purpose, and gets provable termination and reproducibility in return.


Chapter 14 — Where to go next

The language teaches itself, too. Any source can carry a single option-verb line to print a lesson — bare --help for an index, or --help <topic> for a specific area:

--help types

From here:

  • The Language Specification is the normative contract — the full grammar (Appendix A), the type system (§5), the verb surface (§7), the error model (§10), and the lifecycle (§11).
  • The Language Reference is the descriptive companion, organised by feature.
  • Appendix C of the Specification collects more self-contained sample programs.

Everything you record is a claim, at a cell, with a source and a type you get back unchanged; the engine will reason over those claims on your behalf; and why will always show you how a value came to be. That — not the Python-shaped syntax — is the whole of DKE Python.

DKE Studio Run any example against the engine

DKE Python

Result

Run an example to see the engine’s answer and its proof.