Skip to content

Facts and rows

Example data

The examples use this data and the names todo, today and tasks.

import datetime as dt
from enum import StrEnum
from string.templatelib import Template

from scriptt.world import Fact, Goal, Key, Log, Ref, Row, World


class TodoList(Fact):
    name: Key[str]


class Status(StrEnum):
    OPEN = "open"
    DONE = "done"


class Task(Fact):
    id: Key[str]
    list: Ref[TodoList]
    title: str
    status: Status = Status.OPEN
    due: dt.date | None = None

    def __describe__(self) -> Template:  # how say() names a task
        return t"task {self.id}"


class Note(Fact):
    task: Key[Ref[Task]]
    text: str


todo = World.new(TodoList, Task, Note)
for name in ["home", "work"]:
    todo.add(
        TodoList(name=name),
        source="me",
        true_from="2 Nov 2026",
        known="2 Nov 2026 09:00",
    )
for key, home, title, due, true_from, known in [
    ("t1", "home", "Buy milk", dt.date(2026, 11, 3), "2 Nov 2026", "2 Nov 2026 09:00"),
    (
        "t2",
        "work",
        "Send the report",
        dt.date(2026, 11, 4),
        "2 Nov 2026",
        "2 Nov 2026 09:00",
    ),
    (
        "t3",
        "work",
        "Book the room",
        dt.date(2026, 11, 6),
        "2 Nov 2026",
        "2 Nov 2026 09:00",
    ),
    ("t4", "home", "Call the plumber", None, "2 Nov 2026", "2 Nov 2026 09:00"),
    (
        "t5",
        "home",
        "Water the plants",
        dt.date(2026, 11, 4),
        "2 Nov 2026",
        "2 Nov 2026 09:00",
    ),
    (
        "t6",
        "home",
        "Pay the rent",
        dt.date(2026, 11, 5),
        "3 Nov 2026",
        "3 Nov 2026 12:00",
    ),
]:
    todo.add(
        Task(id=key, list=home, title=title, due=due),
        source="me",
        true_from=true_from,
        known=known,
    )
for task, text in [("t1", "Oat milk"), ("t2", "Use the new template")]:
    todo.add(
        Note(task=task, text=text),
        source="me",
        true_from="2 Nov 2026",
        known="2 Nov 2026 09:00",
    )
milk = todo.on("2 Nov 2026").one(Task, id="t1")
todo.add(
    milk.but(status=Status.DONE),
    source="me",
    true_from="3 Nov 2026",
    known="3 Nov 2026 18:30",
)

today = todo.on("4 Nov 2026")
tasks = today.facts(Task)

Facts

Facts(items: Iterable[T] = ())

An ordered collection of facts or of rows. It has dplyr-style verbs. Get one from World.facts.

Each verb gives a new collection and leaves this one as it was. You can also do these things with a Facts.

  • Iterate it.
  • Take its len.
  • Index it with a number.
  • Slice it.
  • Compare two with ==.

Printing a Facts shows table().

Parameters:

Name Type Description Default
items Iterable[T]

The elements in order. The default is an empty collection.

()
Example
from scriptt.world import Facts

tasks.count()  # -> 6
Facts([3, 1, 2]).arrange(lambda n: n).first()  # -> 1
tasks[0].list in {"home", "work"}  # -> True

__add__

__add__(other: Facts[T]) -> Facts[T]

Join two collections. The result has the elements of this one and then the elements of the other.

Parameters:

Name Type Description Default
other Facts[T]

The collection to add after this one.

required

Returns:

Type Description
Facts[T]

A new collection. Neither input changes.

Example
both = tasks.where(list="home") + tasks.where(list="work")
both.count()  # -> 6

where

where(
    rule: Callable[[T], object] | None = None,
    /,
    **equal: Any,
) -> Facts[T]

Keep the elements for which rule is true and every field=value is true.

