Writing a class

Python sources live in the package's python folder. [tap.python~ name] loads python/name.py and instantiates the class name defined in it (the file, class, and argument must share the same name, which must be a valid Python identifier; with no argument, default is loaded). The file is loaded by its path, never through import, so a file named like a standard-library module (random.py, json.py) works and does not shadow that module for anyone else. Other files in the python folder can be imported by your class as helper modules (the folder is on sys.path, after the standard library) — a helper is imported afresh whenever a class file that changed is saved (the loader forgets every module imported from the folder first), but a save of the helper alone is not watched: save the class file too, with a change, to pick it up. No bytecode is written for anything in the folder. Text is UTF-8: the interpreter runs in Python's UTF-8 mode, so open() reads and writes UTF-8 unless you pass another encoding, whatever the machine's locale.

Attributes

Class-level annotated fields (e.g. via attrs) become Max attributes. int maps to a Max long, float to float64, bool to an on/off long (your field receives a real bool), anything else to a symbol; Optional[X], X | None, Annotated[X, …] and Final[X] count as X, a subclass of one of those types (np.float64, an IntEnum) as its base, and numpy's scalar types (np.int64, np.bool_) as the kind they hold. A union of several kinds (float | int, str | float) is a symbol attribute in Max whose value reaches your field as the atom carried it — an int, float or str — like an unannotated parameter. Names starting with _ are private, and ClassVars are not fields. If a hint cannot be resolved (say a name imported only under TYPE_CHECKING), the console says so and the annotation is read as written.

Messages

Public methods (including classmethods and staticmethods) become Max messages, called according to their signature: parameters with defaults are optional, *args takes any number of arguments, and a keyword-only parameter must have a default (a method with a required keyword-only parameter is not exposed). Argument hints (int, float, bool, str) drive the conversion from Max atoms; an unannotated parameter receives the atom as it is (an int, float or str). A last parameter hinted list[float] (or list[int], list[str], list) or np.ndarray, with no *args after it, takes every atom left after the others — as one list, or a float64 array — so def taps(self, values: list[float]) answers taps 0.1 0.2 0.3; with none left it is empty, or its default. Methods named int, float, symbol, and bang map to those standard Max messages. Names Max or the object handle themselves (filechanged, dsp64, notify, assist, loadbang, dblclick, anything, …, and the object's own attributes mode, latency and latencysamples) are not exposed — as methods, or as fields; the console names any skipped this way so you can rename it.

Hot reload

Saving the .py file reloads it in place, as a fresh module (names you deleted from the file are gone): the attributes and messages follow the new class, and audio resumes with the new code. Until the new code is ready the object keeps running the old one, so a successful reload swaps in without a gap in the audio. Attribute values carry over — set from the patcher or by your own code — for every attribute the new class still has with the same type; an attribute whose type changed starts from its new default, and one you removed disappears from the object. If the file has an error, the object prints the traceback to the Max console and outputs silence until the next successful reload (and the attribute values come back with it); a save that breaks a file shared by many objects is reported once, not by each, while an object created later with the file still broken says so again. Every object using the same file shares one execution of it per save, and the console says so once — Loaded name.py: process() bound, one call per sample (or per vector), from whichever object ran the file — along with anything true of the class, such as a method skipped for its name; a save that changes nothing reloads silently. Errors that belong to one object, such as an exception in its constructor, are still reported by each. Max's file watcher notices a save within a couple of seconds, but it coalesces saves made in quick succession — a script saving once a second, say — so the object then reloads for only some of them, and can lag behind until they stop; sending the object filechanged reloads it at once.