| 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. |
TouchDesigner Python API Reference
Always research TD features on the wiki before writing code. Assumptions about TD's Python API are frequently wrong.
Parameter Access Patterns
value = op('geo1').par.tx.eval()
value = op('geo1').par.tx.val
op('geo1').par.tx = 5
op('geo1').par.tx.val = 5
op('geo1').par.xord = 'trs'
op('geo1').par.xord = 5
me.par.tx.eval().hex()
me.par.tx.hex()
Creating Custom Parameters
All append* methods return a ParGroup (tuple-like), not a single Par -- always index with [0].
page = comp.appendCustomPage('Controls')
pg = page.appendFloat('Speed', label='Speed')
p = pg[0]
p.default = 0.5
p.normMin = 0; p.normMax = 2
p.min = 0; p.clampMin = True
p.help = "Playback speed multiplier."
p.startSection = True
page.appendInt('Count')
page.appendToggle('Active')
page.appendStr('Label')
page.appendMenu('Mode')
page.appendPulse('Reset')
page.appendRGB('Color')
page.appendXYZ('Pos')
page.appendOP('Target')
page.appendFile('Path')
p.help = "Tooltip text shown on hover"
p.startSection = True
p.order = 11.5
p.readOnly = True
comp.destroyCustomPars()
par.Speed.destroy()
Naming rule: First letter MUST be uppercase, rest lowercase/numbers. No underscores.
op() vs opex()
node = op('/nonexistent/path')
node = opex('/nonexistent/path')
all_noises = ops('noise*')
Use op() only when None is an acceptable result.
Operator Referencing Patterns
How you reference an operator matters. A wrong choice works today and breaks tomorrow -- when the component is renamed, instanced, or moved. Always pick the narrowest, most portable reference that correctly resolves from where the code runs. Absolute paths (op('/project1/...')) are always wrong -- in code, expressions, AND parameter values.
Relative Paths -- for operators near you
Use relative paths when the target operator is in the same network or a nearby one. These are the simplest and most portable references because they describe relationships, not locations.
op('sibling_name') -- another operator in the same network (same parent COMP).
op('./child_name') -- an operator inside me (only valid from a COMP).
op('../sibling_of_parent') -- an operator in the parent's network (go up one level, then find by name).
Relative paths break down when you need to reach across distant parts of the network. That's where shortcuts come in.
Parent Shortcuts (parent.CompName) -- for reaching your owner
A Parent Shortcut is set on a COMP's Common page via the parentshortcut parameter. Once configured, any operator that is a descendant of that COMP (child, grandchild, etc.) can reference it as parent.CompName. TD resolves this by walking up the parent chain from the caller until it finds a COMP whose Parent Shortcut matches.
This is the right choice when code running inside a component needs to reach the component itself -- typically to call extension methods or navigate relative to the component root.
parent.Embody.Update() -- call a promoted extension method.
parent.Embody.ext.Embody.helperMethod() -- reach a non-promoted method.
parent.Embody.op('subpath/op_name') -- navigate from the component root to find an internal operator.
Key properties:
- Reusable across instances: Multiple COMPs can use the same Parent Shortcut name. Each descendant resolves to its own nearest matching ancestor -- so the same code works identically across every instance of a component.
- Only resolves from inside: Code that is not a descendant of the COMP will not find it via
parent.CompName. This is a feature, not a limitation -- it keeps references scoped to where they belong.
- Not the same as
parent(): parent() always returns the immediate parent COMP. parent.CompName searches upward by name and can skip multiple levels.
Global OP Shortcuts (op.CompName) -- for project-wide access
A Global OP Shortcut is set on a COMP's Common page via the opshortcut parameter. It registers the COMP so that op.CompName resolves to it from anywhere in the project.
This is the right choice for singleton services that many unrelated parts of the project need to reach -- logging, test runners, shared managers.
op.Embody.Log('message') -- call Embody's logging from anywhere.
op.unit_tests.RunTests() -- kick off the test runner from any script.
Key properties:
- Globally unique: Only one COMP can hold a given Global OP Shortcut name at a time. Assigning a name already in use removes it from the previous holder.
- Use sparingly: If
parent.CompName works, prefer it. Global shortcuts create invisible coupling -- any code anywhere can depend on the name existing, making renames and refactors risky.
Always verify references resolve correctly from the calling context -- a reference that returns None silently (or finds the wrong operator) is a latent bug.
debug() vs print()
debug('value is', x)
print('value is', x)
Module-Level Code Hazard
Never call op(), parent(), or access TD objects at module level. They execute during import, before the network is ready.
my_op = op('base1')
class MyExt:
def doSomething(self):
my_op = op('base1')
Import Shadowing
TD searches for DATs by name before sys.path. A DAT named json shadows Python's json module.
mod() for Module Access
mod.utils.myFunction()
import utils
m = mod.utils; m.func()
mod.utils.func()
mod('utils').myFunction()
op('myDat').module.myFunction()
extensionsReady Guard
parent().MyExtensionProperty if parent().extensionsReady else 0
onInitTD and TDN Import Timing
Any initialization that sets up state inside a TDN-strategy COMP will be destroyed when TDN import runs. TDN reconstruction (ReconstructTDNComps) calls ImportNetwork with clear_first=True, which deletes all children and recreates them from the .tdn file. If an extension's onInitTD creates operators, sets parameters, stores values, or builds internal state inside a TDN COMP, that work is wiped out by the import.
This applies to:
- Project open:
ReconstructTDNComps runs at frame 60. Extensions inside TDN COMPs initialize earlier (when the COMP shell is created), so onInitTD fires before the import overwrites everything.
- Ctrl+S /
project.save(): The strip/restore cycle deletes children pre-save, then re-imports them post-save. Extensions reinitialize after the restore, but the import may still be completing.
Rules:
- Defer initialization that depends on network state. Use
run('self.mySetup()', delayFrames=5) in onInitTD so the setup executes after the TDN import completes. The delay must be long enough for all import phases to finish.
- Never assume
onInitTD runs once. Inside TDN COMPs, extensions may reinitialize multiple times: on project open, after every save (strip/restore), and on manual TDN reimport. onInitTD must be idempotent.
- Guard against missing children. During the strip phase of a save, the COMP's children are temporarily gone. If
onInitTD fires during this window, op('child') returns None. Always null-check operators before accessing them.
- Store persistent state outside the TDN boundary. If an extension needs state that survives reimport, use
store() on the COMP itself (storage is preserved through TDN import) or on an ancestor outside the TDN COMP.
Operator Storage
op('base1').store('count', 42)
val = op('base1').fetch('count', 0)
op('base1').unstore('count')
op('base1').storeStartupValue('version', 1)
Gotchas: fetch() searches UP hierarchy by default -- use search=False for local-only. store() triggers recooks. Cannot store TD operator references -- use path strings.
tdu.Dependency for Reactive Values
dep = tdu.Dependency(0)
dep.val = 5
dep = 5
current = dep.peekVal
dep.val = [1, 2, 3]
dep.val.append(4)
dep.modified()
tdu Utility Functions
tdu.clamp(val, min, max)
tdu.remap(val, fromMin, fromMax, toMin, toMax)
tdu.rand(seed)
tdu.base('noise3')
tdu.digits('noise3')
tdu.validName('my op!')
tdu.match('noise*', ['noise1', 'c1'])
tdu.expand('A[1-3]')
tdu.tryExcept(expr, fallback)
DAT Cell and Text Behavior
All DAT cells are internally strings. Auto-cast to numbers in expression contexts.
n = op('table1')
n[1,2] + 1
n[1,2].val + 1
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
ch = op('noise1')['chan1']
chs = op('noise1').chans('tx*')
val = ch.eval()
ch[0], ch[10]
ch.evalFrame(30)
arr = op('noise1').numpyArray()
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).
| Context | Origin | Y direction |
|---|
TOP.sample(x, y) | Bottom-left | Up |
GLSL gl_FragCoord | Bottom-left | Up |
| UV coordinates (0-1) | Bottom-left | Up |
| Crop/Transform TOP params | Bottom-left | Up |
scriptTOP pixel writing | Bottom-left | Up |
TOP.numpyArray() return | Top-left | Down |
| PIL / OpenCV images | Top-left | Down |
| Panel/widget screen coords | Top-left | Down |
TOP.sample(x, y) downloads the entire texture from GPU -- extremely expensive. Never in loops.
r, g, b, a = op('noise1').sample(x=0.5, y=0.5)
r, g, b, a = op('noise1').sample(x=0, y=0)
arr = op('noise1').numpyArray()
arr_td = np.flipud(arr)
Color domain: numpyArray() is NOT sRGB file bytes. TOP.numpyArray() returns the TOP's raw pixel values -- linearized/linear-light floats for a float TOP -- NOT the sRGB-gamma-encoded 8-bit bytes that cv2/PIL read from a .png/.jpg. A direct pixel diff across the two domains is invalid: it shows a ~0.1-0.4 baseline difference that swamps any real per-pixel change. For any pixel-comparison or frame-exactness workflow (e.g. verifying a movie encode), compare same-domain only -- reader numpyArray() vs reader numpyArray() -- or convert one side (apply/remove the sRGB transfer) before comparing. This is why in-TD readback and out-of-process file decodes must not be diffed against each other directly.
POPs -- GPU-Accelerated Point Operators
POPs process 3D geometry on the GPU (analogous to SOPs but GPU-accelerated).
grid = parent.create(gridPOP, 'grid1')
n = pop_op.numPoints(delayed=True)
pts = pop_op.points('P')
bounds = pop_op.bounds(delayed=True)
attrs = pop_op.pointAttributes
Common types: gridPOP, noisePOP, transformPOP, particlePOP, spherePOP, linePOP, mergePOP, nullPOP, selectPOP, mathPOP, cachePOP, glslPOP. For files: fileinPOP (File In POP -- meshes/geometry) vs pointfileinPOP (Point File In POP -- 3D point clouds: .ply/.pts/.xyz/.e57, Gaussian splats) are distinct operators.
run() -- Delayed Code Execution
run("me.cook(force=True)", fromOP=op('base1'), delayFrames=1)
run("print('done')", delayMilliSeconds=500)
run("op.Embody.Update()", endFrame=True)
run(myFunction, arg1, arg2, delayFrames=5)
Cook Model Gotchas
cook(force=True) does NOT advance a feedback loop within a frame. A Feedback TOP captures its target on frame boundaries, so force-cooking the chain repeatedly inside one synchronous Python loop returns the same state each time (totalCooks may not even increment). Evolution needs real frames to pass with the chain demanded -- drive it with run(..., delayFrames=1) or an Execute DAT onFrameStart, never a for loop.
- A Movie File In reload lands only across a real frame advance -- and even then not same-pass downstream. Changing
par.file / pulsing reloadpulse then cook(force=True) in the SAME frame can silently serve the PREVIOUS texture (no error, no warning, right resolution); a pull-based reader that nothing demands never cooks at all. Worse, when the reload DOES apply mid-pass, ops DOWNSTREAM in that same forced-cook pass can still consume the pre-reload texture -- even with the whole chain force-cooked in dependency order -- so the reader's own numpyArray() shows fresh content while the chain output lags by one frame. Verify content at the POINT OF CAPTURE (the writer's input TOP), not at the source, and let each reload settle across a real frame advance. See /movie-export ("Async file readers").
- Animate cheaply: static source + cheap downstream. A heavy generator (high-octave fBm, large feedback sim) cannot re-render every frame at high resolution. Make it static (remove every time reference so it cooks once and caches) and put the motion in a cheap downstream op -- animate the sampling (drift/rotate/warp the read coordinates), not the source. Verify with
cookedThisFrame: the source reads False, the animated op True.
Background and Long-Running Work
Ironclad rule (a read is treated exactly like a write). From any thread but the main thread, NEVER touch a main-thread-owned TD object: op()/opex(), a Par/ParGroup (read OR write, including .eval()/.val on a live parameter), DAT/CHOP/SOP/TOP content, storage (fetch/store), tdu.Dependency (setting .val recooks on the main thread), or debug()/print() (they route to the Textport / a DAT). Never call run()/td.run() from a worker - it raises tdError; this is exactly what froze TD in the field. A worker may use ONLY: pure Python (math, json, requests), tdu math/value utilities (tdu.clamp/remap/Vector/Matrix - they do not reference TD data), parameter VALUES evaluated on the main thread and passed in, queue.Queue, threading.Event/Lock, td.isMainThread() as a guard, and the Thread Manager's InfoQueue/Get/Set*Safe/SafeLogger. Resolve every op path and value on the main thread BEFORE spawning the worker; the worker returns plain data for a main-thread callback to apply.
Do NOT reach for threading first. Match the rung to the problem TYPE (these are routes, not a strict escalation); threading is the LAST resort:
- Prototype synchronously to prove the URL/auth/parse - one-shot only, short explicit timeout, never in a per-frame callback or on project open, never shipped.
- Fast, TD-only, no I/O -> run it inline. Any network/disk/subprocess call is NEVER this step (latency is unbounded).
- Fetch data -> a native TD I/O operator, NOT Python threading. HTTP one-shot or streaming -> Web Client DAT:
op('webclient1').request(url, 'GET', timeout=8000) returns a connection id immediately and never blocks the frame; the onResponse callback fires on the MAIN thread, so write the result there. Parse with a JSON DAT (or dat.jsonObject) and bridge numbers to channels with DAT to CHOP (there is no Web Client CHOP). ws:// -> WebSocket DAT; inbound/host -> Web Server DAT; control -> OSC; files -> File In / Folder DAT.
- Long main-thread (TD-touching) work -> chunk with
run(delayFrames=N), each chunk small enough to fit one frame. run() controls WHEN, not HOW MUCH; it is not a thread and is main-thread-only (a single heavy parse deferred with run() still blocks whatever frame it lands on).
- Blocking pure-Python work (custom auth, file/disk, subprocess, heavy CPU) -> the Thread Manager. Prefer the Palette Thread Manager Client; advanced:
op.TDResources.ThreadManager + a TDTask whose target touches ZERO TD objects, applying results only in its main-thread hooks. Never call EnqueueTask() from a worker (ThreadManager is itself a TD COMP).
- Long-lived server/loop -> ThreadManager
standalone=True (Envoy's own MCP server, drained by its RefreshHook on the main thread) or a top-level threading.Thread that touches ZERO TD objects, never calls run(), and hands results to a queue.Queue drained every frame by a main-thread callback (an Execute DAT onFrameStart or a ThreadManager RefreshHook). A worker spawning a run()-calling sub-thread is the crash, not a rung.
Engine COMP / TouchEngine offloads heavy COOKING to a separate process (TOP/CHOP/DAT I/O only) - never use it for an I/O fetch. Stock asyncio blocks the frame loop; a worker-hosted loop still needs zero TD access and a queue handoff.
Triggers. Prefer a user-driven Pulse parameter (onPulse / Par.pulse()) for a one-shot/manual fetch, or a Timer CHOP for genuine intervals (fire one request per tick). Never a sleep loop, a self-rescheduling run() poller, or an auto-fetch on project open unless asked; do not start a new request while one is still pending.
Gates. Do not pre-optimize: a synchronous fetch that does not measurably drop a frame may not need anything above Step 2 (measurement decides whether a callback needs chunking or a worker - it never makes shipped blocking I/O acceptable on the main thread). After wiring, verify with primary evidence: get_project_performance shows fps/frameTime held vs baseline and droppedFrames flat, AND the result actually arrived (read the DAT/CHOP back; branch on statusCode['code'] - a callback that never fires leaves TD running but empty).
For code patterns (Web Client DAT example, Thread Manager Client, polling, large payloads), load /td-api-reference.
Code patterns
Fetch data: Web Client DAT (no threading)
The TD-native way to hit an HTTP API. request() is async - it returns a connection id immediately and never blocks the frame; TD does the networking on its own thread and delivers the response to the Callbacks DAT onResponse, which runs on the MAIN thread (so TD access there is safe).
conn_id = op('webclient1').request(
'https://air-quality-api.open-meteo.com/v1/air-quality?latitude=43.7&longitude=-79.4&hourly=pm2_5',
'GET',
timeout=8000,
)
def onResponse(webClientDAT, statusCode, headerDict, data):
if statusCode['code'] != 200:
op('status').text = 'error %s' % statusCode['code']
return
op('raw_json').text = data.decode('utf-8')
return
Then parse and shape with native ops instead of Python in the callback:
webclient1 -> raw_json (Text DAT) -> JSON DAT (Filter = JSONPath, Output Format = Table) -> DAT to CHOP -> null_chop / out1. That yields BOTH the table (DAT) and the channels (CHOP) - the canonical "CHOP and DAT" deliverable. There is no Web Client CHOP. For a quick parse you can also read op('raw_json').jsonObject. request() also accepts authType + basic/appKey/OAuth params - prefer them over hand-rolled auth headers. TD has no built-in retry: on failure, re-issue request() via run(..., delayFrames=N) with a capped attempt count, never a synchronous loop.
Triggers: a user-driven Pulse parameter (onPulse / Par.pulse()) for a one-shot/manual fetch; a Timer CHOP (onCycleStart fires one request()) for periodic; an Execute DAT onFrameStart frame-counter only for sub-second work. Never a sleep loop, a self-rescheduling run() poller, or an auto-fetch on project open unless asked.
Pick the operator:
| Need | Operator | Callback (main thread) |
|---|
| HTTP request/response or HTTP streaming | Web Client DAT | onResponse |
Persistent push/stream (ws://) | WebSocket DAT | onReceiveText/onReceiveBinary |
| TD must RECEIVE requests / host an endpoint | Web Server DAT | onHTTPRequest (NOT a fetch tool) |
| Low-latency control between apps | OSC In/Out DAT | per-message |
| Local file / directory listing | File In DAT / Folder DAT | cooked DAT, no thread |
(For streaming, enable the Web Client DAT's Stream mode + Clamp Output as Rows so the DAT does not grow unbounded and inflate cook time.)
Blocking pure-Python work: the Thread Manager
When no operator expresses the work (custom auth/sessions, a blocking SDK, a big subprocess, heavy CPU on plain data), run it OFF the main thread. Prefer the Palette Thread Manager Client (Palette > ThreadManager > threadManagerClient) - a callback-oriented component with a generated callback DAT; Derivative recommends it over the raw COMP.
Advanced (raw API): the target runs on a worker and must touch ZERO TD objects; hand results back through a queue.Queue you create on the MAIN thread and drain in the RefreshHook (exactly how Envoy's MCP server works - see EnvoyExt.py):
import queue
results = queue.Queue()
def fetch(url, out):
import requests
r = requests.get(url, timeout=(2, 8))
r.raise_for_status()
out.put(r.json())
def on_refresh(*args):
while not results.empty():
data = results.get_nowait()
op('table_out').text = repr(data)
task = op.TDResources.ThreadManager.TDTask(target=fetch, args=('https://...', results), RefreshHook=on_refresh)
op.TDResources.ThreadManager.EnqueueTask(task)
Key: the target runs on a worker and must touch ZERO TD objects (no op(), no parameter read OR write, no DAT/CHOP content, no storage, no tdu.Dependency, no debug()/print()); apply results to TD only on the MAIN thread (a RefreshHook/SuccessHook/ExceptHook, or a queue.Queue drained by an Execute DAT onFrameStart). standalone=True for long-lived tasks; the worker pool defaults to 4 (capped at os.cpu_count()). Never call EnqueueTask() from a worker (ThreadManager is a TD COMP). For worker logging use the Thread Manager's SafeLogger, not debug()/print().
Large payloads
A large response is delivered to onResponse on the MAIN thread, so a heavy parse there still stalls the frame. For big/expensive parsing: onResponse validates status and copies the raw string/bytes only, then hands it to a Thread Manager worker (zero TD access) that parses and returns plain data for a main-thread drain to write. Do not "fix" it with run() - that defers the parse, it does not shrink it.
Not for fetching
Engine COMP / TouchEngine runs a .tox in a separate PROCESS to parallelize heavy COOKING (sims, geometry, render) - it exchanges only TOP/CHOP/DAT across the boundary and does no network I/O, so it is the wrong tool for a fetch. Stock asyncio blocks TD's frame loop; if an advanced worker hosts an asyncio loop it still obeys the zero-TD-access + queue-handoff rules.
Heavy-Build Safety: Crash Causes and Safe-Default Caps
Load this section before any heavy build; the gating protocol and stop conditions live in rules/performance.md and always apply.
| Cause | Mechanism | Warning metric (threshold) | Mitigation |
|---|
| Resolution explosion (Resolution TOP, Optimize) | Pixel count and TOP memory scale with width*height | gpuCookTimeMs spikes or GPU headroom < 20% | Clamp to <= 1920x1080, lower format, use Limit Resolution |
| Unbounded feedback loop (Feedback TOP) | Loop keeps accumulating data every frame | Feedback gpuCookTime or memory.gpuMemUsedMB rises each check | Fixed resolution, decay < 1, Reset wired, bypass while wiring |
| Always-cooking operators compounding (Cook, Optimize) | Render, output, viewer, or export chains demand cooks every frame (time-dependent ops are only flagged -- undemanded they do not cook; see td-python.md Cook Model) | totalCooks climbs and cookedThisFrame stays true while idle | Bypass during build, terminate in Null, disable viewers/outputs until measured |
| Expression-driven cook cascade (Cook) | Parameter references pull upstream nodes repeatedly | Null/In/Out cpuCookTime is large | Cache stable values, remove cross-network expressions, inspect dependent path |
| GLSL infinite loop or GPU timeout (GLSL crash debugging) | GPU work never completes or OS resets the device | Frame time spike, UI hang, fatal Vulkan error | Constant-bounded loops only; reduce shader complexity |
| GLSL out-of-bounds array access (GLSL crash debugging) | Illegal sampler or uniform array index can crash TD | Info DAT error or crash on cook | Guard dynamic indexes with TD_NUM_*_INPUTS; validate uniform array sizes |
| Huge SOP geometry on CPU (Optimize) | CPU transforms or rebuilds many points/primitives | cpuCookTimeMs or childrenCPUCookTime jumps |
Safe-Default Caps (apply when creating risky operators)
- TOP resolution: default new TOPs to bounded resolution (
<= 1920x1080). Never create 4K, 8K, or 16k unless the user explicitly asked. Before allocating, confirm w*h*channels*bytes against memory.totalGpuMemMB. Prefer 8/16-bit fixed pixel formats over 32-bit float unless precision is required.
- Feedback loops: ALWAYS bound them. Fix the resolution inside the loop, add a decay/multiply
< 1, wire a Reset, and keep the loop bypassed while wiring so it is not live during construction. Terminate the loop, and every TOP/CHOP chain, in a Null.
- Bypass while wiring: bypass or disable cooking while wiring heavy chains. Do not leave Movie File In, Audio, Render, Timer, feedback, output, or viewer-driven ops live and cooking while building around them. Enable only after the chain is complete and measured.
- Geometry and duplication: cap SOP point/primitive counts. For many duplicates, use GPU instancing or POPs, not Copy SOP or
comp.copy(). Transform at the Geometry COMP object level, not the SOP level.
- Instances and particles: start modest and ramp up while watching
memory.gpuMemUsedMB. Never default to millions. CPU particle systems should start around 10k max; beyond that, go GPU/instancing.
- CHOPs: keep sample rates and Trail/buffer windows small. Enable Time Slicing. Never create audio-scale sample-rate CHOPs without it. Use Audio File In CHOP, not Audio Play CHOP, for long files.
- GLSL: never write unbounded
for or while loops. Cap iterations with a constant. Bounds-check every dynamic array index with TD_NUM_*_INPUTS guards. Check the Info DAT for compile errors before relying on the op.
- Python via
execute_python: keep calls short and non-blocking. No synchronous blocking I/O or sleep on the main thread. No TOP.sample() in loops; use numpyArray(). Avoid store() in hot paths. Chunk large builds across frames.
Pre-Installed Packages
Commonly importable without installation: numpy, cv2 (OpenCV), requests, yaml (PyYAML), cryptography, attrs (only numpy and cv2 are documented as bundled; verify the rest in your build before relying on them). Auto-imported stdlib: math, re, sys, collections, enum, inspect, traceback, warnings.
requests blocks the frame - see the Threading ladder above. execute_python, parameter expressions, and operator/cook callbacks all run on TD's main thread, so a synchronous requests.get(...) (or urllib/socket, a large file read, subprocess.run, or a blocking DB call) freezes the whole UI/cook cycle for the round-trip - on a slow endpoint it can hang TD or exceed the 30s MCP timeout. requests has no default timeout; always pass timeout=(connect, read) in seconds. To fetch data, use the Web Client DAT (async, never blocks); if you must use requests, run it in a Thread Manager worker.
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.