An element whose rule uses a missing value (None in t.due > cutoff) is not kept. This is as in dplyr's filter. Any other error in the rule is raised.

Parameters:

Name Type Description Default
rule Callable[[T], object] | None

A function that takes 1 element and gives a true or false value. The default None means no rule.

None
**equal Any

Fields that must equal a value. An example is status=Status.OPEN. The default is none.

{}

Returns:

Type Description
Facts[T]

A new collection with the elements that agree. They keep their order.

Example
open_tasks = tasks.where(status=Status.OPEN)
open_tasks.where(lambda t: t.list == "home").count()  # -> 3

where_known

where_known(
    value: Callable[[T], V | None],
    rule: Callable[[V], object] = lambda v: True,
) -> Facts[T]

Keep the elements whose value is not None and agrees with rule.

The rule gets the value with None taken out. So a type checker accepts it.

Parameters:

Name Type Description Default
value Callable[[T], V | None]

A function that takes 1 element and gives a value or None.

required
rule Callable[[V], object]

A function that takes that value and gives a true or false value. The default keeps every element that has a value.

lambda v: True

Returns:

Type Description
Facts[T]

A new collection with the elements that agree. They keep their order.

Example
tasks.where_known(lambda t: t.due, lambda due: due.day > 4).count()
# -> 2

arrange

arrange(
    by: Callable[[T], Any], *, descending: bool = False
) -> Facts[T]

Put the elements in order of a value. Elements whose value is None come last. This is as in dplyr.

Parameters:

Name Type Description Default
by Callable[[T], Any]

A function that takes 1 element and gives the value to sort by.

required
descending bool

Whether the largest value comes first. The default False means the smallest value comes first.

False

Returns:

Type Description
Facts[T]

A new collection in the new order.

Example
tasks.arrange(lambda t: t.id, descending=True)[0].title  # -> 'Pay the rent'

first

first() -> T | None

Give the first element. It is None if the collection is empty.

Returns:

Type Description
T | None

The first element or None.

Example
tasks.arrange(lambda t: t.title).first().title  # -> 'Book the room'
tasks.where(list="school").first()  # -> None

one

one() -> T

Give the only element of the collection.

Returns:

Type Description
T

The element.

Raises:

Type Description
LookupError

The collection has no element or has 2 or more.

Example
tasks.where(title="Buy milk").one().list  # -> 'home'

mutate

mutate(**new: Callable[[T], Any]) -> Facts[Row]

Add fields that you compute from each element. Each element becomes a Row with its old fields and the new ones.

A new field with the name of an old field replaces it. This is as in dplyr. A value that you compute from a missing value (None) is None.

Parameters:

Name Type Description Default
**new Callable[[T], Any]

For each new field, a function that takes 1 element and gives the value.

{}

Returns:

Type Description
Facts[Row]

A new collection of rows.

Example
milk = tasks.where(title="Buy milk")
milk.mutate(shout=lambda t: t.title.upper()).one().shout  # -> 'BUY MILK'

map

map(function: Callable[[T], U]) -> Facts[U]

Give a new collection of the results of a function on each element.

Parameters:

Name Type Description Default
function Callable[[T], U]

A function that takes 1 element and gives any value.

required

Returns:

Type Description
Facts[U]

A new collection of the results, in order.

Example
tasks.where(title="Buy milk").map(lambda t: t.title.upper()).one()
# -> 'BUY MILK'

count

count() -> int

Count the elements.

Returns:

Type Description
int

The number of elements.

Example
tasks.count()  # -> 6

distinct

distinct(value: Callable[[T], U]) -> Facts[U]

Give the different values of a function. They are in the order that you first see them.

Parameters:

Name Type Description Default
value Callable[[T], U]

A function that takes 1 element and gives a value that can be a dictionary key.

required

Returns:

Type Description
Facts[U]

A new collection of the different values.

Example
tasks.distinct(lambda t: t.list).count()  # -> 2

sum

