Getting started
Installation
effecton requires Python 3.14 or later.
uv add effectonGetting real
Generator syntax lets you write standard generator functions (decorated with E.gen) that produce effects and read like async/await code. In this example, check_secret returns an effect that fetches a secret from a web page and checks whether it is valid. If it is not, the effect fails with a specific error.
from dataclasses import
from typing import
import effecton as E
@
@(frozen=True)
class (E.):
:
@E.
def () -> E.[
,
| E.HttpClient. | E.HttpClient.,
E.HttpClient.,
]:
= yield from E.(E.HttpClient.)
= yield from .("https://example.com/secret")
= yield from E.HttpClient.()
if . != "hunter2":
yield from E.((.))
return .
program = ().(E.HttpClient.)(E.HttpClient.())
result = E.()
Three things to notice:
- The signature is the contract.
E.EffectGen[A, E, R]sayscheck_secretsucceeds with astr, can fail with one of three errors, and needs an HTTP client. - Requirements are asked for, not looked up.
yield from E.require(E.HttpClient.Protocol)puts the client intoR. Nothing is read from a global. providemakes it runnable. Providing the client turnsRintoNever. Before that, handingprogramto a runner is a type error.
Running effects
run_sync returns the value and, on failure, raises the error as an exception (every EffectonError is an Exception). run_sync_exit never raises. It returns an Exit[A, E] that you can pattern match on:
match E.():
case E.(value):
() # "hunter2"
case E.(cause):
() # Fail(SecretInvalidError(...)), Fail(StatusError(...)), Die(...)Async / Sync split
Notice how we provided E.HttpClient.SyncLive(). Live means it is the real implementation that makes HTTP requests, as opposed to a Test implementation that returns canned responses. There is also E.HttpClient.AsyncLive, which awaits an async HTTP client, so the event loop keeps turning while a request is in flight. A program that uses it must run under an async runner:
= ().(E.HttpClient.)(E.HttpClient.())
result = E.()
check_secret itself did not change. The same effect runs synchronously or asynchronously depending on the implementations you provide and the runner you pick. run_async owns the event loop through asyncio.run. From inside a loop you already own, await E.run_async_coroutine(program) returns the Exit instead. See Wrapping async code for mixing in existing coroutines.
Testing
Because the HTTP client is a requirement rather than a global, a test provides E.HttpClient.Test in its place. Seed it with a URL-to-body mapping, and it answers those URLs with a 200 and everything else with a 404:
def ():
= E.HttpClient.(responses={"https://example.com/secret": "hunter2"})
= E.(().(E.HttpClient.)())
assert == "hunter2"
def ():
= E.HttpClient.(responses={"https://example.com/secret": "letmein"})
= E.(().(E.HttpClient.)())
assert == E.(E.(("letmein")))No network, no monkeypatching, and the test client records every request it received in http.requests. Every standard library service ships with a Test implementation like this one. See HttpClient.
Handling errors
catch handles one error class and removes it from the error channel, so the type checker knows exactly what can still go wrong:
tolerant = ().()(
lambda : E.(f"wrong secret: {.}")
)SecretInvalidError is gone from the error type. The two HTTP errors remain, and catch_all handles whatever is left. See Error handling.
Where next
- Building effects starts the Core section: constructing and composing effects, typed errors, requirements, resources and generator syntax.
- Logger starts the Standard library section: the built-in services, each with a live and a test implementation, plus fibers, racing, timeouts and retries.
- The API Reference lists every exported name with its signature and docstring.
- skills-cli is a complete CLI built entirely on effecton services.