What it is
PyMini is a Python-subset interpreter written in Swift and built into the keyboard. Every custom panel and every custom action runs on it. It reads the shape of Python you already know, and the same language serves whether a script draws an interface or rewrites a word.
# an action: text in, text out
def transform(text):
return text.upper()
# a panel: a view, and what a tap does
def view(state):
return button("Shout", "shout")
def on_action(action, state):
insert(context()["word"].upper())
return stateWhy use it
We built PyMini because Clink needed a way for people to script their own keyboard, and nothing we found would fit inside one. Nothing about it is tied to keyboards, though, so it is now open source as its own Swift package, for any app that wants to let its users write a little Python.
It is not CPython, and it does not embed it. A keyboard extension gets a fraction of the memory an app does, and CPython's binary and heap would spend that budget before your script ran. PyMini is a few hundred kilobytes of Swift with no binary dependencies.
Reach for it wherever users want logic of their own: text transforms in a writing app, rules in a to-do or budgeting app, formulas, automations, game mods, or a plugin system for an app with a community around it. Plenty of people already know some Python, so there is no new language to teach, and a script is plain text you can store, sync and share like any other setting.
import PyMini
let py = PyRuntime(modules: ["math", "json", "re"])
// your app, as a Python function
py.define("shout") { call in
try call.argument(0, as: String.self).uppercased()
}
// a script one of your users wrote
try py.load("""
def tidy(text):
return shout(" ".join(text.split()))
""")
try py.call("tidy", " hello there ") // HELLO THERE| Call | What it does |
|---|---|
import PyMini | No dependencies and nothing to vendor. Add the Swift package and it runs on iOS 15, macOS 12, tvOS 15, watchOS 8 and visionOS 1 or later. |
py.define("shout") { call in ... } | Your app's features become Python functions and modules. Arguments arrive as Swift types, and what you return goes back to the script as a Python value. |
PyLimits(steps: 200_000) | Every call runs on a step budget, and running out is an error no script can catch. A careless while True ends with a message, not a frozen app. |
PyRuntime(modules: ["math"]) | Scripts get no files, no network and no processes. They reach only the functions you define and the standard modules you leave switched on. |
py.call(hook: "on_open") | Call into a script when something happens in your app. A hook the script never defined is skipped, so every hook can be optional. |
catch let error as PyError | Mistakes come back as a Python exception name and a line number, ready to show the person who wrote the script. |
import sys | sys.version names the interpreter and the version, and the pymini command prints the same line. A script can report which build the app around it carries. |
The source, the language reference and the tests are on GitHub.
What it promises
There is no filesystem, no network and no process for a script to reach: the sandbox is the shape of the interpreter, not a list of things it refuses. import is the one door out, and it opens onto a short list of namespaces that only compute. Everything else costs steps. Every expression, every turn of a loop, every regular-expression step and every line of JSON spends one of 2,000,000, and strings and lists are capped as they grow.
Importable in a shared panel
jsonmathrandomresystime
The budget cannot be caught. An except clause catches a KeyError; it does not catch the abort that ends a runaway loop, because catching it is exactly how a runaway script would carry on running.
Changelog
Each version of PyMini, newest first, and what it added.
PyMini 2.0
The first version could uppercase a word and lay out a grid of buttons. The second is a language you can write a small program in, and a panel that answers a tap rather than only carrying a fixed one.
| Example | What changed |
|---|---|
[word for word in words if word] | Comprehensions, lambdas, tuples, sets, and try / except / finally / raise. |
f"{total:,.2f}" | f-string conversions and full format specs, shared with str.format and format(). |
f"""Hello {name}""" | Triple-quoted and raw strings, and f-strings that run over several lines. |
import math, random, time, json, re | Five namespaces, and nothing else, every one of them pure. |
re.sub(r"\s+", " ", text) | Regular expressions on Clink's own engine, which spends the same step budget. |
on_action(action, value, state) | Panels handle their own taps, keep real state, and have eleven more controls. |
replace(len(word), word.upper()) | Scripts can ask the keyboard to act, and read what is around the cursor. |
Current sentence | Actions read any slice of the field, not just the word and what precedes it. |
import re
def initial():
return {"words": []}
def view(state):
top = sorted(set(state["words"]), key=lambda w: -len(w))[:3]
return vstack([text(f"{len(state['words']):,} words", mono=True)]
+ [row(w, detail=f"{len(w)}") for w in top])
def on_open(state):
state["words"] = re.findall(r"\w+", context()["before"])
return statePyMini 1.0
The first version. An action turned one string into another, and a panel was a few controls whose buttons typed a fixed string or set a value.
| Example | What changed |
|---|---|
def transform(text): | int, float, str, bool, None, list and dict, with if, while, for, and def with default and keyword arguments. |
f"{n} words" | f-strings without format specs, and indexing and slicing with negative indices. |
sorted(text.split()) | 24 builtins and the common str, list and dict methods. |
button("+", set={"count": 1}) | Text, buttons, fields, stacks and a grid. A button could type text or set state, and nothing more. |
while True: | No imports at all, a step budget on every script, and caps on how far strings and lists can grow. |
Where to go next
The language surface in full, the panel vocabulary, and a builder that writes both without you typing a line.
Actions ›Panels ›Plugins ›Creator ›