Visual walkthrough — Animation — FuncAnimation
Before we start, three plain words we will use constantly. We anchor each to a picture in Step 1, but here are the one-liners:
- Figure — the whole rectangular drawing surface (the sheet of paper). See Matplotlib Figure and Axes.
- Artist — any single thing drawn on that surface: a line, a dot, a tick mark, a label. The moving curve is one artist. See Artists in Matplotlib.
- Frame — one still picture in the slideshow, numbered
Step 1 — The stage: one Figure, drawn once
WHAT. We set up a blank sheet with fixed edges and lay out the fixed samples. Nothing moves yet.
WHY. A movie is a stack of almost identical pictures. If the axis limits, ticks, and grid changed every frame, every frame would be a different picture and there would be nothing to reuse. So the very first act is to freeze the stage — set the and limits once — so that everything except our moving line stays pixel-for-pixel identical from frame to frame. We also build the array x of horizontal sample positions now, because it never changes across frames.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
fig, ax = plt.subplots()
ax.set_xlim(0, 2*np.pi) # freeze stage edges ONCE
ax.set_ylim(-1.1, 1.1)
x = np.linspace(0, 2*np.pi, 400) # the 400 fixed horizontal sample pointsx = np.linspace(0, 2*np.pi, 400)— 400 evenly spaced numbers from to ; these are the horizontal positions we evaluate the wave at every frame. Built with `np.linspace` and never rebuilt.
PICTURE. Below: the cream stage, its edges frozen at and . The teal box is the region that will never change. That frozen region is the key to the whole speed trick.

Step 2 — One artist, created once and left empty
WHAT. We add a single line artist to the stage, but give it no data yet.
WHY. This is the heart of the parent note's [!mistake] callout. If we called ax.plot(x, y) inside the loop, each frame would manufacture a brand-new line and staple it onto the stage — the old ones never leave. After 200 frames you have 200 lines piled up. Instead we make one line object now and, each frame, only change what data it points at. Same object, new numbers.
The line below (burnt orange) is drawn with empty data — so it is invisible, but it exists. Think of it as an empty picture frame hung on the wall, waiting for a photo.
[], []— empty list, empty list → the line has no points yet, so nothing shows.lw=2— line width, purely cosmetic.- the trailing comma in
line,—ax.plotalways returns a list of lines;line,unpacks the single element out of that list. Without the comma,linewould be the list, not the line, andline.set_data(...)would explode.

