---
name: td-api-reference
description: "MUST READ before writing TD Python via execute_python, set_dat_content, or edit_dat_content. API reference for parameters, storage, operators, threading."
---
<!-- Generated by Embody/Envoy - Do not remove this comment -->

# TouchDesigner Python API Reference

**Always research TD features on the wiki before writing code.** Assumptions about TD's Python API are frequently wrong.

- **Wiki home**: https://docs.derivative.ca/Main_Page
- **OP Class**: https://docs.derivative.ca/OP_Class
- **COMP Class**: https://docs.derivative.ca/COMP_Class
- **Par Class**: https://docs.derivative.ca/Par_Class
- **Extensions**: https://docs.derivative.ca/Extensions
- **Cook cycle**: https://docs.derivative.ca/Cook

## Parameter Access Patterns

```python
# CORRECT — always use .eval() for the current runtime value:
value = op('geo1').par.tx.eval()

# WRONG — .val only works in constant mode:
value = op('geo1').par.tx.val

# Setting values:
op('geo1').par.tx = 5
op('geo1').par.tx.val = 5  # CAUTION: silently switches to CONSTANT mode

# Menu parameters accept string name or index:
op('geo1').par.xord = 'trs'   # by name
op('geo1').par.xord = 5       # by index

# Type casting requires explicit .eval():
me.par.tx.eval().hex()  # CORRECT
me.par.tx.hex()         # WRONG
```

## Creating Custom Parameters

All `append*` methods return a **ParGroup** (tuple-like), not a single Par — always index with `[0]`.

```python
page = comp.appendCustomPage('Controls')
pg = page.appendFloat('Speed', label='Speed')  # Returns ParGroup
p = pg[0]                                       # Get the actual Par
p.default = 0.5
p.normMin = 0; p.normMax = 2    # Slider range
p.min = 0; p.clampMin = True    # Hard clamp
p.help = "Playback speed multiplier."  # Tooltip on hover
p.startSection = True           # Draw separator line above

# Other methods:
page.appendInt('Count')
page.appendToggle('Active')
page.appendStr('Label')
page.appendMenu('Mode')        # Creates EMPTY menu — set .menuNames/.menuLabels separately
page.appendPulse('Reset')
page.appendRGB('Color')        # Creates Color1r, Color1g, Color1b
page.appendXYZ('Pos')          # Creates Pos1, Pos2, Pos3
page.appendOP('Target')
page.appendFile('Path')

# Post-creation properties:
p.help = "Tooltip text shown on hover"  # ALWAYS set this
p.startSection = True   # Draw separator line above this parameter
p.order = 11.5          # Insert between order 11 and 12
p.readOnly = True       # Display-only parameter

# Cleanup:
comp.destroyCustomPars()       # Remove ALL custom pars
par.Speed.destroy()            # Remove single custom par
```

**Naming rule:** First letter MUST be uppercase, rest lowercase/numbers. No underscores.
- **Parameter design rules**: See `.claude/rules/parameters.md` for full conventions
- Docs: https://docs.derivative.ca/Custom_Parameters

## `op()` vs `opex()`

```python
node = op('/nonexistent/path')   # Returns None — silent failure
node = opex('/nonexistent/path') # Raises tdError — clear message

all_noises = ops('noise*')       # Returns a LIST (supports wildcards)
```

Use `op()` only when `None` is an acceptable result.

## `debug()` vs `print()`

```python
debug('value is', x)  # "myScript line 42: value is 42" (with source info)
print('value is', x)  # "value is 42" (no source info)
```

## Module-Level Code Hazard

Never call `op()`, `parent()`, or access TD objects at module level. They execute during import, before the network is ready.

```python
# WRONG:
my_op = op('base1')  # May be None during init

# CORRECT — defer to methods:
class MyExt:
    def doSomething(self):
        my_op = op('base1')  # Resolved at call time
```

## Import Shadowing

TD searches for DATs by name before `sys.path`. A DAT named `json` shadows Python's `json` module.

## `mod()` for Module Access

```python
# In a parameter expression (import not available):
mod.utils.myFunction()

# In a script (decreasing performance):
import utils                         # Fastest
m = mod.utils; m.func()              # OK — cache the reference
mod.utils.func()                     # Slowest — re-resolves every call

# Access by path:
mod('/project1/utils').myFunction()

# Direct module property:
op('myDat').module.myFunction()
```
- Docs: https://docs.derivative.ca/MOD_Class

## `extensionsReady` Guard

```python
# In a parameter expression:
parent().MyExtensionProperty if parent().extensionsReady else 0
```

## Operator Storage

```python
op('base1').store('count', 42)
val = op('base1').fetch('count', 0)  # 0 is default
op('base1').unstore('count')
op('base1').storeStartupValue('version', 1)  # Restored on project load
```

**Gotchas:** `fetch()` searches UP hierarchy by default — use `search=False` for local-only. `store()` triggers recooks. Cannot store TD operator references — use path strings.
- Docs: https://docs.derivative.ca/Storage

## `tdu.Dependency` for Reactive Values

