- name
- td-api-reference
- description
- MUST READ before writing TD Python via execute_python, set_dat_content, or edit_dat_content, and before any heavy build. Parameters, storage, operators, referencing, threading, cook model, heavy-build safety caps.
<!-- Generated by Embody/Envoy - Do not remove this comment - sha:1517fd24f8397a92 -->
# TouchDesigner Python API Reference
Verify unfamiliar TD behavior before asserting it: `get_docs('<Operator or Class>')` reads the offline help mirror first, the wiki (https://docs.derivative.ca: OP Class, COMP Class, Par Class, Extensions, Cook) second, and `describe_op_type` gives an operator type's real parameter names. Assumptions about TD's Python API are frequently wrong. Two companions in this skill's folder hold the long tail: `references/background-work.md` (the Web Client DAT and Thread Manager code) and `references/heavy-build-safety.md` (crash causes and caps). Read them before any fetch, thread or heavy build; without a skills folder, `get_guidance(topic='td-api-reference/background-work')`.
## Parameter Access Patterns
```python
value = op('geo1').par.tx.eval() # CORRECT: the current runtime value
value = op('geo1').par.tx.val # WRONG: constant-mode value only
op('geo1').par.tx = 5 # set; .val = 5 also silently switches the mode to CONSTANT
op('geo1').par.xord = 'trs' # menus take the name or the index
me.par.tx.eval().hex() # cast AFTER eval(); me.par.tx.hex() is wrong
```
## Creating Custom Parameters
All `append*` methods return a **ParGroup** (tuple-like), never a single Par: index with `[0]`.
```python
page = comp.appendCustomPage('Controls')
p = page.appendFloat('Speed', label='Speed')[0]
p.default = 0.5; p.val = p.default # .default never sets the value
p.normMin = 0; p.normMax = 2 # slider range
p.min = 0; p.clampMin = True # hard clamp
p.help = "Playback speed multiplier." # ALWAYS set help
p.startSection = True # separator above
# appendInt / appendToggle / appendStr / appendMenu (EMPTY menu: set .menuNames/.menuLabels)
# appendPulse / appendRGB ('Color' -> Colorr/g/b) / appendXYZ ('Pos' -> Pos1/2/3) / appendOP / appendFile
```
Names start uppercase, then lowercase letters and digits, no underscores. Get-or-create (never a blind `append*`), callbacks, styles, ranges, help text: `/parameter-design`. Docs: https://docs.derivative.ca/Custom_Parameters
## `op()` vs `opex()`
`op('/missing')` returns `None` silently; `opex('/missing')` raises `tdError` with a clear message. Use `op()` only when `None` is an acceptable result. `ops('noise*')` returns a list (wildcards). `opex('level1').asType(levelTOP, checkType=True)` is the typed, runtime-asserted one-liner for a known type.
## Operator Referencing Patterns
Absolute paths (`op('/project1/...')`) are always wrong, in code, expressions AND parameter values. Take the highest rung that resolves from where the code runs:
| Rung | Pattern | Use when | Prerequisite |
|---|---|---|---|
| 1 | `self.ownerComp` | Anywhere inside an extension; capture it once in `__init__` | none |
| 2 | `parent.CompName` | Inside the COMP but outside the extension: callback DATs, parameter expressions | the COMP sets `par.parentshortcut` |
| 3 | `op('sibling')`, `op('./child')`, `op('../up_one')` | The target is in the same network or nearby | none |
| 3b | `iop.Name` / `ipar.Name` | An internal op or parameter the COMP publishes for its own descendants | Internal OP shortcuts on the Common page |
| 4 | `op.CompName` | A project-wide singleton no parent shortcut can reach | the COMP sets `par.opshortcut` |
- `parent.CompName` searches UP by shortcut name and survives renesting; `parent(2)` encodes the depth and breaks the moment anything moves. Set `par.parentshortcut` when you create a reusable COMP.
- The bare global `parent()` / `parent(N)` is banned inside an extension class body: use `self.ownerComp`. Exemptions: a COMP's own callback DAT (where `parent()` IS the owner), generic tooling inside a COMP it did not author, parameter expressions on replicated or cloned widgets. `some_op.parent()` is documented OP-class API and untouched.
- `iop.Name` names one internal operator, `ipar.Name` a parameter-holding COMP; inside-only, and an operator-valued internal par needs `op(ipar.X.Opname)`. Reserve them for stable internal addresses.
- `op.CompName` is globally unique: assigning a name in use steals it from the previous holder. Use it for singleton services (`op.Embody.Log(...)`), otherwise prefer `parent.CompName`.
- Always verify a reference resolves from the calling context; a reference that returns `None` (or finds the wrong operator) is a latent bug.
- After ANY rename (operator, method or parameter) sweep every live surface, not just DAT text on disk: DAT text inside the `.toe` (`findChildren(type=DAT)` and read `.text`), `par.expr`, `par.bindExpr`, constant-mode OP-reference values (`par.val`), and `run()` strings composed at call time (issue #94: an agent swept DAT content only and left stale expressions and binds behind). `findChildren(depth=N)` matches EXACTLY depth N, a silent all-clear for anything shallower; walk with no depth argument or `maxDepth=N`.
## Type Hints
Annotate every `def` (arguments AND return) and let a checker read them; Python ignores them at runtime. TD's classes are the types (`COMP`, `DAT`, `OP`, `Par`, `constantCHOP`, ...); put `from __future__ import annotations` at the top of a module DAT. `x: str = None` declares a `str` that is `None`: write `Optional[str] = None`. A callback returns `-> None`; a query that can miss returns `-> Optional[COMP]`. The hint is the soft check; `asType(..., checkType=True)` is the runtime assertion. Check softly, in CI, with a baseline (pyright `basic`, failing only on NEW errors).
## Everyday Essentials
- `debug('value is', x)` prints with the source DAT and line number; `print()` does not.
- Never call `op()`, `parent()` or touch TD objects at module level: module code runs at import, before the network is ready. Defer to methods.
- A DAT named `json` shadows Python's `json`: TD searches DATs by name before `sys.path`.
- `mod.utils.func()` re-resolves the DAT every call; in a loop cache it (`m = mod.utils`). In a parameter expression `import` is unavailable, `mod.utils.f()` works. `mod('utils')` resolves relative to the current op, never an absolute path.
- Expressions that read a promoted attribute guard with `parent().X if parent().extensionsReady else 0`.
- `changeType()` returns the NEW op (capture it); `copyOPs([list])` preserves wires, `COMP.copy()` does not; `addError()`/`addWarning()` work only in cook callbacks (use `addScriptError()` from extension code).
## onInitTD and TDXN Import Timing
Inside a TDXN-strategy COMP, `onInitTD` fires BEFORE the import that rebuilds the children (project open around frame 60, every Ctrl+S strip/restore, every manual reimport with `clear_first=True`), so anything it creates, sets or stores inside the COMP is wiped. Defer network-dependent setup with `run('args[0].postInit()', self, delayFrames=5)`, keep it idempotent (it runs several times), null-check children (`op('child')` is `None` during the strip window), and keep durable state OUTSIDE the boundary or in `store()` on the COMP.
## Parameter, Storage, or Dependency?
User-facing state is a **custom parameter**; hidden durable bookkeeping is **`storage`** (lives on the COMP, survives an extension reinit, wiped when the COMP's contents are replaced by a tox reload or TDXN reconstruction); a derived value that must recook its readers is a **`tdu.Dependency`** (dies with the extension instance on every source save, rebuilt in `__init__`). Publish once and let readers pull; never push values into consumers. The lifetime table, what actually notifies dependents, and the storage mechanics are measured in `/parameter-design` and its `references/state-lifetimes.md`.
```python
op('base1').store('count', 42) # write THROUGH store(); comp.storage[k] = v notifies nobody
val = op('base1').fetch('count', 0) # no default -> a miss RAISES; fetch() searches UP the parents
op('base1').unstore('count') # glob-matched: never a computed key
op('base1').storeStartupValue('version', 1) # a separate dict restored on load
dep = tdu.Dependency(0)
dep.val = 5 # CORRECT: recooks readers. dep = 5 destroys the Dependency
dep.val.append(4); dep.modified() # in-place mutation needs modified()
current = dep.peekVal # read without subscribing
```
`tdu.clamp`, `tdu.remap`, `tdu.rand(seed)`, `tdu.base('noise3')` -> `'noise'`, `tdu.digits`, `tdu.validName`, `tdu.match`, `tdu.expand('A[1-3]')`, `tdu.tryExcept(expr, fallback)`.
## DAT, CHOP and TOP Access
- DAT cells are strings; `n[1,2] + 1` auto-casts, `n[1,2].val + 1` raises. `dat.text` strips multi-line cells (`dat.csv` keeps them); `dat.jsonObject` parses directly; `dat.write()` APPENDS; `dat.module` imports it.
- CHOP: `op('noise1')['chan1']` by name (no wildcard), `.chans('tx*')` for patterns, `ch.eval()`, `ch[10]`, `ch.evalFrame(30)`, `numpyArray()` shaped `(numChans, numSamples)`.
- TOP coordinates: `TOP.sample(x, y)`, GLSL `gl_FragCoord`, UVs and Crop/Transform parameters put (0, 0) at the BOTTOM-LEFT with Y up. `TOP.numpyArray()` follows the same y-up order: row 0 is y = 0, the bottom of the image (shape `[height, width, channels]`; measured 2025.33230 against `sample()` on a vertical ramp). PIL/OpenCV images and panel screen coordinates are top-down, so `np.flipud` converts between the two. `sample()` downloads the whole texture: never in a loop. `numpyArray()` returns linear-light floats, not the sRGB bytes a file decode gives: compare same-domain only.
- POPs: `pop.numPoints(delayed=True)` and `bounds(delayed=True)` are non-blocking; `pop.points('P')` downloads from the GPU. `fileinPOP` (meshes) and `pointfileinPOP` (point clouds, splats) are distinct operators.
- `run("me.cook(force=True)", fromOP=op('base1'), delayFrames=1)`, `run(fn, arg, delayMilliSeconds=500)`, `run("...", endFrame=True)`; resolve ops through `fromOP` or shortcuts, never absolute paths.
## Cook Model Gotchas
- `cook(force=True)` does not advance a feedback loop within a frame: evolution needs real frames with the chain demanded (`run(..., delayFrames=1)` or an Execute DAT `onFrameStart`), never a `for` loop.
- A Movie File In reload (`par.file`, `reloadpulse`) lands only across a real frame advance, and ops downstream in the same forced pass can still serve the pre-reload texture. Verify content at the point of capture and let each reload settle (`/movie-export`).
- A CHOP Execute DAT is a per-frame demand source: its watched CHOP cooks every frame (measured 2025.33230). An Execute DAT's `framestart` toggle is OFF by default, so a frame driver that was never enabled demands nothing.
- Animate cheaply: make a heavy generator static (no time reference, cooks once) and put the motion in a cheap downstream op that animates the SAMPLING. Confirm with `cookedThisFrame`.
## Background and Long-Running Work
**Ironclad rule: a worker thread touches NO TD object, and a read counts as a write.** No `op()`/`opex()`, no `Par` (not even `.eval()`), no DAT/CHOP/SOP/TOP content, no `storage`, no `tdu.Dependency`, no `debug()`/`print()`, and never `run()`/`td.run()` from a worker: it does not raise, the scheduled code even runs, and the crash lands later far from the call site (Derivative-confirmed 2026-08-17). A worker gets pure Python, `tdu` math utilities, values resolved on the main thread and passed in, `queue`/`threading` primitives, `td.isMainThread()`, and the Thread Manager's `InfoQueue`/`SafeLogger`; it hands plain data back for a main-thread callback to apply.
Match the rung to the problem; threading is the LAST resort:
0. Prototype a fetch synchronously once to prove URL, auth and parse; never ship it.
1. Fast, TD-only, no I/O: inline. Any network, disk or subprocess call is never this rung.
2. Fetch data with a native operator: Web Client DAT (`request(url, 'GET', timeout=8000)` returns at once; `onResponse` runs on the MAIN thread), WebSocket DAT, Web Server DAT, OSC, File In / Folder DAT. Parse with a JSON DAT, bridge with DAT to CHOP.
3. Long TD-touching work: chunk with `run(delayFrames=N)`; `run()` controls WHEN, not HOW MUCH.
4. Blocking pure-Python work: the Thread Manager (Palette Thread Manager Client preferred), a `target` with zero TD access, results applied in main-thread hooks. Never `EnqueueTask()` from a worker.
5. Long-lived loop or server: ThreadManager `standalone=True` or a top-level `threading.Thread` with zero TD access draining a `queue.Queue` from a main-thread callback armed BEFORE the worker starts.
Trigger a fetch from a Pulse parameter or a Timer CHOP (cycle on, limit off), never a sleep loop, a self-rescheduling `run()` poller or an auto-fetch on open. Gate with primary evidence: fps and `droppedFrames` held (`get_project_performance`) AND the result actually arrived. `requests` has no default timeout and blocks the frame: `timeout=(2, 8)`, and only ever inside a worker. Code patterns: `references/background-work.md`.
## Heavy-Build Safety
Load `references/heavy-build-safety.md` (crash causes with their warning metrics and mitigations) before render chains, feedback loops, instancing, large geometry, GLSL, high-resolution TOPs or many operators at once; the gating protocol and stop conditions in `performance.md` always apply. The caps in one breath: TOPs <= 1920x1080 in 8/16-bit fixed formats unless asked; feedback loops bounded (fixed resolution, decay below 1, reset wired, bypassed while wiring, terminated in a Null); heavy sources bypassed while wiring; GPU instancing or POPs over Copy SOP, counts ramped with metrics (CPU particles ~10k max); Time Slicing on and small buffers for CHOPs; constant-bounded GLSL loops with `TD_NUM_*` index guards; short non-blocking `execute_python` calls with no sleep, no `TOP.sample()` in loops, and large builds chunked across frames. `allowCooking = False` is COMP-only; gate other operators with `bypass`.
## Pre-Installed Packages and Types
`numpy` and `cv2` are documented as bundled; `requests`, `yaml`, `cryptography`, `attrs` are commonly present (verify in your build). Auto-imported stdlib: `math`, `re`, `sys`, `collections`, `enum`, `inspect`, `traceback`, `warnings`. TD parameters auto-cast in expressions but stay TD objects: convert with `int()`, `float()`, `str()` before handing them to standard Python; `repr()` reveals the real type.
Auf GitHub ansehen