lazy
lazy is Typhon’s deferred-evaluation keyword. It comes in three forms: lazy import, lazy let, and lazy[T] return type.
lazy import
Defer module loading until first attribute access:
lazy import np = numpy
def main() -> None: if len(sys.argv) > 1: let arr: np.ndarray = np.array([1, 2, 3]) # numpy loaded hereclass __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_()np is a proxy until first access. Subsequent accesses pass through to the loaded module. The desugarer generates a per-module proxy class with double-checked locking — thread-safe, one-shot, and no runtime dependency required (the proxy is inlined).
Native lowering on a 3.15+ target (PEP 810)
Under tyc run (v0.9.0)
The in-process VM (tyc run) uses a simpler lowering for lazy import name = module: a plain import module as name rewrite. The descriptor-based proxy class the build path emits has nothing useful to bind against in a tree-walking interpreter (the VM doesn’t go through Python’s class-attribute lookup), so the VM treats lazy import as a regular aliased import. Module loading still happens on first import resolution rather than at parse time, so the deferral semantics are preserved for the cases the VM actually models.
Before v0.9.0 lazy import np = numpy raised ImportError under tyc run (the compiled proxy class generation didn’t apply); since v0.9.0 the same source runs cleanly under both modes.
lazy from needs a 3.15 target
lazy from numpy import array # ✅ on [python] target = "3.15"; an error on 3.13 / 3.14On a 3.15+ target lazy from lowers to the native PEP 810 statement, which makes each imported name lazy. On 3.13 and 3.14 it is tyc::requires_newer_python: without PEP 810 a from-import has to load the module to bind the name, so use lazy import np = numpy plus np.array(...) there. lazy from numpy import *, lazy from __future__ import … and a lazy from inside a function or block are tyc::lazy_usage errors on every target, as they are in CPython 3.15. Under tyc run a lazy from loads its module at the import statement.
lazy let (module-level)
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())Defers the initialiser until first access. Lowers to a sentinel-cached lazy_let(lambda: ...) helper in typhon_runtime.lazy. Unlike functools.cached_property (instance-scoped, race-prone, writable after first eval), lazy_let is thread-safe and one-shot — the factory runs at most once across all threads, the result is cached, and the alias name (__typhon_lazy_let) keeps the helper out of the way of any user lazy_let binding.
lazy let (class-level)
Inside a class body, lazy let lowers to @cached_property. The right-hand
side is an expression — it runs once per instance, on first access, and
the result is cached on the instance:
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.
lazy[T] return type (roadmap)
def f(...) -> lazy[list[T]]: ... is a designed-but-not-yet-implemented
form. The intent is to emit a generator function so a caller that only
takes a prefix doesn’t pay for materialising the whole list:
# Designed form (not parsed today):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]]Until the compiler grows this, write a plain generator by hand:
from collections.abc import Iterator
def primes_up_to(n: int) -> Iterator[int]: let sieve: list[bool] = [True] * (n + 1) # ... compute ... for i in range(2, n + 1): if sieve[i]: yield iWhen to use which
| Form | Use |
|---|---|
lazy import foo = bar | Defer expensive module imports (numpy, pandas, torch) for CLIs / short-running scripts. |
lazy let X = expr (module-level) | Defer expensive module initialisation (config parsing, DB pool creation). |
lazy let x = expr (class-level) | Per-instance lazily-computed property. |
def f() -> lazy[T] | Caller may only need part of the result. |
Common mistakes
lazy from on an older target
lazy from numpy import array # ❌ before [python] target = "3.15"Use lazy import np = numpy plus np.array(...), or raise the target to 3.15.
Mutating a lazy let
lazy let CONFIG: Config = load_config()CONFIG = Config(...) # ❌ tyc::immutable_assign — lazy let is implicitly immutableUse typhon_runtime.lazy.lazy_let directly plus an explicit mut wrapper if you genuinely need re-evaluation.
Where next
lazy→ proxies (lowering) — implementation details.- Advanced Features (tour) — teaching page.
- The typhon_runtime module —
lazy_letand the_LazyValueproxy.