Starlark is a Python-like configuration language designed for deterministic, hermetic execution. If you know Python, you already know 90% of Starlark. This guide covers the 10% that differs.
Starlark was created by Google for the Bazel build system. It uses Python syntax but restricts features that could cause non-determinism or unbounded computation. function-starlark uses the Go implementation (google/starlark-go).
Key design goals:
- Deterministic -- the same input always produces the same output.
- Hermetic -- no file I/O, no network access, no system calls.
- Finite -- no unbounded loops, no recursion.
These constraints make Starlark ideal for configuration languages where you want safety guarantees without sacrificing readability.
No try/except -- all errors are fatal. There is no way to catch exceptions.
# Python
try:
value = data["key"]
except KeyError:
value = "default"
# Starlark -- use get() for safe access
value = get(oxr, "spec.key", "default")No while loops -- only for loops with finite iterables.
# Python
while not ready:
check()
# Starlark -- use for with range()
for i in range(100):
if is_ready(i):
breakNo recursion -- functions cannot call themselves.
# Python
def flatten(lst):
return [x for sub in lst for x in (flatten(sub) if isinstance(sub, list) else [sub])]
# Starlark -- use iterative approaches insteadNo classes -- use dicts and functions. Starlark is not object-oriented.
# Python
class Bucket:
def __init__(self, name, region):
self.name = name
self.region = region
# Starlark -- use dicts
bucket = {"name": "my-bucket", "region": "us-east-1"}No import -- use load() instead.
# Python
from helpers import my_function
# Starlark
load("helpers.star", "my_function")
# Starlark -- namespace import (all exports in a struct)
load("helpers.star", h="*")
h.my_function()Namespace alias imports wrap all exports in a struct, useful when multiple modules export the same names.
No with statement -- no context managers.
No generators/yield -- use list comprehensions.
# Python
def evens(n):
for i in range(n):
if i % 2 == 0:
yield i
# Starlark
evens = [i for i in range(n) if i % 2 == 0]No **kwargs spread -- no {**a, **b} dict merging.
# Python
merged = {**defaults, **overrides}
# Starlark -- merge manually
merged = {}
for k, v in defaults.items():
merged[k] = v
for k, v in overrides.items():
merged[k] = vNo dict.update() -- merge dicts manually with a loop (see above).
Global variables are immutable after top-level assignment. You cannot reassign a global variable inside a function.
count = 0
def increment():
count = count + 1 # ERROR: local variable referenced before assignment
# Instead, use a mutable container:
state = {"count": 0}
def increment():
state["count"] = state["count"] + 1No is operator -- use == for comparison.
# Python
if x is None:
# Starlark
if x == None:No chained comparisons -- 1 < x < 5 is invalid.
# Python
if 1 < x < 5:
# Starlark
if 1 < x and x < 5:Booleans are not integers -- True + 1 is an error. You cannot use
booleans in arithmetic.
Deterministic dict iteration -- insertion order is guaranteed (unlike Python < 3.7). Dicts always iterate in the order keys were inserted.
No mutation during iteration -- you cannot modify a dict or list while iterating over it. Copy first.
# ERROR: cannot modify dict during iteration
for k, v in d.items():
if v == "remove":
d.pop(k)
# Correct: copy the items first
to_remove = [k for k, v in d.items() if v == "remove"]
for k in to_remove:
d.pop(k)Starlark supports only the % operator for string formatting:
# Works
name = "hello %s" % user
msg = "%s has %d items" % (user, count)
# Does NOT work -- f-strings are invalid
name = f"hello {user}"
# Does NOT work -- .format() does not exist
name = "hello {}".format(user)Starlark supports these built-in types:
| Type | Example | Notes |
|---|---|---|
| bool | True, False |
Not integers -- cannot use in arithmetic |
| int | 42, 0xFF |
Arbitrary precision |
| float | 3.14, 1e10 |
IEEE 754 double |
| string | "hello", 'world' |
Immutable, % formatting only |
| list | [1, 2, 3] |
Mutable, ordered |
| tuple | (1, 2, 3) |
Immutable, ordered |
| dict | {"a": 1} |
Mutable, insertion-ordered |
| set | set([1, 2, 3]) |
Mutable, no literal syntax |
| None | None |
Singleton null value |
| function | def f(): pass |
First-class, no recursion |
These are standard Starlark builtins (from the language specification), not function-starlark-specific:
| Function | Description |
|---|---|
len(x) |
Length of a string, list, tuple, dict, or set |
range(n) / range(start, stop, step) |
Integer sequence |
str(x) |
Convert to string |
int(x) |
Convert to integer |
float(x) |
Convert to float |
bool(x) |
Convert to boolean |
list(x) |
Convert iterable to list |
tuple(x) |
Convert iterable to tuple |
dict(pairs) |
Create dict from key-value pairs |
type(x) |
Return type name as string |
hash(x) |
Hash a string |
sorted(x) |
Return sorted list |
reversed(x) |
Return reversed iterator |
enumerate(x) |
Yield (index, value) pairs |
zip(a, b) |
Pair elements from iterables |
any(x) |
True if any element is truthy |
all(x) |
True if all elements are truthy |
min(a, b, ...) |
Minimum value |
max(a, b, ...) |
Maximum value |
hasattr(x, name) |
Check if attribute exists |
getattr(x, name) |
Get attribute value |
dir(x) |
List attribute names |
repr(x) |
String representation |
print(...) |
Print to function logs (stderr), not to resource output |
fail(msg) |
Halt with error (standard Starlark -- prefer fatal() in function-starlark) |
On top of standard Starlark, function-starlark adds 34 predeclared names:
6 globals (oxr, dxr, observed, context, environment,
extra_resources), 22 functions (Resource, skip_resource, get,
get_label, get_annotation, set_condition, emit_event, fatal,
set_connection_details, set_xr_status, get_observed,
require_extra_resource, require_extra_resources, schema, field,
struct, get_extra_resource, get_extra_resources, is_observed,
observed_body, get_condition, set_response_ttl), and 6 namespace
modules (json, crypto, encoding, dict, regex, yaml).
See the builtins reference for complete signatures, parameter types, defaults, and examples.
region = get(oxr, "spec.region", "us-east-1")
name = get(oxr, "metadata.name", "unknown")env = get(oxr, "spec.environment", "dev")
if env == "prod":
Resource("monitoring", {
"apiVersion": "monitoring.example.io/v1",
"kind": "Dashboard",
"spec": {"enabled": True},
})count = get(oxr, "spec.replicas", 3)
for i in range(count):
Resource("worker-%d" % i, {
"apiVersion": "apps.example.io/v1",
"kind": "Worker",
"metadata": {"name": "worker-%d" % i},
"spec": {"index": i},
})base = {"tier": "standard", "env": "dev"}
override = {"env": "prod", "team": "platform"}
merged = {}
for k, v in base.items():
merged[k] = v
for k, v in override.items():
merged[k] = v
# merged: {"tier": "standard", "env": "prod", "team": "platform"}name = "%s-%s-%d" % (prefix, env, index)
endpoint = "https://%s.%s.svc.cluster.local" % (service, namespace)The top 5 mistakes Python developers make when writing Starlark:
# WRONG -- f-strings do not exist in Starlark
name = f"bucket-{region}"
# CORRECT
name = "bucket-%s" % region# WRONG -- try/except does not exist
try:
value = data["missing"]
except KeyError:
value = "default"
# CORRECT -- check conditions or use get()
value = get(data, "missing", "default")# WRONG -- cannot modify list during iteration
for item in items:
if item == "remove":
items.remove(item)
# CORRECT -- build a new list
items = [item for item in items if item != "remove"]# WRONG -- True is not 1 in Starlark
total = count + True
# CORRECT
total = count + (1 if flag else 0)# WRONG -- cannot reassign globals
counter = 0
def bump():
counter = counter + 1 # ERROR
# CORRECT -- use a mutable container
state = {"counter": 0}
def bump():
state["counter"] = state["counter"] + 1- Builtins reference -- Complete API reference for all function-starlark globals and functions
- Starlark language specification -- The official Starlark language spec