Skip to main content

py2pd - Python <-> PureData

Roundtrip parsing and generation of pure-data patches from python.

py2pd is a fork and extensive rewrite of Dylan Burati's puredata-compiler using some of the ideas from py2max.

Features

  • Builder API -- imperative patch construction with add(), link(), and automatic layout

  • AST API -- round-trip parsing of .pd files via frozen dataclasses, preserving statements it does not model (data structures, scalars, array data) verbatim

  • Bridging -- convert freely between Builder and AST with from_builder() / to_builder()

  • All GUI types -- Bang, Toggle, NumberBox, Symbol, HSlider, VSlider, HRadio, VRadio, Canvas, VU

  • Subpatches -- nested patches with auto-inferred inlet/outlet counts and graph-on-parent support

  • Abstractions -- reference external .pd files with auto-inferred I/O

  • Connection validation -- index checking against a registry of common Pd objects, including those whose inlet and outlet counts depend on their creation arguments

  • Patch optimization -- deduplicate connections, collapse pass-throughs, remove unused nodes

  • SVG export -- visualize patches as SVG

  • Externals discovery -- platform-aware scanning for .pd abstractions and binary externals

  • libpd validation -- load patches into libpd via cypd and check for errors (pip install py2pd[extras])

  • hvcc integration -- validate and compile patches with the Heavy Compiler Collection (pip install py2pd[extras])

  • Zero runtime dependencies -- Python 3.13+, no required dependencies

Install

pip install py2pd

Quick Start

from py2pd import Patcher

# Create a simple synthesizer patch
p = Patcher('synth.pd')

osc = p.add('osc~ 440')
gain = p.add('*~ 0.3')
dac = p.add('dac~')

p.link(osc, gain)
p.link(gain, dac)
p.link(gain, dac, inlet=1)  # stereo

p.save()

Builder API

The Patcher class provides methods to add nodes and connect them.

Adding Nodes

from py2pd import Patcher

p = Patcher('example.pd')

# Objects
osc = p.add('osc~ 440')
filter_obj = p.add('lop~ 1000')

# Messages
bang = p.add_msg('bang')
freq_msg = p.add_msg('440')

# Comments (semicolons and commas are escaped, so they display as written)
p.add_comment('gain stage; adjust to taste')

# GUI Elements
slider = p.add_hslider(min_val=20, max_val=20000, width=150)
toggle = p.add_toggle(default_value=1, send='onoff')
numbox = p.add_numberbox(min_val=0, max_val=127)
bang_btn = p.add_bang(send='trigger', label='Click')

Connecting Nodes

Use link() to connect nodes. By default, outlet 0 connects to inlet 0:

p.link(osc, gain)              # outlet 0 -> inlet 0
p.link(gain, dac)              # left channel
p.link(gain, dac, inlet=1)     # right channel (stereo)
p.link(trigger, pack, outlet=1, inlet=2)  # specific ports

Outlet and inlet indices are validated eagerly when the node's I/O counts are known. Out-of-range indices raise PdConnectionError at link() time rather than silently creating invalid connections. Objects with unknown counts -- externals, abstractions, and anything absent from the registry -- skip validation.

Counts come from lookup_object_io(), which resolves the object's full text rather than just its class name, because much of Pd's arity depends on creation arguments:

from py2pd import lookup_object_io

lookup_object_io('dac~')            # (2, 0)
lookup_object_io('dac~ 1 2 3 4')    # (4, 0) -- one inlet per named channel
lookup_object_io('trigger b f s')   # (1, 3)
lookup_object_io('route a b c')     # (1, 4)
lookup_object_io('some-external')   # (None, None) -- unknown, not validated

Pass validate_links=False to Patcher() to downgrade the check to a PdConnectionWarning. Reading an existing patch does this automatically, since the registry cannot know the arity of every object a patch may contain.

Subpatches

Create reusable subpatches:

def make_envelope() -> Patcher:
    p = Patcher()
    inlet = p.add('inlet')
    vline = p.add('vline~')
    outlet = p.add('outlet~')
    p.link(inlet, vline)
    p.link(vline, outlet)
    return p

main = Patcher('main.pd')
osc = main.add('osc~ 440')
env = main.add_subpatch('envelope', make_envelope())
vca = main.add('*~')

main.link(osc, vca)
main.link(env, vca, inlet=1)

Graph-on-Parent

Subpatches can expose their GUI elements to the parent patch using graph-on-parent mode:

inner = Patcher()
slider = inner.add_hslider(min_val=0, max_val=1000, label='freq')
outlet = inner.add('outlet')
inner.link(slider, outlet)

main = Patcher('main.pd')
ctrl = main.add_subpatch(
    'controls', inner,
    graph_on_parent=True,
    hide_name=True,
    gop_width=150,
    gop_height=40,
)

