Skip to main content

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 Protocol interfaces, 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:

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()))

See basics — instrumentation.

Status

Alpha (0.1.0a, M1–M7 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), and the HTTP client family (with the optional httpx transport). See PLAN.md for the full milestone history.

Requirements

  • Python ≥ 3.13 (PEP 695 type parameters with PEP 696 defaults)
  • pyright for type checking (ty cannot yet infer the core patterns; mypy is not supported — it rejects passing a Protocol class where type[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:

  1. 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" },
    ]
    
  2. Per-file: add # pyright: reportUnknownParameterType=false as the first line of files that define effect generators. Zero config; travels with the file.

  3. 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

effect_python-0.1.0a1.tar.gz (80.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

effect_python-0.1.0a1-py3-none-any.whl (96.8 kB view details)

Uploaded Python 3

File details

Details for the file effect_python-0.1.0a1.tar.gz.

File metadata

  • Download URL: effect_python-0.1.0a1.tar.gz
  • Upload date:
  • Size: 80.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for effect_python-0.1.0a1.tar.gz
Algorithm Hash digest
SHA256 ff67c127b66812cd613fe90b828fa92165f65507eea4e9a12d6b8afec2881979
MD5 07a66f1265ff7a392104645e3fc5cf7a
BLAKE2b-256 052f93511a337dd24733800444af14bba5ce65aab46d852d26c0af9e156667fc

See more details on using hashes here.

Provenance

The following attestation bundles were made for effect_python-0.1.0a1.tar.gz:

Publisher: publish.yml on dev-ankit/effect-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file effect_python-0.1.0a1-py3-none-any.whl.

File metadata

  • Download URL: effect_python-0.1.0a1-py3-none-any.whl
  • Upload date:
  • Size: 96.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for effect_python-0.1.0a1-py3-none-any.whl
Algorithm Hash digest
SHA256 a525de489f3a3f104435f90d802fac0141581577f2b0882373cba2d06aa2b132
MD5 2253b8c1d7e5f684d6f075317a8dfdc7
BLAKE2b-256 26968160c23f6a22182baa81675830dac5e3ab459e1f611b8ea0cc07e1fe6a08

See more details on using hashes here.

Provenance

The following attestation bundles were made for effect_python-0.1.0a1-py3-none-any.whl:

Publisher: publish.yml on dev-ankit/effect-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page