Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Dictionaries

Dictionaries use the common collection rules. insert(key, value) adds a key or replaces its value. Counting uses *counts.get_or_insert(key, 0)? += 1, which looks the key up once; see lookup and removal. The ? exposes allocation failure.

Dictionaries have no indexed assignment. dictionary[key] = value, dictionary[key] += n, and &mut dictionary[key] are compile errors whose messages name insert or get_mut: key presence is a run-time fact, so a write through an absent key would trap where --check cannot see it. Reading dictionary[key] remains, and traps on an absent key. Writing through dictionary[key].field or calling a mutating method on dictionary[key] is rejected the same way; use get_mut for one key, or a mutable value loop for every value.

def main() -> Result[(), AllocError | IoError | OverflowError[i64]]:
    mut stock = {"apples": 3}?
    stock.insert("pears", 5)?
    match stock.get_mut("apples"):
        case Some(count):
            *count -= 1
        case Nothing:
            pass
    print(stock)?
    Ok(())
{"apples": 2, "pears": 5}

Updating every value

for value in (&mut dictionary).values(): lends each value as &mut V in insertion order; for key, value in (&mut dictionary).items(): adds a shared &K key. &mut dictionary.items() is the same loop. Each step reaches the next entry directly, with no key lookup, item tuple, or copy, so it works for affine values and allocates nothing. Inside a function taking d: &mut dict[K, V], write (&mut d).values(); d.values() stays a shared loop.

class Player:
    name: str
    hp: i64
    items: list[str]

def main() -> Result[(), AllocError | IoError | OverflowError[i64]]:
    mut players = {"ada": Player("Ada", 3, []?), "lin": Player("Lin", 9, []?)}?
    for player in (&mut players).values():
        player.hp += 10
        player.items.append("potion")?
    for key, player in (&mut players).items():
        if *key == "lin":
            player.hp = 0
            break
    for key, player in players.items():
        print(*key, player.hp, player.items)?
    Ok(())
ada 13 ["potion"]
lin 0 ["potion"]

The loop holds an exclusive loan of the whole dictionary until it ends, so the body cannot insert, remove, clear, read, or borrow the dictionary again, including through get_mut or a nested loop; these are conflicting borrow errors. break, return, and ? end the loan without dropping or moving any value. The references are loop-local: storing one in a collection, assigning it to an outer reference binding, or returning it from a function that does not take the dictionary as its first reference parameter is rejected. A mutable projection is an immediate loop source; (&mut dictionary).values() cannot be bound to a name. There is no values_mut() or items_mut(): the exclusive receiver selects the mutable loop, as for x in &mut xs does for a list, and those names are compile errors that point here. To change values while also adding or removing keys, collect the keys first with keys_snapshot() and use get_mut per key.

Lookup and removal

dictionary.get(key) -> Option[V] observes receiver and key once in source order, including references. It copies or retains only non-affine values; collections, classes, and enums containing affine payloads are rejected. Lookup allocates nothing. Results outlive source replacement or destruction. Copying and borrowed lookup explains why get copies and how borrowed lookups are named.

dictionary.get(key, default) -> V accepts a second positional argument of the value type and returns it only when the key is absent. Receiver, key, and default evaluate once in that order; the default is eager even for a present key. Lookup still requires a copyable value and adds no allocation. Zero, False, and an empty string are present values. This does not insert the key. A stored Nothing produces Some(Nothing), distinct from a missing key.

dictionary.get_mut(key) -> Option[&mut V] lends the value for key exclusively, or returns Nothing. It works for every value type, including affine ones, and allocates nothing. The key is observed before the receiver is borrowed. The result holds an exclusive loan of the whole dictionary until its last use, as a reference-returning method’s result does, so the dictionary cannot be read, mutated, or borrowed again meanwhile. Match on the result to update a value in place: *count += 1 for a scalar, items.append(x)? for a list, or point.x = 0 for a class. replace(*value, new) returns the previous value and keeps the entry’s position.

dictionary.get_or_insert(key, default) -> Result[&mut V, AllocError] lends the value for key exclusively, first inserting key and default when the key is absent. Key and default evaluate in that order before the receiver is borrowed, and both move into the call. A present key keeps its stored entry and value; the call drops the incoming key and the unused default before returning. Either way the key is hashed once, so counting or grouping needs one lookup per item rather than the two of get_mut followed by insert. A missing key may grow the dictionary as insert does; on AllocError the key and default are dropped and the dictionary is unchanged. The result holds the same whole-dictionary loan as get_mut’s. The default is eager: it is built even when the key is present, so prefer get_mut when building it costs an allocation and the key is usually present.

def main() -> Result[(), AllocError | IoError | OverflowError[i64]]:
    mut counts: dict[str, i64] = {}?
    for word in "to be or not to be".split()?:
        *counts.get_or_insert(word, 0)? += 1
    mut lengths: dict[i64, list[str]] = {}?
    for word in counts.keys():
        group = lengths.get_or_insert(len(*word), []?)?
        group.append(*word)?
    print(counts, lengths)?
    Ok(())
{"to": 2, "be": 2, "or": 1, "not": 1} {2: ["to", "be", "or"], 3: ["not"]}

dictionary.pop(key) -> Option[V] requires exclusive access and supports all permitted value types. A hit removes the entry, drops its stored key, and transfers the value’s owner into Some; a miss returns Nothing without mutation. The key is observed once before exclusive receiver access. Removal takes exactly one key, with no default argument.

