Worked examples — Animation — FuncAnimation
Before anything, one promise: every symbol is earned. interval is a number of milliseconds between two flips of the flip-book. frames is the list of "page numbers" (or values) the robot walks through. FPS is how many pages flip per second. func (we'll call it update) is the rubber stamp that says "for this page, draw the picture like THIS". Keep those four pictures in your head.
Two more small pieces we'll lean on repeatedly, defined here, before first use:
The scenario matrix
Every question about a FuncAnimation lives in one of these cells. Our examples below are each tagged with the cell they land in, and together they touch all of them.
| Axis | Cases we must cover |
|---|---|
| Frame source | integer N · explicit list/array · generator (unknown length) |
| Timing direction | given interval → find FPS/total time · given FPS/time → find interval |
| Blit | blit=True (must return artists) · blit=False (return value ignored) |
| Artist count | one artist · many artists (line + dot) |
| Degenerate / limiting | frames=0 (empty) · interval=0 (as-fast-as-possible) · one-frame animation · generator that never stops |
| Saving | save to mp4 at chosen fps · why interval is ignored when saving · generator + save_count |
| Word / exam twist | "I want an 8-second clip" real problem · "why is my window blank?" bug-hunt |
The number that ties timing together, derived once in the parent, is:
Example 1 — integer frames → FPS and total time (cell: integer N · interval→FPS · one artist)
Forecast: guess the FPS before reading. Is 20 ms between frames fast or slow?
- Read off the numbers.
interval = 20ms,N = 200frames. Why this step? Every timing problem starts by naming which number isintervaland which isN, so we don't mix them. - FPS from the first equation. . Why this step? "How many 20 ms slots fit in 1000 ms?" — that count is frames-per-second.
- Total time from the second equation. ms; convert with the bridge: s. Why this step? One pass is every frame's little wait, stacked end to end — in ms — then divided by 1000 to read it in seconds.
Verify: cross-check with the other form: s. ✓ Units: . ✓
Example 2 — the inverse: "I want an exact-length clip" (cell: given FPS/time→interval · word problem · saving)
Forecast: will interval even matter when you save the file?
- Frames = FPS × seconds. frames. Why this step? Rearranging gives — we know the two on the right.
- Interval from FPS. ms.
Why this step? Same first equation, solved for
interval: the slot length is "one second (1000 ms) split into FPS pieces." - Saving passes
fps, notinterval.anim.save("clip.mp4", fps=60). Why this step? The parent note's key rule: when saving, matplotlib spaces frames at seconds and ignoresinterval(there's no live GUI timer to obey). See Saving figures and ffmpeg.
Verify: with , : s exactly. ✓ And ms ms s for live playback. ✓
Example 3 — array of frames feeding a moving dot (cell: explicit array · many artists · blit=True)

First fix the data, so every symbol below is concrete:
import numpy as np
x = np.linspace(0, 2*np.pi, 200) # 200 evenly-spaced x-values from 0 to 2π
# y at any x is np.sin(x); see [[NumPy linspace and broadcasting]]So ==x== is a NumPy array of 200 evenly spaced numbers from to (built by `linspace`), and np.sin(x) is the matching array of heights. x[:i] means "the first i entries of x" — a slice.
Forecast: if you return only the dot and forget the trail line — what happens on screen?
- Create both artists once, empty.
line, = ax.plot([], [], 'b-')anddot, = ax.plot([], [], 'ro'). Why this step? Per the parent's rule, artists are built outsideupdateand only mutated inside — see Artists in Matplotlib. - Frame
islices the data. trail = pointsx[:i]; dot = the single point(x[i], sin(x[i])). Why this step? Growing the slicex[:i]withiis what makes the trail lengthen — look at the figure: the mint trail grows left-to-right while the coral dot rides its tip. - Return BOTH artists.
return line, dot. Why this step? Withblit=True, matplotlib redraws exactly what you return. Omit the line and the trail freezes.
Verify (sanity, no arithmetic): at , trail is empty (x[:0]), dot at — a single starting point, matches "frame before motion." At the last , x[:i] is the whole curve. ✓ The figure's frames-side captions confirm the slice lengths .
Example 4 — a generator as frames, with save_count (cell: generator · saving · unknown length)
Forecast: can matplotlib know the length of a generator by itself?
- Count the yields. Values are up to but not including the first . Count .
Why this step? A
while t < 6.28with step yields indices through (since but ). That's 126 values. - Live playback is fine — the timer just keeps pulling from the generator until it's exhausted. See Generators in Python.
- Saving needs a count. A generator has no
len.save_count=200tells.savean upper bound on how many frames to pull. Why this step? The saver must pre-know (or cap) how many images to write; it can't ask a generator "how long are you?"
Verify: ✓ and ✓, so exactly 126 frames. Since , save_count=200 captures all of them with room to spare. ✓
Example 5 — blit=False: the return value stops mattering (cell: blit off · one artist)
Forecast: will forgetting the return break a blit=False animation?
- With
blit=False, the whole axes is redrawn each frame. The return value is ignored. Why this step? Blitting is the only feature that reads your returned artists (it needs to know what to copy over the cached background). No blit → nothing reads the return. - So
return line,is harmless but optional here. Keeping it means the same code works if you later flipblit=True. Why this step? Future-proofing: one habit (always return artists) works for both modes. - Cost: slower, because ticks/grid/labels are re-rendered every frame — the exact expense blitting was invented to skip.
Verify (behavioural): a blit=False animation with no return statement still animates correctly (proves the return is ignored); a blit=True one with no return raises/freezes (proves blit reads it). This is a logic check, not arithmetic — the asymmetry is the whole point.
Example 6 — degenerate: zero frames, one frame, interval 0 (cell: empty / single / limiting)

