PythonTap

Write Max objects in Python. PythonTap embeds a CPython interpreter in a Max package and runs a Python class as a Max object, in one of two ways:

  • tap.python~ runs the class as an audio object: its process() method is called on the signal — once per signal vector with numpy arrays, or once per sample with floats.
  • tap.python runs it as an ordinary object, without audio: messages in, messages out — and what a method returns is what the object outputs.

Either way:

  • The class's type-annotated attributes become Max attributes (@gain 0.5 in the object box, gain 0.5, getgain, attrui — it all works).
  • The class's public methods become Max messages, called according to their signatures, with arguments converted according to their type hints.
  • The source file is watched and hot-reloaded every time you save it, keeping attribute values, so you can live-code with Max running.
  • Python's print() output and tracebacks land in the Max console, posted from Max's main thread whatever thread printed — and no exception your code raises, sys.exit() included, can take Max down.
from attrs import define, field


@define
class default:
    """The class loaded by tap.python~ and tap.python when no source argument is given.

    Demonstrates the mapping from Python to Max:
    - the annotated 'gain' field becomes a Max attribute
    - any public method (like 'greet') becomes a Max message
    - methods named 'int' and 'float' answer those standard Max messages
    - process() runs on the audio signal in tap.python~, once per sample (see
      numpy_gain.py for the much cheaper once-per-vector form)
    - in tap.python, what a method returns is output: bang outputs the gain
    """

    gain: float = field(default = 1.0)

    def greet(self, name: str) -> None:
        print(f"hello {name}, from python!")

    def process(self, x: float) -> float:
        return x * self.gain

    def bang(self) -> float:
        return self.gain

    # These two come last on purpose: once `def float` has run, the name `float` in
    # the class body means this method, so an annotation written after it would no
    # longer mean the built-in type. (Inside method bodies the built-ins are unaffected.)

    def int(self, new_gain: int) -> None:
        self.gain = float(new_gain)

    def float(self, new_gain: float) -> None:
        self.gain = new_gain
[tap.python~ default]   ← loads python/default.py: a 'gain' attribute, process() on the signal
[tap.python]            ← the same file, without audio: a bang outputs the gain

The package comes with CPython 3.13, numpy and attrs, on macOS (Apple Silicon and Intel) and Windows, for Max 9.

This book

In Max itself, the package's guide Writing Max Objects in Python and three tutorials are in the Documentation window (Package Docs › PythonTap), and PythonTap Overview is in the Extras menu.