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.
Release files for algoviz 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| algoviz-0.4.0.tar.gz | 74.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| algoviz-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 125.3 kB
Release files / algoviz-0.4.0.tar.gz
| Download URL | algoviz-0.4.0.tar.gz |
|---|---|
| Size | 74.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
227a8c5f38177df883ed4583d36a570520804f9454762f38a92f71d57de816bf
|
|
BLAKE2b-256 checksum How to use checksums |
58b739b96f7457989bc8875178d2e4900f014431ed80a464f93dcdaafa037e2d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 19, 2026.
Transparency logRelease files / algoviz-0.4.0-py3-none-any.whl
| Download URL | algoviz-0.4.0-py3-none-any.whl |
|---|---|
| Size | 50.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7eb9a5e7300edf2bfead0c6f0c7fedc0a5896aca8c50186a5437cc00eeb792f3
|
|
BLAKE2b-256 checksum How to use checksums |
09e9c95822f1b2d218fdc9b494613bf525f8d93c2ac35ec5daaab7e60c2a997f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 19, 2026.
Transparency log