sum(value: Callable[[T], N | None]) -> N

Add the values of a function over the elements. An element whose value is None adds nothing.

Parameters:

Name Type Description Default
value Callable[[T], N | None]

A function that takes 1 element and gives a number or None.

required

Returns:

Type Description
N

The total. It is 0 for an empty collection.

Example
tasks.where(list="work").sum(lambda t: t.due.day)  # -> 10

mean

mean(value: Callable[[T], float | None]) -> float | None

Give the mean of the values of a function. An element whose value is None is left out.

Parameters:

Name Type Description Default
value Callable[[T], float | None]

A function that takes 1 element and gives a number or None.

required

Returns:

Type Description
float | None

The mean. It is None if no element has a value.

Example
tasks.where(list="work").mean(lambda t: t.due.day)  # -> 5.0
tasks.where(list="school").mean(lambda t: t.due.day)  # -> None

max

max(value: Callable[[T], Any]) -> Any

Give the largest value of a function. An element whose value is None is left out.

Parameters:

Name Type Description Default
value Callable[[T], Any]

A function that takes 1 element and gives a value that can be compared or None.

required

Returns:

Type Description
Any

The largest value. It is None if no element has a value.

Example
tasks.max(lambda t: len(t.title))  # -> 16

min

min(value: Callable[[T], Any]) -> Any

Give the smallest value of a function. An element whose value is None is left out.

Parameters:

Name Type Description Default
value Callable[[T], Any]

A function that takes 1 element and gives a value that can be compared or None.

required

Returns:

Type Description
Any

The smallest value. It is None if no element has a value.

Example
tasks.min(lambda t: len(t.title))  # -> 8

top

top(n: int, by: Callable[[T], Any]) -> Facts[T]

Keep the n elements with the largest value. Elements whose value is None come last.

Parameters:

Name Type Description Default
n int

How many elements to keep.

required
by Callable[[T], Any]

A function that takes 1 element and gives the value to rank by.

required

Returns:

Type Description
Facts[T]

A new collection of at most n elements. The largest comes first.

Example
[t.title for t in tasks.top(2, by=lambda t: len(t.title))]
# -> ['Call the plumber', 'Water the plants']

count_by

count_by(**by: Callable[[T], Any]) -> Facts[Row]

Count the elements in each group. The largest group comes first.

Parameters:

Name Type Description Default
**by Callable[[T], Any]

A name and a function for each grouping value. The function takes 1 element and gives the value.

{}

Returns:

Type Description
Facts[Row]

A collection of rows. Each row has the grouping values and n. n is the

Facts[Row]

count.

Example
rows = tasks.count_by(status=lambda t: t.status)
[(row.status.value, row.n) for row in rows]
# -> [('open', 5), ('done', 1)]

group_by

group_by(**by: Callable[[T], Any]) -> Groups[T]

Put the elements in groups by one value or more. Then call summarise to make 1 row for each group.

Parameters:

Name Type Description Default
**by Callable[[T], Any]

A name and a function for each grouping value. The function takes 1 element and gives the value.

{}

Returns:

Type Description
Groups[T]

A Groups object. You can iterate it or call summarise on it.

Example
groups = tasks.group_by(list=lambda t: t.list)
rows = groups.summarise(
    n=lambda g: g.count(), last=lambda g: g.max(lambda t: t.id)
)
sorted((row.list, row.n, row.last) for row in rows)
# -> [('home', 4, 't6'), ('work', 2, 't3')]

join

join(
    other: Iterable[U], on: Callable[[T, U], bool]
) -> Facts[Pair[T, U]]

Match each element of this collection with each element of another collection when a rule is true.

Parameters:

Name Type Description Default
other Iterable[U]

The other collection. Any iterable works.

required
on Callable[[T, U], bool]

A function that takes 1 element of this collection and 1 of the other. It gives a true or false value.

required

Returns:

Type Description
Facts[Pair[T, U]]