Abstractions

Reference external .pd files as objects in your patch:

from py2pd import Patcher

p = Patcher('main.pd')

# With explicit inlet/outlet counts
synth = p.add_abstraction('my-synth 440 0.5',
                          num_inlets=2, num_outlets=1)

# Auto-infer I/O from the source file
synth = p.add_abstraction('my-synth 440',
                          source_path='my-synth.pd')

dac = p.add('dac~')
p.link(synth, dac)
p.link(synth, dac, inlet=1)

Layout Options

Default layout - nodes flow top-to-bottom:

p = Patcher('patch.pd')
p.add('osc~ 440')   # Row 1
p.add('*~ 0.5')     # Row 2
p.add('dac~')       # Row 3

Grid layout - organized columns:

from py2pd import Patcher, GridLayoutManager

grid = GridLayoutManager(columns=4, cell_width=80, cell_height=35)
p = Patcher('grid.pd', layout=grid)

Canvas geometry - window position, size and font:

p = Patcher('patch.pd', canvas_x=100, canvas_y=80,
            canvas_width=640, canvas_height=480, font_size=12)

Auto layout - arrange by signal flow:

p = Patcher('patch.pd')
# Add nodes in any order...
p.auto_layout(margin=50, row_spacing=50, col_spacing=100)

Saving and Export

p.save()                    # Save to filename from constructor
p.save('other.pd')          # Save to specific file
p.save_svg('patch.svg')     # Export visualization as SVG
svg_str = p.to_svg()        # Get SVG as string

Optimization

Clean up patches by removing unused elements and simplifying connections:

p = Patcher()
osc = p.add('osc~ 440')
gain = p.add('*~ 0.5')
unused = p.add('+~ 0.1')  # not connected to anything
dac = p.add('dac~')
p.link(osc, gain)
p.link(gain, dac)
p.link(gain, dac)  # accidental duplicate

result = p.optimize()
# result == {'nodes_removed': 1, 'connections_removed': 2,
#            'duplicates_removed': 1, 'pass_throughs_collapsed': 0,
#            'subpatches_optimized': 0}

The three passes run in order:

  1. Deduplicate connections -- removes exact-duplicate patch cords.

  2. Pass-through collapse -- bypasses single-in/single-out nodes (opt-in via collapsible_objects). A run of adjacent collapsible nodes is joined end to end into a single connection.

  3. Unused element removal -- removes disconnected Obj nodes. GUI elements, comments, subpatches, abstractions, arrays, messages, and floats are never removed. Nor are objects that do their job without patch cords -- inlet, outlet, table, array, declare, block~, switch~, namecanvas and friends -- since being disconnected says nothing about whether they are used. Nodes with active send/receive parameters are preserved.

Use recursive=True to optimize inner subpatches as well:

result = p.optimize(recursive=True)

Validation

p.validate_connections(check_cycles=True)  # Raises on invalid connections

# Collect problems instead of raising
errors = p.validate_connections(raise_on_error=False)

Cycle detection is iterative, so it handles patches of any depth.

AST API (Round-trip Parsing)

For modifying existing patches with immutable AST nodes:

from py2pd import parse_file, serialize

# Parse existing patch
ast = parse_file('input.pd')

# Modify the AST...

# Write back
with open('output.pd', 'w') as f:
    f.write(serialize(ast))

Converting Between APIs

You can convert between AST and Builder representations:

from py2pd import parse_file, to_builder, from_builder

# AST -> Builder: parse then edit with the more convenient API
ast = parse_file('input.pd')
patch = to_builder(ast)
patch.add('osc~ 880')
patch.save('output.pd')

# Builder -> AST: for analysis or transformation
ast = from_builder(patch)

When to Use Each API

Use Case Recommended API
Creating patches from scratch Builder
Modifying existing patches Builder (via to_builder())
Round-trip of complex patches AST
Building analysis/refactoring tools AST
Batch search/replace across .pd files AST

For most workflows, parse to AST then convert to Builder for editing. Use the AST API directly when you need to preserve elements the Builder doesn't model (e.g., coords, comments) or need immutable transformations.

AST node types are available from the py2pd.ast module:

from py2pd.ast import PdPatch, PdObj, PdMsg, Position, transform, find_objects

GUI Elements

Method Description
add_bang() Bang button
add_toggle() On/off toggle
add_numberbox() Editable number
add_float() Float atom
add_symbol() Symbol/text input
add_hslider() Horizontal slider
add_vslider() Vertical slider
add_hradio() Horizontal radio buttons
add_vradio() Vertical radio buttons
add_canvas() Background/label area
add_vu() VU meter

Non-GUI helpers: add_comment() adds a #X text comment, escaping separators so a semicolon in the text does not end the statement.

