Lists and Keys
When building real applications, you will often need to display lists of items: todo items, search results, user profiles, and more. This section covers how to render lists effectively in Pulse and why keys matter.
Rendering lists with ps.For
The recommended way to render a list in Pulse is using ps.For. Here is a simple example:
items = ["Apple", "Banana", "Cherry"]
return ps.ul()[
ps.For(items, lambda item: ps.li(item, key=item))
]This renders an unordered list with three list items. The ps.For construct takes two arguments:
- The iterable you want to loop over
- A function that receives each item and returns a Pulse element
You might wonder: why not just use a regular Python list comprehension? You certainly can:
return ps.ul()[
[ps.li(item, key=item) for item in items]
]However, ps.For protects you from a subtle Python gotcha called late binding.
The late-binding problem
Consider this code that adds a remove button to each item:
# This has a bug!
items = ["A", "B", "C"]
return ps.ul()[
[
ps.li(
item,
ps.button("Remove", onClick=lambda: remove(item)),
key=item
)
for item in items
]
]If you click "Remove" on item A, you might expect it to remove A. But it actually removes C! This happens because Python lambdas capture variables by reference, not by value. By the time you click the button, the loop has finished and item points to the last value: "C".
Using ps.For avoids this problem:
# This works correctly
items = ["A", "B", "C"]
def render_item(item):
return ps.li(
item,
ps.button("Remove", onClick=lambda: remove(item)),
key=item
)
return ps.ul()[
ps.For(items, render_item)
]With ps.For, each item is properly bound to its own scope, so clicking "Remove" on A will actually remove A.
Understanding keys
Keys tell Pulse how to identify each item in a list. They must be:
- Stable: The same item should always have the same key
- Unique: No two items in the same list should share a key
Good keys are typically IDs from your data:
@ps.component
def TodoList():
with ps.init():
todos = [
{"id": 1, "text": "Learn Pulse"},
{"id": 2, "text": "Build something cool"},
{"id": 3, "text": "Ship it"},
]
return ps.ul()[
ps.For(todos, lambda todo: ps.li(todo["text"], key=todo["id"]))
]Why keys matter
Keys help Pulse efficiently update the UI when your list changes. Without proper keys, Pulse cannot tell which items moved, were added, or were removed.
Imagine you have a list of three input fields. The user types "hello" in the first one. Then you add a new item at the beginning of the list. What happens to the text "hello"?
- With keys: Pulse knows the original first item moved to position 2, so "hello" moves with it
- Without keys: Pulse sees position 1 has a new item and clears its state, so "hello" disappears
Avoid using array indices as keys
Using the array index as a key is tempting but usually wrong:
# Avoid this pattern
ps.For(items, lambda item, i: ps.li(item, key=i))If you insert an item at the beginning, every index shifts. Pulse will think all items changed, defeating the purpose of keys entirely.
Use indices only as a last resort when your items have no stable identifier and the list never reorders.
Complete example
Here is a practical example showing a filterable list with proper keys:
from dataclasses import dataclass
import pulse as ps
@dataclass
class Fruit:
id: int
name: str
color: str
class FruitState(ps.State):
filter_text: str = ""
fruits: list[Fruit] = None
def __init__(self):
self.fruits = [
Fruit(1, "Apple", "red"),
Fruit(2, "Banana", "yellow"),
Fruit(3, "Cherry", "red"),
Fruit(4, "Grape", "purple"),
]
@ps.computed
def filtered_fruits(self) -> list[Fruit]:
if not self.filter_text:
return self.fruits
return [f for f in self.fruits if self.filter_text.lower() in f.name.lower()]
@ps.component
def FruitList():
with ps.init():
state = FruitState()
return ps.div()[
ps.input(
type="text",
placeholder="Filter fruits...",
value=state.filter_text,
onChange=lambda e: setattr(state, "filter_text", e["target"]["value"]),
),
ps.ul()[
ps.For(
state.filtered_fruits,
lambda fruit: ps.li(f"{fruit.name} ({fruit.color})", key=fruit.id)
)
],
]Notice how we use fruit.id as the key. Even as the filtered list changes, Pulse can track each fruit by its ID and update the UI efficiently.