Removal allocates nothing, preserves capacity and remaining insertion order, and appends a reinserted key at the end. Expected lookup/removal cost is constant with well-distributed keys, plus hashing/comparison and cleanup; adversarial collisions can make it linear.

Snapshots and views

dictionary.keys() -> KeyView[K] creates a shared, local-only borrowed view without allocating. A view can be bound to a local, iterated, and used with builtin set operations. Iteration yields &K in insertion order; use [copy(key)? for key in dictionary.keys()]? to collect owned keys explicitly, including move-only class keys without custom cleanup. Keys containing __del__ can be borrowed here but cannot be copied; use into_keys() to transfer them. len(view) is O(1), and key in view / not in use dictionary lookup. The value holds a borrowed owner handle with a compiler-known key projection; the owner retains its row descriptor and each loop keeps its own cursor.

== and != compare keys as sets, ignoring insertion order, against KeyView[K] or set[K] with the same key type. Dictionaries may have different value types. issubset, issuperset, and isdisjoint take &set[K] or &KeyView[K] and return bool. These observations allocate nothing and have no AllocError; relationships perform O(n) expected hash lookups (equality first compares lengths). Empty views are subsets of every set-like operand and disjoint from every operand.

union, intersection, difference, and symmetric_difference take those same operands and return Result[set[K], AllocError]. to_set() materialises the keys as that result type. Algebra reserves its result before inserting keys and leaves both sources unchanged on failure. Move-only keys are copied into independent result storage. All four algebra methods and to_set() reject keys containing custom cleanup, because those keys cannot be copied. Relationship checks remain available. Result order is unspecified. Operators such as |, &, and <= are not part of the view API. Set relationship and algebra methods accept mixed set/view operands in either receiver order. set.intersection_update(view) and set.difference_update(view) also accept key views. Equality accepts either operand order. These builtin methods observe their operands, so an explicit & may be omitted without moving a named set or view.

set(view) and list(view) are not supported. Use view.to_set(), dictionary.keys_snapshot(), or [*key for key in view]? to choose the owned result explicitly.

A bound view holds a shared loan on the whole dictionary until its last use. Insertion, get_mut, get_or_insert, update, removal, clearing, and moving the owner conflict with that loan, including through aliases. Views cannot be stored in fields, collections, tuples, enum payloads, generators or worker/closure captures, or passed to or returned from user functions. Binding a view of a temporary dictionary is rejected; immediate iteration may borrow a temporary. drop(view) and copy(view) are rejected like ordinary references; end the loan by finishing the view’s last use. Conditional view bindings are not supported. General stored/returned views remain separate work.

dictionary.values() creates a shared local view yielding &V, without allocating or copying affine payloads. Both values = dictionary.values() and items = dictionary.items() can be bound and reused in loops; len(view) is O(1). iter(values) and iter(items) start pipelines, with item callbacks taking separate key and value parameters. These views have the same local-only borrow restrictions as key views and do not provide set operations. (&mut dictionary).values() yields &mut V in immediate loops; mutable views cannot be bound. See updating every value. Automatic box dereferencing applies to keys(), values() and items(), including nested boxes and borrowed boxed dictionaries. Their loans protect the boxed owner throughout the observation or loop.

dictionary.keys_snapshot() -> Result[list[K], AllocError] and dictionary.values_snapshot() -> Result[list[V], AllocError] take no arguments and preserve insertion order. Each returns a new list, retaining immutable element storage. Observed dictionaries remain unchanged on failure, and snapshots survive later source mutation or destruction.

A snapshot with affine selected elements requires an owned temporary: this applies to keys_snapshot() for move-only keys and values_snapshot() for move-only values. Those elements transfer and the temporary is consumed on success or failure. Use copy(d)?.keys_snapshot() or copy(d)?.values_snapshot() to preserve such a source explicitly. The unselected side may be affine even when observing a named dictionary. Both snapshots reserve their complete output before transfer: two allocations when nonempty, one for an empty owner.

dictionary.into_keys() -> Result[list[K], AllocError] and dictionary.into_values() -> Result[list[V], AllocError] consume a named owner or owned expression directly, including dictionaries with affine payloads. They preserve insertion order and transfer the selected elements without copying. The dictionary is consumed on success and failure. Output reservation happens before any transfer; allocation failure drops the remaining source exactly once. Use for value in dictionary.into_values()?: when the dictionary is no longer needed. This is an owned snapshot, not a stored borrowed view.

dictionary.items() creates a shared local borrowed view, with an independent cursor for each loop, and creates no item tuples or snapshot buffers. Shared views load shared references to both keys and values, including scalars and strings; mutable views lend mutable values while keys remain shared references. Its loop loans follow the iteration rules.

Transferring an update

dictionary.update(other) -> Result[(), AllocError] requires exclusive access and consumes a same-typed owned dictionary on every outcome. Existing keys keep their position and key owner; incoming values replace old values. New keys append in source insertion order.

All storage for absent keys is reserved before replacement or insertion. Failure preserves destination contents and lookup behavior, though capacity may change, and drops the source. Success transfers entries without copying; old values drop in source traversal order after replacement. Empty and replacement-only updates, or updates fitting reserved capacity, allocate no runtime storage.

Use update(copy(source)?) to preserve the source. References, pair iterables, and keyword updates are unsupported.