Skip to main content

TORADH

codecov pypi version license PyPI Downloads

Minimalistic library intended to bring better flow control to Python.

Motivation:

Have you ever install a new sdk or lib that you really wanted to use for a particular project only to find out, mid development that if you try to .get() something that is not present in the instance it will raise an ValueError which was not mentioned in any docstring. Does it sounds familiar? well, this kind of frustration is what toradh (pronounced "taru") comes to ease. By bringing some of the key fundamental structures of languages such as Rust to Python, we aim to make exception handling a little less chaotic and more predictable when needed.

Install:

pip install toradh

Usage:

We support structural pattern matching with the match operator as well as build in methods for control flow management.

from typing import Literal
from toradh import Result, Ok, Err

DB = {
    1: 'john', 
    2: 'jane',
}

def create_user(name: str) -> Result[int, ValueError | KeyError]:
    if name in DB:
        return Err(KeyError(f'A user by {name} already exists'))
    if len(name) > 10:
        return Err(ValueError('names can not be too long'))
    user_id = len(DB)+1
    DB[user_id] = name
    return Ok(user_id)

def basic_handling():
    # In this example, we don't want to interrupt the execution
    # but we don't really want to handle specific errors
    res = create_user('janet')
    match res:
        case Ok(user_id):
            print(f'successfully persisted under {user_id}')
        case Err(err):
            print(f'There was an error => {err}')

def concrete_error_handling():
    # In this case, we are handling each possible scenario and 
    # taking some sort of action based on the type of error
    res = create_user('janet')
    
    # If all cases aren't handle mypy will alert about this.
    match res.kind():
        case int():
            print(f'successfully persisted under {res.unwrap()}')
        case ValueError():
            print(f'There was a problem with the name\'s length')
        case KeyError():
            print(f'Name already exists')
            #include possible measure to recover from this error
            # ...

def no_handling():
    # in this case, we do not want to handle the possible errors
    # if any are give, the .unwrap() call simply raise them as normal python code
    res = create_user('janet')
    print(f'successfully persisted under {res.unwrap()}')

if __name__ == '__main__':
    basic_handling()

    concrete_error_handling()
    
    no_handling()

why use Toradh?

First let's go over some simple examples:

Let's go over an example not using the framework

DB = {
    1: 'john', 
    2: 'jane',
}

# instead of this
def get_user_by_id_old(user_id: int) -> str | None:
    return DB.get(user_id)    

def main():
    user = get_user_by_id_old(1)
    
    if user is not None:
        print(f'Hello {user}')
    else:
        print('id not found')

and how it would look like if using it

from toradh import Optional, Option, Nothing, Some

DB = {
    1: 'john', 
    2: 'jane',
}

def get_user_by_id_new(user_id: int) -> Optional[str]:
    if user_id not in DB:
        return Nothing()
    
    return Some(DB.get(user_id))
    
def main():
    user = get_user_by_id_new(1)
    
    if user.is_some():
        print(f'Hello {user.unwrap()}')
    else:
        print('id not found')
        

Now, at this point it really doesn't add too much. But if you allow the following state to exist in your DB.

D  = {
    1: 'john',
    2: 'jane',
    3: None
}

Then things, get tricky for the first implementation. How do you distinguish between the DB value None and the state of element not found?

A possible solution would be:

DB = {
    1: 'john', 
    2: 'jane',
}

def get_user_by_id_old(user_id: int) -> str | None:
    return DB[user_id] # this will raise a KeyError if user_id is not part of DB

def main():
    try:
        user = get_user_by_id_old(1)
    except KeyError:
        print('user not found') 
        return # cut control flow here
    
    if user is not None:
        print(f'Hello {user}')
    else:
        print(f'User gone')

Which is not ideal as the KeyError exception is not visible throw the type hint system, which puts the pleasure of correctly handling this behavior on the invoker.

As opposed to this implementation:

from toradh import Optional, Option, Nothing, Some

DB = {
    1: 'john', 
    2: 'jane',
    3: None
}

def get_user_by_id_new(user_id: int) -> Optional[str | None]:
    if user_id not in DB:
        return Nothing()
    
    return Some(DB.get(user_id))
    
def main():
    user = get_user_by_id_new(1)
    
    match user:
        case Nothing():
            print('id not found')    
        case Some(None):
            print('User is gone')
        case Some(name):
            print(f'Hello {name}')
        

In this example (although not really a good use of None) we can see that there is a clear distinction between the absence of what we want and an actual product of calling the function.

Pydantic v2 compatibility

You can use Option, Some, Nothing, Ok, and Err as field types in Pydantic v2 models. Serialization works as follows:

  • Nothing() serializes to None
  • Some(value) serializes to the inner value
  • Ok(value) serializes to the inner value
  • Err(error) serializes to None (no payload)

Example:

from pydantic import BaseModel
from toradh import Option, Some, Nothing, Ok, Err, Result

class Inner(BaseModel):
    x: int

class MyModel(BaseModel):
    opt: Option[int]
    ok: Ok[Inner]
    res: Result[int, Exception]

m = MyModel(opt=Some(1), ok=Ok(Inner(x=5)), res=Ok(10))
assert m.model_dump() == {"opt": 1, "ok": {"x": 5}, "res": 10}

# Accepts bare values and wraps them appropriately
m2 = MyModel(opt=None, ok={"x": 7}, res=5)
assert isinstance(m2.opt, Nothing)
assert m2.model_dump() == {"opt": None, "ok": {"x": 7}, "res": 5}

# Err serializes to None
m3 = MyModel(opt=Nothing(), ok=Ok(Inner(x=1)), res=Err(RuntimeError("boom")))
assert m3.model_dump() == {"opt": None, "ok": {"x": 1}, "res": None}

See examples/pydantic_compat.py for a runnable demonstration.

Metadata

Release files for toradh 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for toradh 0.3.0
File Size Uploaded Provenance Actions
toradh-0.3.0.tar.gz 10.5 kB Without provenance Details for toradh-0.3.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for toradh 0.3.0
File Interpreter ABI Platform Actions
toradh-0.3.0-py3-none-any.whl Python 3 none any Details for toradh-0.3.0-py3-none-any.whl

Total release size: 19.9 kB

Release files / toradh-0.3.0.tar.gz

Download URL toradh-0.3.0.tar.gz
Size 10.5 kB
Tags Source
SHA-256 checksum
How to use checksums
23e86f94f5c807d1a0a3c00e18fe33a190ff2f76d5ad69aabd376ba71f5f8e6e
BLAKE2b-256 checksum
How to use checksums
40c4872abe8242dc801e4a7f03fbfe7da3acfefc825c1cb544b9d0e65a2115ae
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/2.1.4 CPython/3.12.11 Linux/6.11.0-1018-azure

Release files / toradh-0.3.0-py3-none-any.whl

Download URL toradh-0.3.0-py3-none-any.whl
Size 9.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
67af792d57c4bc7bcafaa584afa2b1041524b768763478db1eeea1416541cbb2
BLAKE2b-256 checksum
How to use checksums
e8dc03fd1a4e471e8e53b015032af14a670db9d78cc42e3fa7fbaad808ac214d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/2.1.4 CPython/3.12.11 Linux/6.11.0-1018-azure

Security for toradh 0.3.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page