Manual keyframing works fine for a few shots, but it quickly gets repetitive when you need to do the same animations. The time you spend dragging handles in the Graph Editor is time you could spend on something more interesting.
Python scripting in Blender solves a specific problem: any animation that is repetitive, data-driven, or needs to be reproduced reliably across multiple objects or shots should not be authored by hand. This tutorial will show you exactly how to do it.
By the end of this article, you will have a working script that reads camera position data from a CSV file and produces a complete animation in Blender without a single manual keyframe. You can drop this script into any production pipeline, swap the CSV, and get a new animation in seconds.
You can find the complete source code for the example integration showcased in this guide on our GitHub:
🔗 https://github.com/cgwire/blog-tutorials/tree/main/blender-programmatic-animation
1. Animation Data Structures
Before writing any code, you need to understand the data structures Blender use internally.
The hierarchy works like this: an Object (2D or 3D) holds animation data, that animation data references an Action, the Action contains a collection of F-Curves, and each F-Curve holds a sequence of keyframe points.
Each F-Curve maps to exactly one channel. The channel is identified by two things: a data_path (a string like "location" or "rotation_euler") and an array_index (an integer that identifies which axis, where 0 is X, 1 is Y, and 2 is Z). Each keyframe point on that F-Curve is a coordinate pair: a frame number and a value.
Relevant Python objects to know are bpy.data.actions, action.fcurves, fcurve.keyframe_points, and the data_path and array_index attributes on each F-Curve.
One practical note for studios: it's always a good idea to name your Actions explicitly. Blender will create anonymous Actions by default, and those disappear when an object is duplicated or the file is cleaned up. Naming them gives you something stable to reference in scripts and something identifiable in the Action Editor when a problem needs to be tracked down later.
2. Insert Keyframes Programmatically
There are two methods for inserting keyframes through Python, and choosing the right one matters for performance.
The first method is object.keyframe_insert(data_path, index, frame). It mirrors what happens when you press I in the viewport. It respects drivers and constraints, evaluates the scene correctly, and is the safest option when you are working with a small number of keyframes or when your object depends on other scene elements.
import bpy
obj = bpy.data.objects["Cube"]
for frame in range(0, 60, 5):
obj.location.x = frame * 0.1
obj.keyframe_insert(data_path="location", index=0, frame=frame)
The index=0 argument tells Blender to keyframe only the X axis. Use 1 for Y and 2 for Z. If you omit the index, all three axes get keyframed, which is often not what you want.
A common mistake when using this method is forgetting to set the value before calling keyframe_insert. The function records whatever the property is currently set to. If you call it without updating the value first, every keyframe will have the same value and your animation will be flat.
The second method inserts points directly into an F-Curve:
fcurve.keyframe_points.insert(frame, value)
This method bypasses the UI logic layer entirely, so it's significantly faster when inserting hundreds or thousands of keyframes. Use it when you are building data-driven animations from external sources and speed is a concern. The tradeoff is that it does not trigger constraint or driver evaluation, so use keyframe_insert if your object has dependencies that need to update.
3. Read and Modify F-Curves
Once keyframes exist, you can access and modify the underlying F-Curves directly to precisely control interpolation and timing.
To access an F-Curve after keyframes have been inserted:
action = obj.animation_data.action
fcurve = action.fcurves.find("location", index=0)
# OR
slot = action.slots[0]
channelbag = action.layers[0].strips[0].channelbag(slot)
for fcurve in channelbag.fcurves:
# ... iterate over every fcurve
- An Action is a top-level container holding all animation data.
- A Slot binds the action to a specific object, allowing one action to animate multiple objects.
- A Layer is an animation layer that can be blended with others, like tracks in a video editor.
- A Strip is a time-ranged clip within a layer, similar to a clip on a timeline.
- A Channelbag groups all F-curves belonging to a specific slot within a strip.
- An F-curve animates a single property channel (e.g. X location) over time via keyframes.
- A Keyframe point is a single keyframe on an F-curve, with an interpolation mode controlling movement to the next key.
To read or modify keyframe values:
for kp in fcurve.keyframe_points:
print(kp.co)
kp.co.y *= 2
fcurve.update()
We iterate over all keyframe points in a Blender F-curve, print each keyframe's (frame, value) coordinate pair, double the value component, then update the curve to apply the changes.
The fcurve.update() call is not optional. Blender caches curve data internally, and skipping this call means your changes may not take effect or may produce inconsistent results during rendering.
For interpolation, the three modes you will most often use in production are CONSTANT, LINEAR, and BEZIER. CONSTANT holds the value until the next keyframe, which is useful for cut-out animation and anything with discrete states. LINEAR produces mechanical, even motion, which works well for camera rigs, UI animations, and procedural effects. BEZIER produces organic, eased motion and is the default for most animation work.
To set interpolation on all keyframe points in a curve:
for kp in fcurve.keyframe_points:
kp.interpolation = "LINEAR"
fcurve.update()
4. Build a Data-Driven Animation from a CSV
Now you know the basics, let's use a more complete example you could even use in production. The scenario is a camera fly-through where the position data has been exported from another tool, whether that is a surveying application, a game engine, a previs tool, or a spreadsheet authored by a director or client.
The CSV format for this tutorial is straightforward: one header row followed by data rows with four columns, frame, x, y, and z.
frame,x,y,z
0,0.0,0.0,5.0
10,1.5,0.2,4.8
20,3.1,0.5,4.5
30,4.8,0.9,4.1
Now the complete script to animate a camera along a path defined in the CSV file:
import bpy
import csv
obj = bpy.data.objects["Camera"]
scene = bpy.context.scene
obj.animation_data_clear()
with open("camera_path.csv", newline="") as f:
reader = csv.reader(f)
next(reader)
for row in reader:
frame = int(row[0])
x, y, z = float(row[1]), float(row[2]), float(row[3])
scene.frame_set(frame)
obj.location = (x, y, z)
obj.keyframe_insert(data_path="location", frame=frame)
action = obj.animation_data.action
action.name = "CAM_flythrough_v01"
slot = action.slots[0]
channelbag = action.layers[0].strips[0].channelbag(slot)
for fcurve in channelbag.fcurves:
for kp in fcurve.keyframe_points:
kp.interpolation = "LINEAR"
fcurve.update()
print("Done. Keyframes inserted and interpolation set.")
- First we grab the Camera object and current scene, then clears any existing animation data on it.
- Then we open the camera_path.csv file, read each row (frame number + X/Y/Z position), jump to the corresponding frame with
frame_set, set the camera's location, and insert a location keyframe. - We retrieve the animation action that was just created and gives it the name "CAM_flythrough_v01".
- To set the interpolation to linear, we navigate the animation data structure (slots → layers → strips → channelbag) to reach each F-curve, then sets every keyframe's interpolation to LINEAR so the camera moves at a constant speed between points. In this simple example, we only have one slot/layer/strip/channelbag so it's easier, but otherwise you'd need to iterate recursively.
- Finally, we call fcurve.update() on each curve and print a last confirmation message.
Try out the script yourself: it's available on Github in our repository for blog tutorials.
Conclusion
We now have a repeatable pipeline step that replaces manual keyframing for any animation whose values can be expressed as tabular data.
The same pattern applies to object visibility, material properties, light intensity, camera focal length, or any other property that can be keyframed in Blender: any data source that can produce a CSV, a JSON file, or any structured output can feed directly into a Blender animation without an animator touching the timeline.
And this pattern holds the real value for a studio: external data flows in, Python transforms it into keyframes with consistent interpolation, and the output is deterministic and version-controllable. When you need to change the camera path, you just have to regenerate the scene. When the timing needs to shift across 40 shots, you update the script. You are no longer doing that work by hand.



