This release is a pre-release and may not be stable for production use.
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
pip install 'effect-python[http]' # optional: httpx-backed HTTP client transport
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 - 11 — HTTP Clients: requests/responses, middleware,
HttpApiderived clients, the httpx transport - 13 — Command-Line Interfaces: typed
CliConfigparams, subcommands, global flags, help, testing CLIs
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–M8 of the plan complete): core effects, generators
(gen/fn), typed errors with catch_tag subtraction, services, layers with
memoization (including the dependency-hiding layer_provide), the asyncio
runtime, schedules/retry/timeout, fibers, the TestClock + pytest
integration, schema, config, tracing (with the optional OpenTelemetry
bridge), the HTTP client family (with the optional httpx transport), and the
CLI package (typed CliConfig params, subcommands, global flags, the
Console default service). 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.
Metadata
Release files for effect-python 0.1.0a2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| effect_python-0.1.0a2.tar.gz | 110.0 kB | With provenance | Details for effect_python-0.1.0a2.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| effect_python-0.1.0a2-py3-none-any.whl | Python 3 | none | any | Details for effect_python-0.1.0a2-py3-none-any.whl |
Total release size: 242.9 kB
Release files / effect_python-0.1.0a2.tar.gz
| Download URL | effect_python-0.1.0a2.tar.gz |
|---|---|
| Size | 110.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c83146d4cfed1afb75ef44ecfab6f191b2ce1346c0c9d6e0ee6073f50cf0d307
|
|
BLAKE2b-256 checksum How to use checksums |
57502463602ea64d5604bd955fc5123dd01bd646dfa5b53bb254805dc24a60ef
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 11, 2026.
Transparency logRelease files / effect_python-0.1.0a2-py3-none-any.whl
| Download URL | effect_python-0.1.0a2-py3-none-any.whl |
|---|---|
| Size | 132.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4bb603a90dd16c19af7c5b3c917ca729199059920e8270c298d9edb53c3a4844
|
|
BLAKE2b-256 checksum How to use checksums |
5dd158d34d6c4fed42779b2249d15cb83f24b0b46c0e2fcc49abede1ac5a0348
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 11, 2026.
Transparency logSecurity for effect-python 0.1.0a2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for effect_python-0.1.0a2.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 11, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.