```python
dep = tdu.Dependency(0)
dep.val = 5          # CORRECT — triggers recooks
dep = 5              # WRONG — destroys the Dependency object

current = dep.peekVal  # Read without creating dependency

dep.val = [1, 2, 3]
dep.val.append(4)      # Does NOT trigger update
dep.modified()         # Required
```
- Docs: https://docs.derivative.ca/Dependency_Class

## `tdu` Utility Functions

```python
tdu.clamp(val, min, max)
tdu.remap(val, fromMin, fromMax, toMin, toMax)
tdu.rand(seed)                        # Deterministic [0.0, 1.0)
tdu.base('noise3')                    # 'noise'
tdu.digits('noise3')                  # 3
tdu.validName('my op!')               # 'my_op_'
tdu.match('noise*', ['noise1', 'c1']) # ['noise1']
tdu.expand('A[1-3]')                  # ['A1', 'A2', 'A3']
tdu.tryExcept(expr, fallback)
```

## DAT Cell and Text Behavior

All DAT cells are internally **strings**. Auto-cast to numbers in expression contexts.

```python
n = op('table1')
n[1,2] + 1          # 4 (auto-cast)
n[1,2].val + 1      # TypeError: str + int
```

- `dat.text` — tab/newline delimited; **strips multi-line cell content**. Use `dat.csv` for cells with newlines
- `dat.jsonObject` — parses as JSON directly (no `json.loads()` needed)
- `dat.module` — access as Python module
- `dat.write(content)` — **appends** (does not overwrite)
- Docs: https://docs.derivative.ca/DAT_Class

## CHOP Channel Access

```python
ch = op('noise1')['chan1']        # By name — NO wildcard
chs = op('noise1').chans('tx*')  # Pattern matching
val = ch.eval()                   # Current value

ch[0], ch[10]                     # Sample by index
ch.evalFrame(30)                  # At specific frame
arr = op('noise1').numpyArray()   # Shape: (numChans, numSamples)
```
- Docs: https://docs.derivative.ca/Channel_Class

## TOP Pixel Access

**Coordinate system:** TD places **(0, 0) at the bottom-left, with Y increasing upward** for all texture operations. `TOP.numpyArray()` is the exception: it returns rows top-to-bottom (numpy convention).

`TOP.sample(x, y)` downloads the **entire texture** from GPU — extremely expensive. Never in loops.

```python
# sample() uses TD texture coords: y=0 is BOTTOM of image
r, g, b, a = op('noise1').sample(x=0.5, y=0.5)  # Center of texture
r, g, b, a = op('noise1').sample(x=0, y=0)       # Bottom-left corner

# numpyArray() returns rows TOP-to-BOTTOM (opposite of TD texture coords)
arr = op('noise1').numpyArray()  # [height, width, channels] — NOT [width, height]
# arr[0] is the TOP of the image (highest TD Y)
# arr[-1] is the BOTTOM of the image (TD y=0)

# Flip to match TD bottom-up order:
arr_td = np.flipud(arr)
```

## POPs — GPU-Accelerated Point Operators

POPs process 3D geometry on the GPU (analogous to SOPs but GPU-accelerated).

```python
grid = parent.create(gridPOP, 'grid1')
n = pop_op.numPoints(delayed=True)    # Non-blocking
pts = pop_op.points('P')              # Downloads (blocks GPU)
bounds = pop_op.bounds(delayed=True)  # Non-blocking
attrs = pop_op.pointAttributes        # Set of attribute names
```

Common types: `gridPOP`, `noisePOP`, `transformPOP`, `particlePOP`, `spherePOP`, `linePOP`, `mergePOP`, `nullPOP`, `selectPOP`, `mathPOP`, `cachePOP`, `fileinPOP`, `glslPOP`
- Docs: https://docs.derivative.ca/POP_Class

## `run()` — Delayed Code Execution

```python
run("op('/project1/base1').cook(force=True)", delayFrames=1)
run("print('done')", delayMilliSeconds=500)
run("op.Embody.Update()", endFrame=True)
run(myFunction, arg1, arg2, delayFrames=5)
run("me.cook(force=True)", fromOP=op('/project1/base1'), delayFrames=1)
```
- Docs: https://docs.derivative.ca/Td_Module#Methods

## Thread Manager

Long-running Python must run in background threads. Use `op.TDResources.ThreadManager`.

```python
task = op.TDResources.ThreadManager.TDTask(
    target=my_background_function,
    args=(arg1, arg2),
    SuccessHook=on_success,    # Main thread
    ExceptHook=on_error,       # Main thread
    RefreshHook=on_refresh,    # Main thread, every frame
)
op.TDResources.ThreadManager.EnqueueTask(task)
op.TDResources.ThreadManager.EnqueueTask(task, standalone=True)  # Dedicated thread
```

**Key:** `standalone=True` for long-lived tasks. Worker pool = 4 threads. `InfoQueue` for worker → main communication. Never call `EnqueueTask()` from a worker thread (THREAD CONFLICT).
- Docs: https://docs.derivative.ca/Thread_Manager

## Explicit Type Conversion

TD parameters auto-cast in expression contexts but remain TD objects. Convert with `int()`, `float()`, `str()` for standard Python functions. Use `repr()` to reveal actual type.
