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])import threading
class __TyphonLazy_np_: _module = None _lock = threading.Lock()
@classmethod def _load(cls): if cls._module is None: with cls._lock: if cls._module is None: import numpy cls._module = numpy return cls._module
def __getattr__(self, name): return getattr(self._load(), name)
np = __TyphonLazy_np_()
def main() -> None: if len(sys.argv) > 1: 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 npNo 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()from typhon_runtime.lazy import lazy_let as __typhon_lazy_let
CONFIG: Config = __typhon_lazy_let(lambda: 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:
import threadingfrom 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)import dataclassesfrom functools import cached_property as _typhon_cached_property
@dataclasses.dataclass()class Server: config_path: str
@_typhon_cached_property def parsed_config(self) -> Config: return 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]]from collections.abc import Iterator
def primes_up_to(n: int) -> Iterator[int]: sieve: list[bool] = [True] * (n + 1) # ... compute ... for i in range(2, n + 1): if sieve[i]: yield ireturn [...] 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 statementThere 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
lazyreference — the full surface.- The typhon_runtime module —
lazy_letand_LazyValuedetails.