Skip to content

Purity Errors

tyc::impure_pure_fn

A function marked @pure violates one of the six purity conditions:

  1. Synchronous. No coroutines or generators.
  2. Hashable parameters.
  3. No I/O in the transitive call graph.
  4. No non-determinism (no time.*, random.*, uuid.*, os.urandom).
  5. No reads/writes of mutable module-level state.
  6. No exceptions raised.

Example:

@pure
def fetch(url: str) -> str:
import urllib.request
return urllib.request.urlopen(url).read().decode() # ❌ I/O
error[tyc::impure_pure_fn]: `fetch` is annotated `@pure` but performs I/O
(urllib.request.urlopen)
┌─ src/main.ty:3:12
│
3 │ return urllib.request.urlopen(url).read().decode()
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ I/O is not allowed in `@pure` functions

Fix: drop @pure (and lose memoisation eligibility), or refactor to take the I/O result as a parameter.

What’s I/O?

  • open(...), socket, subprocess, urllib, requests, httpx, aiohttp.
  • print(), logger calls (logger.warning(...) on any logger-named receiver).
  • Database drivers.
  • Filesystem operations, and I/O method names on any receiver (.read_text(), .write(), .send(), .sleep(), .execute()).
  • unsafe: calls (unless the stub is annotated @pure).

Calls are resolved through the module’s import aliases, so datetime.now() after from datetime import datetime and np.random.rand() after import numpy as np are seen for what they are. A call the checker cannot classify — a method on a value of unknown type, a module outside the pure stdlib allow-list — is trusted under @pure; only the automatic optimisations insist on provable purity.

What’s non-determinism?

  • time.time(), time.monotonic(), datetime.now().
  • random.*, secrets.*.
  • uuid.uuid4() (the others — uuid1, uuid3, uuid5 — are deterministic if their inputs are).
  • os.urandom().

What’s mutable module state?

  • Reads from a module-level mut binding (or one rebound after its definition, or declared global in some function).
  • Writes to any module-level binding (including let, via mutation through fields or a mutating method such as REGISTRY.append(x)).
  • Writes to an argument — c.n = c.n + 1, xs.pop(), next(it) on a parameter — are impure for the same reason: the caller can see them.

comptime let reads are fine — those are inlined as literals.

Where next