effecton 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. effecton-0.1.0/.gitignore +10 -0
  2. effecton-0.1.0/LICENSE +9 -0
  3. effecton-0.1.0/PKG-INFO +307 -0
  4. effecton-0.1.0/README.md +291 -0
  5. effecton-0.1.0/pyproject.toml +44 -0
  6. effecton-0.1.0/src/effecton/__init__.py +91 -0
  7. effecton-0.1.0/src/effecton/attempt.py +24 -0
  8. effecton-0.1.0/src/effecton/effect.py +155 -0
  9. effecton-0.1.0/src/effecton/exit.py +21 -0
  10. effecton-0.1.0/src/effecton/gen.py +47 -0
  11. effecton-0.1.0/src/effecton/implicit_requirement.py +48 -0
  12. effecton-0.1.0/src/effecton/provide.py +60 -0
  13. effecton-0.1.0/src/effecton/py.typed +0 -0
  14. effecton-0.1.0/src/effecton/run_sync.py +161 -0
  15. effecton-0.1.0/src/effecton/std/__init__.py +1 -0
  16. effecton-0.1.0/src/effecton/std/logger.py +214 -0
  17. effecton-0.1.0/src/effecton/std/pretty_logger.py +119 -0
  18. effecton-0.1.0/src/effecton/std/scope.py +58 -0
  19. effecton-0.1.0/src/effecton/suspend.py +49 -0
  20. effecton-0.1.0/tests/std/test_logger.py +132 -0
  21. effecton-0.1.0/tests/std/test_pretty_logger.py +104 -0
  22. effecton-0.1.0/tests/std/test_scope.py +225 -0
  23. effecton-0.1.0/tests/std/test_types_logger.py +46 -0
  24. effecton-0.1.0/tests/std/test_types_scope.py +138 -0
  25. effecton-0.1.0/tests/test_attempt.py +81 -0
  26. effecton-0.1.0/tests/test_gen.py +168 -0
  27. effecton-0.1.0/tests/test_implicit_requirement.py +111 -0
  28. effecton-0.1.0/tests/test_run_sync.py +489 -0
  29. effecton-0.1.0/tests/test_sample_program.py +63 -0
  30. effecton-0.1.0/tests/test_suspend.py +171 -0
  31. effecton-0.1.0/tests/test_types_attempt.py +22 -0
  32. effecton-0.1.0/tests/test_types_gen.py +123 -0
  33. effecton-0.1.0/tests/test_types_implicit_requirement.py +84 -0
  34. effecton-0.1.0/tests/test_types_run_sync.py +266 -0
  35. effecton-0.1.0/tests/test_types_suspend.py +66 -0
