Post 1 of 10 — Blender Add-on Development with Python
Add-ons, you use them every day to customize Blender, speed up your workflow, and unlock new capabilities. But have you ever wondered how “extensions” or “add-ons” are made? The secret isn’t complicated. They are nothing more than simple Python scripts with a little extra structure.
Rather than walking through a bunch of disconnected examples, we’re going to build one real, useful add-on across all 10 posts: a Batch Render Manager.
At some point, you have probably needed to render multiple scenes, camera angles, or lighting setups. You decide to render overnight, only to find yourself babysitting Blender to kick off each one manually. As you sit there in the middle of the night, you understand the problem our add-on seeks to solve. Our add-on will let you:
- Queue up multiple render jobs and run them automatically
- Override render settings (resolution, samples, output path) per job, without touching your scene
- Send a notification when the queue finishes
- Recover gracefully when something goes wrong
Each post in this series builds on the last, so by Post 10 you’ll have a complete, packaged add-on you can actually use as well as a solid understanding of how it works and why it’s structured the way it is.
Before We Write Any Code
Let’s get your environment set up. Nothing fancy required.
What You Need
- Blender 4.x — Download it from blender.org if you haven’t already. The concepts here apply to 3.x as well, but we’ll be targeting 4.x.
- A text editor — Blender has a built-in one, but for anything beyond a quick experiment, you’ll want something external. VS Code with the Python extension is a great choice.
- Python basics — You don’t need to be an expert. If you understand variables, functions, and classes, you’re good to go.
That’s it. No special tools, no complex build pipeline.
Setting Up VS Code for Blender Development
VS Code won’t know about Blender’s Python environment out of the box, so autocomplete and type hints won’t work for Blender-specific stuff like bpy. The easiest fix is the Blender Development extension by Jacques Lucke.
- Open VS Code and go to the Extensions panel (
Ctrl+Shift+XorCmd+Shift+X) - Search for “Blender Development” and install it
- Press
Ctrl+Shift+PorCmd+Shift+P, type Blender: Start, and point it to your Blender executable
Once connected, you can run and reload scripts directly from VS Code.
Next, we need to install the Python fake-bpy-module package that gives you bpy type stubs for autocomplete even without the extension. Install it with pip install fake-bpy-module-4-0 (note you can change the 4-0 to whatever version of Blender you are using).
“Add-ons” or “Extensions”?
If you’re on Blender 4.2 or later and you’ve poked around in Preferences looking for the Add-ons section, you may have noticed it’s gone. In its place is a new Extensions panel. So what’s going on?
Starting with 4.2, Blender introduced a new packaging and distribution format called Extensions. An extension is essentially a .zip file containing your add-on’s code plus a manifest. The manifest is a small file that declares metadata like the name, version, license, and what permissions it needs (like internet or file system access). Extensions can be published to and installed from Blender’s official online platform at extensions.blender.org, which replaced the old bundled add-ons system.
The important thing to understand is that the Python code inside an extension is still just a Blender add-on. The operators, panels, properties, register(), unregister() all work exactly the same way. What changed is the packaging and distribution format, not the API.
The old format — a plain .py file or folder installed from disk — is now called a legacy add-on. Blender still supports it and will continue to do so, but it’s officially considered deprecated going forward.
For this series, we’re going to start with the legacy add-on format. It’s simpler to set up and iterate on while you’re learning, and it lets us focus on the Python and Blender API concepts rather than packaging details. In Post 9, when we cover packaging and distribution, we’ll walk through what it takes to convert our finished add-on into a proper extension.
TL;DR: Extensions are the new way to distribute add-ons. The code is the same. We’ll get there, but not yet.
Your First Add-on
Let’s write a minimal but valid Blender add-on right now — just to see all the moving parts.
Open Blender’s Scripting workspace (it’s one of the tabs along the top). Click New to create a fresh script, and paste this in:
bl_info = {
"name": "Hello Blender",
"author": "You",
"version": (1, 0, 0),
"blender": (4, 0, 0),
"location": "View3D > Sidebar > Hello Tab",
"description": "A minimal add-on to prove it works",
"category": "Development",
}
import bpy
class HELLO_OT_say_hello(bpy.types.Operator):
"""Print a message to the Info log"""
bl_idname = "hello.say_hello"
bl_label = "Say Hello"
def execute(self, context):
self.report({'INFO'}, "Hello World from your first add-on!")
return {'FINISHED'}
class HELLO_PT_panel(bpy.types.Panel):
"""A simple panel in the 3D viewport sidebar"""
bl_label = "Hello Panel"
bl_idname = "HELLO_PT_panel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Hello Tab"
def draw(self, context):
layout = self.layout
layout.operator("hello.say_hello")
def register():
bpy.utils.register_class(HELLO_OT_say_hello)
bpy.utils.register_class(HELLO_PT_panel)
def unregister():
bpy.utils.unregister_class(HELLO_PT_panel)
bpy.utils.unregister_class(HELLO_OT_say_hello)
if __name__ == "__main__":
register()PythonHit Run Script (the play button, or Alt+P). Then go to the 3D viewport, press N to open the sidebar, and look for the Hello Tab. Click Say Hello. You should see Hello from your first add-on! appear in the Info log at the top of the screen.
Congratulations. You just wrote a Blender add-on.
What Just Happened
It feels like a lot of code to just say Hello World. Let’s break down the key pieces, because you’ll see all of these in every add-on you ever write.
bl_info = {
"name": "Hello Blender",
...
}Pythonbl_info
This dictionary is how Blender identifies your add-on in the Preferences window. It’s metadata — name, author, version, which Blender version you’re targeting, and where it lives in the UI.
Operators (bpy.types.Operator)
An Operator is a thing you can do in Blender. Clicking a button, applying a modifier, running a render — these are all operators. When you write bpy.types.Operator, you’re defining a new action that Blender can execute.
Every operator needs two identifiers:
bl_idname— a unique ID in the format"category.action". This is what you use to call the operator from code or the UI.bl_label— the human-readable name shown in buttons and menus.
The execute method is where the actual work happens. It must return {'FINISHED'} (or {'CANCELLED'}) to tell Blender whether it succeeded.
Panels (bpy.types.Panel)
A Panel is a section of the UI — like the sidebar tabs you see in the 3D viewport. You define where it lives (bl_space_type, bl_region_type) and what tab it belongs to (bl_category). The draw method builds the UI elements inside it.
register() and unregister()
These two functions tell Blender about your classes when the add-on is enabled, and clean up when it’s disabled. Order matters — we unregister in reverse order of registration.
The bpy Module: Your Gateway to Everything
bpy is Blender’s Python API. It’s how you access and control almost everything in Blender from code. It has a few major sub-modules you’ll use constantly:
| Module | What it’s for |
|---|---|
bpy.types | Base classes for operators, panels, properties, etc. |
bpy.props | Property types (strings, floats, booleans, enums…) |
bpy.utils | Registering/unregistering classes |
bpy.data | The actual scene data — meshes, objects, materials, scenes |
bpy.context | The current state — active object, mode, selected objects |
bpy.ops | Calling built-in (and custom) operators |
Don’t worry about memorizing all of this. You’ll get comfortable with each piece naturally as we use them throughout the series.
What’s Coming Next
In our next post, we will give our project proper structure. Right now our add-on is a single file. For anything more than a toy script, you’ll want a package — a folder with multiple files, each responsible for a different part of the add-on. We’ll set that up and add the metadata and registration scaffolding that will carry us through the rest of the series.
By the end of the next post, we’ll have the skeleton of the Batch Render Manager in place — nothing functional yet, but everything organized and ready to build on.
See you there.
Have questions or ran into a snag? Subscribe to the newsletter — I share work-in-progress updates, upcoming tutorials, and early looks at what I’m building, including the ongoing PyMinMaximus chess engine series.
