Python Forum
returning an error code - Printable Version

+- Python Forum (https://python-forum.io)
+-- Forum: General (https://python-forum.io/forum-1.html)
+--- Forum: News and Discussions (https://python-forum.io/forum-31.html)
+--- Thread: returning an error code (/thread-32419.html)



returning an error code - Skaperen - Feb-08-2021

my function that works on the file system should inform the caller what error it encounters and where. the where would just be the path in a str. i am pondering the best way to tell the caller what the error is. the obvious is to let the error raise an exception. but not all errors would normally do so such as discovering something not being the way that is wanted, such as a file having 600 bytes when wanting one with 512 bytes. the function and why it is called has a different kind of abstraction. i want the caller to be able to figure this out quickly. is it considered OK to return an int code number or a word in a str that can be tested by the caller?

i'm also thinking that i should just make up my own exceptions but i have never done that and don't know how involved that is.


RE: returning an error code - Gribouillis - Feb-09-2021

Nothing is simpler than writing your own exception
class MyException(RuntimeError):
    pass
class MySubException(MyException):
    pass

raise MySubException('That file is too large!')
The drawback of returning an error code is that it forces the caller to check the error code. On the other hand, if a function raises exceptions, you can just call the function and let the exceptions propagate.


RE: returning an error code - ndc85430 - Feb-28-2021

I don't think returning an error code is necessarily a bad thing; it's up to the programmer to decide which error handling strategy they want to employ. I had to review some Ruby code the other day and they'd returned a string in the error case, rather than throwing an exception. I think it's fine for quite simple cases and AFAIK Go does error handling that way too. Of course, if you need to do a lot of computations in a sequence, any of which could fail, then this simple strategy doesn't scale because the code becomes cluttered with all the if/else checks you have to do.

An alternative to exceptions is to use a result type (or "result monad") that represents a successful computation, or a failure. These can be chained together, resulting in code that's quite flat and readable and it's up to you when you actually do something with the failure. This is the functional approach, since in FP, you don't use exceptions as functions are pure and just return values. In Rust, it's called a Result, Scala has two types :Try and Either (my codebase at work actually uses both for whatever reason) and Kotlin has different library implementations (one of which is Nat Pryce's Result4k, from which I nicked the idea of the result_from function you see below).

Here are some test cases showing the idea, with an implementation following (note the code isn't meant for production; it's literally just for demo purposes. There are likely more fully featured implementations out there).

import unittest
 
from result import *
 
class TestResult(unittest.TestCase):
    def test_a_computation_that_completes_is_a_success(self):
        def a_computation_that_runs_successfully():
            x = 5
            return x * 10
 
        self.assertEqual(
            result_from(a_computation_that_runs_successfully),
            Success(50)
        )

    def test_a_computation_that_throws_an_exception_is_a_failure(self):
        def a_computation_that_fails():
            return int("foobar")
 
        self.assertEqual(
            result_from(a_computation_that_fails),
            Failure("invalid literal for int() with base 10: 'foobar'")
        )

    def test_composing_computations_runs_them_in_succession(self):
        self.assertEqual(
            Success(4)
                .flatmap(lambda x: Success(x + 1))
                .flatmap(lambda x: Success(x * 2)),
            Success(10)
        )

    def test_no_computation_is_carried_out_after_a_failure(self):
        self.assertEqual(
            Success(4)
                .flatmap(lambda x: Success(x + 1))
                .flatmap(lambda x: Failure("something bad happened"))
                .flatmap(lambda x: Success(x * 2)),
            Failure("something bad happened")
        )
        
    def test_a_success_can_be_transformed_into_another(self):
        self.assertEqual(
            Success(4).map(lambda x: x + 1),
            Success(5)
        )
 
    def test_transforming_a_failure_leaves_it_unchanged(self):
        self.assertEqual(
            Failure("something bad happened").map(lambda x: x + 1),
            Failure("something bad happened")
        )
   
if __name__ == '__main__':
    unittest.main(verbosity=2)
class Result(object):
    pass

class Success(Result):
    def __init__(self, value):
        self._value = value

    @property
    def value(self):
        return self._value

    def map(self, f):
        return Success(f(self.value))

    def flatmap(self, f):
        return f(self.value)

    def __eq__(self, other):
        return self.value == other.value

class Failure(Result):
    def __init__(self, value):
        self._value = value

    @property
    def value(self):
        return self._value

    def map(self, f):
        return Failure(self.value)

    def flatmap(self, f):
        return Failure(self.value)
    
    def __eq__(self, other):
        return self.value == other.value

def result_from(f):
    try:
        return Success(f())
    except Exception as e:
        return Failure(str(e))
I've kept the name of the method that lets you chain computations as flatmap as that's what it's called in the functional programming literature, since it really is analogous to map, but gets rid of any nesting (if you were to map, you'd end up with a Result inside another of course). Having said that, I did see a blog post where they'd called it andThen in a particular Kotlin implementation, because that made it more readable.

A Result is just a value, so at the point you need to know whether the computation succeeded or failed, you just check what kind of result you have:

result = some_computation_that_either_succeeds_or_fails()

if isinstance(result, Success):
    print(f"It succeeded and the result was: {result.value}")
else:
    print(f"It failed and the error was: {result.value}")
Other languages have pattern matching or otherwise more powerful versions of switch that make this look nicer, but the idea is the same.


RE: returning an error code - Skaperen - Mar-01-2021

(Feb-09-2021, 07:11 AM)Gribouillis Wrote: The drawback of returning an error code is that it forces the caller to check the error code

or forces the caller to explicitly pass the error code back to its caller. BTDT way too often in C. many reasons to prefer exceptions.

(Feb-09-2021, 07:11 AM)Gribouillis Wrote: Nothing is simpler than writing your own exception
now you're having me wonder what i can put in place of pass in your example.


RE: returning an error code - Gribouillis - Mar-01-2021

Skaperen Wrote:now you're having me wonder what i can put in place of pass in your example.
For example
>>> class Exc(RuntimeError):
...     def __init__(self, *args, **kwargs):
...         super().__init__(*args)
...         self.__dict__.update(kwargs)
... 
>>> e = Exc('foo happened', length=25)
>>> 
>>> e
Exc('foo happened',)
>>> e.length
25



RE: returning an error code - Skaperen - Mar-02-2021

that looks like odd behavior. what if something just raises it as an exception?


RE: returning an error code - Gribouillis - Mar-02-2021

It is only an example. The idea is that exception classes can be customized with arbitrarily complex behavior like any other class. The code that creates these instances is fully responsible for using them appropriately. The most obvious use case of this is to decorate the exception with additional data found in the context where the exception is raised.


RE: returning an error code - Skaperen - Mar-02-2021

i think i need to go back and re-read about exceptions. my original takeaway was too simple.


RE: returning an error code - ndc85430 - Mar-03-2021

I've refactored the tests for Result to tell a better story. What I think one cares about first is how you produce them in the first place, then how you compose them and only lastly that you can transform the value inside a Success. Because of my bias of having worked with these and thus knowing what the common operations are, I actually ended up implementing map first, hence the original order.