Effect-TS style typed effects for Python: Effect[A, E, R] with tracked errors and dependencies, services, layers.
Project description
effect-py
Effect-TS style typed effects for Python. effect-py brings the
Effect programming model — Effect[A, E, R] with
tracked success values, tracked errors, and tracked dependencies, plus
services and layers for dependency injection — to Python, checked end-to-end
by pyright in strict mode.
from dataclasses import dataclass
from typing import Protocol
from effect_py import fn, layer, provide, run_sync, service, succeed
from effect_py.errors import TaggedError, catch_tag
# tags are Protocol classes -- impls conform structurally, no subclassing
class Database(Protocol):
def find(self, user_id: str) -> str | None: ...
class InMemoryDatabase:
def __init__(self, rows: dict[str, str]) -> None:
self.rows = rows
def find(self, user_id: str) -> str | None:
return self.rows.get(user_id)
@dataclass
class UserNotFound(TaggedError):
user_id: str
@fn("Users.greet")
def greet(user_id: str):
db = yield from service(Database) # R channel: requires Database
name = db.find(user_id)
if name is None:
return (yield from UserNotFound(user_id)) # E channel: may fail
return f"Hello, {name}!"
# greet("u1") : Effect[str, UserNotFound, Database] — fully inferred, zero annotations
program = greet("u1").pipe(
catch_tag(UserNotFound)(lambda e: succeed(f"Who is {e.user_id}?")),
provide(layer.succeed(Database, InMemoryDatabase({"u1": "Alice"}))),
)
run_sync(program) # type error here if any error is unhandled or service unprovided
The two guarantees, enforced by pyright at the run_sync boundary:
- No unhandled errors: every typed error must be caught (or explicitly converted to a defect) before an effect can run.
- No missing dependencies: every service an effect uses must be provided by a layer.
- Interface/impl split for free: services are
Protocolinterfaces, so live and test implementations swap in without subclassing or@runtime_checkable— plain classes work as tags too (tag == impl) when you don't need the split.
See examples/quickstart.py for a runnable version.
Design notes and the validated feasibility experiments behind the typing
patterns live in PLAN.md and
research/experiments/.
Install
pip install effect-python # or: uv add effect-python
pip install 'effect-python[otel]' # optional: OpenTelemetry tracing bridge
Requires Python ≥ 3.13. The package ships a py.typed marker, so pyright
sees its types out of the box.
Documentation
Task-focused guides, each mirroring an Effect docs chapter with Python examples sourced from the test suite:
- 03 — Basics:
gen,@fn, pipe instrumentation, retry/timeout - 04 — Services & Layers: Protocol tags, layers, memoization
- 05 — Data Modeling: brands, records, tagged variants, JSON codecs
- 06 — Error Handling:
TaggedError,catch_tag, defects - 07 — Config: typed config, providers,
Redactedsecrets - 08 — Testing: the pytest integration,
TestClock, suite-shared layers
Each guide ends with a "Deviations from Effect-TS" section explaining where Python's type system or runtime forces a different spelling.
Observability
@fn and with_span emit tracing spans through a Tracer default service —
zero overhead until you provide one. provide(layer.succeed(Tracer, ...))
installs any backend; effect_py.otel (the [otel] extra) bridges to
OpenTelemetry:
from effect_py import provide, run_sync, with_span
from effect_py import otel # requires: pip install 'effect-python[otel]'
program.pipe(with_span("handle-request")).pipe(provide(otel.layer()))
Status
Alpha (0.1.0a, M1–M6 of the plan complete): core effects, generators
(gen/fn), typed errors with catch_tag subtraction, services, layers with
memoization, the asyncio runtime, schedules/retry/timeout, fibers, the
TestClock + pytest integration, schema, config, and tracing (with the
optional OpenTelemetry bridge). See PLAN.md for the full
milestone history.
Requirements
- Python ≥ 3.13 (PEP 695 type parameters with PEP 696 defaults)
- pyright for type checking (
tycannot yet infer the core patterns; mypy is not supported — it rejects passing aProtocolclass wheretype[T]is expected, which Protocol service tags rely on); see Pyright strict: the one carve-out for the one inference gap every consumer hits
Pyright strict: the one carve-out
The library's pitch is full E/R/A inference from unannotated generator
functions. The one hole: pyright cannot infer a generator's send type
through yield from — it treats the send channel as a contravariant input
(like a parameter, hence the rule name) and infers Unknown. TypeScript
propagates send types through yield*, which is why Effect-TS needs no
equivalent carve-out.
Symptom, under typeCheckingMode = "strict": every unannotated effect
generator's def line errors with reportUnknownParameterType:
Return type "Generator[Effect[...], Unknown, ...]" is partially unknown
The Unknown is def-site noise only — gen/@fn still solve the returned
Effect[A, E, R] exactly, and it never leaks into consuming code.
Asked upstream, declined: microsoft/pyright#10279 — closed as not planned, though the maintainer will revisit given upvote signal. Upvotes welcome.
Three postures, ranked:
-
Scope the rule off for effect-heavy directories via
executionEnvironments— the rest of the project stays fully strict:[tool.pyright] typeCheckingMode = "strict" executionEnvironments = [ { root = "src/myapp", reportUnknownParameterType = "none" }, ]
-
Per-file: add
# pyright: reportUnknownParameterType=falseas the first line of files that define effect generators. Zero config; travels with the file. -
Zero carve-out: annotate generators with the exported
EffectGen[A, E = Never, R = Never]alias. Explicit E/R/A instead of inferred — but checked, not trusted: under-declaring E or getting A wrong is a type error at the def site.from effect_py import EffectGen, fn, service @fn("Users.find") def find(user_id: str) -> EffectGen[str, UserNotFound, Database]: db = yield from service(Database) ...
gen(lambda: f(x)) additionally trips reportUnknownLambdaType; use @fn
for parameterized generators (the library already steers this way).
This repo dogfoods posture 1 — the carve-out is scoped to tests and
examples; src/effect_py itself passes full strict with zero
suppressions. tests/typecheck/cases/send_type_hole.py pins the pyright
behavior: if pyright ever fixes the inference, that test fails and this
section can be deleted.
Development
uv sync
uv run pytest
uv run pyright
uv run ruff check
tests/typecheck/ pins pyright's inference behavior — the library's core
guarantees live in the type checker, so inference regressions fail CI like
any other bug.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file effect_python-0.1.0a0.tar.gz.
File metadata
- Download URL: effect_python-0.1.0a0.tar.gz
- Upload date:
- Size: 66.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bd20303031be91e8003b4c945ccf1f6cc860c66f7cdc63c5c19c3c4ce2383224
|
|
| MD5 |
28607025f944dc59bd66ecde8b54bfb5
|
|
| BLAKE2b-256 |
72627a5133b5b8974deafc0d70726cfb0de8037cb7e07a93d75ca5a7b91b91de
|
Provenance
The following attestation bundles were made for effect_python-0.1.0a0.tar.gz:
Publisher:
publish.yml on dev-ankit/effect-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
effect_python-0.1.0a0.tar.gz -
Subject digest:
bd20303031be91e8003b4c945ccf1f6cc860c66f7cdc63c5c19c3c4ce2383224 - Sigstore transparency entry: 2136151958
- Sigstore integration time:
-
Permalink:
dev-ankit/effect-py@6899b708c6e2beee6696d46cfbf454273627cc12 -
Branch / Tag:
refs/tags/v0.1.0a0 - Owner: https://github.com/dev-ankit
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@6899b708c6e2beee6696d46cfbf454273627cc12 -
Trigger Event:
release
-
Statement type:
File details
Details for the file effect_python-0.1.0a0-py3-none-any.whl.
File metadata
- Download URL: effect_python-0.1.0a0-py3-none-any.whl
- Upload date:
- Size: 79.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a6852a1393ef2fb43a4d11815f929faaf06de89b4e6339e8266d640339a49499
|
|
| MD5 |
c251e365d4c5b6cbcb4427683e2ffd60
|
|
| BLAKE2b-256 |
ec9ab958d22a5883edae6e97cfb96ee600594893cfd778e4f0b579678667b0c6
|
Provenance
The following attestation bundles were made for effect_python-0.1.0a0-py3-none-any.whl:
Publisher:
publish.yml on dev-ankit/effect-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
effect_python-0.1.0a0-py3-none-any.whl -
Subject digest:
a6852a1393ef2fb43a4d11815f929faaf06de89b4e6339e8266d640339a49499 - Sigstore transparency entry: 2136151960
- Sigstore integration time:
-
Permalink:
dev-ankit/effect-py@6899b708c6e2beee6696d46cfbf454273627cc12 -
Branch / Tag:
refs/tags/v0.1.0a0 - Owner: https://github.com/dev-ankit
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@6899b708c6e2beee6696d46cfbf454273627cc12 -
Trigger Event:
release
-
Statement type: