Decorators
Decorators modify or extend functions and classes by wrapping them with another callable. Scriptling supports the @ syntax familiar from Python.
Basic Function Decorator
A decorator is a function that takes a function and returns a (usually modified) function:
def double_result(fn):
def wrapper(*args):
return fn(*args) * 2
return wrapper
@double_result
def add(a, b):
return a + b
print(add(3, 4)) # 14The @ syntax is equivalent to:
def add(a, b):
return a + b
add = double_result(add)Decorators with Arguments
When a decorator needs configuration, use a factory function that returns the actual decorator:
def repeat(times):
def decorator(fn):
def wrapper(*args):
results = []
for i in range(times):
results.append(fn(*args))
return results
return wrapper
return decorator
@repeat(3)
def greet(name):
return "Hello, " + name
print(greet("World")) # ["Hello, World", "Hello, World", "Hello, World"]The pattern is: @factory(args) calls the factory, which returns a decorator, which wraps the function. Three levels of nesting.
Stacking Decorators
Multiple decorators apply bottom-up (innermost first):
def add_one(fn):
def wrapper(*args):
return fn(*args) + 1
return wrapper
def double(fn):
def wrapper(*args):
return fn(*args) * 2
return wrapper
@add_one
@double
def value():
return 5
print(value()) # 11
# Evaluated as: add_one(double(value))()
# value() = 5 → double wraps: 10 → add_one wraps: 11Identity Decorators
A decorator can return the function unchanged — useful for registration patterns:
_registry = {}
def register(fn):
_registry[fn.__name__] = fn
return fn
@register
def my_handler():
return "handled"
# my_handler is still callable normally
print(my_handler()) # "handled"
print(_registry.keys()) # ["my_handler"]The Registration Pattern (Decorator with Arguments)
Combining factory decorators with a registry is the idiomatic way to declare metadata alongside a function:
_tools = {}
def tool(description, params=None):
def decorator(fn):
_tools[fn.__name__] = {
"description": description,
"params": params,
"fn": fn,
}
return fn
return decorator
@tool("Calculate expression", params={"expr": "Math expression"})
def calc(expr):
return eval(expr)
# calc is callable normally AND registered with metadata
print(calc("2+3")) # 5
print(_tools["calc"]["description"]) # "Calculate expression"This pattern is used by scriptling.runtime.mcp for MCP tool registration.
Accessing fn.__name__
Every named function exposes its name via __name__:
def my_function():
pass
print(my_function.__name__) # "my_function"This is how decorators identify which function they’re wrapping without requiring the name to be passed explicitly.
Class Decorators
Decorators can also wrap classes:
def add_version(cls):
cls.version = "1.0"
return cls
@add_version
class App:
pass
a = App()
print(a.version) # "1.0"Built-in Decorators
@property
Turns a method into a read-only attribute:
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
return self._radius
@property
def area(self):
return 3.14159 * self._radius ** 2
c = Circle(5)
print(c.radius) # 5 (no parentheses needed)
print(c.area) # 78.53975@property.setter
Add a setter to a property:
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Below absolute zero")
self._celsius = value
t = Temperature(100)
t.celsius = 0 # uses the setter
print(t.celsius) # 0@staticmethod
Declares a method that doesn’t receive self:
class MathUtils:
@staticmethod
def square(x):
return x * x
print(MathUtils.square(4)) # 16
print(MathUtils().square(4)) # 16 (also works on instances)@classmethod
Declares a method that receives the class as its first argument:
class Counter:
count = 0
@classmethod
def increment(cls):
cls.count += 1
return cls.count
print(Counter.increment()) # 1
print(Counter.increment()) # 2Limitations
- Function objects don’t support attribute assignment.
fn.tag = "value"will error. Use a registry dict instead (see the registration pattern above). - Lambdas can’t be decorated with
@syntax — they have no name or statement form to attach the decorator to. Wrap manually:my_lambda = decorator(lambda x: x). - No type annotations on parameters. Scriptling doesn’t support
def f(x: int)syntax. Metadata must be passed explicitly (e.g. via decorator arguments).
See Also
- Functions — Function definition, parameters, closures
- Classes —
@property,@staticmethod,@classmethod - runtime.mcp — MCP tool registration via decorators