[UPDATE] safe-result 4.0: Better memory usage, chain operations, 100% test coverage
Posted by a_deneb@reddit | Python | View on Reddit | 39 comments
Hi Peeps,
The previous version introduced pattern matching and type guards.
Target Audience
Anybody.
Comparison
This new version takes everything one step further by reducing the Result
class to a simple union type and employing __slots__
for reduced memory usage.
The automatic traceback capture has also been decoupled from Err
and now works as a separate utility function.
Methods for transforming and chaining results were also added: map
, map_async
, and_then
, and_then_async
, and flatten
.
I only ported from Rust's Result what I thought would make sense in the context of Python. Also, one of the main goals of this library has always been to be as lightweight as possible, while still providing all the necessary features to work safely and elegantly with errors.
As always, you can check the examples on the project's page.
Thank you again for your support and continuous feedback.
whoEvenAreYouAnyway@reddit
You've changed major version numbers 4 times in just 2 weeks. How do you expect anybody to use this if you break compatibility every few days?
a_deneb@reddit (OP)
This is going to be the last major version.
whoEvenAreYouAnyway@reddit
I doubt it. But why even start shipping major version numbers when you're breaking compatibility daily?
a_deneb@reddit (OP)
To me, it sounds like you're feeling entitled to other people's work. If you're so concerned about version management in this project, take a deep breath, and remember that nobody is forcing you to use this library. Additionally, major versions can be pinned in the
pyproject.toml
file.whoEvenAreYouAnyway@reddit
I think you’re misunderstanding my question. I’m asking something purely practical. Who is going to use version 1.0.0, 2.0.0 or 3.0.0 given that you released all of those versions days apart and are already on 4.0.0? Why even start major versioning when your interface is this unstable?
a_deneb@reddit (OP)
The interface wasn't unstable. Version 1 was something I used for some time in my personal and work related projects. It just happened that after I released the first version, I got so much feedback from the community that I had to iterate really fast. Also, with the exception of the initial release (which I don't recommend anymore), all the other major versions are stable. Every single cycle tests have been written and passed. So yes, if you see version 2.0, 3.0 and 4.0 it means they are all stable and tested. The progression and development just happened to be really fast (it's not a huge library).
whoEvenAreYouAnyway@reddit
I’m talking about unstable meaning in rapid development. Not in the sense of using it would result in failures.
If I installed version 1 it would be superseded by your new interface in a matter of a few days. If I switched to using your new preferred interface it would have been deprecated in another few days. How can you expect anybody to take this project seriously when you didn’t even start 0.x.x versioning when the project was clearly not in any kind of stable state?
drooltheghost@reddit
If i understand correctly this thing makes a possible caller of let's say a module function aware that the function may raise a well defined exception, right? But I do not understand what it really improves. As a software developer you simply cannot just use some foreign code without reading and understanding it. So you should anyhow know that thing will raise. And then you caller code uses a explicit try except block. Instead of pattern match which adds runtime and complexity and therefore may add additional problems. And I do not see any good reason to add this complexity of your lib and of additional code in my call.
No_Flounder_1155@reddit
checked exceptions, they just aren't enforced, for now.
xspirus@reddit
u/a_deneb First of all, really nice helper! I like everything the library provides, but I'd like to make a suggestion since we can leverage python a bit more here. Why not merge `safe` and `safe_with` as one decorator. Then you can use `typing.overload` to annotate the case where the arguments are exceptions and the cases where the argument is a single function. Something like:
a_deneb@reddit (OP)
I value your suggestion and I think it's on point. The decision to have separate decorators (similar to having synchronous and asynchronous versions) stems from a commitment to the single responsibility principle - I wanted to keep each decorator focused and prevent feature bloat. Since these decorators will likely be used frequently, I wanted each call to be as lightweight as possible.
flavius-as@reddit
What's the difference at runtime between the two implementation-wise, that they warrant making the client code more noisy?
xspirus@reddit
I understand, it's a valid reason! Keep up the good work!
JanEric1@reddit
Super cool project.
I think some open points that you could do would be to setup a CI so that everyone can see that you run tests, they pass and their coverage.
Then you could also try to set up some type checker testing to verify that type checkers actually always handle this as you expect. Would probably make sense to test against mypy and pyright. I think pandas-stubs does something to that effect.
a_deneb@reddit (OP)
Done!
raptored01@reddit
Good stuff. Good stuff.
the-scream-i-scrumpt@reddit
what's the point of this? If I wanted to indicate my function ran into an error, I'd change the return type to
T | MyExceptionType
; there's no need for a special result type because python supports inline unionsIf I'm raising an error, it means I want to blow up the call stack.
a_deneb@reddit (OP)
The
Result
type isn't just about allowing a function to return an error; it's about providing a structured way to handle expected failures explicitly and enabling cleaner, more functional composition of operations that might fail. It makes the success/failure outcome a first-class part of the return value, encouraging the caller to deal with it directly (and most importantly, explicitly). It's a complementary approach, not necessarily a replacement. It's up to you to use whatever approach you think is best.the-scream-i-scrumpt@reddit
Can I get you to critique *my* new library that makes handling expected failures explicit and enables cleaner, more functional composition of operations that might fail?
Switching is simple, just `pip uninstall safe_result` and delete it from your code...
```py3
def divide(a: int, b: int) -> float | ZeroDivisionError:
if b == 0:
return ZeroDivisionError("Cannot divide by zero") # no need to wrap with Err(...)
return a / b # no need to wrap with Ok(...)
# Function signature clearly communicates potential failure modes (still!)
foo = divide(10, 0) # -> float | ZeroDivisionError
# Type checking will prevent unsafe access to the value (still!)
bar = 1 + foo
# \^\^\^ Type checker indicates the error:
# "Operator '+' not supported for types 'Literal[1]' and 'float | ZeroDivisionError'"
# Safe access pattern using the type guard function
if isinstance(foo, float): # Verifies foo is an Ok result and enables type narrowing (but no import!)
bar = 1 + foo # Safe! Type checker knows the value is a float here (still! no need for `.value`)
else:
# Handle error case with full type information about the error
print(f"Error: {foo}") # no need for `.error`
# Pattern matching is also a great way to handle results (literally the same)
match foo:
case float() as value:
print(f"Success: {value}")
case ZeroDivisionError() as e:
print(f"Division Error: {e}")
```
a_deneb@reddit (OP)
Frankly, it looks like you missed the whole concept - it appears you're battling a straw man armed with nothing but a union type and misplaced confidence.
the-scream-i-scrumpt@reddit
my union type does everything your library does, but with less boilerplate. Plus it plays perfectly well with all other python code. If someone introduced a library to my codebase that implements a very specific union type, with zero added benefit, I'd be pissed. This is the equivalent of an
is_even
libraryCzyDePL@reddit
Does type hinting work like this with exceptions? Or by MyExceptionType you don't mean an exception raised, but an error type that's returned as a value? In the latter case, what is client supposed to do, check type of the returned value and then handle appropriately? That gets out of hand when you get multiple different possible error types to handle
the-scream-i-scrumpt@reddit
correct
isinstance(result, Exception) should catch all exception subclasses
ChilledRoland@reddit
Railway oriented programming
the-scream-i-scrumpt@reddit
sounds like an overcomplicated way of writing a try/except in a language that doesn't support try/except
^ this railway doesn't need to worry about error types as inputs... the additional control flow makes it work just fine
guyfrom7up@reddit
looks nice! What might be helpful is an example in the README that does an apples-to-apples comparison to vanilla try/except and demonstrate situations where this may result in cleaner code? Currently it just seems like an alternative way of implementing something with similar lines of code and similar readability. If it doesn't bring substantial value (lines of code, or readability), then someone is unlikely to use it (an additional dependency, less pythonic code, etc).
No_Indication_1238@reddit
Just a heads up, ignore if you wish, but explaining briefly what your project does for people that aren't aware would help you get more leads. For example, I would read an intro since I read the post, but I wouldn't and didn't click any links you posted because the update information and the title didn't tell me what your project was about so it never peaked my curiosity.
cymrow@reddit
*piqued
No_Indication_1238@reddit
I have no idea what you are talking about.
hanleybrand@reddit
As a generalization, I believe they were saying that, when introducing new libraries with short descriptions that use domain specific lingo the developer can often forget that many python developers might not know what the library is supposed to do.
there’s lots of examples of this happening in this sub, but ML libraries often are a good example, where for people not doing ML the posted description of the library sounds nonsensical.
In this case where a useful feature of another language is being implemented in python, I think offby meant that it would be helpful if the description explained in a bit more detail why the library’s solution is preferable to try-catch
ExdigguserPies@reddit
Yeah I'm in this camp. I can see what it does, it looks neat, but I don't know why this is useful or what the advantages are over just using try-except.
Finndersen@reddit
Nice one that's quite elegant!
Effection@reddit
As someone that also misses Swift/Rust style enums this looks promising. Would be awesome to see flat_map also included on Result.
offby2@reddit
Do you have plans to publish this to pypi? While I could use this directly from git, I generally try to avoid using GitHub as a package repo.
a_deneb@reddit (OP)
Done! https://pypi.org/project/safe-result/
offby2@reddit
You rock!
ZachVorhies@reddit
As someone that does highly concurrent pipelines in python, this is the way. You do not want to mix exception throwing and thread pool manager / futures.
This is the way!
ArabicLawrence@reddit
I am one of the people who wrote ‘I see little value over try except’, but you have come a long way and I like the solution
chub79@reddit
While I'm not sure I'd use this for now in my Python projects, I think your project is an excellent test bed to help a discussion (a long journey) whether this could make sense natively in the language.
Also kudos for a fast response time to the comments you get from the community.