Working with Immutable Lists

In this tutorial, we will build a product inventory system using ImmutableList and NonEmptyList. Along the way, we will encounter fmap, list concatenation, monadic bind, and the safety guarantee that immutable collections are never modified in place.

Prerequisites

Step 1: Create Your First Immutable List

Create a new file called inventory.py with the following contents:

from katharos.types import ImmutableList

prices = ImmutableList([19.99, 99.99, 699.99])

print(len(prices))
print(prices[0])
print(list(prices))

Run the file:

python inventory.py

You should see:

3
19.99
[19.99, 99.99, 699.99]

Notice that you can read elements by index and convert to a plain list, but the list itself cannot be modified in place.

Step 2: Work with Product Data

Now we will create a realistic product inventory. Replace the contents of inventory.py with:

from katharos.types import ImmutableList
from dataclasses import dataclass

@dataclass(frozen=True)
class Product:
    name: str
    price: float
    category: str

products = ImmutableList([
    Product("Book", 19.99, "books"),
    Product("Laptop", 999.99, "electronics"),
    Product("Phone", 699.99, "electronics"),
])

print(f"Total products: {len(products)}")
print(f"First product: {products[0].name}")

Run the file:

python inventory.py

You should see:

Total products: 3
First product: Book

Step 3: Transform Data with fmap

Now we will extract prices and names from the product list. Add this code at the bottom of inventory.py:

prices = products.fmap(lambda p: p.price)
print(prices)

names = products.fmap(lambda p: p.name)
print(names)

Run the file. The new lines at the bottom should be:

ImmutableList([19.99, 999.99, 699.99])
ImmutableList(['Book', 'Laptop', 'Phone'])

Notice that fmap creates a new list without modifying the original products list.

Step 4: Combine Lists with Concatenation

Now we will add more products to our inventory. Add this code at the bottom of inventory.py:

new_products = ImmutableList([
    Product("Tablet", 399.99, "electronics"),
    Product("Pen", 2.99, "stationery"),
])

all_products = products + new_products
print(len(all_products))
print(len(products))

Run the file. The new lines should be:

5
3

Notice that products still has 3 items. Concatenation produced a new list; it did not modify either of the originals.

Step 5: Filter Products with bind

We will now filter products by category. Add this code at the bottom of inventory.py:

def get_electronics(product: Product) -> ImmutableList[Product]:
    if product.category == "electronics":
        return ImmutableList([product])
    return ImmutableList([])

electronics = products.bind(get_electronics)
print([p.name for p in electronics])

Run the file. The new line should be:

['Laptop', 'Phone']

Notice how bind works: it applies get_electronics to every element and flattens the results. Elements that return an empty list are effectively removed.

Step 6: Use Lists as Dictionary Keys

Because ImmutableList is hashable, it can be used as a dictionary key. Add this code at the bottom of inventory.py:

cache: dict[ImmutableList[str], float] = {}

electronics_names = ImmutableList(["Laptop", "Phone"])
cache[electronics_names] = 1699.98

total = cache[electronics_names]
print(f"Cached total: {total}")

Run the file. The new line should be:

Cached total: 1699.98

A plain Python list cannot be used as a dictionary key because it is mutable. ImmutableList can because it guarantees the contents never change.

Step 7: Guarantee Non-Empty Lists with NonEmptyList

When you need to ensure a list always has at least one element, use NonEmptyList. Add this code at the bottom of inventory.py:

from katharos.types import NonEmptyList

electronics_nel = NonEmptyList(
    Product("Laptop", 999.99, "electronics"),
    [
        Product("Phone", 699.99, "electronics"),
        Product("Tablet", 399.99, "electronics"),
    ]
)

print(electronics_nel.head)
total = electronics_nel.head.price + sum(p.price for p in electronics_nel.tail)
print(f"Total: {total}")

Run the file. The new lines should be:

Product(name='Laptop', price=999.99, category='electronics')
Total: 2099.97

Notice that you can access .head without checking whether the list is empty, because NonEmptyList enforces the non-empty invariant at construction time.

What We Built

We built a product inventory system that stores products in an ImmutableList, transforms and filters them with fmap and bind, combines lists without mutation, uses an ImmutableList as a cache key, and enforces a non-empty invariant with NonEmptyList.