Package: koda
KodaKoda is a collection of practical type-safe tools for Python.At its core are a number of datatypes that are common in functional programming.MaybeMaybeis similar to Python'sOptionaltype. It has two variants:NothingandJust, and they work in similar ways
to what you may have seen in other languages.fromkodaimportMaybe,Just,nothinga:Maybe[int]=Just(5)b:Maybe[int]=nothingTo know if aMaybeis aJustor aNothing, you'll need to inspect it.fromkodaimportJust,Maybemaybe_str:Maybe[str]=function_returning_maybe_str()# python 3.10 +matchmaybe_str:caseJust(val):print(val)caseNothing:print("No value!")# python 3.9 and earlierifisinstance(maybe_str,Just):print(maybe_str.val)else:print("No value!")Maybehas methods for conveniently stringing logic together.Maybe.mapfromkodaimportJust,nothingdefadd_10(x:int)->int:returnx+10Just(5).map(add_10)# Just(15)nothing.map(add_10)# nothingJust(5).map(add_10).map(lambdax:f"abc{x}")# Just("abc15")Maybe.flat_mapfromkodaimportMaybe,Just,nothingdefsafe_divide(dividend:int,divisor:int)->Maybe[float]:ifdivisor!=0:returnJust(dividend/divisor)else:returnnothingJust(5).flat_map(lambdax:safe_divide(10,x))# Just(2)Just(0).flat_map(lambdax:safe_divide(10,x))# nothingnothing.flat_map(lambdax:safe_divide(10,x))# nothingResultResultprovides a means of representing whether a computation succeeded or failed. To represent success, we can useOK;
for failures we can useErr. Compared toMaybe,Resultis perhaps most useful in that the "failure" case also returns data,
whereasNothingcontains no data.fromkodaimportOk,Err,Resultdefsafe_divide_result(dividend:int,divisor:int)->Result[float,str]:ifdivisor!=0:returnOk(dividend/divisor)else:returnErr("cannot divide by zero!")Ok(5).flat_map(lambdax:safe_divide_result(10,x))# Ok(2)Ok(0).flat_map(lambdax:safe_divide_result(10,x))# Err("cannot divide by zero!")Err("some other error").map(lambdax:safe_divide_result(10,x))# Err("some other error")Resultcan be convenient withtry/exceptlogic.fromkodaimportResult,Ok,Errdefdivide_by(dividend:int,divisor:int)->Result[float,ZeroDivisionError]:try:returnOk(dividend/divisor)exceptZeroDivisionErrorasexc:returnErr(exc)divided:Result[float,ZeroDivisionError]=divide_by(10,0)# Err(ZeroDivisionError("division by zero"))Another way to perform the same computation would be to usesafe_try:fromkodaimportResult,safe_try# not safe on its own!defdivide(dividend:int,divisor:int)->float:returndividend/divisor# safe if used with `safe_try`divided_ok:Result[float,Exception]=safe_try(divide,10,2)# Ok(5)divided_err:Result[float,Exception]=safe_try(divide,10,0)# Err(ZeroDivisionError("division by zero"))Conversion betweenResults,Maybes, andOptionalsResult and MaybeConvert aResultto aMaybetype.fromkodaimportJust,nothing,Ok,ErrassertOk(5).to_maybe==Just(5)assertErr("any error").to_maybe==nothingConvert aMaybeto aResulttype.fromkodaimportJust,nothing,Ok,Errassertnothing.to_result("value if nothing")==Err("value if nothing")assertJust(5).to_result("value if nothing")==Ok(5)MaybeandOptionalConvert anOptionalvalue to aMaybe.fromkodaimportto_maybe,Just,nothingassertto_maybe(5)==Just(5)assertto_maybe("abc")==Just("abc")assertto_maybe(False)==Just(False)assertto_maybe(None)==nothingConvert aMaybeto anOptional.fromkodaimportJust,nothingassertJust(5).to_optional==5assertnothing.to_optionalisNone# note that `Maybe[None]` will always return None,# so `Maybe.get_or_else` would be preferable in this caseassertJust(None)isNoneResultandOptionalConvert anOptionalvalue to aResult.fromkodaimportto_result,Ok,Errassertto_result(5,"fallback")==Ok(5)assertto_result("abc","fallback")==Ok("abc")assertto_result(False,"fallback")==Ok(False)assertto_result(None,"fallback")==Err("fallback")Convert aResultto anOptional.fromkodaimportOk,ErrassertOk(5).to_optional==5assertErr("some error").to_optionalisNone# note that `Result[None, Any]` will always return None,# so `Result.get_or_else` would be preferable in this caseassertOk(None)isNoneMoreThere are many other functions and datatypes included. Some examples:composeCombine functions by sequencing.fromkodaimportcomposefromtypingimportCallabledefint_to_str(val:int)->str:returnstr(val)defprepend_str_abc(val:str)->str:returnf"abc{val}"combined_func:Callable[[int],str]=compose(int_to_str,prepend_str_abc)assertcombined_func(10)=="abc10"mapping_getTry to get a value from aMappingobject, and return an unambiguous result.fromkodaimportmapping_get,Just,Maybe,nothingexample_dict:dict[str,Maybe[int]]={"a":Just(1),"b":nothing}assertmapping_get(example_dict,"a")==Just(Just(1))assertmapping_get(example_dict,"b")==Just(nothing)assertmapping_get(example_dict,"c")==nothingAs a comparison, note thatdict.getcan return ambiguous results:fromtypingimportOptionalexample_dict:dict[str,Optional[int]]={"a":1,"b":None}assertexample_dict.get("b")isNoneassertexample_dict.get("c")isNoneWe can't tell from the resulting value whether theNonewas the
value for a key, or whether the key was not present in thedictload_onceCreate 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.fromrandomimportrandomfromkodaimportload_oncecall_random_once=load_once(random)# has not called random yetretrieved_val:float=call_random_once()assertretrieved_val==call_random_once()IntentKoda 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.