All GUI add_* methods accept every parameter from the underlying constructor, including IEM styling options (label_x, label_y, font, font_size, bg_color, fg_color, label_color, etc.). Defaults match PureData's standard values.

Discovery

Scan the filesystem for installed PureData externals:

from py2pd import discover_externals, default_search_paths, extract_declare_paths

# Find all externals on platform-default search paths
registry = discover_externals()
# registry == {'my-external': (2, 1), 'reverb~': (None, None), ...}

# See which paths are searched
paths = default_search_paths()

# Extract -path declarations from a parsed patch
from py2pd import parse_file
ast = parse_file('input.pd')
declared = extract_declare_paths(ast)

discover_externals() returns a dict mapping external names to (num_inlets, num_outlets) tuples. For .pd abstractions the counts are inferred from the file; binary externals get (None, None).

Integrations

Validation (cypd/libpd)

Validate patches by loading them into libpd and checking for errors:

pip install py2pd[extras]
from py2pd.integrations.cypd import validate_patch

p = Patcher('synth.pd')
osc = p.add('osc~ 440')
dac = p.add('dac~')
p.link(osc, dac)

result = validate_patch(p)
if not result.ok:
    print("Errors:", result.errors)
    print("Warnings:", result.warnings)

The patch is written to a temporary file, so a patch that references sibling abstractions will report them as missing. Pass work_dir to validate in the directory the patch belongs to:

result = validate_patch(p, work_dir='patches/')

libpd is a process-wide singleton, so calls are serialised internally and the search path is reset afterwards.

hvcc (Heavy Compiler Collection)

Build patches compatible with the hvcc compiler for generating C/C++ code:

pip install py2pd[extras]

HeavyPatcher validates objects at add-time, across every add_* method -- GUI constructors included, so an unsupported object such as vu is rejected where it is added rather than surfacing at compile time:

from py2pd.integrations.hvcc import HeavyPatcher, HvccGenerator

p = HeavyPatcher(generators=[HvccGenerator.DPF])

# add_param creates an [r __hv_param_name ...] receive object
freq = p.add_param('freq', min_val=20.0, max_val=20000.0, default=440.0)
osc = p.add('osc~')
dac = p.add('dac~')
p.link(freq, osc)
p.link(osc, dac)
p.link(osc, dac, inlet=1)

Validate existing patches:

from py2pd.integrations.hvcc import validate_for_hvcc

result = validate_for_hvcc(p)
if not result.ok:
    print("Errors:", result.errors)

Compile to C/C++ (requires hvcc installed):

from py2pd.integrations.hvcc import compile_hvcc

result = compile_hvcc(p, name='MySynth', output_dir='build/', timeout=600)
if not result.ok:
    print(result.stderr)

Error Handling

from py2pd import (
    PdConnectionError,      # Invalid connection arguments (including out-of-range indices)
    PdConnectionWarning,    # Out-of-range index when validate_links=False
    NodeNotFoundError,      # Node not in patch
    InvalidConnectionError, # Bad inlet/outlet index (from validate_connections)
    CycleWarning,           # Feedback loop detected
)

PdConnectionError is raised eagerly by link() when outlet or inlet indices exceed the node's known I/O counts. Node.__getitem__ (e.g., osc[2]) raises ValueError for out-of-range outlet indices. Both checks are skipped for objects with unknown counts (num_outlets=None / num_inlets=None), and link() warns instead of raising when the patch was created with validate_links=False.

to_builder() issues UnsupportedElementWarning (from py2pd.ast) for any statement the Builder cannot represent, rather than dropping patch content silently.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

py2pd-0.2.0.tar.gz (59.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

py2pd-0.2.0-py3-none-any.whl (62.3 kB view details)

Uploaded Python 3

File details

Details for the file py2pd-0.2.0.tar.gz.

File metadata

  • Download URL: py2pd-0.2.0.tar.gz
  • Upload date:
  • Size: 59.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.2

File hashes

Hashes for py2pd-0.2.0.tar.gz
Algorithm Hash digest
SHA256 64217b70e9d60efc9c10927e092da658dbc273d5c650809a77fbc1ef16579850
MD5 1ccf1f71d54e9a72ac82cc6454da889a
BLAKE2b-256 5e90e6d424684df138ba168dced83489a859111e172927ce47ca9ff73fb2c0be

See more details on using hashes here.

File details

Details for the file py2pd-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: py2pd-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 62.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.2

File hashes

Hashes for py2pd-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ea8d860d3bae23a355114d12802ef210fe74b9d6552945168b002bb82d8d7dbb
MD5 abdee0ae26eab72494a387ac8785f389
BLAKE2b-256 be147fa1c5e4e10f6aa6e95e595fc64807bb481fee5735a2137b6f34fa74ba72

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page