Forecast: does frames=0 crash, freeze, or show a static picture?
frames=0→ one pass shows nothing move.updateis never called with a frame value; you see only whateverinit_funcdrew (the clean background we defined at the top). s of motion. Why this step? With the second equation gives : no frames means no elapsed animation time. The figure's left panel shows just the cleaninit_funcbackground.frames=1→ a single flip.updateruns once (frame0), then (becauserepeat=Trueby default — the loop-when-frames-run-out switch we defined at the top) loops that one frame forever, so it looks like a still image. . Why this step? It's the smallest non-empty case; useful to test that your artists appear at all.interval=0→ "as fast as the backend allows". is undefined mathematically; in practice the GUI timer fires as quickly as the event loop can, so real FPS is capped by your machine, not the formula. Why this step? This is the limiting behaviour of the first equation: as , FPS , but hardware clamps it. Never divide by literal zero in your head — read it as a limit.
Verify: (a) s ✓. (b) s ms for the single frame ✓. (c) limit check: as interval shrinks ms, FPS rises , confirming FPS as interval . ✓
Example 7 — the never-ending generator (cell: generator that never stops · limiting)
Forecast: will live playback ever stop? Will saving hang forever?
- Live playback runs indefinitely. The generator never raises
StopIteration, so the timer keeps pulling newtvalues everyintervalms until you close the window. Why this step?FuncAnimationstops a pass only when the frame source is exhausted; an infinite source is never exhausted. Withrepeatirrelevant (there's no "end" to loop from), it's just an endless stream. - Saving must be capped.
.savewalks the generator to write images. With no length and nosave_count, matplotlib cannot know when to stop — it would try to pull frames forever. Why this step? This is exactly whysave_countexists (Example 4): it is mandatory for an infinite generator.anim.save("out.mp4", fps=30, save_count=300)writes the first 300 frames and stops. - Fix pattern. Either make the generator finite (add a stop condition) or always pass
save_countwhen saving. Why this step? Both give the saver a definite endpoint; the finite generator also lets live playback end naturally.
Verify: with save_count=300 at fps=30, the saved clip length is s — a definite, finite file even though the generator is infinite. ✓
Example 8 — the classic "window is blank" bug (cell: exam twist · degenerate reference-loss)
Forecast: is the bug in update, in interval, or somewhere else entirely?
- Spot the missing assignment. The call result was not stored: no
anim = .... Why this step?FuncAnimationreturns an object that owns the timer. Nothing else keeps it alive. - Python garbage-collects it. Once the unreferenced object is collected, its timer dies — no more frames. Why this step? This ties to the event loop: the timer is a live object; drop the reference, drop the heartbeat.
- Fix:
anim = FuncAnimation(...)and keepanimin scope (module-level or returned from your function).
Verify (behavioural): identical code that only adds anim = animates; removing it reproduces the blank window every time — isolating the reference as the sole cause. ✓
Active recall
Connections
- Animation — FuncAnimation — parent: the machine these scenarios exercise.
- Matplotlib Figure and Axes — the canvas being updated each frame.
- Artists in Matplotlib —
Line2D,set_data, blit-return. - Generators in Python — Examples 4 & 7's frame source.
- Event Loops / GUI backends — the timer behind Examples 6 & 8.
- NumPy linspace and broadcasting — building the per-frame arrays (Example 3's
x). - Saving figures and ffmpeg — Examples 2, 4 & 7's
.save.