Skip to content

Type System

Starlark supports optional type annotations similar to Python’s type hints. Different implementations offer varying levels of type checking.

Add types to function parameters and return values:

def add(x: int, y: int) -> int:
return x + y
def greet(name: str) -> str:
return "Hello, " + name
TypeDescriptionExample
intInteger values42
floatFloating-point numbers3.14
strString values"hello"
boolBoolean valuesTrue, False
NoneThe None valueNone
listList values[1, 2, 3]
dictDictionary values{"a": 1}
tupleTuple values(1, 2)

Specify element types for collections:

def sum_numbers(numbers: list[int]) -> int:
total = 0
for n in numbers:
total += n
return total
def lookup(data: dict[str, int], key: str) -> int:
return data[key]
def point() -> tuple[int, int]:
return (10, 20)
# Fixed-size tuple with specific types
def rgb() -> tuple[int, int, int]:
return (255, 128, 0)
# Variable-length tuple (all same type)
def numbers() -> tuple[int, ...]:
return (1, 2, 3, 4, 5)

Allow multiple types using |:

def process(value: int | str) -> str:
if type(value) == "int":
return str(value)
return value
def maybe_find(items: list, key: str) -> str | None:
for item in items:
if item.key == key:
return item.value
return None

From the typing module (where available):

TypeDescription
typing.AnyMatches any value
typing.CallableAny callable (function)
typing.IterableAny iterable value
typing.NeverNo valid values (e.g., fail() return)
def accepts_anything(x: typing.Any) -> None:
print(x)
def run_callback(fn: typing.Callable) -> None:
fn()

Records provide structured data with type-checked fields:

# Define a record type
Person = record(
name = str,
age = int,
email = str,
)
# Create instances
alice = Person(name="Alice", age=30, email="alice@example.com")
# Access fields
print(alice.name) # "Alice"
print(alice.age) # 30
# Type-safe: this would be a runtime error
# bob = Person(name="Bob", age="thirty", email="bob@example.com")

Use field() for optional fields with defaults:

Config = record(
host = str,
port = field(int, 8080), # Default: 8080
debug = field(bool, False), # Default: False
)
# port and debug are optional
cfg = Config(host="localhost")
print(cfg.port) # 8080

Enums represent a fixed set of values:

# Define an enum
Status = enum("pending", "running", "completed", "failed")
# Create values
current = Status("running")
# Access properties
print(current.value) # "running"
print(current.index) # 1
# List all values
print(Status.values()) # ["pending", "running", "completed", "failed"]
# Iterate
for s in Status:
print(s.value)
# Length and indexing
print(len(Status)) # 4
print(Status[0]) # Status("pending")
LogLevel = enum("debug", "info", "warn", "error")
def log(level: LogLevel, message: str) -> None:
print("[{}] {}".format(level.value.upper(), message))
log(LogLevel("info"), "Server started")
# [INFO] Server started

Types can be checked at different times:

  1. Runtime - As functions execute (most common)
  2. Static analysis - Without execution (tooling)
  3. Compile time - During loading (build systems)
# Runtime checking: error raised when called with wrong type
def square(n: int) -> int:
return n * n
square(5) # OK: returns 25
square("5") # Runtime error: expected int, got str
  1. Add types to public APIs - Makes interfaces clear
  2. Use records over dicts - Better type safety and memory efficiency
  3. Prefer specific types - list[str] over list when possible
  4. Document with types - Types serve as documentation
# Good: Clear types with defined record
User = record(name=str, age=int)
def create_user(name: str, age: int) -> User:
return User(name=name, age=age)
# Avoid: Unclear types, untyped dict
def create_user_bad(name, age):
return {"name": name, "age": age}
Featurestarlark-rust (Buck2)starlark-java (Bazel)starlark-go
Basic annotations
Generic types⚠️ Limited⚠️ Limited
Union types
record type
enum type
Static checking⚠️ WIP⚠️ Limited