either
The either package provides both functions and an Either class, that allows both method chaining and do notation style syntax.
Either[L, R]
Either[L, R] is roughly defined as:
class Left(Generic[L]):
  value: L
  tag: Literal['left']
class Right(Generic[R]):
  value: R
  tag: Literal['right']
Either[L, R] = Left[L] | Right[R]
 
Do Notation
Thought it supports both method chaining and either.tag == 'left' branching, the most common way to use an either is with a sort of do notation:
from haskellian import either as E, Either
def fetch_this() -> Either[KeyError, str]:
  ...
def fetch_that() -> Either[ValueError, str]:
  ...
@E.do[KeyError|ValueError]()
def do_function() -> str:
  this = fetch_this().unwrap() # str
  that = fetch_that().unwrap() # str
  return f'{this} and {that}'
do_function() # Either[KeyError|ValueError, str]
 
If you're familiar with Rust's ? operator, this should be familiar.
Functions
The either module has just a few utility functions:
    
  
            safe
    
            
              Bases: Generic[Err]
        A decorator to catch exceptions and return them as Left.
@E.safe[OSError]()
def safe_write(path: str, content: bytes):
  with open(path, 'wb') as f:
    f.write(content)
result = safe_write('file.txt', b'content') # Either[OSError, None]
 
              
                Source code in haskellian/src/haskellian/either/funcs.py
                 | class safe(Generic[Err]):
  """A decorator to catch exceptions and return them as `Left`.
  ```python
  @E.safe[OSError]()
  def safe_write(path: str, content: bytes):
    with open(path, 'wb') as f:
      f.write(content)
  result = safe_write('file.txt', b'content') # Either[OSError, None]
  ```
  """
  @property
  def exc(self) -> type[Err]:
    return get_args(self.__orig_class__)[0] # type: ignore (yep, typing internals are messed up)
  @overload
  def __call__(self, fn: Callable[P, Coroutine[None, None, R]]) -> Callable[P, Coroutine[None, None, Either[Err, R]]]: # type: ignore
    ...
  @overload
  def __call__(self, fn: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[Either[Err, R]]]: # type: ignore
    ...
  @overload
  def __call__(self, fn: Callable[P, R]) -> Callable[P, Either[Err, R]]:
    ...
  def __call__(self, fn): # type: ignore
    if iscoroutinefunction(fn):
      @wraps(fn)
      async def _wrapper(*args: P.args, **kwargs: P.kwargs):
        try:
          return Right(await fn(*args, **kwargs))
        except Exception as e:
          if isinstance(e, self.exc):
            return Left(e)
          raise e
      return _wrapper
    else:
      @wraps(fn)
      def wrapper(*args: P.args, **kwargs: P.kwargs):
        try:
          return Right(fn(*args, **kwargs))
        except Exception as e:
          if isinstance(e, self.exc):
            return Left(e)
          raise e
      return wrapper
  | 
 
               
  
  
     
 
            filter(eithers)
    
        
            
              Source code in haskellian/src/haskellian/either/funcs.py
               | def filter(eithers: Iterable[Either[L, R]]) -> Iterable[R]:
  """"""
  for e in eithers:
    match e:
      case Right(value):
        yield value
  | 
 
             
     
 
            filter_lefts(eithers)
    
        
            
              Source code in haskellian/src/haskellian/either/funcs.py
               | def filter_lefts(eithers: Iterable[Either[L, R]]) -> Iterable[L]:
  """"""
  for e in eithers:
    match e:
      case Left(err):
        yield err
  | 
 
             
     
 
            maybe(x)
    
        Converts a nullable value to Either
            
              Source code in haskellian/src/haskellian/either/funcs.py
               | def maybe(x: R | None) -> 'Either[None, R]':
  """Converts a nullable value to `Either`"""
  return Left(None) if x is None else Right(x)
  | 
 
             
     
 
            sequence(eithers)
    
        List of lefts if any, otherwise list of all rights.
E.sequence([Left(1), Right(2), Right(3), Left(4)]) # Left([1, 4])
E.sequence([Right(2), Right(3)]) # Right([2, 3])
 
            
              Source code in haskellian/src/haskellian/either/funcs.py
               | def sequence(eithers: Iterable[Either[L, R]]) -> Either[list[L], list[R]]:
  """List of lefts if any, otherwise list of all rights.
  ```python
  E.sequence([Left(1), Right(2), Right(3), Left(4)]) # Left([1, 4])
  E.sequence([Right(2), Right(3)]) # Right([2, 3])
  ```
  """
  lefts: list[L] = []
  rights: list[R] = []
  for x in eithers:
    x.match(lefts.append, rights.append)
  return Right(rights) if lefts == [] else Left(lefts)
  | 
 
             
     
 
            take_while(eithers)
    
        
            
              Source code in haskellian/src/haskellian/either/funcs.py
               | def take_while(eithers: Iterable[Either[L, R]]) -> Iterable[R]:
  """"""
  for e in eithers:
    match e:
      case Right(x):
        yield x
      case _:
        return
  | 
 
             
     
 
   
     
 Next up, asyn_iter