Self-rendering data structures for visualizing algorithms in the terminal
Project description
AlgoViz
Data structures that draw themselves as your algorithm runs.
Quick start · Structures · Demos · Configuration · Upgrading
Wrap a data structure, run your algorithm unchanged, and watch every step render in your terminal with the cells you touched highlighted. Reads light up in one colour, writes in another, and each frame shows only what that step touched.
No decorators. No callbacks. No rewriting your solution to fit a framework.
from algoviz import VizList
dp = VizList([0] * 4, title_name='Coin Change')
dp[0] = 1
for coin in (1, 2):
for value in range(coin, 4):
dp[value] += dp[value - coin]
Coin Change Init
┏━━━┳━━━┳━━━┳━━━┓
┃ 0 ┃ 1 ┃ 2 ┃ 3 ┃
┡━━━╇━━━╇━━━╇━━━┩
│ 0 │ 0 │ 0 │ 0 │
└───┴───┴───┴───┘
Coin Change
┏━━━┳━━━┳━━━┳━━━┓
┃ 0 ┃ 1 ┃ 2 ┃ 3 ┃
┡━━━╇━━━╇━━━╇━━━┩
│ 1 │ 0 │ 0 │ 0 │
└───┴───┴───┴───┘
Why
Reading a DP recurrence tells you what the code does. It does not tell you what the table looks like three iterations in, which is where the bug usually is. AlgoViz prints that table on every write, so the state is in front of you instead of in your head.
It is built for the moment you are stuck on a problem, teaching someone who is, or writing up a solution and want the intermediate states to show.
Installation
pip install algoviz
uv add algoviz
Requires Python 3.10 or newer. The only dependency is rich.
Quick start
Every structure takes the data you already have and hands back something that behaves the same way:
from algoviz import VizList, VizStack, VizHeap, VizGrid
VizList([0] * 5) # 1D and 2D tables
VizStack() # LIFO, top always labelled
VizHeap([5, 3, 8], max_heap=True)
VizGrid(['110', '011']) # terrain plus a visited/queued overlay
The structures
| Class | Backed by | Reach for it when |
|---|---|---|
VizList |
MutableSequence |
1D and 2D DP tables, prefix sums, any array problem |
VizStack |
list |
Valid parentheses, monotonic stack, next greater element |
VizQueue |
deque |
BFS, level-order traversal, shortest path |
VizDeque |
deque |
Sliding window maximum, monotonic deque |
VizHeap |
heapq |
Top-K, merge K lists, task scheduler, running median |
VizDict |
MutableMapping |
Index maps, memoisation, seen-value lookups |
VizCounter |
MutableMapping |
Frequency maps, group anagrams, sliding windows |
VizSet |
MutableSet |
Seen-sets, cycle detection, longest consecutive |
VizGrid |
list[list] |
Islands, flood fill, word search, shortest path |
VizLinkedList |
ListNode |
Reverse a list, cycle detection, two pointers |
VizTree |
TreeNode |
Binary tree and BST traversals |
VizTrie |
prefix tree | Word search, autocomplete, word break |
VizUnionFind |
rank + path compression | Connected components, redundant connection |
ListNode and TreeNode use the same .val / .next / .left / .right
shape LeetCode uses, so existing solutions paste in unchanged.
Implementing it yourself
When the structure is the exercise, the library gets out of the way. Each of these hands you the real nodes so your own code does the walking, and a hook to light up the step you are on:
| Structure | Nodes | Drive the animation with |
|---|---|---|
VizTree |
root_node, TreeNode |
visit(node) |
VizTrie |
root_node, TrieNode |
visit(node, writing=...) |
VizLinkedList |
head, nodes(), ListNode |
set_pointer(name, node) |
VizList |
positions | set_pointer(name, index) |
from algoviz import TrieNode, VizTrie
viz = VizTrie(title_name='Trie')
node = viz.root_node
for char in 'cat': # your insert, not the library's
node.children.setdefault(char, TrieNode())
node = node.children[char]
viz.visit(node, writing=True)
node.is_word = True
Sizing note for
VizTrie. Keep it to roughly a dozen short words. The render is one line per node, and a trie's node count grows with the total number of characters, not the number of words, so 20 words of 5 to 8 letters is already about 62 nodes and fills a terminal in a single frame. The other structures are far less dense: 20 values in aVizHeapis 20 nodes, and in aVizListit is 3 lines. Nothing breaks past that point, it just stops being readable.
Demo gallery
Thirty runnable solutions live in demo/. Every one is executed
and checked against the correct answer in CI, so none of them can rot.
Each demo writes its own algorithm. Where the library offers a shortcut that would be the answer, the demo deliberately does not call it: the level-order demo drives its own queue, the top-K demo maintains its own bounded heap, and the trie demo builds its own nodes.
python demo/num_islands.py
Arrays and dynamic programming
| Demo | Problem | Idea |
|---|---|---|
climbing_stairs.py |
70 Climbing Stairs | The smallest interesting DP |
coin_change.py |
518 Coin Change II | Count the ways to make an amount |
house_robber.py |
198 House Robber | Skip-or-take DP |
jump.py |
45 Jump Game II | Fewest jumps to the last index |
count_palin.py |
647 Palindromic Substrings | Expand a 2D table |
longest_common_subsequence.py |
1143 LCS | The canonical 2D table |
Two pointers and prefix sums
| Demo | Problem | Idea |
|---|---|---|
dutch_national_flag.py |
75 Sort Colors | Three-pointer partition in one pass |
container_with_most_water.py |
11 Container With Most Water | Converge from both ends |
three_sum.py |
15 3Sum | Sort, then two-pointer the rest |
subarray_sum_equals_k.py |
560 Subarray Sum Equals K | Prefix sums in a map |
Stacks, queues, and deques
| Demo | Problem | Idea |
|---|---|---|
valid_parentheses.py |
20 Valid Parentheses | The classic stack warm-up |
daily_temperatures.py |
739 Daily Temperatures | A monotonic stack of indices |
min_stack.py |
155 Min Stack | Constant-time minimum via a paired stack |
sliding_window_max.py |
239 Sliding Window Maximum | The monotonic deque |
rotting_oranges.py |
994 Rotting Oranges | BFS measured in levels |
Heaps, maps, and sets
| Demo | Problem | Idea |
|---|---|---|
kth_largest.py |
215 Kth Largest | A bounded min-heap |
top_k_frequent.py |
347 Top K Frequent | A heap capped at k |
two_sum.py |
1 Two Sum | The hash-map one-pass |
group_anagrams.py |
49 Group Anagrams | Bucket by sorted letters |
longest_consecutive.py |
128 Longest Consecutive | A set, not a sort |
Grids, lists, trees, and graphs
| Demo | Problem | Idea |
|---|---|---|
num_islands.py |
200 Number of Islands | BFS over a grid overlay |
flood_fill.py |
733 Flood Fill | Recolour a connected region |
reverse_linked_list.py |
206 Reverse Linked List | Pointer surgery, one node at a time |
linked_list_cycle.py |
141 Linked List Cycle | Floyd's tortoise and hare |
binary_tree_level_order.py |
102 Level Order | Drive the BFS queue yourself |
validate_bst.py |
98 Validate BST | Bounds, not just parents |
implement_trie.py |
208 Implement Trie | Walk and build the nodes yourself |
word_break.py |
139 Word Break | A trie plus a DP sweep |
number_of_provinces.py |
547 Number of Provinces | Union-find over a matrix |
redundant_connection.py |
684 Redundant Connection | The edge that closes a cycle |
Highlights
Two pointers
VizList takes named pointers by index, so the labels track your loop
variables and the invariant is visible instead of implied.
from algoviz import VizList
colors = VizList([2, 0, 2, 1, 1, 0], title_name='Sort Colors')
colors.set_pointer('low', 0)
colors.set_pointer('mid', 0)
colors.set_pointer('high', 5)
Sort Colors
┏━━━━━━━━━━┳━━━┳━━━┳━━━┳━━━┳━━━━━━━┓
┃ 0 ┃ 1 ┃ 2 ┃ 3 ┃ 4 ┃ 5 ┃
┡━━━━━━━━━━╇━━━╇━━━╇━━━╇━━━╇━━━━━━━┩
│ 2 │ 0 │ 2 │ 1 │ 1 │ 0 │
│ ↑low,mid │ │ │ │ │ ↑high │
└──────────┴───┴───┴───┴───┴───────┘
A pointer that runs past the end is remembered but not drawn, so loops that terminate by walking off the array need no special-casing.
Stack
The top is always labelled, and a popped value stays visible for exactly one frame so you can see what left.
from algoviz import VizStack
stack = VizStack(title_name='Parens')
for char in '([])':
stack.push(char) if char in '([' else stack.pop()
Parens
┏━━━━━━━━━━┳━━━━━━━┓
┃ ┃ value ┃
┡━━━━━━━━━━╇━━━━━━━┩
│ ✗ popped │ [ │
│ ← top │ ( │
└──────────┴───────┘
Heap
Renders the backing array and the tree it represents, which is the part a flat list never shows you.
from algoviz import VizHeap
heap = VizHeap([5, 3, 8, 1], title_name='Min Heap')
heap.push(2)
Min Heap
┏━━━┳━━━┳━━━┳━━━┳━━━┓
┃ 0 ┃ 1 ┃ 2 ┃ 3 ┃ 4 ┃
┡━━━╇━━━╇━━━╇━━━╇━━━┩
│ 1 │ 2 │ 8 │ 5 │ 3 │
└───┴───┴───┴───┴───┘
Min Heap (tree)
1
├── 2
│ ├── 5
│ └── 3
└── 8
Ordering comes straight from the standard library's heapq, so what you see is
the real thing rather than a reimplementation. Pass max_heap=True for a
max-heap.
Grid
Grid cells usually hold terrain. What matters is the overlay: which cells are visited, queued, or on the current path.
from algoviz import VizGrid
grid = VizGrid(['11000', '11000', '00100'], title_name='Islands')
grid.mark_start(0, 0)
for row, col in grid.neighbors(0, 0):
grid.mark_queued(row, col)
neighbors() handles bounds for you, diagonal=True gives all eight, and
grid.batch() suspends redrawing so you get one frame per BFS level instead of
one per cell.
Linked list
Named pointers make two-pointer and cycle problems legible, and a cyclic list renders its back-edge instead of hanging.
from algoviz import VizLinkedList
linked = VizLinkedList([1, 2, 3, 4, 5])
slow = fast = linked.head
while fast and fast.next:
slow, fast = slow.next, fast.next.next
linked.set_pointer('slow', slow)
linked.set_pointer('fast', fast)
Union-find
Path compression and union by rank are both implemented, and the render shows each element's parent alongside the current components, so you can watch compression flatten the tree.
from algoviz import VizUnionFind
uf = VizUnionFind(5)
uf.union(0, 1)
uf.union(3, 4)
uf.component_count # 3
Configuration
Every structure accepts the same settings, as keyword overrides or a shared
VizConfig:
from algoviz import VizConfig, VizList
VizList([1, 2, 3], get_color='green', set_color='magenta', sleep_time=0.3)
quiet = VizConfig(auto_print=False, show_init=False)
dp = VizList([0] * 5, config=quiet)
dp[2] = 7
dp.show() # draw only when you ask
| Setting | Default | Meaning |
|---|---|---|
get_color |
blue |
Colour for cells that were read |
set_color |
red |
Colour for cells that were written |
sleep_time |
0 |
Seconds to pause after each frame |
auto_print |
True |
Redraw automatically after every write |
show_init |
True |
Draw the initial state on construction |
show_header |
True |
Show the index header row |
Set sleep_time and let it run to get an animation. Set auto_print=False and
call .show() yourself to control exactly which frames appear.
Development
This project uses uv.
uv sync # create the environment
uv run pytest # 584 tests
uv run pytest --cov # with coverage
uv run ruff check # lint
uv run ruff format # format
uv run mypy # type check
uv build # build sdist and wheel
CI runs the suite on Python 3.10 through 3.14 across Linux, macOS, and Windows, plus lint, format, types, and a packaging check.
Upgrading from 0.2.x
Version 0.3.0 rewrote the core. Three changes can affect existing code:
VizListis aMutableSequence, not alistsubclass. It behaves like a list everywhere that matters, butisinstance(dp, list)is nowFalse. The old inheritance was broken regardless: the realliststorage was always empty, so anything reading it saw the wrong data. Usedp.datawhen you need a plain list.a + bno longer mutatesa. The old__add__calledextend, so concatenation modified the left operand in place. It now returns a new list, as+should.print(expr)is sandboxed. Expressions still use#for the underlying data, but they evaluate with no builtins and no access to internals.
Python 3.9 users can stay on 0.2.3.
Contributing
Issues and pull requests are welcome. Please run uv run pytest,
uv run ruff check, and uv run mypy before opening one. New demos should
include their expected output in tests/test_demos.py, which is what keeps the
gallery honest.
License
MIT. See LICENSE.txt.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file algoviz-0.4.0.tar.gz.
File metadata
- Download URL: algoviz-0.4.0.tar.gz
- Upload date:
- Size: 74.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
227a8c5f38177df883ed4583d36a570520804f9454762f38a92f71d57de816bf
|
|
| MD5 |
ef41ffb8d76aa025596577ec4caa695f
|
|
| BLAKE2b-256 |
58b739b96f7457989bc8875178d2e4900f014431ed80a464f93dcdaafa037e2d
|
Provenance
The following attestation bundles were made for algoviz-0.4.0.tar.gz:
Publisher:
release.yml on algometrix/algoviz
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
algoviz-0.4.0.tar.gz -
Subject digest:
227a8c5f38177df883ed4583d36a570520804f9454762f38a92f71d57de816bf - Sigstore transparency entry: 2202476095
- Sigstore integration time:
-
Permalink:
algometrix/algoviz@7254094c45ccdd8be5defeec4ddee9c3c7a96781 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/algometrix
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@7254094c45ccdd8be5defeec4ddee9c3c7a96781 -
Trigger Event:
push
-
Statement type:
File details
Details for the file algoviz-0.4.0-py3-none-any.whl.
File metadata
- Download URL: algoviz-0.4.0-py3-none-any.whl
- Upload date:
- Size: 50.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7eb9a5e7300edf2bfead0c6f0c7fedc0a5896aca8c50186a5437cc00eeb792f3
|
|
| MD5 |
b2eb445b6489319e1f9af42c88418a06
|
|
| BLAKE2b-256 |
09e9c95822f1b2d218fdc9b494613bf525f8d93c2ac35ec5daaab7e60c2a997f
|
Provenance
The following attestation bundles were made for algoviz-0.4.0-py3-none-any.whl:
Publisher:
release.yml on algometrix/algoviz
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
algoviz-0.4.0-py3-none-any.whl -
Subject digest:
7eb9a5e7300edf2bfead0c6f0c7fedc0a5896aca8c50186a5437cc00eeb792f3 - Sigstore transparency entry: 2202476323
- Sigstore integration time:
-
Permalink:
algometrix/algoviz@7254094c45ccdd8be5defeec4ddee9c3c7a96781 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/algometrix
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@7254094c45ccdd8be5defeec4ddee9c3c7a96781 -
Trigger Event:
push
-
Statement type: