dylanroscover / td-api-reference
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
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 wrongCreating Custom Parameters
All append* methods return a ParGroup (tuple-like), never a single Par: index with [0].
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 / appendFileNames 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.CompNamesearches UP by shortcut name and survives renesting;parent(2)encodes the depth and breaks the moment anything moves. Setpar.parentshortcutwhen you create a reusable COMP.- The bare global
parent()/parent(N)is banned inside an extension class body: useself.ownerComp. Exemptions: a COMP's own callback DAT (whereparent()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.Namenames one internal operator,ipar.Namea parameter-holding COMP; inside-only, and an operator-valued internal par needsop(ipar.X.Opname). Reserve them for stable internal addresses.op.CompNameis globally unique: assigning a name in use steals it from the previous holder. Use it for singleton services (op.Embody.Log(...)), otherwise preferparent.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), andrun()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 ormaxDepth=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
jsonshadows Python'sjson: TD searches DATs by name beforesys.path. mod.utils.func()re-resolves the DAT every call; in a loop cache it (m = mod.utils). In a parameter expressionimportis 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 (useaddScriptError()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.
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 subscribingtdu.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] + 1auto-casts,n[1,2].val + 1raises.dat.textstrips multi-line cells (dat.csvkeeps them);dat.jsonObjectparses directly;dat.write()APPENDS;dat.moduleimports 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), GLSLgl_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 againstsample()on a vertical ramp). PIL/OpenCV images and panel screen coordinates are top-down, sonp.flipudconverts 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)andbounds(delayed=True)are non-blocking;pop.points('P')downloads from the GPU.fileinPOP(meshes) andpointfileinPOP(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 throughfromOPor 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 DATonFrameStart), never aforloop.- 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
framestarttoggle 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:
- Prototype a fetch synchronously once to prove URL, auth and parse; never ship it.
- Fast, TD-only, no I/O: inline. Any network, disk or subprocess call is never this rung.
- Fetch data with a native operator: Web Client DAT (
request(url, 'GET', timeout=8000)returns at once;onResponseruns on the MAIN thread), WebSocket DAT, Web Server DAT, OSC, File In / Folder DAT. Parse with a JSON DAT, bridge with DAT to CHOP. - Long TD-touching work: chunk with
run(delayFrames=N);run()controls WHEN, not HOW MUCH. - Blocking pure-Python work: the Thread Manager (Palette Thread Manager Client preferred), a
targetwith zero TD access, results applied in main-thread hooks. NeverEnqueueTask()from a worker. - Long-lived loop or server: ThreadManager
standalone=Trueor a top-levelthreading.Threadwith zero TD access draining aqueue.Queuefrom 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.