TouchDesigner + Python
Updated: 2026-05*
This article was generated by Claude and some portions are still unverified.
1. Introduction
This lecture covers the fundamentals of Python integration in TouchDesigner (hereafter TD), for both Windows and macOS. TD is a node-based visual programming environment, but it embeds a Python 3 interpreter internally. Python lets you express things that would be verbose as a node graph, and connect to external data and external libraries concisely. The material assumes “Python used alongside nodes,” and walks from minimal working examples up to applications that use external libraries.
1.1 Topics covered
- TD’s Python runtime and OS-specific caveats
- Referencing Python from DATs and CHOPs, and manipulating nodes from Python
- Event-driven processing with the Execute family of DATs
- Installing and using external Python libraries via
pip - External communication via OSC and HTTP
1.2 References
This material draws on the following sites and official documentation.
References)
- Python in TouchDesigner (Derivative official)
- Introduction to Python Tutorial (Derivative official)
- matthewragan.com (Matthew Ragan’s collected explanations)
- MediaPipe in TouchDesigner (Magic & Love Interactive)
2. Development environment
2.1 Do you need to install Python?
The short answer is no — installing TouchDesigner alone is enough; Python is available out of the box. TD bundles a Python interpreter and major libraries such as numpy, opencv, and requests are already included. The examples in chapter 3 and the first half of chapter 4 run with no additional installation.
Additional installation is only needed when you use libraries that are not bundled with TD (python-osc, pyautogui, mediapipe, anthropic, etc.). In this material that applies to section 4.3 onward and to the chapter 5 “fun extras.”
| What you want to do | Windows | macOS |
|---|---|---|
| Examples in ch. 3 / first half of ch. 4 (bundled-only) | No additional install | No additional install |
| Section 4.3 onward / ch. 5 (external libraries) | No extra Python; just run pip |
Need extra Python via Homebrew |
Note for Mac students: On Windows you can call pip directly from TD’s bundled python.exe, so no separate Python install is needed. On Mac, however, TD is an .app bundle, which makes the bundled Python awkward to call externally. Derivative officially recommends “installing a separate matching-version Python via Homebrew and gathering libraries with that pip.” The point is not to use the separately-installed Python itself, but to use its pip command. Mac students therefore need to complete the 2.5 setup before moving on to section 4.3 and later.
2.2 Differences in OS assumptions
TD supports both Windows and macOS, but Python handling differs significantly.
| Item | Windows | macOS |
|---|---|---|
| TD install path | C:\Program Files\Derivative\TouchDesigner |
/Applications/TouchDesigner.app |
| Bundled Python executable | Callable directly as bin\python.exe |
Inside .app bundle (direct external use discouraged) |
| Mainstream way to add external libs | pip --target against the bundled python |
Install a matching Python via Homebrew |
| Apple Silicon consideration | Not needed | ARM and Intel TD have different Homebrew paths |
On macOS, rather than “dropping libraries directly into the TD .app,” the Derivative-recommended approach is to install a Python of the same version separately via Homebrew, install libraries into its site-packages with pip, and reference them from TD.
2.3 Checking the bundled Python version
Same procedure on both OSes. Launch TD and open the Textport with Alt+T (Option+T on Mac). In the Textport, run the following.
import sys
print(sys.version)
print(sys.executable)The mapping between major TD versions and bundled Python is roughly as follows.
| TouchDesigner | Python |
|---|---|
| 2022.xxxxx | 3.9 series |
| 2023.xxxxx to current | 3.11 series |
These are approximate. Always confirm against your own TD install.
On Mac TD, sys.executable outputs something like this.
/Applications/TouchDesigner.app/Contents/Frameworks/Python.framework/Versions/3.11/bin/python3.11This path is informational only. Calling this Python directly to run pip is not recommended; use the Homebrew-based approach described below instead.
2.4 Installing external libraries: Windows
Creating a project folder
Installing libraries inside the project folder is recommended because the project travels with its dependencies — easier to share and reproduce.
In Command Prompt or PowerShell, move to the project folder.
cd C:\TD_Projects\my_projectIf that folder does not exist, create C:\TD_Projects\my_project in Explorer beforehand or with the mkdir command.
Creating a folder to hold the libraries
mkdir python_libsInstalling a library
"C:\Program Files\Derivative\TouchDesigner\bin\python.exe" -m pip install --target=./python_libs python-oscOn success you’ll see something like Successfully installed python-osc-x.x.x.
Verifying from TD
In TD’s Textport, run the following.
import sys
sys.path.append(project.folder + '/python_libs')
from pythonosc.udp_client import SimpleUDPClient
print(SimpleUDPClient)If <class 'pythonosc.udp_client.SimpleUDPClient'> is printed, you’re good.
To make the path persistent, either write it into the Execute DAT’s onStart (covered in 3.4), or specify the folder under Edit > Preferences > Python 64-bit Module Path.
2.5 Installing external libraries: macOS
Installing Homebrew
If Homebrew is not installed, follow the steps at brew.sh. Paths differ between Apple Silicon (M1/M2/M3/M4) and Intel.
| Mac | Homebrew binary |
|---|---|
| Apple Silicon | /opt/homebrew/bin/brew |
| Intel | /usr/local/bin/brew |
Install a matching Python
If TD’s bundled Python is in the 3.11 series, in Terminal run:
brew install python@3.11After installation, verify that the pip path exists.
ls /opt/homebrew/opt/python@3.11/bin/pip3.11If the filename is displayed, you’re good. If you get an error, the installation failed.
Note: Previously you could reach pip through the bin/pip3 symlink, but in current Homebrew pip3 has moved under libexec/bin/. The version-numbered pip3.11 is more stable, so this material uses that.
Creating a project folder
Put the project folder under your home directory. Avoid placing it directly on the Desktop or in Documents.
First, create the TD_Projects folder and a my_project folder.
mkdir -p ~/TD_Projects/my_projectThe -p option means “create the parent folder if it doesn’t exist,” which creates TD_Projects and my_project in one command.
Move into the new folder.
cd ~/TD_Projects/my_projectCreating a folder to hold the libraries
mkdir python_libsInstalling a library
/opt/homebrew/opt/python@3.11/bin/pip3.11 install --target=./python_libs python-oscOn success you’ll see something like Successfully installed python-osc-x.x.x.
Note: After installation you may see a trailing pip self-update notice (A new release of pip is available). Ignore it. Pip updates are best left to Homebrew via brew upgrade python@3.11.
Intel Macs
Paths differ on Intel Macs. Use the following.
/usr/local/opt/python@3.11/bin/pip3.11 install --target=./python_libs python-oscIf you run Intel TD on Apple Silicon, you need the Intel Homebrew. An alias helps avoid confusion.
alias iPIP311=/usr/local/opt/python@3.11/bin/pip3.11Verifying from TD
In TD’s Textport, run the following.
import sys
sys.path.append(project.folder + '/python_libs')
from pythonosc.udp_client import SimpleUDPClient
print(SimpleUDPClient)If <class 'pythonosc.udp_client.SimpleUDPClient'> is printed, the library loaded successfully.
macOS-specific notes
- Match TD’s architecture. Use ARM-Homebrew pip with ARM TD; use Intel-Homebrew pip with Intel TD (Rosetta). Mixing them causes import-time crashes.
- numpy, opencv, and requests are bundled with TD, so installing them yourself easily causes dependency conflicts. Don’t touch them unless you must. Verifying with a bundled library doesn’t actually test the external-Python integration path — for verification use a library that isn’t bundled (such as python-osc).
2.6 The TOE file location and project.folder
project.folder returns the path to the folder where TD’s .toe file is saved. It’s used frequently for specifying python_libs by relative path.
For this property to do what you want, the TOE file must be saved in the same project folder as python_libs.
- TOE file:
~/TD_Projects/my_project/myproject.toe - Libraries:
~/TD_Projects/my_project/python_libs/
With this layout, project.folder + '/python_libs' resolves to ~/TD_Projects/my_project/python_libs.
Common pitfall: if the TOE file is saved on the Desktop while libraries are installed under ~/TD_Projects/my_project/python_libs, project.folder points to the Desktop and the import fails. In Textport, run the following to check the save location.
print(project.folder)If the path is not what you expect, use File > Save As to save the TOE file in the correct place.
2.7 Using the Textport
Same on both OSes. Open it with Alt+T (Option+T on Mac). It is the output destination for print() and the most basic experimentation surface. Error stack traces also appear here. Keeping it open at all times speeds up debugging.
Note: If you run code containing time.sleep() in the Textport, TD’s entire UI freezes for that duration. For continuous processing, put the work in the Execute DAT’s onFrameStart (introduced in 3.4) instead — it’s the cleaner approach.
3. Simple Python integration examples
Starting from “what is a DAT?”, here are four minimal ways to connect nodes and Python. The code itself is OS-independent.
3.1 DAT = Data Operator
A DAT is the Operator family that handles text and tables. It is where you write Python scripts, and it’s the only family in TD that handles character information. Representative DATs are as follows.
- Text DAT: holds a single script or piece of text
- Table DAT: holds tabular data
- Execute DAT: runs automatically on events such as start, exit, every frame, etc.
- CHOP Execute DAT: triggers on CHOP value changes
- Parameter Execute DAT: triggers on parameter changes
3.2 Example 1: Run a script with Text DAT
The minimal example. Add a Text DAT and right-click > Run Script to execute it.
- Text DAT
- Enter the code below:
print('hello touchdesigner from python')
op('constant1').par.value0 = 0.5op('constant1') returns a reference to the CHOP named constant1 at the same hierarchy. .par.value0 is that parameter. The fact that node parameters are readable and writable from Python is the single most important principle of TD × Python.
3.3 Example 2: Express a parameter as a Python expression
Right-click any parameter > Python and the parameter enters Python expression mode (turns purple). The result of the expression is re-evaluated every frame.
- Transform TOP
- Rotate:
absTime.seconds * 30
- Rotate:
absTime.seconds is the elapsed seconds since TD started. Without wiring nodes together, a one-line Python expression gives you a dynamic parameter.
You can also read CHOP values from an expression.
- Transform TOP
- Rotate:
op('lfo1')['chan1'].eval() * 360
- Rotate:
3.4 Example 3: Use Execute DAT to set up the environment at startup
Add an Execute DAT and turn on the Start checkbox in the Parameters tab — the corresponding callback (onStart) is then called automatically at startup. A typical use is to add an external library path on launch.
- Execute DAT
- Parameters tab > Start: ON
- Enter the code below:
def onStart():
import sys
libs = project.folder + '/python_libs'
if libs not in sys.path:
sys.path.append(libs)
returnThis runs as-is on both Mac and Windows (using / as the path separator keeps it OS-independent).
3.5 Example 4: Detect value changes with CHOP Execute DAT
Example: increment a counter the moment the mouse is clicked.
- Mouse In CHOP
- CHOP Execute DAT
- CHOP: specify the Mouse In CHOP
- Channels: left
- Off to On: ON
- Enter the code below:
def onOffToOn(channel, sampleIndex, val, prev):
op('count').par.value0 += 1
returnBecause it’s event-driven (running only when a change happens), this is lighter than per-frame monitoring.
4. Simple experiments toward practical use
From here are three experiments that are hard to do with nodes alone — uniquely Python territory.
4.1 Experiment A: Display a value from a JSON API as text
Fetch a value from an external Web API and render it as text in TD. Without installing requests, you can finish this with TD’s bundled Web Client DAT (same procedure on Mac and Windows).
Connect the node graph as follows.
- Web Client DAT
- Request URL:
https://api.open-meteo.com/v1/forecast?latitude=35.68&longitude=139.76¤t_weather=true - Method: GET
- Request: send with Pulse
- Callbacks DAT: specify a Text DAT (the callbacks one defined below)
- Request URL:
- Text DAT (for callbacks)
- Enter the code below:
def onResponse(webClientDAT, statusCode, headerDict, data, id):
import json
d = json.loads(data)
temp = d['current_weather']['temperature']
op('text1').par.text = f'Tokyo: {temp} C'
return- Text TOP
- Text: text1 (references the Text rewritten by the callback)
The temperature fetched from the API is reflected in the Text TOP.
That alone gives you the foundation of “visuals that react to external data.” Mapping temperature to hue or wind speed to particle velocity follows naturally.
4.2 Experiment B: Feed a numpy-generated waveform into a CHOP
A typical pattern for passing an array built in an external library into TD. Use Script CHOP. numpy is bundled with TD, so no additional install is needed (same on both OSes).
- Script CHOP
- In
script1_callbacks, enter the code below:
- In
import numpy as np
def onCook(scriptOp):
scriptOp.clear()
n = 512
scriptOp.numSamples = n # ← omit this and only the first sample sticks
t = np.linspace(0, 4 * np.pi, n)
y = np.sin(t) * np.exp(-t / 8) # decaying sine
chan = scriptOp.appendChan('decay_sine')
chan.vals = y.tolist()
returnNote: Script CHOP’s default numSamples is 1. Even if you assign a long list to chan.vals, without bumping numSamples only the first element is taken.
- Timer CHOP (optional)
- Connect upstream of Script CHOP to trigger Cook
Pass the generated decay_sine through Math CHOP or Eval CHOP and route it into your visuals.
4.3 Experiment C: OSC communication with python-osc
From here we use an external library (python-osc). Install python-osc ahead of time using the procedure in 2.4 (Windows) or 2.5 (macOS).
OSC is a de facto communication protocol for connecting to sensor devices, machine-learning inference servers, and audio software (Ableton Live, Max/MSP, etc.).
Set up the receiver with OSC In CHOP
- OSC In CHOP
- Network Port: 7000
- Active: ON
One-shot send from the Textport
In TD’s Textport, run:
import sys
sys.path.append(project.folder + '/python_libs')
from pythonosc.udp_client import SimpleUDPClient
client = SimpleUDPClient('127.0.0.1', 7000)
client.send_message('/test/hello', 1.0)
client.send_message('/test/position', [120.0, 240.0])
print('sent')Check the OSC In CHOP viewer and the following channels appear.
- test/hello: 1.0
- test/position1: 120.0
- test/position2: 240.0
Continuous sending from Execute DAT’s frame callback
Running continuously in the Textport freezes the UI, so put the work in Execute DAT’s onFrameStart.
- Execute DAT
- Parameters tab > Frame Start: ON
- Enter the code below:
import sys, math
libs = project.folder + '/python_libs'
if libs not in sys.path:
sys.path.append(libs)
from pythonosc.udp_client import SimpleUDPClient
client = SimpleUDPClient('127.0.0.1', 7000)
def onFrameStart(frame):
client.send_message('/test/hello', math.sin(absTime.seconds))
returnThe test/hello value of OSC In CHOP keeps swinging as a sine wave.
Note: OSC Out CHOP and OSC Out DAT are different things. TD has both OSC Out CHOP and OSC Out DAT, but only the DAT exposes the sendOSC() method to Python. The CHOP is a node that auto-sends values from its input wires and is not suited to sending arbitrary messages from Python. This material sends via python-osc, so we use neither.
Firewall permission
- Windows: On the first send/receive a “Windows Defender Firewall” dialog appears — allow it.
- macOS: Allow it under System Settings > Network > Firewall. In some cases you also need to check TouchDesigner under System Settings > Privacy & Security > Local Network.
5. Three fun extras
The three examples here all use libraries not bundled with TD. Windows students can just run the pip command in each “preparation” section. Mac students should first complete the Homebrew + Python 3.11 install in 2.5.
5.1 Extra 1: A bot that draws with the mouse cursor
Use pyautogui so TD itself moves its own cursor — a self-playing machine. Useful for “the screen is moving on its own” effects in presentations and installations.
Windows: install the library
"C:\Program Files\Derivative\TouchDesigner\bin\python.exe" -m pip install --target=./python_libs pyautoguimacOS: install the library
/opt/homebrew/opt/python@3.11/bin/pip3.11 install --target=./python_libs pyautoguimacOS extra step: Accessibility permission
On Mac, you must grant TouchDesigner Accessibility permission. Steps:
- Open System Settings > Privacy & Security > Accessibility
- Use the + button to add TouchDesigner.app and enable it
- Restart TD
Without Accessibility permission, no error is raised but the cursor simply will not move.
Script
Write a Lissajous-trajectory cursor-mover in Execute DAT’s onFrameStart.
- Execute DAT
- Parameters tab > Frame Start: ON
- Enter the code below:
import sys, math
sys.path.append(project.folder + '/python_libs')
import pyautogui
# Required: onFrameStart fires at 60 fps. The default pyautogui.PAUSE = 0.1
# blocks for 100 ms per call, tanking TD's frame rate.
pyautogui.PAUSE = 0
pyautogui.FAILSAFE = False # don't abort when the cursor reaches the top-left corner
def onFrameStart(frame):
t = absTime.seconds
cx, cy = 960, 540
x = int(cx + 300 * math.sin(t * 1.7))
y = int(cy + 300 * math.sin(t * 2.3))
pyautogui.moveTo(x, y)
returnTD’s Mouse In CHOP picks up that moved cursor, so if you wire your visuals to react to mouse coordinates you get a system that performs itself. Useful for unattended exhibition demos.
5.2 Extra 2: Real-time finger joints with MediaPipe
Co-locate Google MediaPipe with TD. A single webcam is enough to extract hand or face landmarks.
Windows: install the libraries
"C:\Program Files\Derivative\TouchDesigner\bin\python.exe" -m pip install --target=./python_libs mediapipe opencv-pythonmacOS: install the libraries
/opt/homebrew/opt/python@3.11/bin/pip3.11 install --target=./python_libs mediapipe opencv-pythonmacOS extra step: Camera permission
The first time you add a Video Device In TOP, a dialog appears under System Settings > Privacy & Security > Camera to allow TouchDesigner. After granting it, TD needs to be restarted.
Notes on MediaPipe on Apple Silicon
- ARM-native MediaPipe support is relatively recent. On older TD versions the wheel may not be findable.
- If
pip installfails, themediapipe-siliconfork is worth trying. - The ARM TD + ARM Python combination is the most stable.
Node setup
- Video Device In TOP
- Script CHOP
- References the Video Device In TOP above from Python
- Output channels: 21 joints × xy = 42 channels
- Geometry COMP
- Instancing: reference Script CHOP’s xy to make particles follow the fingertips
Script CHOP code
- Script CHOP
- Enter the code below:
import sys, numpy as np
sys.path.append(project.folder + '/python_libs')
import mediapipe as mp
mp_hands = mp.solutions.hands
hands = mp_hands.Hands(max_num_hands=1, min_detection_confidence=0.7)
def onCook(scriptOp):
scriptOp.clear()
img = op('videodevicein1').numpyArray(delayed=False)
if img is None:
return
# TD's TOPs use a Y-up (OpenGL) convention in the numpy array, so row 0 is
# the *bottom* of the image. MediaPipe expects image coordinates (Y-down,
# row 0 at the top), so flipud the array to make it upright before passing
# it in. Skip this and MediaPipe sees an upside-down hand (fingers pointing
# down), and detection accuracy drops significantly.
img = np.flipud(img).copy()
img = (img[:, :, :3] * 255).astype(np.uint8)
result = hands.process(img)
if result.multi_hand_landmarks:
lm = result.multi_hand_landmarks[0].landmark
for i, p in enumerate(lm):
# Because we passed in a vertically flipped image, MediaPipe's p.y
# is already aligned with TD's Y axis — use it as-is.
scriptOp.appendChan(f'x{i}').vals = [p.x]
scriptOp.appendChan(f'y{i}').vals = [p.y]
return“Particles emitted from fingertips,” “switch filters based on hand shape,” “pinch to zoom” — a wide range of gesture-interaction applications becomes immediately within reach.
5.3 Extra 3: Have an LLM propose visual parameters
Call the Claude or OpenAI API with a request like “suggest parameters that fit the current musical atmosphere,” then pour the returned JSON into TD’s parameters — driving the node graph through an “art director.” Network communication only, so no OS difference; only API-key handling needs care.
Windows: install the library
"C:\Program Files\Derivative\TouchDesigner\bin\python.exe" -m pip install --target=./python_libs anthropicmacOS: install the library
/opt/homebrew/opt/python@3.11/bin/pip3.11 install --target=./python_libs anthropicStoring the API key
Pass it through an environment variable rather than hardcoding it in source — that prevents many accidents.
- Windows: Add
ANTHROPIC_API_KEYunder System > About > Environment Variables, then restart TD. - macOS: Add
export ANTHROPIC_API_KEY="..."to~/.zshrc, then launch TD from Terminal withopen /Applications/TouchDesigner.app— TD inherits the environment variable.
Note: On macOS, if you launch the app by double-clicking in Finder, it may not inherit .zshrc environment variables. Launching from Terminal with open is the reliable approach.
Script
- Text DAT
- Enter the code below and run with Pulse:
import sys, os, json
sys.path.append(project.folder + '/python_libs')
import anthropic
client = anthropic.Anthropic(api_key=os.environ.get('ANTHROPIC_API_KEY'))
prompt = '''
We are about to direct a TouchDesigner visual.
Return JSON only, with the keys below. No explanation, no code fences.
- hue: 0.0-1.0
- saturation: 0.0-1.0
- speed: 0.0-2.0
- noise_amount: 0.0-1.0
Atmosphere theme: "the silence of the deep sea."
'''
msg = client.messages.create(
model='claude-haiku-4-5-20251001',
max_tokens=200,
system='You are a parameter designer. Output strict JSON only, no prose, no code fences.',
messages=[{'role': 'user', 'content': prompt}]
)
text = msg.content[0].text.strip()
# LLMs occasionally wrap the response in markdown code fences — strip them.
if text.startswith('```'):
text = text.split('```')[1]
if text.startswith('json'):
text = text[4:]
text = text.strip()
try:
params = json.loads(text)
except json.JSONDecodeError:
debug(f'LLM response was not valid JSON: {text!r}')
raise
op('constant_color').par.colorr = params['hue']
op('constant_color').par.colorg = params['saturation']
op('speed').par.value0 = params['speed']
op('noise1').par.amp = params['noise_amount']If you “switch themes every 30 seconds and ask the LLM for fresh parameters,” you get an AI-directed self-staging installation (bonus extension).
Note: Because of API billing and response latency (a few seconds), use this as a scene-switch director rather than for real-time control — that’s the realistic fit.
6. Summary
Python in TD is not a replacement for nodes. The role becomes clear once you treat it as the tool that takes on areas nodes are bad at — external communication, array processing, branchy logic, machine-learning inference.
OS-specific stumbling blocks, summarized:
| Stumbling block | Windows | macOS |
|---|---|---|
| How to install external libraries | pip --target against bundled TD python |
Parallel Python via Homebrew → use that pip with --target |
| pip command path | python.exe -m pip of bundled TD |
python@3.11/bin/pip3.11 (the version-numbered one is more stable) |
| Architecture conflicts | Basically no issue | Watch out for mixing ARM and Intel |
| Camera / mouse permissions | First-time dialog only | Explicit grant in System Settings + TD restart |
| Environment variable inheritance | Restart TD after setting env vars | Launching from Terminal with open is reliable |
Frequent import-related stumbling blocks include:
- The TOE file is saved outside the project folder (on the Desktop, etc.), so
project.folderpoints somewhere unexpected. - You verified with a bundled library such as numpy or requests, which doesn’t actually exercise the external-library integration path.
- You tried to call
sendOSC()on an OSC Out CHOP and it failed (use the DAT version).
A practical learning order:
- Get comfortable with parameter Python expressions (3.3)
- Become fluent with event-driven Execute DATs (3.4 / 3.5)
- Pull in external libraries through Script CHOP/TOP/DAT (4.2 / 5.2)
- Open TD up to the world through communication (4.1 / 4.3)
Once you have these, situations where a 5-minute node setup is replaced by 3 lines of Python become common, and the iteration loop for experimentation gets dramatically faster.
