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

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–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 (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.0a0.tar.gz (66.8 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.0a0-py3-none-any.whl (79.7 kB view details)

Uploaded Python 3

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

Hashes for effect_python-0.1.0a0.tar.gz
Algorithm Hash digest
SHA256 bd20303031be91e8003b4c945ccf1f6cc860c66f7cdc63c5c19c3c4ce2383224
MD5 28607025f944dc59bd66ecde8b54bfb5
BLAKE2b-256 72627a5133b5b8974deafc0d70726cfb0de8037cb7e07a93d75ca5a7b91b91de

See more details on using hashes here.

Provenance

The following attestation bundles were made for effect_python-0.1.0a0.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.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

Hashes for effect_python-0.1.0a0-py3-none-any.whl
Algorithm Hash digest
SHA256 a6852a1393ef2fb43a4d11815f929faaf06de89b4e6339e8266d640339a49499
MD5 c251e365d4c5b6cbcb4427683e2ffd60
BLAKE2b-256 ec9ab958d22a5883edae6e97cfb96ee600594893cfd778e4f0b579678667b0c6

See more details on using hashes here.

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

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