A collection of Pair objects in the order of this collection. An element

Facts[Pair[T, U]]

with no partner gives no pair.

Example
open_tasks = tasks.where(status=Status.OPEN)
pairs = today.facts(Note).join(
    open_tasks, on=lambda note, task: note.task == task.id
)
pairs.count()  # -> 1

left_join

left_join(
    other: Iterable[U], on: Callable[[T, U], bool]
) -> Facts[Pair[T, U | None]]

Match elements as join does. Keep an element that has no partner.

Parameters:

Name Type Description Default
other Iterable[U]

The other collection. Any iterable works.

required
on Callable[[T, U], bool]

A function that takes 1 element of this collection and 1 of the other. It gives a true or false value.

required

Returns:

Type Description
Facts[Pair[T, U | None]]

A collection of Pair objects. An element with no partner gives

Facts[Pair[T, U | None]]

Pair(element, None).

Example
open_tasks = tasks.where(status=Status.OPEN)
pairs = today.facts(Note).left_join(
    open_tasks, on=lambda note, task: note.task == task.id
)
sorted((pair.left.task, pair.right is None) for pair in pairs)
# -> [('t1', True), ('t2', False)]

table

table(limit: int = 20) -> str

Write the elements as an aligned text table. repr and print of a Facts call this.

Parameters:

Name Type Description Default
limit int

The most rows to show. The default is 20. A last line tells how many more rows exist.

20

Returns:

Type Description
str

The table as text. An empty collection gives "(no items)".

Example
print(tasks.table(limit=2))

Groups

Groups(
    names: tuple[str, ...], groups: dict[tuple, Facts[T]]
)

The groups that Facts.group_by makes. Do not call the constructor. Use group_by.

Iterate it to get (values, facts) for each group. values is a tuple of the grouping values.

summarise

summarise(
    **values: Callable[[Facts[T]], Any],
) -> Facts[Row]

Make 1 row for each group. The row has its grouping values and one or more summary values.

Parameters:

Name Type Description Default
**values Callable[[Facts[T]], Any]

A name and a function for each summary. The function takes the Facts of 1 group and gives a value.

{}

Returns:

Type Description
Facts[Row]

A collection of rows in the order that the groups first appear.

Example
by_list = tasks.group_by(list=lambda t: t.list)
rows = by_list.summarise(titles=lambda g: g.sum(lambda t: len(t.title)))
sorted((row.list, row.titles) for row in rows)
# -> [('home', 52), ('work', 28)]

Row

Row(**values: Any)

A plain set of named fields. mutate, map and summarise make rows. You can also make one.

Read a field as an attribute such as row.task. A row cannot change. Two rows with the same fields are equal.

Parameters:

Name Type Description Default
**values Any

The fields of the row as keyword arguments.

{}

Raises:

Type Description
AttributeError

You read a field that the row does not have or you try to set a field.

Example
from scriptt.world import Row

row = Row(task="t1", hours=1.5)
row.task  # -> 't1'
row.fields()  # -> {'task': 't1', 'hours': 1.5}

fields

fields() -> dict[str, Any]

Give the fields of the row as a dictionary.

Returns:

Type Description
dict[str, Any]

A new dictionary from field name to value in the order of the row.

Example
from scriptt.world import Row

list(Row(task="t1", hours=1.5).fields())  # -> ['task', 'hours']

Pair dataclass

Pair(left: L, right: R)

Two elements that join or left_join matched.

Attributes:

Name Type Description
left L

The element from the collection that you called join on.

right R

The element from the other collection. With left_join it is None when no element matched.

Example
open_tasks = tasks.where(status=Status.OPEN)
pairs = today.facts(Note).join(
    open_tasks, on=lambda note, task: note.task == task.id
)
sorted((pair.left.text, pair.right.title) for pair in pairs)
# -> [('Use the new template', 'Send the report')]

left instance-attribute

left: L

right instance-attribute

right: R