pip install small-python
# or
uv add small-pythonThe distribution is named small-python; the import name is small_python.
import small_python
print(small_python.__version__) # version of the package, e.g. 0.0.1
print(small_python.SPY_VERSION) # version of the engine bundled with itA Runtime owns one engine context, running in a worker process of its own.
Use it as a context manager so the worker ends when you are done, or call
close() yourself.
from small_python import Runtime
with Runtime() as rt:
print(rt.eval("sum(range(10))")) # 45Both take a string of Python source and an optional filename used in error
messages.
eval(source)runs the source and returns the value of its last expression statement. The source may contain any statements, not just one expression.exec(source)runs the source for its effects and returnsNone.
from small_python import Runtime
with Runtime() as rt:
rt.exec("""
def fib(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a
""")
print(rt.eval("fib(30)")) # 832040
print(rt.eval("x = fib(10)\nx * 2")) # 110
print(rt.exec("y = 1")) # NoneGlobals persist between calls, so a runtime works like a long-lived module.
A runtime is subscriptable by global name.
from small_python import Runtime
with Runtime() as rt:
rt["config"] = {"retries": 3, "hosts": ["a", "b"]}
rt.exec("count = config['retries'] * len(config['hosts'])")
print(rt["count"]) # 6
del rt["count"]
print("count" in rt.eval("globals()")) # FalseA missing name raises KeyError. Plain data (None, bool, int, float,
complex, str, bytes, bytearray, list, tuple, dict, set,
frozenset) is copied across in both directions, so changing the copy you get
back does not change the engine's value. See
how values convert.
call takes either a global name or an engine object, then positional and
keyword arguments.
from small_python import Runtime
with Runtime() as rt:
rt.exec("def greet(name, *, punctuation='!'):\n return 'hello ' + name + punctuation")
print(rt.call("greet", "spy")) # hello spy!
print(rt.call("greet", "spy", punctuation="?")) # hello spy?
print(rt.call("sorted", [3, 1, 2], reverse=True)) # [3, 2, 1]
greet = rt["greet"] # a small_python.Object
print(greet("world")) # hello world!Values that are not plain data, such as functions, class instances,
generators, and modules, come back as small_python.Object. An Object forwards
calls, attributes, items, len, in, iteration, comparisons, hashing,
repr, str, dir, and truth testing to the engine.
from small_python import Runtime
with Runtime() as rt:
rt.exec("""
class Counter:
def __init__(self):
self.n = 0
def bump(self, by=1):
self.n += by
return self.n
""")
c = rt.call("Counter")
c.bump()
c.bump(by=5)
print(c.n) # 6
c.n = 100
print(rt.eval("[i * i for i in range(3)]")) # [0, 1, 4] (a list, copied)
print(list(rt.eval("iter('abc')"))) # ['a', 'b', 'c']An Object belongs to its runtime. Once the runtime closes, using it raises
RuntimeError, and passing it into a different runtime raises ValueError.
An exception that escapes the engine is raised in Python as
small_python.Error.
from small_python import Error, Runtime
with Runtime() as rt:
try:
rt.exec("x = 1\nraise ValueError('bad value')", filename="job.py")
except Error as e:
print(e) # ValueError: bad value
print(e.type, e.message) # ValueError bad value
print(e.filename, e.lineno) # job.py 2Syntax errors are reported the same way, with e.type == "SyntaxError".
More in the embedding guide.
- Embedding guide to give code access to your functions, modules, and files.
- Limits and safety before running code you did not write.