Python Scripting in TouchDesigner
Master Python scripting in TouchDesigner's three execution contexts to drive parameters, respond to events, and automate complex operator networks.
TouchDesigner embeds a full Python 3 interpreter — the same language you use anywhere else, but augmented with a rich module called td that gives you direct access to every operator, parameter, and network in your project. Understanding the three contexts where Python runs, and how op() ties everything together, unlocks a fundamentally different way of working.
The op() Function
Every operator in your network is reachable from anywhere in the project through the op() function. Pass it a path string and it returns the operator object, or None if nothing is found at that path.
# Absolute path from the project root
noise_op = op('/project1/base1/noise1')
# Relative path from the current network level
noise_op = op('noise1')
# Navigate up and across
sibling = op('../other_base/lfo1')
Once you have the operator object, the par attribute gives you access to all its parameters:
noise = op('noise1')
# Read the current roughness value
current = noise.par.roughness.val
# Set a new value (overrides any expression)
noise.par.roughness = 0.5
# Set a parameter by binding it to an expression string
noise.par.roughness.expr = "math.sin(absTime.seconds) * 0.5 + 0.5"
Parameter names match exactly what you see in the TD parameter dialog — look at the “Parameter Name” column in the operator info or hover over the parameter label to find the scripting name.
Three Execution Contexts
1. Parameter Expressions
The simplest context: click the = button on any parameter to enter expression mode, then type a single-line Python expression. The expression is re-evaluated every cook.
# Inside a Noise TOP's Amplitude parameter (expression mode)
op('audioin1').CHOP.chan('L').eval()
Expression mode is powerful but limited to one line. Use it for live-computed values that depend on other operators.
2. Script DATs
A Script DAT is a Text DAT with a Python script that you run manually (right-click > Run Script) or trigger from elsewhere. It is the equivalent of a standalone script file.
# Script DAT: rebuild a Table DAT from a directory listing
import os
table = op('data_table')
table.clear()
table.appendRow(['filename', 'size', 'modified'])
folder = project.folder
for f in os.listdir(folder):
full = os.path.join(folder, f)
if os.path.isfile(full):
stat = os.stat(full)
table.appendRow([f, stat.st_size, stat.st_mtime])
3. Execute DATs
Execute DATs are event-driven scripts. TouchDesigner calls specific functions inside them when events fire. The most common is the standard Execute DAT, which has callbacks for the frame lifecycle.
# Execute DAT — fires every frame
def onFrameStart(frame):
# frame is the current frame number (int)
noise = op('noise1')
noise.par.tx = math.sin(frame * 0.05) * 100
return
def onFrameEnd(frame):
return
The CHOP Execute DAT triggers when a CHOP channel value changes, perfect for driving events from audio, MIDI, or any continuous signal:
# CHOP Execute DAT attached to a Threshold CHOP
def onValueChange(channel, sampleIndex, val, prev):
if channel.name == 'chan1' and val > prev:
# Rising edge — fire a visual event
op('feedback1').par.resetpulse.pulse()
return
def onOffToOn(channel, sampleIndex, val, prev):
op('constant1').par.value0 = 1.0
return
def onOnToOff(channel, sampleIndex, val, prev):
op('constant1').par.value0 = 0.0
return
Practical Example: Table DAT → 12 Constant CHOPs
Suppose you have a Table DAT named color_data with 12 rows and three columns (R, G, B). You want each row to drive a separate Constant CHOP so downstream operators can pick up individual colour values. An Execute DAT on onFrameStart handles this cleanly:
def onFrameStart(frame):
table = op('color_data')
if table is None:
return # Guard: table might not exist during startup
num_rows = table.numRows
for i in range(min(num_rows, 12)):
chop_name = f'constant{i+1}'
chop = op(chop_name)
if chop is None:
debug(f'Warning: {chop_name} not found')
continue
try:
r = float(table[i, 'R'])
g = float(table[i, 'G'])
b = float(table[i, 'B'])
chop.par.value0 = r
chop.par.value1 = g
chop.par.value2 = b
except (ValueError, KeyError) as e:
debug(f'Row {i} parse error: {e}')
return
Key points in this script:
table[i, 'R']accesses rowi, column named'R'— Table DAT cells return strings, sofloat()is required.min(num_rows, 12)prevents index errors if the table has fewer than 12 rows.- Checking
if chop is Nonebefore accessing.paravoids attribute errors if a Constant CHOP was renamed or deleted. debug()is a TD built-in that prints to the Textport, useful during development.
The me Reference
Inside any DAT script, me refers to the operator that owns the script. This is essential for writing generic code that works regardless of where the component lives in your network:
# Inside a component's local script — works at any network path
sibling_chop = me.parent().op('lfo1')
my_param = me.par.Speed.val
me.parent() returns the container that holds me, letting you traverse the hierarchy without hardcoding paths.
Error Handling Best Practices
TouchDesigner continues cooking even when Python errors occur, but unhandled exceptions will silently disable the script until you fix and re-save it. Always guard operator lookups and type conversions:
def onFrameStart(frame):
target = op('some_chop')
# Never call .par on a potentially None operator
if target is None:
return
try:
val = float(target.par.value0)
op('noise1').par.amplitude = val * 2.0
except Exception as e:
# td.debug prints to the Textport without crashing the cook
debug(f'onFrameStart error frame {frame}: {e}')
return
Use the Textport (Dialogs > Textport and DATs) to read debug() output and Python tracebacks. Enable “Python” in the Textport filter if output is missing.
Performance Notes
Python in TD runs on the CPU in the main thread, so heavy per-frame scripts compete directly with rendering. Cache operator references — op('noise1') involves a dictionary lookup — and store them in a module-level variable if called every frame. The storage dictionary on any operator (op('base1').storage) is a convenient place to stash persistent state between frames without polluting global scope.