Step 3 — init_func: photograph the empty stage
WHAT. init_func runs before frame 0. It clears the line's data and hands the changed artist back.
WHY two reasons. First, it defines "what the world looks like before anything moves" — a clean slate. Second, and this is the deep one: when blit=True, matplotlib uses this moment to take a snapshot of the frozen background (Step 1's teal box) and store it as a raw bitmap in memory. That snapshot is taken once and reused for every single frame.
def init():
line.set_data([], []) # clean slate
return line, # tell blit: this is the artist I touchedline.set_data([], [])— empty the line again, guaranteeing frame is blank.return line,— a 1-tuple of artists. Blit needs to know which artists it must manage. The comma makes(line)into(line,), a tuple;return line(no comma) returns a bare line and blit cannot iterate it.
PICTURE. The camera icon = matplotlib copying the background into a memory buffer. This copy is the investment we cash in every frame.

Step 4 — update(frame): slide the same line
WHAT. For frame number , we compute new -values and pour them into the existing line with set_data. We do not create anything.
WHY. This is the "rubber stamp" from the parent's flip-book analogy. One function describes frame ; the robot calls it for . The shift moves the wave rightward a little more each frame.
- — the fixed 400 sample points we built in Step 1 (never changes).
- — the frame number, our clock.
- — the phase shift. Increasing it slides the whole curve to the right. At the wave sits at its starting position; near it has slid almost one full period (since ).
- — turns each shifted input into a height between and , which is why -limits were in Step 1.
def update(n):
y = np.sin(x - 0.1*n)
line.set_data(x, y) # MUTATE the one line
return line, # hand blit the changed artistPICTURE. Three frozen snapshots of the same orange line at . Notice: identical stage, only the curve's data changed.

Step 5 — What blitting actually does each frame (the payoff)
WHAT. For every frame, blit=True performs exactly three cheap operations.
WHY. Redrawing a full axes means re-rasterising ticks, grid, labels, spines — hundreds of little artists — every 20 ms. That is wasteful because they never change. Blitting replaces "redraw everything" with:
- Restore the saved background bitmap (a fast memory copy).
- Draw only the artists
updatereturned (just our one line). - Blit (copy) that small patch onto the screen.
PICTURE. Left column = the expensive path (redraw the entire stage). Right column = the blit path (paste background, then draw only the line). The right path touches far fewer pixels' worth of work.

Step 6 — The stopwatch: turning interval into motion
WHAT. The GUI event loop owns a timer. Every interval milliseconds it fires and asks for the next frame.
WHY. We must not sleep() in a loop — that would freeze the window. Instead the timer is a background alarm clock: "wake me in interval ms, then call update once, then re-arm." Between alarms the window stays responsive (you can drag it, resize it).
- — milliseconds in one second.
interval— the gap the timer waits. Smaller gap → more frames per second → smoother, faster motion.- — total frames in one pass, the same from Step 5's cost formula. Multiply by the per-frame gap to get the total wall-clock time.
Worked numbers. With interval=20: . For frames: ms s per pass.
PICTURE. A timeline of alarm ticks spaced interval apart; each tick triggers exactly one update call.

Step 6b — Assembling the machine: the FuncAnimation call
WHAT. We hand all the pieces — the figure, the update function, how many frames, the timer gap, the init snapshot, and blit — to FuncAnimation, and we store the result.
WHY. Everything above defined the parts; this single call wires them together and starts the timer. Crucially, we assign it to anim so Python keeps the object (and its timer) alive.
anim = FuncAnimation(
fig, # which Figure to redraw (Step 1)
update, # the per-frame rubber stamp (Step 4)
frames=200, # N = 200 -> frames n = 0,1,...,199 (the N in the cost/timing formulas)
init_func=init, # the background snapshot (Step 3)
interval=20, # timer gap in ms -> 1000/20 = 50 FPS (Step 6)
blit=True, # paste background + draw only returned artists (Step 5)
)
plt.show() # hand control to the GUI event loop; the timer now fires
# anim.save("wave.mp4", fps=50) # optional export, see Saving figures and ffmpegframes=200— this is where enters. An integer means frames ; you choose it, and it flows into and the cost formulas. (A generator can go here instead — see the parent's Worked Example 2.)anim = ...— the reference that keeps the animation from being garbage-collected (see Step 7, Case A).

Step 7 — Degenerate & edge cases (the traps)
WHAT. The cases where the machine looks broken.
WHY. You must never hit a scenario the walkthrough didn't show. Three ways the loop dies:
PICTURE. Three mini-panels, one per failure, each showing the symptom (blank / one frozen artist / a question mark on frame count).

The one-picture summary
Everything above, compressed: the once column (build stage + x, make line, snapshot background) feeds the per-frame loop (timer fires → update mutates line → blit restores + draws + copies) — with the stored anim reference keeping the whole timer alive.

Recall Feynman: the whole walkthrough in plain words
We laid down a sheet of paper, nailed its edges so it can never wiggle, and marked 400 fixed dots along the bottom to measure from (Step 1). We drew one line on it but left it empty (Step 2). Before the show began, a camera took a photo of the still, empty sheet and kept that photo in its pocket (Step 3 — the blit snapshot). Then a stopwatch started ticking every 20 milliseconds. On each tick, a little rubber-stamp function figured out where the wave should be now and slid the same line there — not a new line, the same one (Step 4). To draw the new frame cheaply, the machine slaps down the pocket photo of the empty sheet and then paints only the line on top — never re-drawing the frozen ticks and grid (Step 5, and "blit" just means "copy a block of pixels"). The stopwatch keeps the window awake between ticks so you can still click it (Step 6). Finally we handed all the pieces to FuncAnimation, telling it to run 200 frames, and stored it in anim so it wouldn't be thrown away (Step 6b). Three things kill the show: forgetting to hold onto the projector, forgetting to hand back an artist, and not telling a generator-fed save how many frames to grab (Step 7). Put together, you pay the big background cost once and the tiny line cost many times — and that is the whole reason animation is fast.
Active recall
Connections
- Animation — FuncAnimation — the parent topic this page derives in pictures.
- Matplotlib Figure and Axes — the frozen stage of Step 1.
- Artists in Matplotlib — the single
Line2Dwe mutate. - Generators in Python — the
framessource in edge Case C. - Event Loops / GUI backends — the timer of Step 6.
- NumPy linspace and broadcasting — builds the fixed
xsamples. - Saving figures and ffmpeg — the save path that needs
save_count.