Async Python integration¶
lclang is async-first because configuration often selects work that is already asynchronous: service discovery, pricing, calendar loading, or resource use. Host values may be immediate or awaitable, and host callables may return either.
What you will learn¶
- how LCL automatically awaits host calls;
- how lazy branches prevent unnecessary async work;
- how expression-form
withmanages sync or async resources; - how Frame ownership differs from host capability ownership.
Await a host function only when selected¶
The host owns the exchange-rate provider. LCL owns the policy that decides whether to call it and how to use its result.
import asyncio
from lclang.lang import define_frame, define_module
PRICING = define_module(
"pricing",
{
"subtotal": "unit_price * quantity",
"converted": (
"subtotal if currency == 'USD' "
"else subtotal * exchange_rate(currency)"
),
"label": 'f"{currency} {converted:.2f}"',
},
)
async def main() -> None:
calls: list[str] = []
async def exchange_rate(currency: str) -> float:
calls.append(currency)
await asyncio.sleep(0)
return {"EUR": 0.92, "GBP": 0.79}[currency]
async with define_frame(
PRICING,
preset={
"unit_price": 20,
"quantity": 3,
"currency": "GBP",
"exchange_rate": exchange_rate,
},
) as frame:
assert await frame.get("subtotal") == 60
assert calls == []
assert await frame.get("label") == "GBP 47.40"
assert calls == ["GBP"]
asyncio.run(main())
The direct subtotal lookup only multiplies 20 * 3, so it never reaches
exchange_rate. Resolving label then follows converted; because the
currency is GBP, the conditional awaits 0.79, multiplies it by 60, and
formats 47.4 with two decimal places. Recording one call proves the lazy
boundary.
lclang recursively resolves awaitables returned by lookup, calls, iteration, and context-manager protocols. It does not add a timeout to blocking Python code; host implementations remain responsible for their own I/O policy.
Acquire and release an async resource¶
LCL with is an expression. It prefers an async context-manager protocol when
available, binds the entered value, evaluates the body, and awaits cleanup.
import asyncio
from lclang.lang import define_frame, define_module
class Session:
def __init__(self, events: list[str]) -> None:
self.events = events
async def __aenter__(self) -> "Session":
self.events.append("open")
return self
async def __aexit__(self, *details: object) -> bool:
del details
self.events.append("close")
return False
async def fetch(self, key: str) -> int:
self.events.append(f"fetch {key}")
return {"inventory": 7}[key]
async def main() -> None:
events: list[str] = []
module = define_module(
"inventory",
{
"total": (
"with open_session() as session: session.fetch(key) * requested"
),
},
)
async with define_frame(
module,
preset={
"open_session": lambda: Session(events),
"key": "inventory",
"requested": 3,
},
) as frame:
assert await frame.get("total") == 21
assert events == ["open", "fetch inventory", "close"]
asyncio.run(main())
The with expression awaits __aenter__, binds the returned Session, and then
awaits fetch("inventory"). That call returns 7, the body multiplies it by
the requested 3, and cleanup runs before the result 21 is returned. The
event assertion verifies the exact open, use, close sequence.
The named total snapshot belongs to the Frame; the Session's expression-form
with has already closed the resource before that snapshot is published.
If acquisition or the body fails, previously entered managers unwind in reverse
order. Ordinary host exceptions become source-aware LclEvaluationError
instances; cancellation remains cancellation after required cleanup.
Keep the boundary narrow¶
Expose exchange_rate(currency) rather than a complete payments container.
Expose open_session() rather than ambient network access. LCL can be creative
with the capabilities supplied to it, but cannot invent capabilities that are
not in its Frame hierarchy.
An owned Frame should itself be used with async with. Frame close cancels and
settles its in-flight definition tasks and closes cached result resources that
provide close or aclose. Host values supplied directly are borrowed and are
not automatically closed.
Previous: Configuration files | Next: Caching and recalculation | Return to the series introduction