Skip to content

lazy → proxies

lazy import → proxy class

lazy import np = numpy
def main() -> None:
if len(sys.argv) > 1:
let arr: np.ndarray = np.array([1, 2, 3])

Thread-safe (double-checked locking), one-shot. No typhon_runtime dependency — the proxy is inlined.

On a 3.15+ target: native PEP 810 instead

When [python] target is "3.15" / "3.15t", the proxy-class lowering above is skipped entirely. tyc build instead emits the native PEP 810 statement:

lazy import numpy as np

No proxy class, no typhon_runtime dependency at all for this feature — a project whose only runtime-touching construct was lazy import ships no generated typhon_runtime/ package on a 3.15+ target. 3.13 / 3.14 targets keep the proxy-class emission above, byte-for-byte unchanged.

lazy let (module-level) → lazy_let

lazy let CONFIG: Config = load_config_from_disk()

The runtime helper is typhon_runtime.lazy.lazy_let, which returns a _LazyValue proxy that materialises on first attribute access (or call, subscript, iteration, str, bool, …). The aliased import name __typhon_lazy_let keeps the helper out of the way of any user binding called lazy_let. Excerpt:

build/typhon_runtime/lazy.py
import threading
from typing import Callable, TypeVar
_T = TypeVar("_T")
_UNSET = object()
class _LazyValue:
__slots__ = ("_factory", "_value", "_lock")
def __init__(self, factory: Callable[[], _T]) -> None:
object.__setattr__(self, "_factory", factory)
object.__setattr__(self, "_value", _UNSET)
object.__setattr__(self, "_lock", threading.Lock())
def _materialise(self) -> object:
value = object.__getattribute__(self, "_value")
if value is _UNSET:
with object.__getattribute__(self, "_lock"):
value = object.__getattribute__(self, "_value")
if value is _UNSET:
factory = object.__getattribute__(self, "_factory")
value = factory()
object.__setattr__(self, "_value", value)
return value
def __getattr__(self, name: str) -> object:
return getattr(self._materialise(), name)
# __call__, __getitem__, __iter__, __str__, __bool__, __eq__, __hash__,
# __len__ all forward to the materialised value as well, so the proxy
# is mostly transparent.
def lazy_let(factory: Callable[[], _T]) -> _T:
return _LazyValue(factory) # type: ignore[return-value]

Access is transparent: write CONFIG.host, CONFIG["port"], print(CONFIG) and the proxy materialises on first use, then forwards every subsequent operation.

Why not functools.cached_property? Because cached_property is instance-scoped, writable after first evaluation, and not robust under concurrency. lazy_let is the module-level equivalent that gets all three right.

lazy let (class-level) → @cached_property

class Server:
config_path: str
impl Server:
lazy let parsed_config: Config = load_config(self.config_path)

Per-instance caching is the intended semantics inside classes; cached_property is the right primitive.

lazy[T] return type → generator (roadmap)

This form is designed but not yet wired through the parser. The intended lowering is below; for now, hand-write the generator.

def primes_up_to(n: int) -> lazy[list[int]]:
let sieve: list[bool] = [True] * (n + 1)
# ... compute ...
return [i for i in range(2, n + 1) if sieve[i]]

return [...] rewrites to a generator emission. Useful when the caller may only need a prefix.

lazy from (3.15 targets)

lazy from numpy import array # Typhon, [python] target = "3.15"
lazy from numpy import array # emitted Python: PEP 810's native statement

There is no lowering for older targets: without PEP 810 a from-import has to load the module to bind the name. On 3.13 / 3.14 it is tyc::requires_newer_python, which points you to lazy import np = numpy.

Where next