@@ -0,0 +1,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
effecton-0.1.0/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Krzysztof Kaczor
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,307 @@
1
+ Metadata-Version: 2.5
2
+ Name: effecton
3
+ Version: 0.1.0
4
+ Summary: A typed effect system for Python, inspired by Effect-TS
5
+ Project-URL: Repository, https://github.com/krzkaczor/effecton
6
+ Author-email: Krzysztof Kaczor <chris@kaczor.io>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: dependency-injection,effect-system,effect-ts,functional,typed
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Programming Language :: Python :: 3.14
12
+ Classifier: Typing :: Typed
13
+ Requires-Python: >=3.14
14
+ Requires-Dist: typing-extensions>=4.16.0
15
+ Description-Content-Type: text/markdown
16
+
17
+ # Effecton
18
+
19
+ [![Discord](https://img.shields.io/badge/Discord-join%20chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/fNhY7AxMyh)
20
+
21
+ A typed effect system for Python, inspired by [Effect-TS](https://effect.website/). Early stage and experimental.
22
+
23
+ An `Effect[A, E, R]` is a description of a computation that succeeds with `A`, fails with a typed error `E` and requires `R` dependencies.
24
+
25
+ ```python
26
+ from dataclasses import dataclass
27
+
28
+ import effecton as E
29
+
30
+ # Custom errors need to extend EffectonError
31
+ @dataclass(frozen=True)
32
+ class SecretInvalidError(E.EffectonError):
33
+ actual: str
34
+
35
+
36
+ # signature means that it succeeds with str, fails with SecretInvalidError or HttpError and it requires HttpClient
37
+ @E.gen
38
+ def check_secret() -> E.EffectGen[str, SecretInvalidError | HttpError, HttpClient.Protocol]:
39
+ http = yield from E.require(HttpClient.Protocol) # requires HttpClient.Protocol
40
+
41
+ secret = yield from http.get_text("https://example.com/secret") # secret is a str; HttpError joins the error channel
42
+ if secret != "hunter2":
43
+ yield from E.fail(SecretInvalidError(secret)) # SecretInvalidError joins the error channel
44
+ return secret
45
+
46
+
47
+ # program can be executed only after its requirements are provided
48
+ program = (
49
+ E.RequirementProvider()
50
+ .and_provide(HttpClient.Protocol)(HttpClient.Live())
51
+ .apply(check_secret())
52
+ )
53
+
54
+ match E.run_sync(program):
55
+ case E.Succeeded(value):
56
+ print(value) # "hunter2"
57
+ case E.Failure(cause):
58
+ print(cause) # Fail(SecretInvalidError(...)) or Fail(HttpStatusError(...))
59
+ ```
60
+
61
+ ## Installation
62
+
63
+ Requires Python 3.14 or later.
64
+
65
+ ```sh
66
+ uv add effecton
67
+ # or
68
+ pip install effecton
69
+ ```
70
+
71
+ ## Features
72
+
73
+ * *Type-safe errors* -- stop guessing what a given function throws; implement surgical error handling to build reliable systems.
74
+ * *Dependency injection* -- with requirements, dependencies become visible. In tests, another implementation can be trivially injected. Forgetting to do so is a type error.
75
+ * *Finalizers* -- granular resource management.
76
+ * *Ergonomic* -- generator-based syntax with `@E.gen` and functional-style `flat_map`, `map`, and friends.
77
+
78
+
79
+ ## Motivation
80
+
81
+ Effect based systems provide programmers with building blocks that might be difficult at first but yield benefits in the future. Handling edge cases and thorough testing might be optional in the prototype stage but becomes critical in production.
82
+
83
+ Furthermore, *agents love* strict type systems and building blocks.
84
+
85
+ *Full example*: [skills-cli](https://github.com/krzkaczor/effecton/tree/main/packages/examples/skills-cli), a small CLI for installing agent skills built entirely on effecton services.
86
+
87
+ ## Current limitations & plan forward
88
+
89
+ The whole project was designed with the `mypy` type checker in mind, which turns out might not be the best idea. Requirements, error channels, and generator syntax work, but there are some rough edges. In particular: `mypy` can't type requirement subtraction, so `RequirementProvider` needs all requirements provided at once (at the root of the program) or the `R` channel degenerates to `object`.
90
+
91
+ My current plan is to standardize on the `ty` type checker. The only blocker is its lack of support for the `yield from` trick that we use.
92
+
93
+ ## Overview
94
+
95
+ ### Building effects
96
+
97
+ ```python
98
+ E.success(21).map(lambda x: x * 2) # Effect[int]
99
+
100
+ E.sync(lambda: print("hi")) # Effect[None] — defers a side effect until the effect runs
101
+
102
+ # Custom errors need to extend EffectonError
103
+ @dataclass(frozen=True)
104
+ class OopsError(E.EffectonError):
105
+ msg: str
106
+
107
+ E.fail(OopsError(msg="oops")) # Effect[Never, OopsError]
108
+ ```
109
+
110
+ `suspend` defers building an effect. The thunk form wraps one effect; as a decorator on a function with parameters, each call captures its arguments and defers the body until the effect runs:
111
+
112
+ ```python
113
+ E.suspend(lambda: E.fail(OopsError(msg="later"))) # Effect[Never, OopsError]
114
+
115
+ @E.suspend
116
+ def find_user(user_id: int) -> E.Effect[str, OopsError]:
117
+ print("runs only when the effect is interpreted")
118
+ return E.success(f"user-{user_id}")
119
+
120
+ find_user(1) # Effect[str, OopsError] — nothing printed yet
121
+ ```
122
+
123
+ More examples: [`test_run_sync.py`](https://github.com/krzkaczor/effecton/blob/main/packages/effecton/tests/test_run_sync.py), [`test_suspend.py`](https://github.com/krzkaczor/effecton/blob/main/packages/effecton/tests/test_suspend.py).
124
+
125
+ ### Running effects
126
+
127
+ Effects are inert values; `run_sync` interprets one and returns an `Exit`:
128
+
129
+ ```python
130
+ match E.run_sync(effect): # Exit[A, E] = Succeeded[A] | Failure[E]
131
+ case E.Succeeded(value):
132
+ ...
133
+ case E.Failure(cause):
134
+ ... # cause is Fail(error) for typed failures, Die(defect) for unexpected exceptions
135
+ ```
136
+
137
+ More examples: [`test_run_sync.py`](https://github.com/krzkaczor/effecton/blob/main/packages/effecton/tests/test_run_sync.py).
138
+
139
+ ### Error handling
140
+
141
+ Use `catch_all` to handle errors:
142
+
143
+ ```python
144
+ p = E.fail(OopsError(msg="oops")).catch_all(
145
+ lambda e: E.success(f"recovered from {e.msg}")
146
+ ) # Effect[str] — the error channel is now Never
147
+
148
+ E.run_sync(p) # Succeeded("recovered from oops")
149
+ ```
150
+
151
+ Use `catch_all` with `if` to selectively handle errors:
152
+
153
+ ```python
154
+ n = random.randint(1, 4)
155
+
156
+ p = E.success(n).flat_map(
157
+ lambda r: E.fail(FatalError()) if r == 2 else E.fail(RecoverableError())
158
+ ) # Effect[never, FatalError | RecoverableError]
159
+
160
+ p2 = p.catch_all(
161
+ lambda e: E.success(42) if isinstance(e, RecoverableError) else E.fail(e)
162
+ ) # Effect[int, FatalError]
163
+ ```
164
+
165
+ More examples: [`test_run_sync.py`](https://github.com/krzkaczor/effecton/blob/main/packages/effecton/tests/test_run_sync.py).
166
+
167
+ ### Requirements and providing them
168
+
169
+ `require(T)` reads a dependency and records it in the `R` channel; composing effects unions their requirements, exactly like errors. `run_sync` only accepts `Effect[A, E]`, so running an effect with unmet requirements is a type error, not a runtime surprise.
170
+
171
+ ```python
172
+ @dataclass(frozen=True)
173
+ class Db:
174
+ url: str
175
+
176
+ needs_db = E.require(Db).map(lambda db: db.url) # Effect[str, Never, Db]
177
+
178
+ program = (
179
+ E.RequirementProvider()
180
+ .and_provide(Db)(Db("postgres://x"))
181
+ .apply(needs_db)
182
+ ) # Effect[str] — requirements discharged, runnable
183
+
184
+ E.run_sync(program) # Succeeded("postgres://x")
185
+ ```
186
+
187
+ `and_provide(T)(impl)` is curried so a mismatched implementation is a static error, and `apply` demands that the provided union covers the effect's whole `R`. Requirement keys are exact types: providing a base class for a subclass requirement type-checks but dies with a `MissingRequirement` defect.
188
+
189
+ More examples: [`test_run_sync.py`](https://github.com/krzkaczor/effecton/blob/main/packages/effecton/tests/test_run_sync.py).
190
+
191
+ ### Implicit requirements
192
+
193
+ Some dependencies, such as a logger or a log level, should work out of the box yet stay overridable. An implicit requirement is a class that extends the `ImplicitRequirement` protocol with a `default()` classmethod. Reading one with `require_implicit(X)` types as `Effect[X]`: it never enters `R`, so a program that only uses implicit requirements runs bare. If the lookup misses, the interpreter falls back to `X.default()`, computed once per process and memoized, so defaults must be immutable values.
194
+
195
+ ```python
196
+ @final
197
+ @dataclass(frozen=True)
198
+ class Greeting(E.ImplicitRequirement):
199
+ text: str
200
+
201
+ @classmethod
202
+ def default(cls) -> Greeting:
203
+ return Greeting("hello")
204
+
205
+ E.run_sync(E.require_implicit(Greeting)) # Succeeded(Greeting("hello")) — nothing provided
206
+
207
+ # override for a sub-effect only; the env is restored when it settles
208
+ E.run_sync(E.provide_implicit(E.require_implicit(Greeting), Greeting("hi")))
209
+ ```
210
+
211
+ `provide_implicit(effect, value)` is keyed by `type(value)`, so mark implicit requirement classes `@final`. Overrides also compose in a `RequirementProvider` chain through `and_provide`. `require_implicit` is a separate accessor rather than an overload on `require` because the overload pair silently drops requirements from `R` in some inference positions (pinned in `test_types_implicit_requirement.py`). One footgun: the runtime check only tests that a `default` attribute exists, so a plain requirement class that defines one gets the default fallback instead of a `MissingRequirement` defect.
212
+
213
+ More examples: [`test_implicit_requirement.py`](https://github.com/krzkaczor/effecton/blob/main/packages/effecton/tests/test_implicit_requirement.py).
214
+
215
+ ### Resource management with `on_exit` and Scope
216
+
217
+ `on_exit` attaches a finalizer that runs when the effect settles, on success and failure alike. A `Scope` collects finalizers from a whole sub-tree: `acquire_and_release` registers a release for an acquired resource, and `scoped` provides the `Scope` and runs the collected finalizers in reverse order when the wrapped effect settles.
218
+
219
+ ```python
220
+ E.success(21).on_exit(E.log_info("done")) # finalizer runs on success and failure alike
221
+
222
+ conn = E.acquire_and_release(
223
+ E.sync(lambda: pool.connect()), # acquire
224
+ lambda c: E.sync(c.close), # release, guaranteed by the enclosing scope
225
+ ) # Effect[Connection, Never, Scope]
226
+
227
+ program = E.scoped(conn.flat_map(run_queries)) # Scope discharged; close() runs when program settles
228
+ ```
229
+
230
+ A finalizer that dies doesn't skip the remaining finalizers; its defect surfaces in the final `Exit`.
231
+
232
+ More examples: [`test_scope.py`](https://github.com/krzkaczor/effecton/blob/main/packages/effecton/tests/std/test_scope.py).
233
+
234
+ ### Generator syntax
235
+
236
+ `@E.gen` turns a generator function into a factory of effects: the interpreter runs each yielded effect and sends its success value back into the generator, and the generator's return value becomes the effect's success value. Write `x = yield from effect`, not `x = yield effect` — `Effect.__iter__` is typed so `yield from` gives `x` the effect's success type, while a bare `yield` types as `Any`.
237
+
238
+ ```python
239
+ @E.gen
240
+ def total(n: int) -> E.EffectGen[int, OopsError]:
241
+ a = yield from E.success(20) # a: int — yield from types the sent-back value
242
+
243
+ if n < 0:
244
+ yield from E.fail(OopsError(msg="negative")) # OopsError joins the error channel
245
+ return a + n
246
+
247
+ E.run_sync(total(22)) # Succeeded(42)
248
+ ```
249
+
250
+ A failing yielded effect abandons the generator, so `try/except` around a `yield` never observes effect failures — use `catch_all` on the resulting effect instead.
251
+
252
+ More examples: [`test_gen.py`](https://github.com/krzkaczor/effecton/blob/main/packages/effecton/tests/test_gen.py).
253
+
254
+ ### Wrapping third party code
255
+
256
+ `attempt` runs an exception-throwing thunk lazily and maps expected exceptions into the typed error channel. Re-raise unexpected exceptions from the mapper so they stay defects:
257
+
258
+ ```python
259
+ @dataclass(frozen=True)
260
+ class InvalidJson(E.EffectonError):
261
+ text: str
262
+
263
+ def parse_json(text: str) -> E.Effect[Any, InvalidJson]:
264
+ def to_error(e: Exception) -> InvalidJson:
265
+ if isinstance(e, json.JSONDecodeError):
266
+ return InvalidJson(text)
267
+ raise e # anything else stays a defect
268
+
269
+ return E.attempt(lambda: json.loads(text), to_error)
270
+ ```
271
+
272
+ More examples: [`test_attempt.py`](https://github.com/krzkaczor/effecton/blob/main/packages/effecton/tests/test_attempt.py).
273
+
274
+ ## Standard library
275
+
276
+ ### Logger
277
+
278
+ Effecton comes with pretty logger out of the box.
279
+
280
+ ```python
281
+ E.run_sync(E.log_info("user created", 42)) # pretty-printed to stderr, no setup needed
282
+
283
+ program = E.annotate_logs(handle_request(), request_id="r-1") # every log inside carries request_id=r-1
284
+
285
+ captured: list[E.LogData] = []
286
+ E.run_sync(E.provide_implicit(program, E.CurrentLoggers((E.EffectonLogger(log=captured.append),))))
287
+ ```
288
+
289
+ More examples: [`test_logger.py`](https://github.com/krzkaczor/effecton/blob/main/packages/effecton/tests/std/test_logger.py), [`test_pretty_logger.py`](https://github.com/krzkaczor/effecton/blob/main/packages/effecton/tests/std/test_pretty_logger.py).
290
+
291
+ ## Roadmap
292
+
293
+ - [ ] `ty` support
294
+ - [ ] Support for async/sync code
295
+ - [ ] Retries
296
+ - [ ] Timeouts
297
+ - [ ] `Random` implicit service
298
+ - [ ] More examples of integrations with existing ecosystem (fastapi, pydantic etc.)
299
+
300
+ ## Inspirations
301
+
302
+ * Effect-TS/ZIO
303
+ * stateless
304
+
305
+ ## Contributing
306
+
307
+ See [CONTRIBUTING.md](https://github.com/krzkaczor/effecton/blob/main/CONTRIBUTING.md) for repo setup and development commands.
@@ -0,0 +1,291 @@
1
+ # Effecton
2
+
3
+ [![Discord](https://img.shields.io/badge/Discord-join%20chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/fNhY7AxMyh)
4
+
5
+ A typed effect system for Python, inspired by [Effect-TS](https://effect.website/). Early stage and experimental.
6
+
7
+ An `Effect[A, E, R]` is a description of a computation that succeeds with `A`, fails with a typed error `E` and requires `R` dependencies.
8
+
9
+ ```python
10
+ from dataclasses import dataclass
11
+
12
+ import effecton as E
13
+
14
+ # Custom errors need to extend EffectonError
15
+ @dataclass(frozen=True)
16
+ class SecretInvalidError(E.EffectonError):
17
+ actual: str
18
+
19
+
20
+ # signature means that it succeeds with str, fails with SecretInvalidError or HttpError and it requires HttpClient
21
+ @E.gen
22
+ def check_secret() -> E.EffectGen[str, SecretInvalidError | HttpError, HttpClient.Protocol]:
23
+ http = yield from E.require(HttpClient.Protocol) # requires HttpClient.Protocol
24
+
25
+ secret = yield from http.get_text("https://example.com/secret") # secret is a str; HttpError joins the error channel
26
+ if secret != "hunter2":
27
+ yield from E.fail(SecretInvalidError(secret)) # SecretInvalidError joins the error channel
28
+ return secret
29
+
30
+
31
+ # program can be executed only after its requirements are provided
32
+ program = (
33
+ E.RequirementProvider()
34
+ .and_provide(HttpClient.Protocol)(HttpClient.Live())
35
+ .apply(check_secret())
36
+ )
37
+
38
+ match E.run_sync(program):
39
+ case E.Succeeded(value):
40
+ print(value) # "hunter2"
41
+ case E.Failure(cause):
42
+ print(cause) # Fail(SecretInvalidError(...)) or Fail(HttpStatusError(...))
43
+ ```
44
+
45
+ ## Installation
46
+
47
+ Requires Python 3.14 or later.
48
+
49
+ ```sh
50
+ uv add effecton
51
+ # or
52
+ pip install effecton
53
+ ```
54
+
55
+ ## Features
56
+
57
+ * *Type-safe errors* -- stop guessing what a given function throws; implement surgical error handling to build reliable systems.
58
+ * *Dependency injection* -- with requirements, dependencies become visible. In tests, another implementation can be trivially injected. Forgetting to do so is a type error.
59
+ * *Finalizers* -- granular resource management.
60
+ * *Ergonomic* -- generator-based syntax with `@E.gen` and functional-style `flat_map`, `map`, and friends.
61
+
62
+
63
+ ## Motivation
64
+
65
+ Effect based systems provide programmers with building blocks that might be difficult at first but yield benefits in the future. Handling edge cases and thorough testing might be optional in the prototype stage but becomes critical in production.
66
+
67
+ Furthermore, *agents love* strict type systems and building blocks.
68
+
69
+ *Full example*: [skills-cli](https://github.com/krzkaczor/effecton/tree/main/packages/examples/skills-cli), a small CLI for installing agent skills built entirely on effecton services.
70
+
71
+ ## Current limitations & plan forward
72
+
73
+ The whole project was designed with the `mypy` type checker in mind, which turns out might not be the best idea. Requirements, error channels, and generator syntax work, but there are some rough edges. In particular: `mypy` can't type requirement subtraction, so `RequirementProvider` needs all requirements provided at once (at the root of the program) or the `R` channel degenerates to `object`.
74
+
75
+ My current plan is to standardize on the `ty` type checker. The only blocker is its lack of support for the `yield from` trick that we use.
76
+
77
+ ## Overview
78
+
79
+ ### Building effects
80
+
81
+ ```python
82
+ E.success(21).map(lambda x: x * 2) # Effect[int]
83
+
84
+ E.sync(lambda: print("hi")) # Effect[None] — defers a side effect until the effect runs
85
+
86
+ # Custom errors need to extend EffectonError
87
+ @dataclass(frozen=True)
88
+ class OopsError(E.EffectonError):
89
+ msg: str
90
+
91
+ E.fail(OopsError(msg="oops")) # Effect[Never, OopsError]
92
+ ```
93
+
94
+ `suspend` defers building an effect. The thunk form wraps one effect; as a decorator on a function with parameters, each call captures its arguments and defers the body until the effect runs:
95
+
96
+ ```python
97
+ E.suspend(lambda: E.fail(OopsError(msg="later"))) # Effect[Never, OopsError]
98
+
99
+ @E.suspend
100
+ def find_user(user_id: int) -> E.Effect[str, OopsError]:
101
+ print("runs only when the effect is interpreted")
102
+ return E.success(f"user-{user_id}")
103
+
104
+ find_user(1) # Effect[str, OopsError] — nothing printed yet
105
+ ```
106
+
107
+ More examples: [`test_run_sync.py`](https://github.com/krzkaczor/effecton/blob/main/packages/effecton/tests/test_run_sync.py), [`test_suspend.py`](https://github.com/krzkaczor/effecton/blob/main/packages/effecton/tests/test_suspend.py).
108
+
109
+ ### Running effects
110
+
111
+ Effects are inert values; `run_sync` interprets one and returns an `Exit`:
112
+
113
+ ```python
114
+ match E.run_sync(effect): # Exit[A, E] = Succeeded[A] | Failure[E]
115
+ case E.Succeeded(value):
116
+ ...
117
+ case E.Failure(cause):
118
+ ... # cause is Fail(error) for typed failures, Die(defect) for unexpected exceptions
119
+ ```
120
+
121
+ More examples: [`test_run_sync.py`](https://github.com/krzkaczor/effecton/blob/main/packages/effecton/tests/test_run_sync.py).
122
+
123
+ ### Error handling
124
+
125
+ Use `catch_all` to handle errors:
126
+
127
+ ```python
128
+ p = E.fail(OopsError(msg="oops")).catch_all(
129
+ lambda e: E.success(f"recovered from {e.msg}")
130
+ ) # Effect[str] — the error channel is now Never
131
+
132
+ E.run_sync(p) # Succeeded("recovered from oops")
133
+ ```
134
+
135
+ Use `catch_all` with `if` to selectively handle errors:
136
+
137
+ ```python
138
+ n = random.randint(1, 4)
139
+
140
+ p = E.success(n).flat_map(
141
+ lambda r: E.fail(FatalError()) if r == 2 else E.fail(RecoverableError())
142
+ ) # Effect[never, FatalError | RecoverableError]
143
+
144
+ p2 = p.catch_all(
145
+ lambda e: E.success(42) if isinstance(e, RecoverableError) else E.fail(e)
146
+ ) # Effect[int, FatalError]
147
+ ```
148
+
149
+ More examples: [`test_run_sync.py`](https://github.com/krzkaczor/effecton/blob/main/packages/effecton/tests/test_run_sync.py).
150
+
151
+ ### Requirements and providing them
152
+
153
+ `require(T)` reads a dependency and records it in the `R` channel; composing effects unions their requirements, exactly like errors. `run_sync` only accepts `Effect[A, E]`, so running an effect with unmet requirements is a type error, not a runtime surprise.
154
+
155
+ ```python
156
+ @dataclass(frozen=True)
157
+ class Db:
158
+ url: str
159
+
160
+ needs_db = E.require(Db).map(lambda db: db.url) # Effect[str, Never, Db]
161
+
162
+ program = (
163
+ E.RequirementProvider()
164
+ .and_provide(Db)(Db("postgres://x"))
165
+ .apply(needs_db)
166
+ ) # Effect[str] — requirements discharged, runnable
167
+
168
+ E.run_sync(program) # Succeeded("postgres://x")
169
+ ```
170
+
171
+ `and_provide(T)(impl)` is curried so a mismatched implementation is a static error, and `apply` demands that the provided union covers the effect's whole `R`. Requirement keys are exact types: providing a base class for a subclass requirement type-checks but dies with a `MissingRequirement` defect.
172
+
173
+ More examples: [`test_run_sync.py`](https://github.com/krzkaczor/effecton/blob/main/packages/effecton/tests/test_run_sync.py).
174
+
175
+ ### Implicit requirements
176
+
177
+ Some dependencies, such as a logger or a log level, should work out of the box yet stay overridable. An implicit requirement is a class that extends the `ImplicitRequirement` protocol with a `default()` classmethod. Reading one with `require_implicit(X)` types as `Effect[X]`: it never enters `R`, so a program that only uses implicit requirements runs bare. If the lookup misses, the interpreter falls back to `X.default()`, computed once per process and memoized, so defaults must be immutable values.
178
+
179
+ ```python
180
+ @final
181
+ @dataclass(frozen=True)
182
+ class Greeting(E.ImplicitRequirement):
183
+ text: str
184
+
185
+ @classmethod
186
+ def default(cls) -> Greeting:
187
+ return Greeting("hello")
188
+
189
+ E.run_sync(E.require_implicit(Greeting)) # Succeeded(Greeting("hello")) — nothing provided
190
+
191
+ # override for a sub-effect only; the env is restored when it settles
192
+ E.run_sync(E.provide_implicit(E.require_implicit(Greeting), Greeting("hi")))
193
+ ```
194
+
195
+ `provide_implicit(effect, value)` is keyed by `type(value)`, so mark implicit requirement classes `@final`. Overrides also compose in a `RequirementProvider` chain through `and_provide`. `require_implicit` is a separate accessor rather than an overload on `require` because the overload pair silently drops requirements from `R` in some inference positions (pinned in `test_types_implicit_requirement.py`). One footgun: the runtime check only tests that a `default` attribute exists, so a plain requirement class that defines one gets the default fallback instead of a `MissingRequirement` defect.
196
+
197
+ More examples: [`test_implicit_requirement.py`](https://github.com/krzkaczor/effecton/blob/main/packages/effecton/tests/test_implicit_requirement.py).
198
+
199
+ ### Resource management with `on_exit` and Scope
200
+
201
+ `on_exit` attaches a finalizer that runs when the effect settles, on success and failure alike. A `Scope` collects finalizers from a whole sub-tree: `acquire_and_release` registers a release for an acquired resource, and `scoped` provides the `Scope` and runs the collected finalizers in reverse order when the wrapped effect settles.
202
+
203
+ ```python
204
+ E.success(21).on_exit(E.log_info("done")) # finalizer runs on success and failure alike
205
+
206
+ conn = E.acquire_and_release(
207
+ E.sync(lambda: pool.connect()), # acquire
208
+ lambda c: E.sync(c.close), # release, guaranteed by the enclosing scope
209
+ ) # Effect[Connection, Never, Scope]
210
+
211
+ program = E.scoped(conn.flat_map(run_queries)) # Scope discharged; close() runs when program settles
212
+ ```
213
+
214
+ A finalizer that dies doesn't skip the remaining finalizers; its defect surfaces in the final `Exit`.
215
+
216
+ More examples: [`test_scope.py`](https://github.com/krzkaczor/effecton/blob/main/packages/effecton/tests/std/test_scope.py).
217
+
218
+ ### Generator syntax
219
+
220
+ `@E.gen` turns a generator function into a factory of effects: the interpreter runs each yielded effect and sends its success value back into the generator, and the generator's return value becomes the effect's success value. Write `x = yield from effect`, not `x = yield effect` — `Effect.__iter__` is typed so `yield from` gives `x` the effect's success type, while a bare `yield` types as `Any`.
221
+
222
+ ```python
223
+ @E.gen
224
+ def total(n: int) -> E.EffectGen[int, OopsError]:
225
+ a = yield from E.success(20) # a: int — yield from types the sent-back value
226
+
227
+ if n < 0:
228
+ yield from E.fail(OopsError(msg="negative")) # OopsError joins the error channel
229
+ return a + n
230
+
231
+ E.run_sync(total(22)) # Succeeded(42)
232
+ ```
233
+
234
+ A failing yielded effect abandons the generator, so `try/except` around a `yield` never observes effect failures — use `catch_all` on the resulting effect instead.
235
+
236
+ More examples: [`test_gen.py`](https://github.com/krzkaczor/effecton/blob/main/packages/effecton/tests/test_gen.py).
237
+
238
+ ### Wrapping third party code
239
+
240
+ `attempt` runs an exception-throwing thunk lazily and maps expected exceptions into the typed error channel. Re-raise unexpected exceptions from the mapper so they stay defects:
241
+
242
+ ```python
243
+ @dataclass(frozen=True)
244
+ class InvalidJson(E.EffectonError):
245
+ text: str
246
+
247
+ def parse_json(text: str) -> E.Effect[Any, InvalidJson]:
248
+ def to_error(e: Exception) -> InvalidJson:
249
+ if isinstance(e, json.JSONDecodeError):
250
+ return InvalidJson(text)
251
+ raise e # anything else stays a defect
252
+
253
+ return E.attempt(lambda: json.loads(text), to_error)
254
+ ```
255
+
256
+ More examples: [`test_attempt.py`](https://github.com/krzkaczor/effecton/blob/main/packages/effecton/tests/test_attempt.py).
257
+
258
+ ## Standard library
259
+
260
+ ### Logger
261
+
262
+ Effecton comes with pretty logger out of the box.
263
+
264
+ ```python
265
+ E.run_sync(E.log_info("user created", 42)) # pretty-printed to stderr, no setup needed
266
+
267
+ program = E.annotate_logs(handle_request(), request_id="r-1") # every log inside carries request_id=r-1
268
+
269
+ captured: list[E.LogData] = []
270
+ E.run_sync(E.provide_implicit(program, E.CurrentLoggers((E.EffectonLogger(log=captured.append),))))
271
+ ```
272
+
273
+ More examples: [`test_logger.py`](https://github.com/krzkaczor/effecton/blob/main/packages/effecton/tests/std/test_logger.py), [`test_pretty_logger.py`](https://github.com/krzkaczor/effecton/blob/main/packages/effecton/tests/std/test_pretty_logger.py).
274
+
275
+ ## Roadmap
276
+
277
+ - [ ] `ty` support
278
+ - [ ] Support for async/sync code
279
+ - [ ] Retries
280
+ - [ ] Timeouts
281
+ - [ ] `Random` implicit service
282
+ - [ ] More examples of integrations with existing ecosystem (fastapi, pydantic etc.)
283
+
284
+ ## Inspirations
285
+
286
+ * Effect-TS/ZIO
287
+ * stateless
288
+
289
+ ## Contributing
290
+
291
+ See [CONTRIBUTING.md](https://github.com/krzkaczor/effecton/blob/main/CONTRIBUTING.md) for repo setup and development commands.
@@ -0,0 +1,44 @@
1
+ [project]
2
+ name = "effecton"
3
+ version = "0.1.0"
4
+ description = "A typed effect system for Python, inspired by Effect-TS"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [{ name = "Krzysztof Kaczor", email = "chris@kaczor.io" }]
9
+ keywords = ["effect-system", "functional", "typed", "effect-ts", "dependency-injection"]
10
+ classifiers = [
11
+ "Development Status :: 3 - Alpha",
12
+ "Programming Language :: Python :: 3.14",
13
+ "Typing :: Typed",
14
+ ]
15
+ requires-python = ">=3.14"
16
+ dependencies = [
17
+ "typing-extensions>=4.16.0",
18
+ ]
19
+
20
+ [project.urls]
21
+ Repository = "https://github.com/krzkaczor/effecton"
22
+
23
+ [build-system]
24
+ requires = ["hatchling"]
25
+ build-backend = "hatchling.build"
26
+
27
+ [tool.hatch.build.targets.wheel]
28
+ packages = ["src/effecton"]
29
+
30
+ [tool.mypy]
31
+ python_version = "3.14"
32
+ strict = true
33
+ explicit_package_bases = true
34
+ mypy_path = "src"
35
+ files = ["src", "tests"]
36
+
37
+ [[tool.mypy.overrides]]
38
+ module = "tests.*"
39
+ disallow_untyped_defs = false
40
+ disallow_incomplete_defs = false
41
+
42
+ [tool.pytest.ini_options]
43
+ testpaths = ["tests"]
44
+ addopts = "-qs"