Houdini's Python Environment
Houdini ships with its own Python interpreter and the hou module - a comprehensive API that exposes virtually every aspect of the application. You can run Python in several places:
- Python Shell - Windows -> Python Shell. Interactive REPL for quick tests.
- Python SOP - A SOP node that runs Python to generate or modify geometry.
- Shelf Tools - Custom buttons that execute Python scripts when clicked.
- Parameter Expressions - Python expressions on individual parameters (use sparingly - VEX expressions are faster).
- HDA Callbacks - Scripts triggered by events like parameter changes, node creation, or button presses.
- Session Module - Per-scene Python module (
hou.session) for scene-specific utilities.
# Basic hou module exploration - run in the Python Shell
import hou
# Get the current scene file path
print(hou.hipFile.path())
# Get the current frame
print(hou.frame())
# List all node categories
for cat in hou.nodeTypeCategories().values():
print(cat.name())
Houdini 19.5+ uses Python 3.9+. Older versions (18.x and below) used Python 2.7. Always check hou.applicationVersion() and import sys; sys.version to confirm your environment.
Navigating the Node Graph with Python
Everything in Houdini is a node. The hou module lets you traverse, query, and manipulate the entire node hierarchy.
import hou
# Get a node by its absolute path
geo_node = hou.node("/obj/geo1")
wrangle = hou.node("/obj/geo1/attribwrangle1")
# Get children of a node
for child in geo_node.children():
print(f"{child.name()} ({child.type().name()})")
# Find nodes by type
all_wrangles = hou.nodeType(hou.sopNodeTypeCategory(), "attribwrangle").instances()
print(f"Found {len(all_wrangles)} Attribute Wrangles in scene")
# Navigate up and down the hierarchy
parent = wrangle.parent() # Returns geo1
root = hou.node("/") # Scene root
obj_level = hou.node("/obj") # Object level
# Find a node relative to another
output = geo_node.node("OUT_final")
# Get the display node (blue flag) of a SOP network
display = geo_node.displayNode()
print(f"Display node: {display.name()}")
# Get node connections
for connection in wrangle.inputConnections():
input_node = connection.inputNode()
print(f"Input {connection.inputIndex()}: {input_node.path()}")
for connection in wrangle.outputConnections():
output_node = connection.outputNode()
print(f"Output: {output_node.path()}")
Use hou.selectedNodes() to get the current selection - it's perfect for building tools that operate on whatever the artist has selected. Always validate the selection type before acting on it.
Reading and Writing Parameters
Parameters control every node in Houdini. Python can read, set, key, and even create new parameters.
import hou
node = hou.node("/obj/geo1/transform1")
# Read parameter values
tx = node.parm("tx").eval() # Evaluated value (respects expressions)
tx_raw = node.parm("tx").rawValue() # Raw string (may be an expression)
print(f"Translate X: {tx}")
# Set parameter values
node.parm("tx").set(5.0)
node.parm("ty").set(2.5)
# Set multiple parms at once (more efficient)
node.setParms({
"tx": 5.0,
"ty": 2.5,
"tz": -1.0,
"rx": 45.0,
})
# Read vector parameter as a tuple
translate = node.parmTuple("t").eval()
print(f"Translate: {translate}") # (5.0, 2.5, -1.0)
# Set expressions on parameters
node.parm("ty").setExpression('sin($FF * 0.1) * 2', hou.exprLanguage.Hscript)
# Set keyframes
key1 = hou.Keyframe()
key1.setFrame(1)
key1.setValue(0)
key2 = hou.Keyframe()
key2.setFrame(100)
key2.setValue(10)
node.parm("tx").setKeyframes([key1, key2])
# Access string and menu parameters
wrangle = hou.node("/obj/geo1/attribwrangle1")
vex_code = wrangle.parm("snippet").eval()
run_over = wrangle.parm("class").eval() # 0=Detail, 1=Prim, 2=Point, 3=Vertex
# Batch parameter editing - useful for pipeline tools
import hou
def set_cache_paths(base_dir, version):
"""Update all File Cache nodes to use a new output path."""
cache_nodes = hou.nodeType(
hou.sopNodeTypeCategory(), "filecache"
).instances()
for node in cache_nodes:
name = node.name()
new_path = f"{base_dir}/{name}/v{version:03d}/{name}.$F4.bgeo.sc"
node.parm("file").set(new_path)
print(f"Updated {node.path()} -> {new_path}")
set_cache_paths("$HIP/cache", 3)
Creating Geometry with Python
The hou.Geometry class lets you build geometry from scratch in a Python SOP. This is ideal for importing custom data formats or generating geometry from algorithmic rules.
# Python SOP - create a grid of points with attributes
import hou
import math
node = hou.pwd()
geo = node.geometry()
geo.clear()
# Parameters
cols = node.parm("columns").eval()
rows = node.parm("rows").eval()
spacing = node.parm("spacing").eval()
# Add custom attributes
color_attrib = geo.addAttrib(hou.attribType.Point, "Cd", (1.0, 1.0, 1.0))
id_attrib = geo.addAttrib(hou.attribType.Point, "id", 0)
height_attrib = geo.addAttrib(hou.attribType.Point, "height", 0.0)
# Create points in a grid pattern
idx = 0
for row in range(rows):
for col in range(cols):
pt = geo.createPoint()
x = col * spacing - (cols - 1) * spacing * 0.5
z = row * spacing - (rows - 1) * spacing * 0.5
# Procedural height
height = math.sin(x * 0.5) * math.cos(z * 0.5) * 2.0
pt.setPosition(hou.Vector3(x, height, z))
# Color based on height
t = (height + 2.0) / 4.0 # Normalize to 0-1
pt.setAttribValue("Cd", (t, 0.5, 1.0 - t))
pt.setAttribValue("id", idx)
pt.setAttribValue("height", height)
idx += 1
# Python SOP - create polygon mesh from data
import hou
node = hou.pwd()
geo = node.geometry()
geo.clear()
# Create a simple quad
pt0 = geo.createPoint()
pt1 = geo.createPoint()
pt2 = geo.createPoint()
pt3 = geo.createPoint()
pt0.setPosition(hou.Vector3(-1, 0, -1))
pt1.setPosition(hou.Vector3( 1, 0, -1))
pt2.setPosition(hou.Vector3( 1, 0, 1))
pt3.setPosition(hou.Vector3(-1, 0, 1))
# Create a polygon from the points
poly = geo.createPolygon()
poly.addVertex(pt0)
poly.addVertex(pt1)
poly.addVertex(pt2)
poly.addVertex(pt3)
# Create geometry from a verb (more efficient for large operations)
# The Verb API runs compiled SOPs from Python
box_verb = hou.sopNodeTypeCategory().nodeVerb("box")
box_verb.setParms({
"sizex": 2.0,
"sizey": 3.0,
"sizez": 2.0,
"divsx": 4,
"divsy": 6,
"divsz": 4,
})
box_verb.execute(geo, [])
Creating geometry point-by-point in Python is significantly slower than VEX for large point counts. For 10,000+ points, prefer VEX or use the Verb API (hou.sopNodeTypeCategory().nodeVerb()) which runs compiled SOP operations at full speed from Python.
Building Shelf Tools
Shelf tools are custom buttons on Houdini's shelf that run Python scripts. They're the simplest way to deliver reusable utilities to artists.
# Shelf tool: Center selected objects at world origin
import hou
selection = hou.selectedNodes()
if not selection:
hou.ui.displayMessage("Select one or more geometry nodes first.")
else:
for node in selection:
if node.type().category() == hou.objNodeTypeCategory():
# Get the bounding box center of the displayed geometry
sop = node.displayNode()
if sop:
geo = sop.geometry()
bbox = geo.boundingBox()
center = bbox.center()
# Offset to bring center to origin
node.parm("tx").set(node.parm("tx").eval() - center[0])
node.parm("ty").set(node.parm("ty").eval() - center[1])
node.parm("tz").set(node.parm("tz").eval() - center[2])
print(f"Centered {node.name()} (offset: {center})")
hou.ui.displayMessage(f"Centered {len(selection)} object(s).")
# Shelf tool: Quick export selected geometry as FBX
import hou
import os
selection = hou.selectedNodes()
if not selection:
hou.ui.displayMessage("Select a SOP node to export.")
raise SystemExit
node = selection[0]
# Prompt for save location
hip_dir = os.path.dirname(hou.hipFile.path())
default_path = os.path.join(hip_dir, "export", f"{node.name()}.fbx")
filepath = hou.ui.selectFile(
title="Export FBX",
file_type=hou.fileType.Geometry,
default_value=default_path,
pattern="*.fbx"
)
if filepath:
# Ensure export directory exists
export_dir = os.path.dirname(hou.text.expandString(filepath))
os.makedirs(export_dir, exist_ok=True)
# Create a temporary ROP FBX node
rop_net = hou.node("/out")
fbx_rop = rop_net.createNode("filmboxfbx", "temp_fbx_export")
fbx_rop.parm("sopoutput").set(filepath)
fbx_rop.parm("startnode").set(node.path())
fbx_rop.parm("execute").pressButton()
fbx_rop.destroy()
hou.ui.displayMessage(f"Exported to:\n{filepath}")
To create a shelf tool: right-click any shelf tab -> New Tool. Paste your script in the Script tab. Set an icon and tooltip in the Options tab. Save the shelf to a .shelf file in $HOUDINI_USER_PREF_DIR/toolbar/ so it persists across sessions.
Python SOPs vs VEX
Both Python SOPs and VEX Wrangles live in the SOP network, but they have fundamentally different execution models:
| Aspect | Python SOP | VEX Wrangle |
|---|---|---|
| Threading | Single-threaded (GIL) | Multi-threaded |
| Per-point speed | Slow (~1000× slower) | Fast (JIT compiled) |
| File I/O | Full access (os, json, csv) | No file access |
| External libraries | numpy, requests, etc. | VEX functions only |
| Node creation | Can create/modify nodes | Cannot |
| Geometry creation | Full API (add points, prims) | Limited (removepoint, addpoint) |
| UI interaction | Dialogs, file pickers | None |
| Best for | Data import, tools, pipeline | Attribute math, deformers |
Use VEX when: You're doing per-point/prim math, noise, deformation, attribute manipulation, or anything that runs on every element. Performance matters here.
Use Python when: You're reading files, creating nodes, building UI, calling external APIs, or doing one-time setup operations. Convenience and access matter here.
Use both together: Python SOP reads a CSV file and creates points -> VEX Wrangle processes those points at full speed. This hybrid pattern is extremely common in production.
Automating Scene Setup
Python excels at automating repetitive scene construction - creating node networks, setting parameters, and wiring connections from a script or config file.
# Automate a complete terrain generation setup
import hou
def create_terrain_setup(name="terrain"):
"""Build a full terrain generation network from scratch."""
obj = hou.node("/obj")
# Create geometry container
geo = obj.createNode("geo", name)
geo.moveToGoodPosition()
# Create the base heightfield
hf_node = geo.createNode("heightfield", "base_heightfield")
hf_node.setParms({
"sizex": 500,
"sizey": 500,
"gridspacing": 0.5,
})
# Add noise layers
noise1 = geo.createNode("heightfield_noise", "large_features")
noise1.setParms({
"amp": 50,
"elementsize": 200,
"roughness": 0.6,
"noise_type": 3, # Sparse Convolution
})
noise1.setInput(0, hf_node)
noise2 = geo.createNode("heightfield_noise", "medium_detail")
noise2.setParms({
"amp": 15,
"elementsize": 50,
"roughness": 0.5,
})
noise2.setInput(0, noise1)
noise3 = geo.createNode("heightfield_noise", "fine_detail")
noise3.setParms({
"amp": 3,
"elementsize": 10,
"roughness": 0.45,
})
noise3.setInput(0, noise2)
# Add erosion
erode = geo.createNode("heightfield_erode", "erosion")
erode.setParms({
"iterations": 50,
"erodability": 0.4,
})
erode.setInput(0, noise3)
# Convert to polygons
mesh = geo.createNode("heightfield_mesh", "convert_to_mesh")
mesh.setInput(0, erode)
# Add output null
out = geo.createNode("null", "OUT_terrain")
out.setInput(0, mesh)
out.setDisplayFlag(True)
out.setRenderFlag(True)
out.setColor(hou.Color(0.2, 0.8, 0.2))
# Layout the network neatly
geo.layoutChildren()
print(f"Created terrain setup: {geo.path()}")
return geo
create_terrain_setup("main_terrain")
# Scene setup from JSON config - production pipeline pattern
import hou
import json
def setup_scene_from_config(config_path):
"""Read a shot config file and build the Houdini scene."""
with open(config_path, "r") as f:
config = json.load(f)
obj = hou.node("/obj")
# Import character caches
for char in config.get("characters", []):
geo = obj.createNode("geo", char["name"])
file_node = geo.createNode("file", "cache_in")
file_node.parm("file").set(char["cache_path"])
# Add material assignment
if "material" in char:
mat = geo.createNode("material", "assign_material")
mat.parm("shop_materialpath1").set(char["material"])
mat.setInput(0, file_node)
mat.setDisplayFlag(True)
else:
file_node.setDisplayFlag(True)
geo.moveToGoodPosition()
geo.layoutChildren()
# Set up camera from config
if "camera" in config:
cam_data = config["camera"]
cam = obj.createNode("cam", cam_data.get("name", "shot_cam"))
cam.setParms({
"tx": cam_data["position"][0],
"ty": cam_data["position"][1],
"tz": cam_data["position"][2],
"rx": cam_data["rotation"][0],
"ry": cam_data["rotation"][1],
"rz": cam_data["rotation"][2],
"focal": cam_data.get("focal_length", 50),
"resx": cam_data.get("resolution", [1920, 1080])[0],
"resy": cam_data.get("resolution", [1920, 1080])[1],
})
cam.moveToGoodPosition()
# Set frame range
if "frame_range" in config:
start, end = config["frame_range"]
hou.playbar.setFrameRange(start, end)
hou.playbar.setPlaybackRange(start, end)
hou.setFrame(start)
# Save the scene
if "hip_path" in config:
hou.hipFile.save(config["hip_path"])
print(f"Scene saved to: {config['hip_path']}")
print(f"Scene setup complete - {len(config.get('characters', []))} characters loaded")
# Usage:
# setup_scene_from_config("$JOB/shots/sh010/config.json")
# Utility: batch rename nodes with search and replace
import hou
def batch_rename(parent_path, search, replace):
"""Rename all children of a node, replacing a substring."""
parent = hou.node(parent_path)
if not parent:
print(f"Node not found: {parent_path}")
return
renamed = 0
for child in parent.children():
old_name = child.name()
if search in old_name:
new_name = old_name.replace(search, replace)
child.setName(new_name, unique_name=True)
print(f" {old_name} -> {child.name()}")
renamed += 1
print(f"Renamed {renamed} nodes in {parent_path}")
# Example: rename all nodes containing "old_" to "new_"
batch_rename("/obj/geo1", "old_", "new_")
Store reusable Python utilities in $HOUDINI_USER_PREF_DIR/scripts/python/. Any module placed there can be imported from anywhere in Houdini with a standard import statement. For studio-wide tools, add a shared path to $HOUDINI_PATH.
Python and VEX work best together. Master both by exploring VEX Programming for high-performance attribute work, and Houdini Workflows for production patterns that combine everything into scalable systems.