Skip to main content

An alternative to try/catch/else statements

Reason this release was yanked:

Doesn't have requirements.txt, which may have caused problems

Project description

GUARDS: Handle Exceptions Like Never Before

# Before
def main():
    key_str: str = input("Insert key -> ")
    try:
        key: int = int(key_str)
    except ValueError:
        print("Key is not an integer")
        return
    try:
        value = my_dict[key]
    except KeyError:
        value = "not found"
    print(f"Value at key {key} is {value}")

# After
from guards import *
from operator import getitem
def main():
    safe_int = guard(int, ValueError)
    key_maybe: Outcome[int, ValueError] = safe_int(input("Insert key -> "))
    if iserror(key_maybe):
        print("Key is not an integer")
        return
    key: int = key_maybe.ok
    value = guard(getitem, KeyError)(my_dict, key)
    print(f"Value at key {key} is {value.ok if isok(value) else 'not found'}")

# More compact
from guards import *
from operator import getitem
def main():
    safe_int = guard(int, ValueError)
    key_maybe = safe_int(input("Insert key -> "))
    if let_not_ok(key := key_maybe.let):
        print("Key is not an integer")
        return
    value = guard(getitem, KeyError)(my_dict, key).or_else("not found")
    print(f"Value at key {key} is {value}")

guards is a Python library implementing an alternative to the classic try/except/else statements. You still throw errors with raise, but can catch them in a sweeter and more functional approach. Requires Python 3.10+.

Guards let you handle errors in expressions, not just statements.

safe_int = guard(int, ValueError)
# Impossible with try/except (needs indentation and blocks)
result = [safe_int(x).or_else(0) for x in user_inputs]

This means:

  • Chain operations without nested try blocks
from operator import getitem
l = [6, 2, 5]
safe_get = guard(getitem, IndexError)
text = outcome_do(
    x1 + x2 + x3
    for x1 in safe_get(l, 0)
    for x2 in safe_get(l, 1)
    for x3 in safe_get(l, 2)
).or_else(0)
  • Pass error-handling logic as values
def assert_raises(func, exception, *args, **kwargs):
    match guard(func, exception)(*args, **kwargs):
        Ok(value): raise AssertionError(f"Expected a raised {exception}, but got value {value}")
        Error(exc): return
  • Type-check your error handling
def f(x: str) -> int | None:
    outcome = guard("Hello World".index, ValueError)(x)
    if isok(outcome):
        reveal_type(outcome) # Ok[int]
        return outcome.ok
    reveal_type(outcome) # Error[ValueError]
    #return outcome.ok # Would raise an issue by the type checker
  • And more!
my_list = ["4.2", "2.7", "pizza"]
numbers, errors = outcome_partition(guard(float, ValueError)(x) for x in my_list)

Installation

guards is not yet available to PyPi. If you want to try guards you can download the guards.py file as a standalone.

Summary

The guard() function blocks another function from raising a set of errors. It takes a function f to guard and one or more BaseException types to guard against, and returns a new function.

open_safe = guard(open, FileNotFoundError, PermissionError)
age_outcome = guard(int, ValueError)(input("Insert age -> "))

The returned function calls the original function f with the same arguments passed to it. The difference comes after the function was called:

  • If f returned a value, return an Ok(value) object.
  • If f raised an exception exc it is guarded against, return an Error(exc) object.
  • If f raised an exception it is not guarded against, the exception is propagated.
safe_float = guard(float, ValueError)
print(safe_float("25"))
print(safe_float("ten"))
print(safe_float([4, 2]))

Outputs:

Ok(25.0)
Error(ValueError("could not convert string to float: 'ten'"))
Traceback (most recent call last):
  File ".../script.py", line 5, in <module>
    print(safe_float([4, 2]))
          ^^^^^^^^^^^^^^^^^^
  File ".../guards.py", line 368, in inner_func
    ok = f(*args, **kwargs)
         ^^^^^^^^^^^^^^^^^^
TypeError: float() argument must be a string or a real number, not 'list'

The union of the return types of a guarded function Ok | Error is called Outcome, which is similar to "result" types in other programming languages.

The simplest way to handle exceptions is to use isok() and iserror() functions to check the type of an outcome, then access their internal values with .ok and .error properties.

file_outcome = guard(open, FileNotFoundError, PermissionError)(PATH)
if isok(file_outcome):
    with file_outcome.ok as file:
        print("File contents:")
        print(file.read())
else: # elif iserror(file_outcome):
    os_error = file_outcome.error
    if isinstance(os_error, PermissionError):
        print("Cannot read the file.")
    else: # elif isinstance(os_error, FileNotFoundError):
        print("The file doesn't exist!")

This is just the basic of error-handling. See the documentation or examples.md for more.

Try/Except VS Guards

Try/Except Guards
❌Needs blocks and indentation ✅Can be used inside expressions
❌Strictly procedural ✅Multi-paradigm
❌Encourages coarse-grained error handling ✅Encourages fine-grained error handling (coarse-grained is still possible)
❌Terrible when used often ✅Multiple uses are not a problem
❌Hard to pass around ✅Returned outcome can be passed around
❌Bare except antipattern ✅Specifying no types to guard against is a runtime warning
❌Risk of unbound variables ✅No need to worry about variables
❌Try/Except is all you get ✅Methods and functions for the most common use-cases

Exceptions and Guards VS Pure Result Values

Exceptions and Guards Pure Result Values
Handling errors is the default Propagating errors is the default
✅Raised exceptions are all you need ❌Needs panics to work well
✅Only the exceptions you care about are handled ❌Needs unwrapping the error even if you only care about a specific one
✅Can be learned whenever you have to handle errors ❌Must be learned when writing or using a fallible function
✅Great compatibility with Python ❌Hard to integrate in a language like Python
✅Exceptions automatically create stack traces ❌You often need libraries for backtraces
❌Raised errors cannot be typed (out of this module's scope) ✅Functions' errors are typed
❌Rarely clear whether a function can error ✅Often clear whether a function can error
❌Needs understanding of functions as values ✅Easier to understand
❌Uses a different control flow ✅Control flow is clearer
❌Error handling is easy to ignore ✅Errors must always be dealt with

Contributing

todo.md contains a short list of features to implement, which includes publishing the library to PyPi at the end.

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

guards-1.0.1.tar.gz (21.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

guards-1.0.1-py3-none-any.whl (15.2 kB view details)

Uploaded Python 3

File details

Details for the file guards-1.0.1.tar.gz.

File metadata

  • Download URL: guards-1.0.1.tar.gz
  • Upload date:
  • Size: 21.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.0

File hashes

Hashes for guards-1.0.1.tar.gz
Algorithm Hash digest
SHA256 7f0de46c4504f9c26d8065b1a1c8b307f2a70f4291a2d44bff9c68e0c35e3f30
MD5 7a981d20dc93a666239ad600b901cc32
BLAKE2b-256 176d3cd2626b249d5663cc69aeabecf068ddfe75a0b43088377ed71bdc268f22

See more details on using hashes here.

File details

Details for the file guards-1.0.1-py3-none-any.whl.

File metadata

  • Download URL: guards-1.0.1-py3-none-any.whl
  • Upload date:
  • Size: 15.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.0

File hashes

Hashes for guards-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 a8b64d53c1a6bce0c262582aba5a02374d3bd723bd787a263c115a0e93be09a5
MD5 341970aca4d36a0fded238ba561a9fb1
BLAKE2b-256 35ca0866b85d6453c4c19f2ac369071faf7b7c3a995e29af717660f4646122af

See more details on using hashes here.

Supported by

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