At some point I fell (again) into the creative coding rabbit hole.
I re-read The Importance of Sketching with Code and it really clicked this time: instead of thinking “I need a big serious project”, just sketch. make tiny things. weird experiments. save them.
The result was this little side project: a Python source visualizer that turns any .py file into a panel of abstract rectangles.
Nothing “useful”, but very fun.
So comments, strings, numbers, keywords, etc. all get their own “visual personality”.
In the end you get something that kind of looks like a Mondrian painting that’s been hit with a syntax highlighter.
def read_text(path):
"""Read the file as plain text, never executing it."""
wit...
At some point I fell (again) into the creative coding rabbit hole.
I re-read The Importance of Sketching with Code and it really clicked this time: instead of thinking “I need a big serious project”, just sketch. make tiny things. weird experiments. save them.
The result was this little side project: a Python source visualizer that turns any .py file into a panel of abstract rectangles.
Nothing “useful”, but very fun.
So comments, strings, numbers, keywords, etc. all get their own “visual personality”.
In the end you get something that kind of looks like a Mondrian painting that’s been hit with a syntax highlighter.
def read_text(path):
"""Read the file as plain text, never executing it."""
with open(path, "r", encoding="utf-8", errors="replace") as f:
return f.read()
Nothing fancy, but the important bit for me: I never import or exec the file.1 It’s just bytes → text. That’s it.
I also explicitly set errors="replace" so if the file has weird encoding issues, the visualizer still works and just throws in some replacement characters. Glitch-friendly.
def tokenize_source(text):
"""
Tokenize Python source and group tokens into semantic categories.
"""
result = []
reader = io.StringIO(text).readline
try:
for tok in tokenize.generate_tokens(reader):
tok_type, tok_str, start, end, line = tok
if tok_type in (tokenize.ENCODING, tokenize.NL, tokenize.ENDMARKER):
continue
if tok_type == tokenize.COMMENT:
group = "comment"
elif tok_type == tokenize.STRING:
group = "string"
elif tok_type == tokenize.NUMBER:
group = "number"
elif tok_type == tokenize.OP:
group = "op"
elif tok_type ==[0, 1] space
its group
the token text that spawned it (not used visually yet, but could be)
depth, so I can adjust styling based on how many splits it went through.
Colors, palettes, and a bit of determinism
I hardcoded a couple of palettes:
PALETTES = [
{
"name": "midnight",
"background": "#050816",
"keyword": "#ff6b81",
"name": "#4dabf7",
"string": "#ffe066",
"number": "#b197fc",
"comment": "#868e96",
"op": "#ff922b",
"other": "#e9ecef",
},
{
"name": "pastel",
"background": "#f8f9fa",
...
},
...
]
Nothing algorithmic here, I just fiddled with colors until the outputs looked pleasant enough.2
To keep things interesting but reproducible, I do this:
def choose_palettes(text):
"""
Pick 3 distinct palettes in a deterministic way based on the file contents.
"""
seed = hash(text) & 0xFFFFFFFF
rng = random.Random(seed)
indices = list(range(len(PALETTES)))
rng.shuffle(indices)
chosen = [PALETTES[i] for i in indices[:3]]
return chosen, rng
I hash the file contents to seed a local RNG.
That RNG:
chooses 3 palettes for the 3 panels
is passed into build_rectangles so splits are deterministic too
So: same file → same picture every time (unless you change the code).
This is that “keep track of the random seed” lesson but kind of smuggled into the design.3
Drawing panels
The function that actually paints rectangles:
def draw_panel(ax, rects, palette, line_mode="thin"):
ax.set_facecolor(palette["background"])
if not rects:
return
max_depth = max(r["depth"] for r in rects) or 1
for r in rects:
group = r["group"]
color = palette.get(group, palette["other"])
depth_factor = (r["depth"] + 1) / (max_depth + 1)
alpha = 0.4 + 0.6 * depth_factor
if line_mode == "none":
lw = 0.0
edgecolor = None
elif line_mode == "thick":
lw = 1.5 + 1.5 * depth_factor
edgecolor = "#000000"
else:
lw = 0.4 + 0.6 * depth_factor
edgecolor = palette["background"]
rect_patch = Rectangle(
(r["x"], r["y"]),
r["w"],
r["h"],
linewidth=lw,
edgecolor=edgecolor,
facecolor=color,
alpha=alpha,
)
ax.add_patch(rect_patch)
Fun bits:
Color is purely based on group, so comments always fight in the same color range, etc.
Deeper rectangles get slightly higher alpha (less transparent).
line_mode lets me flip between:
subtle grid (thin)
bold black grid (thick)
completely flat, no strokes (none)
I then use three subplots to show three different “moods” of the same layout:
def make_figure(rects, palettes, title=None):
fig, axes = plt.subplots(1, 3, figsize=(15, 5), constrained_layout=True)
draw_panel(axes[0], rects, palettes[0], line_mode="thin")
draw_panel(axes[1], rects, palettes[1], line_mode="thick")
draw_panel(axes[2], rects, palettes[2], line_mode="none")
...
return fig
Same structure, different outfits.
Putting it together: CLI
The rest is just a small command-line wrapper:
def main():
parser = argparse.ArgumentParser(
description=(
"Visualize a Python source file as abstract rectangles.\n"
"The file is never executed, only read as plain text."
)
)
parser.add_argument("source", help="Path to the .py file to visualize")
parser.add_argument(
"-o",
"--output",
help="Output image filename (e.g. out.png). "
"If omitted, the window is just shown.",
)
parser.add_argument(
"--max-rects",
type=int,
default=400,
help="Maximum number of rectangles to generate (default: 400)",
)
args = parser.parse_args()
text = read_text(args.source)
tokens = tokenize_source(text)
palettes, rng = choose_palettes(text)
rects = build_rectangles(tokens, max_rects=args.max_rects, rng=rng)
title = f"Visualization of: {args.source}"
fig = make_figure(rects, palettes, title=title)
if args.output:
fig.savefig(args.output, dpi=300)
else:
plt.show()
So running:
python visualizer.py my_script.py -o my_script.png
spits out a PNG of your code.
Running it without -o just pops up a Matplotlib window.
Takeaways
Code is a great medium to sketch with, even when it’s not doing “real work”.
Treating source code as raw data (instead of something to execute) is oddly refreshing.
Randomness is fun, but deterministic randomness is much more usable.
Letting token types leak into visuals (orientation, ratios, colors) makes each file feel like it has its own personality.
And finally, once again: sketching is worth it. This started as a “let’s see what happens if…” evening and now I kinda want to build a whole series of tools like this.
Footnotes
This is both from the “security” side (don’t execute random files) and from the “art” side – the program should not care if the code even runs. ↩
I still think a proper generative palette system would be fun. Maybe next sketch. ↩
In the Gorilla Sun article they mention keeping track of random seeds when sketching. This is my lazy version of that: the seed is literally the file contents. ↩