Metadata-Version: 2.4
Name: effect-python
Version: 0.1.0a0
Summary: Effect-TS style typed effects for Python: Effect[A, E, R] with tracked errors and dependencies, services, layers.
Keywords: effect,effect-ts,typed-effects,functional,pyright,dependency-injection
Author: Ankit Khullar
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries
Classifier: Typing :: Typed
Requires-Dist: opentelemetry-api>=1.20 ; extra == 'otel'
Requires-Python: >=3.13
Project-URL: Documentation, https://github.com/dev-ankit/effect-py/tree/main/docs
Project-URL: Homepage, https://github.com/dev-ankit/effect-py
Project-URL: Issues, https://github.com/dev-ankit/effect-py/issues
Project-URL: Repository, https://github.com/dev-ankit/effect-py
Provides-Extra: otel
Description-Content-Type: text/markdown

# effect-py

Effect-TS style typed effects for Python. `effect-py` brings the
[Effect](https://effect.website) 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](https://github.com/microsoft/pyright) in strict mode.

```python
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`](examples/quickstart.py) for a runnable version.
Design notes and the validated feasibility experiments behind the typing
patterns live in [`PLAN.md`](PLAN.md) and
[`research/experiments/`](research/experiments/).

## Install

```sh
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](https://effect.website) chapter with Python examples sourced from the
test suite:

- [03 — Basics](docs/03-basics.md): `gen`, `@fn`, pipe instrumentation, retry/timeout
- [04 — Services & Layers](docs/04-services-and-layers.md): Protocol tags, layers, memoization
- [05 — Data Modeling](docs/05-data-modeling.md): brands, records, tagged variants, JSON codecs
- [06 — Error Handling](docs/06-error-handling.md): `TaggedError`, `catch_tag`, defects
- [07 — Config](docs/07-config.md): typed config, providers, `Redacted` secrets
- [08 — Testing](docs/08-testing.md): 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:

```python
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](docs/03-basics.md#pipe-for-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`](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](#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](https://github.com/microsoft/pyright/issues/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:

   ```toml
   [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.

   ```python
   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

```sh
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.
