Intuition The ONE core idea
Motion on a screen is a lie your eyes fall for: it is just still pictures swapped so fast the brain smears them into movement. FuncAnimation is a robot that holds your stack of almost-identical pictures and swaps them on a stopwatch — and the whole art is to change one small thing in each picture instead of redrawing everything.
This page assumes you know nothing . Before you can read the line line, = ax.plot([], []) you need to know what a Figure , an Axes , an artist , set_data, a generator , a frame , the update function , init_func, the event loop , and even the lonely trailing comma actually are. We build each from a picture, in an order where each one leans on the one before, and at the end we wire them all together into the real FuncAnimation(...) call and start it running.
Recall Required pre-reading vs. self-contained
This deep dive is self-contained : every pitfall and mechanic below is explained here in full. The linked notes (e.g. Artists in Matplotlib , Generators in Python ) are optional enrichment, not prerequisites — you can finish this page without opening them.
Definition The imports every snippet on this page assumes
Every code block below is written as if these three lines already ran at the top of the file:
import numpy as np # gives us np.linspace, np.sin, np.pi
import matplotlib.pyplot as plt # gives us plt.subplots, plt.show
from matplotlib.animation import FuncAnimation # the star of the topic
np and plt are just short nicknames. Whenever you see plt.subplots() or np.sin(...) remember they come from these two libraries; FuncAnimation comes from the third line.
Intuition Flip-book = animation
Look at the figure. Each card is one drawing. The dot moves only a tiny bit from card to card. Flip them at, say, 50 cards per second and your eye can no longer see individual cards — it sees a flying dot . Every word below is just a name for one part of this flip-book.
The stack of cards → the sequence of frames .
One card → one frame (one still picture).
The speed you flip → the interval (how long each card is shown).
The rule "for card n , put the dot here" → your update function .
The robot flipping → the event loop / timer .
Keep this figure in mind; every symbol maps onto it.
Before anything moves, we need a surface to draw on . Matplotlib splits this into two nested things.
Definition Figure and Axes
A Figure (written fig in code) is the whole sheet of paper — the outer rectangle, the window, the page you would save to a file.
An Axes (written ax) is one plotting rectangle inside that paper — the part with the x-axis, y-axis, grid, and your actual curve. One fig can hold several ax.
In the picture: the pale outer rectangle is the Figure ; the inner boxed region with tick marks is the Axes . The code
fig, ax = plt.subplots() # plt = matplotlib.pyplot (see imports above)
hands you both at once: fig = the paper, ax = the one drawing box on it.
Intuition Why the topic needs this
The whole point of FuncAnimation is to update the same paper over and over instead of throwing it away and buying new paper each frame. So we must first own one persistent fig. That single object is what the robot keeps flipping.
See Matplotlib Figure and Axes for the full anatomy.
You do not draw pixels by hand. Instead, matplotlib gives you objects that know how to draw themselves . These are called artists.
An artist is any drawable object matplotlib manages — a line, a dot, a piece of text, the grid. The line you plot is an artist of type Line2D. Think of it as a puppet : it holds its own data (its x-values and y-values) and repaints itself whenever asked.
When you write
line, = ax.plot([], [])
you are creating one Line2D puppet on the axes. Right now it holds empty data ([] and []), so it draws nothing — but the puppet exists, ready to be told where to be.
Intuition Why "create once" matters
A puppet you make once can be repositioned forever. If instead you called ax.plot(...) every frame, you would spawn a new puppet each time and never remove the old ones — they pile up, the memory grows, and the picture gets crowded with frozen ghosts. So: one puppet, many positions.
More on Line2D, markers, and blitting live in Artists in Matplotlib .
Here is the single most important verb in the whole topic.
==line.set_data(xs, ys)== tells the existing line puppet: "forget your old points, your points are now xs (the horizontal positions) and ys (the vertical positions)." It mutates the puppet in place — same object, new pose.
xs — a sequence of x-coordinates (left-right).
ys — a matching sequence of y-coordinates (up-down).
They must be the same length : point k is at ( x k , y k ) .
Common mistake Passing a bare number to set_data
Why it feels right: for a single dot you think "its x is 5, its y is 3, so dot.set_data(5, 3)."
Why it's fragile: set_data wants sequences , not lone numbers. A bare scalar can raise or behave oddly across versions.
Fix: wrap in lists — dot.set_data([5], [3]).
Intuition Why the topic needs set_data
This is the "nudge the dot a little" step of the flip-book. Each frame calls set_data once to slide the same puppet to its new spot. No set_data, no motion.
To move a whole wave we need many x-points and a y-value at each.
==np.linspace(a, b, N)== builds N evenly spaced numbers from a to b inclusive. np.linspace(0, 2*np.pi, 400) gives 400 points marching from 0 to 2 π across the axes.
Here π (np.pi) is just the number ≈ 3.14159 ; 2 π ≈ 6.283 is one full turn of a sine wave. We choose [ 0 , 2 π ] so exactly one hump-and-trough fits on screen.
To make the wave travel , we write np.sin(x - 0.1*frame). Why subtract?
Intuition Why subtracting shifts the wave right
sin ( x ) has its peak where its input equals 2 π . In sin ( x − s ) the peak now needs x − s = 2 π , i.e. x = 2 π + s . Bigger s ⇒ the peak sits further right . As frame grows, s = 0.1 ⋅ frame grows, so every feature slides right — the wave travels. That single minus sign is the entire animation.
Because one NumPy expression computes all 400 y-values at once, we lean on NumPy linspace and broadcasting .
Definition frame vs frames
A frame is one still picture — one card of the flip-book.
frames (the argument) is what the robot iterates over to know which cards to make. It can be:
an integer N → the values 0 , 1 , 2 , … , N − 1 are fed to your function one at a time;
any iterable (a list, a range, or a generator ) → each item is fed in turn.
So when you write frames=200, the update function receives frame = 0, then 1, … up to 199. When you write frames=frame_gen, each value the generator yields becomes frame.
Intuition Why allow both a number and a generator?
A plain count 200 is perfect when the only thing that changes is a counter. But sometimes each frame needs a custom value (a time t = 0.05, 0.10, …), and computing those inline is cleaner with a generator. That is why generators matter — which forces us to define what a generator is next.
A generator is a function that, instead of return-ing once, uses ==yield== to hand back one value, then pause , remembering exactly where it was. Ask it again and it resumes right after the yield. It produces values lazily — only when asked.
In the figure, the generator is a vending slot: each "next!" pops out the next value (t = 0.00 , then 0.05 , then 0.10 …) and then freezes mid-code until the next request.
def frame_gen ():
t = 0.0
while t < 6.28 :
yield t # hand out t, then FREEZE here
t += 0.05 # on next request, resume from here
Intuition Why generators fit animation perfectly
The robot only ever needs the next frame's value — never all of them at once. A generator supplies exactly that, one at a time, forever if needed, without pre-computing a giant list. But this laziness has a cost: you cannot ask a generator "how long are you?" It doesn't know until it stops. That is why saving a generator-driven animation needs save_count (defined in §14) — a manual cap on how many frames to grab.
Deeper mechanics: Generators in Python .
Now the star of the show. FuncAnimation never draws anything itself — it just calls your function once per frame and lets that function reposition the puppets.
Definition The update function
func
==func(frame)== is a plain Python function you write. Its single argument, frame, is the current item from frames (an integer, or a generator's yielded value). Inside, you (1) compute this frame's data, (2) set_data the puppet(s) to that data, and (3) return the changed artists (needed when blitting — §9). You pass this function by name to FuncAnimation as its second argument.
Worked example The canonical update function — a travelling wave
x = np.linspace( 0 , 2 * np.pi, 400 )
line, = ax.plot([], [], lw = 2 ) # puppet made ONCE, outside func
def update (frame): # frame = 0, 1, 2, ... fed in by FuncAnimation
y = np.sin(x - 0.1 * frame) # this card's y-values (wave shifted right)
line.set_data(x, y) # slide the SAME puppet to the new pose
return line, # tell blit what changed (1-tuple)
update is never called by you — FuncAnimation calls it, handing in frame each tick.
func takes frame and returns artists
It needs frame to know which card to draw (frame 0 vs frame 50 look different). It returns the artists so the fast redraw path (blitting) knows exactly what to repaint. Everything else — the timer, the repainting schedule — is the robot's job, not yours.
Before the very first real frame, matplotlib wants a known blank starting state — think of the cover of the flip-book, the picture "before frame 0."
==init_func()== is an optional function you write that draws the clean background / empty state once at the start (and again at each loop when repeat=True). It takes no frame argument , empties the puppets (e.g. line.set_data([], [])), and — like func — returns the artists so blitting can cache a correct background.
Intuition Why you need it (especially with blit)
Blitting saves the background once and pastes moving artists onto it. If the puppet starts with leftover data, that stale drawing gets baked into the cached background and haunts every frame. init_func guarantees a clean slate. Without blit it is optional; with blit it is how you promise "here is the correct empty starting picture."
Blitting = "save the unchanging background as a photograph once, then each frame only repaint the moving puppet and paste (blit) it onto that saved photo." It skips redrawing the grid, ticks, and labels every frame.
Intuition Why blitting demands a
return
If matplotlib is only going to repaint the moving artists, it must be told which artists moved . That is the entire reason both func and init_func must return the changed artists. Forget to return one, and blit never repaints it — it looks frozen . If you animate a line and a dot, return line, dot.
Two commas confuse every beginner. Here they are, defused.
Definition The trailing comma
ax.plot(...) returns a list containing one line: [Line2D]. Writing ==line, = ax.plot(...)== unpacks that single-element list into the plain variable line. The comma says "expect exactly one item and pull it out."
return line, builds a 1-tuple (line,) — an iterable containing one artist — because blit wants an iterable, not a lone object.
Mnemonic Comma = "there's a container here"
A trailing comma always means a tiny container: on the left it unpacks a 1-item list; on the right it packs a 1-item tuple. Same comma, opposite directions.
==interval== is the number of milliseconds (thousandths of a second) each frame is shown before the next flip. Smaller interval ⇒ faster flipping ⇒ smoother/faster motion.
The frames-per-second (how many cards flip in one second) is:
FPS = interval frame ms 1000 s ms
Worked example Reading the units
interval = 20 ms/frame gives FPS = 20 1000 = 50 frames/s. And one full pass of N = 200 frames lasts T = N ⋅ interval = 200 × 20 = 4000 ms = 4 s .
Who fires the stopwatch? The event loop .
The event loop is a background "keep-checking" loop the GUI runs continuously: it watches for mouse clicks, window resizes, and timer ticks, handling whichever happens next. When you build a FuncAnimation, it registers a timer with this loop that fires every interval milliseconds. On each fire the loop calls your update function and repaints the changed artists — that is one frame.
Intuition Why the event loop matters to FuncAnimation
Because the loop keeps spinning between timer ticks, it can also process your clicks and drags, so the window stays responsive while the animation plays. A naive for-loop with plt.show() inside would instead seize control and freeze the window until it finished. FuncAnimation deliberately hands the flipping job to this loop so it owns the timing and you only describe one frame. This is also why the animation does not start until the loop is actually running (§13).
See Event Loops / GUI backends for why blocking loops feel "hung."
Now every part exists. Here is the full signature, then the complete runnable program that uses every piece above.
Definition The FuncAnimation constructor
anim = FuncAnimation(fig, func, frames = None , init_func = None ,
interval = 200 , blit = False , repeat = True , save_count = None )
fig — the persistent Figure to redraw (§1).
func — your update function func(frame) (§7). Required.
frames — an int N, an iterable, or a generator (§5–6).
init_func — the clean-background function (§8).
interval — ms between frames (§11).
blit — True = only repaint returned artists (§9).
repeat — loop back to the start when frames run out.
save_count — cap on frames grabbed when saving a length-unknown generator (§14).
Worked example Complete travelling-wave program (copy-paste runnable)
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
fig, ax = plt.subplots() # §1: one persistent canvas
ax.set_xlim( 0 , 2 * np.pi) # set limits ONCE so blit can cache background
ax.set_ylim( - 1.1 , 1.1 )
x = np.linspace( 0 , 2 * np.pi, 400 ) # §4: the x-points
line, = ax.plot([], [], lw = 2 ) # §2/§10: puppet made ONCE, empty
def init (): # §8: clean "before frame 0"
line.set_data([], [])
return line,
def update (frame): # §7: the per-card rule
y = np.sin(x - 0.1 * frame)
line.set_data(x, y)
return line, # §9: what blit repaints
anim = FuncAnimation(fig, update, frames = 200 , init_func = init,
interval = 20 , blit = True ) # §12: wire it all
plt.show() # §13: START the event loop (blocks here)
# anim.save("wave.mp4", fps=50) # or "wave.gif" — needs ffmpeg / pillow
Building anim does not by itself make anything move; it only registers the timer. The event loop must be started.
Definition Starting the animation
In a script/GUI window : call ==plt.show()==. This hands control to the event loop, which then fires the timer and drives the frames. plt.show() blocks — it runs until you close the window.
To save to a file instead of showing: call ==anim.save("out.mp4", fps=50)== (needs ffmpeg for mp4, or pillow for .gif). Saving spaces frames by 1/fps seconds, ignoring interval.
Common mistake Forgetting
anim = ...
Why it feels right: FuncAnimation(...) looks like it "starts" the animation, so you write it as a bare statement.
Why it breaks: the returned object owns the timer . If nothing holds a reference to it, Python's garbage collector deletes it, the timer dies, and the window goes blank/frozen.
Fix: always anim = FuncAnimation(...) and keep anim alive until after plt.show().
To save the movie you hand the frames to ffmpeg (mp4) or pillow (gif). See Saving figures and ffmpeg .
==save_count== is the maximum number of frames matplotlib will grab when saving an animation whose frames is a generator (or any iterable of unknown length). Because a generator cannot answer "how many values will you yield?", save_count supplies that number by hand. It has no effect on live plt.show() playback — only on anim.save(...).
Worked example Generator-driven frames with save_count
def frame_gen (): # §6: yields custom time values, unknown length
t = 0.0
while t < 6.28 :
yield t
t += 0.05
def update (t):
line.set_data(x, np.sin(x - t))
return line,
anim = FuncAnimation(fig, update, frames = frame_gen,
init_func = init, interval = 30 , blit = True ,
save_count = 200 ) # grab at most 200 frames when saving
anim.save( "wave.mp4" , fps = 30 )
Here frame_gen yields about 6.28/0.05 ≈ 126 values, so save_count=200 is a safe upper cap — matplotlib stops when the generator is exhausted or the cap is hit, whichever comes first.
Intuition Why only for saving?
Live playback just keeps asking the generator for the next value until it stops — it never needs a total. But saving must allocate/encode a finite reel of frames up front , so it needs a promised count. save_count is that promise.
Figure and Axes - the canvas
set_data - move the puppet
update func - one frame rule
Generators - yield values
init_func - clean background
Blitting - return artists
anim.save via ffmpeg and save_count
Difference between a Figure and an Axes? Figure = the whole sheet/window; Axes = one plotting rectangle (with ticks and grid) inside it.
What is an artist, and which type is a plotted line? A self-drawing object matplotlib manages; a plotted line is a Line2D.
What does line.set_data(xs, ys) do? Mutates the existing line to new x/y points in place — same puppet, new pose.
Why call ax.plot ONCE outside the update function? To reuse one artist via set_data; calling it per frame piles up new artists and breaks blit.
What does np.linspace(0, 2*np.pi, 400) produce? 400 evenly spaced numbers from 0 to 2π inclusive.
Why does np.sin(x - 0.1*frame) move the wave right? Bigger shift pushes the peak (input = π/2) to a larger x, so features slide right.
What is a generator and what does yield do? A function that hands back one value then pauses, resuming on the next request.
What is the update function func and its argument? A function you write that takes the current frame, repositions the puppets via set_data, and returns the changed artists.
What is init_func for, and what argument does it take? It draws the clean empty background before frame 0; it takes no frame argument and returns the artists.
What is the second positional argument to FuncAnimation? func, your update function.
Convert interval=20 ms to FPS. 1000/20 = 50 FPS.
What fires the per-frame timer? The GUI event loop, which also keeps the window responsive.
How do you actually START the animation on screen? Call plt.show(), which runs the event loop (and blocks until the window closes).
With blit=True, what must func (and init_func) return? An iterable of the changed artists, e.g. return line,.
What does the trailing comma in line, = ax.plot(...) do? Unpacks the single-element list ax.plot returns into line.
Why must you keep anim = FuncAnimation(...)? Otherwise it's garbage-collected, the timer stops, and the animation freezes/blanks.
What is save_count for? A cap on how many frames to grab when SAVING a generator-driven animation of unknown length; ignored during live playback.
Animation — FuncAnimation — the parent topic this page prepares you for.
Matplotlib Figure and Axes — the fig/ax canvas.
Artists in Matplotlib — Line2D, set_data, blitting.
Generators in Python — the yield machine behind frames.
Event Loops / GUI backends — the timer that fires each frame.
NumPy linspace and broadcasting — building per-frame arrays.
Saving figures and ffmpeg — exporting to mp4/gif.