Digital Artists

Blender Python: Getting Started With bpy, When the Mouse Stops Being Enough

Open the Scripting workspace, read the operator you just clicked, and turn forty identical clicks into four lines. A first hour in bpy for artists, not programmers.

Today we wrote about 3DCodeVerse, a two-million-program dataset built on the premise that the right representation for a 3D scene is the code that generates it rather than the geometry it produces. The researchers are arguing it to machines. Artists have known it for years, and in Blender the entry point is bpy.

This is a first hour. No prior Python assumed.

When to reach for a script

Be honest about this first, because the failure mode is spending ninety minutes automating a nine-minute job.

Script it when:

  • You are about to do the same thing more than roughly twenty times.
  • The task has a number in it that you will want to change later (count, spacing, seed, angle).
  • You need to touch every object in a scene and the pattern of which ones is a rule, not a selection.
  • You want to rename, re-link, re-path or audit a file with hundreds of datablocks.
  • You need the result to be reproducible — the same file, exactly, next month.

Do not script it when: Geometry Nodes already does it. Nodes are faster to iterate, live-update, and survive being handed to someone who does not write code. Scripting beats nodes for scene assembly, file wrangling, batch operations and anything that needs to touch Blender’s data outside the modifier stack. It loses for geometry.

Step 1: the Scripting workspace

Top bar → the Scripting tab. You get four things that matter:

  1. Python Console — a live prompt into the running Blender. Type, get an answer immediately.
  2. Text Editor — where scripts live. New, then Run Script (or Alt+P).
  3. Info — the log of everything you have done. This is the part people miss.
  4. Outliner / Properties — the normal scene views.

Start in the console. Type:

bpy.data.objects

and press Enter. You get a listing of every object in the file. You are already in.

Step 2: let Blender tell you the API

This is the single most useful habit in Blender scripting and it takes thirty seconds to learn.

Add a cube with the mouse: Add → Mesh → Cube. Now look at the Info editor. It printed:

bpy.ops.mesh.primitive_cube_add(size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))

That is the exact call your click made, with every argument. Copy it into the Text Editor and it works.

Do the same for anything you do not know how to script: do it with the mouse, read Info, copy the line. You almost never need to search documentation to find which function — only to find out what its arguments mean.

Second half of the habit: right-click almost any field in the interface → Copy Full Data Path. That gives you the bpy.data... address of that specific property. Right-click a cube’s X location and you get something like bpy.data.objects["Cube"].location[0].

Two discovery tools, both built in, both pointing at different halves of the API. Which brings us to the thing that confuses everybody.

Step 3: bpy.ops versus bpy.data — the one distinction that matters

bpy.ops is doing what the user does. It runs operators — the same code the buttons run. It depends on context: what is selected, which mode you are in, which editor the mouse is over. bpy.ops.object.delete() deletes the selection.

bpy.data is reaching into the file. It addresses datablocks directly and does not care what is selected. bpy.data.objects["Cube"].location = (1, 2, 3) moves that cube whatever is selected.

The practical rule, learned by everyone the hard way:

Prefer bpy.data whenever you can. Use bpy.ops only for things that have no data equivalent.

Why: bpy.ops calls are slow (each one pushes an undo step and re-evaluates), and they break the moment the context is not what you assumed — which is exactly what happens when you run the same script from a menu entry instead of the Text Editor. A loop of 500 bpy.ops.mesh.primitive_cube_add() calls will take visible seconds and may fail outside the 3D viewport. Building 500 objects through bpy.data is instant and context-free.

So: discover with bpy.ops from the Info log, then rewrite in bpy.data once you know what you are doing.

Step 4: your first real script

A hundred cubes on a grid, with a wobble, as data:

import bpy
import math

COUNT = 10        # per side
SPACING = 2.5
WOBBLE = 0.35

# Make one cube the easy way, then reuse its mesh for every object:
# 100 objects, 1 mesh datablock.
bpy.ops.mesh.primitive_cube_add(size=1)
scratch = bpy.context.active_object
scratch.name = "Scratch"
mesh = scratch.data
mesh.name = "GridCube"

collection = bpy.data.collections.new("Grid")
bpy.context.scene.collection.children.link(collection)

for x in range(COUNT):
    for y in range(COUNT):
        obj = bpy.data.objects.new(f"Cube_{x}_{y}", mesh)
        obj.location = (
            x * SPACING,
            y * SPACING,
            math.sin(x * 0.6) * math.cos(y * 0.6) * WOBBLE * SPACING,
        )
        obj.rotation_euler = (0, 0, (x + y) * 0.1)
        collection.objects.link(obj)

bpy.data.objects.remove(scratch, do_unlink=True)

Read what that does, because the shape generalises to most useful scripts:

  • One bpy.ops call, used once, to get geometry Blender already knows how to make.
  • bpy.data.objects.new(name, mesh) creates an object without touching the scene — and crucially, a hundred objects sharing one mesh. Doing this with primitive_cube_add in the loop gives you a hundred separate meshes and a bloated file.
  • collection.objects.link(obj) is what actually puts it in the scene. New objects are not in the scene until linked, which catches everybody once.
  • Named constants at the top. Change COUNT to 40 and re-run. This is the entire reason to be here.

Run it. Change three numbers. Run it again. That loop is the payoff.

Step 5: iterating over what already exists

The other half of practical scripting is auditing and fixing a file you did not make:

import bpy

# Every mesh object with no material
for obj in bpy.data.objects:
    if obj.type == 'MESH' and not obj.data.materials:
        print(f"no material: {obj.name}")

# Rename by prefix
for obj in bpy.data.objects:
    if obj.type == 'MESH' and not obj.name.startswith("SM_"):
        obj.name = "SM_" + obj.name

# Find every image whose file is missing
for img in bpy.data.images:
    if img.source == 'FILE' and not img.packed_file and img.size[0] == 0:
        print(f"broken texture: {img.name} -> {img.filepath}")

Three snippets, each solving a real problem that is miserable by hand in a file with four hundred objects. Note that none of them select anything, change mode, or depend on where the mouse is.

Step 6: things that will bite you

  • bpy.context.active_object is None more often than you expect — nothing selected, or wrong editor. Guard it.
  • Deleting while iterating. for obj in bpy.data.objects: bpy.data.objects.remove(obj) mutates the collection you are walking. Build a list first: for obj in list(bpy.data.objects):.
  • Units. primitive_cube_add(size=2) gives a 2 m cube, so the default cube is 2 m across, not 1. Scale values multiply on top.
  • Radians, not degrees. rotation_euler is radians. math.radians(45).
  • The API changes between versions. The bpy reference is versioned for a reason; a script from a 3.x tutorial may use an operator argument that no longer exists. When something errors with an unexpected keyword, check the current docs for that operator rather than assuming you typo’d.
  • Undo does not always cover bpy.data edits the way it covers operator calls. Save before running something destructive.

Where to go next

Three natural directions, in order of usefulness:

  1. Drivers and custom properties — scripted relationships that live in the file and need no script to run.
  2. Operators and panels — wrapping your script in a button, with bpy.types.Operator and bpy.types.Panel, which is how an add-on is born.
  3. Headless rendering — blender --background file.blend --python script.py, which turns a script into a pipeline step and is where scripting starts paying for itself on real deadlines.

The official references are the ones to bookmark: the Python API quickstart is short and worth reading end to end, and the manual’s scripting introduction explains how the pieces fit together.

Sources