Skip to content

Embedded Python


Macros substitute text. Jinja generates repetitive structure. But sometimes you need to compute -- read a data file, sort a list, build a lookup table, or share configuration across files. This is where embedded Python enters. IVLS lets you execute Python code at compile time, use the results in your KSP, and even organize that Python code into external files with full editor support.

run<<>> -- Execute at Compile Time


A run<<>> block executes Python code during compilation. It runs for side effects only -- it does not produce any KSP output. The block is removed from the source after execution:

run<<
    BROWSER_ENTRIES = 20
    TOTAL_TAGS = 116
    NUM_LAYERS = 4
>>

After this block runs, the variables BROWSER_ENTRIES, TOTAL_TAGS, and NUM_LAYERS exist in the compile-time Python namespace. They are available to subsequent read<<>> blocks, <: :> Jinja expressions, and any further run<<>> blocks in the same file or compilation unit.

run<<>> can do anything Python can: import modules, read files, perform calculations, build data structures:

run<<
    import json
    with open('sounds.json') as f:
        sounds = json.load(f)

    sound_names = [s['name'] for s in sounds]
    NUM_SOUNDS = len(sounds)
>>

The key constraint: run<<>> produces no output. It only sets up the namespace. To get values into your KSP, you use read<<>> or Jinja.

read<<>> and <: :> -- Inline Python Results


read<<>> evaluates a Python expression and inserts the result directly into your KSP source:

declare sounds[read<<NUM_SOUNDS>>]

If NUM_SOUNDS is 256, this becomes declare sounds[256].

The <: :> Jinja syntax does the same thing for simple expressions:

declare sounds[<: NUM_SOUNDS :>]

Both evaluate Python and inline the result. The difference is processing order: read<<>> is processed during the Python block phase, while <: :> is processed during Jinja rendering. For simple values, they are interchangeable. For expressions that depend on other Jinja constructs (like loop variables), use <: :>.

The result is converted to a string automatically. Integers become their string representation. Lists are joined with newlines. Other types use Python's str() conversion.

Shared Namespace


All run<<>> and read<<>> blocks within a compilation unit share a single Python namespace. Variables defined in one run<<>> block are visible in all subsequent blocks:

run<<
    base_count = 4
>>

{ Later in the same file, or another file in the same compilation }

run<<
    total = base_count * 8
>>

declare items[<: total :>]  { becomes: declare items[32] }

This shared namespace is what makes the system cohesive. You can set up data early in your project and reference it everywhere.

The namespace also includes the Python builtins (len, range, abs, sorted, etc.) and a base_path variable pointing to the current file's directory.

External .py Files


Embedding Python in KSP files works for small snippets, but larger computations benefit from proper .py files. You can import them using standard Python import or exec:

run<<
    exec(open('my_data.py').read())
>>

Or, more cleanly, by structuring your data as a pip-installable Python package (discussed below). External .py files give you full editor support -- syntax highlighting, autocompletion, type checking, linting -- that you do not get inside run<<>> blocks.

The working directory during run<<>> execution is set to the file's directory, so relative paths resolve naturally.

Pip Packages for Cross-File Data


For larger products, the Python data layer can be organized as a pip-installable package. The Polycore product, for example, has a Python package that defines its sound database, tag taxonomy, and build configuration. This package is installed into the project's Python environment, and all KSP files can import it:

run<<
    import polycore
    sound_names = polycore.get_sound_names()
    NUM_SOUNDS = len(sound_names)
>>

The advantages are significant:

  • Cross-file sharing -- multiple KSP files can import the same package without duplicating data
  • Full LSP support -- your editor treats the package as normal Python, with autocompletion and type checking
  • Testability -- you can write unit tests for your data processing, independent of the KSP compilation
  • Version control -- the data package has its own clear structure and change history

When to Compute in Python vs Emit KSP


A common question: should I compute this value in Python and inline the result, or should I emit KSP code that computes it at runtime?

Compute in Python when the value is known at compile time and does not change during instrument operation. Sound counts, tag bitmasks, group indices, UI layout positions, sorted lists -- all of these are fixed once the instrument is built. Computing them in Python means they become constants in the compiled KSP, costing no runtime performance.

Emit KSP when the value depends on runtime state -- user input, MIDI data, engine parameters, or anything that changes while the instrument is loaded. KSP runs inside Kontakt's real-time engine; Python runs only during compilation.

A practical example from Polycore's browser:

run<<
    BROWSER_ENTRIES = 20
    TOTAL_TAGS = 116
    NUM_USED_TAGS = len(polycore.tags)
>>

{ Python computed the counts. KSP uses them as constants. }
declare entry_btn[<: BROWSER_ENTRIES :>]
declare tag_btn[<: TOTAL_TAGS :>]
declare const TAG_COUNT := <: NUM_USED_TAGS :>

{ Jinja stamps the computed tag names directly into a KSP array literal. }
declare !tag_names[] := (<: ','.join([f'"{tag}"' for tag in polycore.tags]) :>)

The tag names, indices, and counts are all known at build time. Python computes them from a structured data source, and the result is baked into the KSP as literal arrays and constants. No runtime computation needed.

Processing Order


Understanding when each layer runs helps avoid surprises:

  1. run<<>> blocks execute first (Python phase)
  2. read<<>> blocks evaluate and inline their results (Python phase)
  3. <: :> and <! !> blocks render via Jinja (template phase)
  4. define and macro substitutions happen (preprocessor phase)
  5. KSP compilation parses the final text into executable Kontakt script

Each layer can only see the output of layers before it. A run<<>> block cannot reference a define (defines have not been processed yet). A Jinja <: :> expression can reference Python variables (Python ran first) but not a macro parameter (macros run later). This ordering is consistent and predictable once you know it.


Embedded Python completes the metaprogramming stack. Defines and macros handle text substitution and reusable snippets. Jinja handles structural repetition and conditional inclusion. Python handles computation, data processing, and cross-file shared state. Together, they let you write a compact, data-driven source that expands into the full KSP your instrument needs.