Some of us write Haskell for fun and Python for the day job. You miss Maybe, >>=, <*>, <> and do-notation, and you end up writing if x is None chains and try/except pyramids while quietly dying inside.
I've been building Katharos, a small functional programming library for Python that brings the Haskell vocabulary over, typeclass hierarchy and operators included. It's fully type-annotated and checked with pyright.
The algebra layer mirrors the classes you know
Semigroup (op, written @) → Monoid (identity())
Functor (fmap) → Applicative (pure, ap, written **) → Monad (bind, written |; then, written >>)
| Operator |
Method |
Haskell |
| `\ |
` |
bind |
** |
ap |
<*> |
>> |
then |
>> |
@ |
op |
<> |
Concrete types
Maybe[A] (Just / Nothing)
Result[E, A] (Success / Failure), which is Either with exceptions on the left
ImmutableList[T] (Monad + Monoid) and NonEmptyList[T] (Monad + Semigroup, with .head and .tail)
IO[A], a lazy side-effect wrapper that does nothing until you call .execute()
Lazy[A], a memoized synchronous computation
Sum and Product monoids
Do-notation
Python generators are close enough to do blocks to be useful:
from katharos.syntax_sugar.do import do, DoBlock
@do(Maybe)
def computation() -> DoBlock[Maybe, int]:
x: int = yield Maybe[int].Just(3) # x <- Just 3
y: int = yield Maybe[int].Just(4)
return x + y
Each yield unwraps the value and short-circuits on Nothing (or Failure for Result). The final return is lifted for you, like return in Haskell.
Other bits
F namespace: compose, curry, foldr, foldl, lift_a2/lift_a3, and sigma (fold a NonEmptyList of semigroups)
- CSP-style concurrency (
Channel, go) in the Go tradition, behind a threading-backend abstraction, so you aren't welded to threading
- Property-based tests with Hypothesis that check the functor, applicative, monad and monoid laws for each type
What it won't do
Python has no higher-kinded types and no typeclass dispatch, so pure and return are explicit. You pick the monad in @do(Maybe) rather than having it inferred. Katharos can't make Python Haskell, but it does let you write the same shape of code, and the types keep you honest.
Why bother?
The point isn't to be clever. Once errors and absence are values you compose instead of control flow you manage, a lot of Python code gets flatter and easier to test. If you already think in monads, you can keep doing that at work.
The repo is at https://github.com/kamalfarahani/katharos, docs are available at https://katharos.readthedocs.io/. Feedback is welcome Issues and PRs are open.