# TangleFunction API


`TangleFunction` introspects a function's type hints and defaults and renders it as a clean call expression such as `train(lr=0.01, epochs=10, optimizer='adam')`. Numbers drag horizontally to scrub (and click to type an exact value), `Literal`/`Enum`/`bool` arguments click to cycle through their known options, and strings are click-to-edit. The live arguments land in `values`, ready to splat into the function with `fn(**tf.value["values"])`.


Numbers are unbounded scrubbers by default. Give one a range or a step by annotating it with `annotated_types` constraints (`Ge`, `Le`, `Gt`, `Lt`, `MultipleOf`) — the same constraints pydantic uses, so `pydantic.Field(ge=..., le=...)` works too — or with a `params=` override.


See also: TangleLatex for the same drag gesture inside a LaTeX formula, and Tangle widgets for individual inline sliders and choices in plain prose.


 Bases: `AnyWidget`


Render a typed Python function as an interactive call expression.


Each argument becomes editable in place: numbers drag horizontally to scrub, `Literal`/`Enum`/`bool` arguments click to cycle through their known options, and strings are click-to-edit. The live arguments are synced to the `values` dict, ready to splat into the function.



```
from wigglystuff import TangleFunction

import marimo as mo
from typing import Literal
from wigglystuff import TangleFunction

def train(lr: float = 0.01, epochs: int = 10,
          optimizer: Literal["adam", "sgd"] = "adam", shuffle: bool = True):
    ...

tf = mo.ui.anywidget(TangleFunction(train))
tf
# in another cell:
train(**tf.value["values"])
```


Numbers are unbounded scrubbers by default. Give one a range or a step by annotating it with `annotated_types` constraints (the same ones pydantic uses, so `pydantic.Field(ge=..., le=...)` works too):


```
```python
from typing import Annotated
from annotated_types import Ge, Le, MultipleOf

def generate(temperature: Annotated[float, Ge(0), Le(2), MultipleOf(0.05)] = 0.7):
    ...

mo.ui.anywidget(TangleFunction(generate))
```
```

 Note

For `Enum` parameters the emitted value is the member's `.value` (so it stays JSON-serializable and matches the displayed label). `Literal` is the recommended way to expose a known set of options.


Create a TangleFunction widget.


  Source code in `wigglystuff/tangle_function.py`

```
def __init__(
    self,
    fn: Callable[..., Any],
    params: Optional[Mapping[str, Mapping[str, Any]]] = None,
    *,
    theme: str = "auto",
    width: int = 560,
    **kwargs: Any,
) -> None:
    """Create a TangleFunction widget.

    Args:
        fn: The function to render. Its parameters are introspected from type
            hints and defaults.
        params: Optional per-parameter overrides keyed by name. For numeric
            parameters: ``min_value``, ``max_value``, ``step``, ``digits``,
            ``pixels_per_step``, and ``value``. Any parameter may also be
            given ``options`` (a list) to force a click-cycle choice. These
            take precedence over ``annotated_types``/``pydantic.Field``
            constraints read from the parameter's ``Annotated`` metadata.
        theme: Color theme: ``"auto"``, ``"light"``, or ``"dark"``.
        width: Maximum width in pixels before the expression wraps.
        **kwargs: Forwarded to ``anywidget.AnyWidget``.
    """
    if not callable(fn):
        raise ValueError("fn must be callable.")
    params = params or {}
    if not isinstance(params, Mapping):
        raise ValueError("params must be a mapping of parameter names to configs.")
    if theme not in {"auto", "light", "dark"}:
        raise ValueError("theme must be 'auto', 'light', or 'dark'.")

    try:
        sig = inspect.signature(fn)
    except (ValueError, TypeError) as exc:
        raise ValueError("fn must be an introspectable callable.") from exc
    try:
        hints = typing.get_type_hints(fn, include_extras=True)
    except Exception:
        hints = {}

    parameters: dict[str, dict[str, Any]] = {}
    param_order: list[str] = []
    for pname, param in sig.parameters.items():
        if pname == "self" or param.kind in _SKIP_KINDS:
            continue
        override = params.get(pname, {})
        if not isinstance(override, Mapping):
            raise ValueError(f"params[{pname!r}] must be a mapping.")
        hint = hints.get(pname, param.annotation)
        if hint is inspect.Parameter.empty:
            hint = None
        parameters[pname] = _classify(pname, param, hint, override)
        param_order.append(pname)

    unknown = sorted(set(params) - set(parameters))
    if unknown:
        raise ValueError(
            "params keys not found among the function's parameters: "
            + ", ".join(unknown)
        )

    values = {name: parameters[name]["value"] for name in param_order}
    super().__init__(
        fn_name=getattr(fn, "__name__", "f"),
        parameters=parameters,
        param_order=param_order,
        values=values,
        theme=theme,
        width=width,
        **kwargs,
    )
    self._fn = fn
```


## Synced traitlets


| Traitlet | Type | Notes |
| --- | --- | --- |
| `fn_name` | `str` | The function's name, shown before the opening parenthesis. |
| `parameters` | `dict` | Per-parameter render spec (kind, options/bounds/step/digits). |
| `param_order` | `list` | Parameter names in signature order. |
| `values` | `dict` | Live current value for each parameter; splat into the function. |
| `theme` | `str` | `"auto"`, `"light"`, or `"dark"`. |
| `width` | `int` | Max width in pixels before the expression wraps one arg per line. |
| `error` | `str` | Validation/render error surfaced from the widget. |
