let's start with the feeling, not the definition.
You open a Python file. You write:
from zef import *
And suddenly you have ET, RT, FX, F, and a flood of unfamiliar names. You see code samples with pipes (|) everywhere. UIDs shaped like π-97421467198ef6d64520. You stop and ask the fair question:
Zef is a python+rust library where your whole program β data, functions,
types, and side effects β is a stream of plain values piped through
transforms. You construct values, you pipe them, and then you
| collect (for pure stuff) or | run (for effects).
That's the whole rhythm.
Most modern applications have a similar shape: you have data flowing in, you want to transform it, you want to persist some of it, expose some of it over HTTP, and you want the whole thing to not fall over under load.
Each of those jobs has its own library, usually. You end up gluing together FastAPI + Pydantic + Celery + Redis + Postgres + an ORM + asyncio + a serialization layer + a logger + a config system. Every glue seam is a place bugs live.
What if those aren't really different problems? What if they all become one problem the moment you treat "doing a thing" as just another value you can print, store, and compare?
Make the side effect a value, and the difference between "running an HTTP server" and "saving to a database" and "printing to the screen" disappears into the same vocabulary.
Here's a complete Zef program. Read it once, then I'll unpack every line.
from zef import *
# 1. pure: pipe some data through a chain of ZefOps
result = [1, 2, 3, 4] | map(multiply(2)) | reduce(add) | collect
print(result) # 20
# 2. effect: construct a side-effect value, run it
FX.Print(content='πΏ hello zef!') | run
Two blocks. Two kinds of thing. Let's name them.
Zef has two worlds:
Same syntax (the pipe), different "end word." Keep this picture β we'll come back to it constantly.
Every Zef value is a contiguous chunk of memory. A Dict? Bytes. A Graph? Bytes. No pointers to stuff off elsewhere. Means you can memcpy a Zef value to a file or socket and it just works.
No static type checker needed. "Is 42 an Int?" is a plain runtime question. And you can refine any type with any predicate: Int & (Z > 18).
FX.HTTPRequest(...) is just data. Nothing happens until you | run it. Means you can build, compare, ship, or log effects before executing.
Because when data, code, and effects look the same, you can use the same tools on all of them. One query language. One serialization. One test strategy. One mental model β not ten.
We're going to spend the rest of this book unpacking exactly how that plays out, chapter by chapter. Ready?
We go in this order:
| operator@zef_functionClose your eyes. Picture two boxes: pure world (ends with | collect)
and effectful world (ends with | run). That's the shape
of a Zef program. When you feel lost later β come back to this picture.
Next up: the three big ideas that underpin everything. β