Skip to content

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 here

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.14

On 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()

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)

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 i

When to use which

FormUse
lazy import foo = barDefer 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 immutable

Use typhon_runtime.lazy.lazy_let directly plus an explicit mut wrapper if you genuinely need re-evaluation.

Where next