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–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 (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.0a2.tar.gz (110.0 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.0a2-py3-none-any.whl (132.8 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: effect_python-0.1.0a2.tar.gz
  • Upload date:
  • Size: 110.0 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.0a2.tar.gz
Algorithm Hash digest
SHA256 c83146d4cfed1afb75ef44ecfab6f191b2ce1346c0c9d6e0ee6073f50cf0d307
MD5 7a347512c8f2df9de29ba5d2e6e154f2
BLAKE2b-256 57502463602ea64d5604bd955fc5123dd01bd646dfa5b53bb254805dc24a60ef

See more details on using hashes here.

Provenance

The following attestation bundles were made for effect_python-0.1.0a2.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.0a2-py3-none-any.whl.

File metadata

  • Download URL: effect_python-0.1.0a2-py3-none-any.whl
  • Upload date:
  • Size: 132.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.0a2-py3-none-any.whl
Algorithm Hash digest
SHA256 4bb603a90dd16c19af7c5b3c917ca729199059920e8270c298d9edb53c3a4844
MD5 f60385ca2d8f83a22eab99d4e932fc18
BLAKE2b-256 5dd158d34d6c4fed42779b2249d15cb83f24b0b46c0e2fcc49abede1ac5a0348

See more details on using hashes here.

Provenance

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