TouchDesigner Intermediate Article 1

Python Scripting in TouchDesigner

Master Python scripting in TouchDesigner's three execution contexts to drive parameters, respond to events, and automate complex operator networks.

⏱ 20 min python scripting automation

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 row i, column named 'R' — Table DAT cells return strings, so float() is required.
  • min(num_rows, 12) prevents index errors if the table has fewer than 12 rows.
  • Checking if chop is None before accessing .par avoids 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.