Skip to main content

Type-safe functional tools for Python

Project description

Koda

Koda is a collection of practical type-safe tools for Python.

At it's core are a number of datatypes that are common in functional programming.

Maybe

Maybe is similar to Python's Optional type. It has two variants: Nothing and Just, and they work in similar ways to what you may have seen in other languages.

from koda.maybe import Maybe, Just, nothing

a: Maybe[int] = Just(5)
b: Maybe[int] = nothing

To know if a Maybe is a Just or a Nothing, you'll need to inspect it.

from koda.maybe import Just, Maybe

maybe_str: Maybe[str] = function_returning_maybe_str()

# unwrap by checking instance type
if isinstance(maybe_str, Just):
    print(maybe_str.val)
else:
    print("No value!")

# unwrap with structural pattern matching (python 3.10 +)
match maybe_str:
    case Just(val):
        print(val)
    case Nothing:
        print("No value!")

Maybe has methods for conveniently stringing logic together.

Maybe.map

from koda.maybe import Just, nothing

Just(5).map(lambda x: x + 10)  # Just(15)
nothing.map(lambda x: x + 10)  # Nothing
Just(5).map(lambda x: x + 10).map(lambda x: f"abc{x}")  # Just("abc15")

Maybe.flat_map

from koda.maybe import Maybe, Just, nothing


def safe_divide(dividend: int, divisor: int) -> Maybe[float]:
    if divisor != 0:
        return Just(dividend / divisor)
    else:
        return nothing

Just(5).flat_map(lambda x: safe_divide(10, x))  # Just(2)
Just(0).flat_map(lambda x: safe_divide(10, x))  # Nothing
nothing.flat_map(lambda x: safe_divide(10, x))  # Nothing

Result

Result provides a means of representing whether a computation succeeded or failed. To represent success, we can use OK; for failures we can use Err. Compared to Maybe, Result is perhaps most useful in that the "failure" case also returns, whereas Nothing contains no data.

from koda.result import Ok, Err, Result 


def safe_divide_result(dividend: int, divisor: int) -> Result[float, str]:
    if divisor != 0:
        return Ok(dividend / divisor)
    else:
        return Err("cannot divide by zero!")


Ok(5).flat_map(lambda x: safe_divide_result(10, x))  # Ok(2)
Ok(0).flat_map(lambda x: safe_divide_result(10, x))  # Err("cannot divide by zero!") 
Err("some other error").map(lambda x: safe_divide_result(10, x))  # Err("some other error")

Result can be convenient with try/except logic.

from koda.result import Result, Ok, Err

def divide_by(dividend: int, divisor: int) -> Result[float, ZeroDivisionError]:
    try:
        return Ok(dividend / divisor)
    except ZeroDivisionError as exc:
        return Err(exc)


divided: Result[float, ZeroDivisionError] = divide_by(10, 0)  # Err(ZeroDivisionError("division by zero"))

Another way to perform the same computation would be to use safe_try:

from koda.result import Result
from koda import safe_try


# not safe on its own!
def divide(dividend: int, divisor: int) -> float:
    return dividend / divisor

# safe if used with `safe_try`
divided_ok: Result[float, ZeroDivisionError] = safe_try(divide, 10, 2)  # Ok(5)
divided_err: Result[float, ZeroDivisionError] = safe_try(divide, 10, 0)  # Err(ZeroDivisionError("division by zero"))

More

There are many other functions and datatypes included. Some examples:

compose

Combine functions by sequencing.

from koda import compose
from typing import Callable

def int_to_str(val: int) -> str:
    return str(val)

def prepend_str_abc(val: str) -> str:
    return f"abc{val}"    

combined_func: Callable[[int], str] = compose(int_to_str, prepend_str_abc)
assert combined_func(10) == "abc10"

mapping_get

Try to get a value from a Mapping object, and return an unambiguous result.

from koda import mapping_get
from koda.maybe import Just, Maybe, nothing

example_dict: dict[str, Maybe[int]] = {"a": Just(1), "b": nothing}

assert mapping_get(example_dict, "a") == Just(Just(1))
assert mapping_get(example_dict, "b") == Just(nothing)
assert mapping_get(example_dict, "c") == nothing

As a comparison, note that dict.get can return ambiguous results:

from typing import Optional

example_dict: dict[str, Optional[int]] = {"a": 1, "b": None}

assert example_dict.get("b") is None
assert example_dict.get("c") is None

We can't tell from the resulting value whether the None was the value for a key, or whether the key was not present in the dict

load_once

Create a lazy function, which will only call the passed-in function the first time it is called. After it is called, the value is cached. The cached value is returned on each successive call.

from random import random
from koda import load_once

call_random_once = load_once(random)  # has not called random yet

retrieved_val: float = call_random_once()
assert retrieved_val == call_random_once()

maybe_to_result

Convert a Maybe to a Result type.

from koda import maybe_to_result
from koda.maybe import Just, nothing
from koda.result import Ok, Err

assert maybe_to_result("value if nothing", nothing) == Err("value if nothing")
assert maybe_to_result("value if nothing", Just(5)) == Ok(5)

result_to_maybe

Convert a Result to a Maybe type.

from koda import result_to_maybe
from koda.maybe import Just, nothing
from koda.result import Ok, Err

assert result_to_maybe(Ok(5)) == Just(5)
assert result_to_maybe(Err("any error")) == nothing 

Intent

Koda is intended to focus on a small set of practical data types and utility functions for Python. It will not grow to encompass every possible functional or typesafe concept. Similarly, the intent of this library is to avoid requiring extra plugins (beyond a type-checker like mypy or pyright) or specific typchecker settings. As such, it is unlikely that things like Higher Kinded Types emulation or extended type inference will be implemented in this library.

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

koda-0.9.0.tar.gz (9.0 kB view details)

Uploaded Source

Built Distribution

koda-0.9.0-py3-none-any.whl (7.9 kB view details)

Uploaded Python 3

File details

Details for the file koda-0.9.0.tar.gz.

File metadata

  • Download URL: koda-0.9.0.tar.gz
  • Upload date:
  • Size: 9.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.1.12 CPython/3.10.0 Darwin/21.1.0

File hashes

Hashes for koda-0.9.0.tar.gz
Algorithm Hash digest
SHA256 27ae4005a1a49696206601cf304486e0cb1a0c084b18b9ba51b29173a27c4e6a
MD5 b0b895a1b5be456cc1a9a9932574bc4f
BLAKE2b-256 0ddef5937e6fc02416eab0fc6fafefee1b1f2ecf583e1791112e42430f61421d

See more details on using hashes here.

File details

Details for the file koda-0.9.0-py3-none-any.whl.

File metadata

  • Download URL: koda-0.9.0-py3-none-any.whl
  • Upload date:
  • Size: 7.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.1.12 CPython/3.10.0 Darwin/21.1.0

File hashes

Hashes for koda-0.9.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a8e56aee98ab4b935731c84b52acedeccb99f7506abb8bd83d6244e297b951d9
MD5 ffd75fbc7b419a2a9242fe1d87b90bb0
BLAKE2b-256 de5b4f4fe36c23d83164b4c2fd4b22d73e878d9fe468161cff59feb33181e4aa

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page