Introduce Result class to allow a modern representation of failure cases

I won’t be adopting it. This isn’t adding anything useful. Async was adopted in python because it improved the ability to reason about concurrency rather than have “callback hell”

Changing existing libraries to use a Result type is possible now with or without standard library use. Existing libraries that haven’t done that clearly have no reason to.

No it would not, but you don’t need standard library adoption if that’s all you want. This is something that already exists on pypi, go play with it outside the standard library and prove it warrants adoption there first.

I’ve written code like the following before instead of what you have as it works better for python and actually handles errors that are known to be handleable:

def chain_functions(val):
    val = f1(val)
    val = f2(val)
    try:
        val = f3(val)
    except InvalidData:
        val = some_default()
    return val

results = [chain_functions(val) for val in iterable]

You could catch and return exceptions (no wrapping type needed, just switch on isinstance(val, Exception) to re-raise in an exception group) and process everything processable, then handle the exceptional cases. I’ve left that out as I would only start chaining functions like this after I validated input data and I believed it could only error in specific ways or have signal handling exceptions that should not be handled in this.

This doesn’t continue to iterate when there’s an un-handleable error, it doesn’t require multiple iterations to apply a chain of functions, and it works properly if you want it lazily too.

people get too clever with some functional programming ideas and and write code that performs worse and loses many of python’s strongest benefits…

2 Likes

But as demonstrated, plenty of libraries have. Not implementing a result wrapper won’t uninvent these libraries. And the fact they opted to return a result type rather than throwing things such as HTTP 401 as an Exception, must have a reason behind it.

What it does is ensure each of these result types will have their own non-exception based way of representing an error even if it’s actually an error we probably should handle because in some cases will have as much of an impact on the validity of the data my app gets as situations we would normally handle with Exceptions. Meaning thats additional cognitive load for me to understand which errors I should be handling for every library I have and if I’m not that familiar with the library there could be error classes I missed.

If you love and agree with this approach then it works but if you would prefer to deal with it using exceptions you are now expecting developers to adopt glue code.

If these libraries suddenly decide to treat all errors as exceptions but you disagree with handling it at that moment because you need more context. Then you are also expecting those developer to add their own glue code.

This reduces the glue code needed by library end-users and lets them pick which approach suits them.

I luckily don’t have to a fairly popular implementation with some level of adoption - GitHub - rustedpy/result: A simple Rust like Result type for Python 3. Fully type annotated.

But the thing is JS had a similar thing, promises were rarely adopted until part of the language libs

This code only works if the API you are calling throws an exception, so your code can often look like this

def chain_functions(val):
    try:
       result = f1(val)
 
       if result.error_code == 500:
            raise Exception('Invalid data')
        else:
           val = result.value

        result = f2(val)
 
         if result.error_code == 500:
            raise Exception('Invalid data')
         else:
            val = result.value

        result = f3(val)
 
         if result.error_code == 500:
            raise Exception('Invalid data')
         else:
            val = result.value
    except InvalidData:
        val = some_default()
    return val

results = [chain_functions(val) for val in iterable]

And sure you can do all kinds of things to make this code more pleasant such as make a decorator for those functions, but this is glue code for the end developer.

In my ideal world we could do this


# Existing API lib
@result
def f1():
   raise ErrorResponse(error_code=500)

@result
def f2():
    raise InvalidData("Data sent was corrupted")

@result
def f3():
    yield Response(error_code=200)

def chain_functions(val):
    try:
       val = f1(val)value # Will raise
        val = f2(val).value # Will raise
        val = f3(val).value   # Won;'t raise

        # You can still handle the error later by passing around the result or handle it without try blocks by reading exception just like with futures (in this case since we aren't reading value it won't raise)
    except InvalidData, Response:
        val = some_default()

    return val

results = [chain_functions(val) for val in iterable]

Below is a typical Python code template (it can run). Note that during normal execution (the “happy path”), the try block does not incur any overhead, making the code efficient.

How would you handle all possible cases using the proposed Result class?

(Please do not skip this post; I am really curious about the benefits of the proposed feature. The best way to understand is by comparing it with the current Pythonic (I believe) approach.)

import logging

# Configure logging
logging.basicConfig(level=logging.INFO)


def download(filename):
    try:
        # Simulating a successful download for demonstration
        return b'binary data'

    except IOError as e:
        # Log the error
        logging.error(f"Failed to download {filename}: {e}")

        # Return None to indicate failure
        return None


def process(filenames):
    for filename in filenames:
        # Download file
        data = download(filename)

        if data is None:
            # Log a message when handling None (optional)
            logging.info(f"Skipping processing for {filename} due to download failure.")

            # Handle the None case here (e.g., skip processing)
            ...

            continue

        # Log a message when handling data (optional)
        logging.info(f"Successfully downloaded {filename}. Processing data...")

        # Process the downloaded data here
        ...


def main():
    filenames = ["file1.txt", "file2.txt"]
    process(filenames)


if __name__ == '__main__':
    main()

So your proposal is to change the stdlib to use such a result type? Because that’s precisely what the comment above implies, that in order to be useful, a result type needs to be “part of the language libs”.

If that is what you’re saying, then you’ll need to explain how to handle the massive breakage caused by changing from an exception model to a result/promise model throughout the stdlib.

If it’s not what you’re saying, then your repeated references to Javascript as an example of how this might work are flawed, because Python would not have a crucial part of the Javascript model, namely the use in the stdlib.

4 Likes

I still don’t think you’re even reading and processing what other people are saying, or you might have noticed that you linked to the exact same library I did, you just linked to it’s source on github rather than it on pypi

You might also have realized that the detractions from this are not solvable as purely technical problems, they are social ones. The solutions needed for this aren’t missing just because result types aren’t in the standard library. You appear to be bothered by the way some libraries are mixing and matching both bespoke types that hold error states and exceptions (And that’s valid, some of the APIs you’ve used to demonstrate are not what I would call ergonomic or having a good developer experience), and are trying to add something to the standard library that those libraries are under no obligation to use rather than work with those libraries or fork them to make them more consistent.

1 Like

So ideally would be no less performant than it currently is to return a Future in python:

I only gave it as an example of how Promises provided a nice API allowing you to easily wrap (Going from callback to promises) and unwrap them (Going from promises to callbacks)

In my ideal case a developer could wrap or unwrap exceptions depending on their needs. If a library developer has built a Value | raise Exception type API but you want a Result<Value | Exception> you can wrap it accordingly

If you want to use a library that has built a Result<Value | Exception> type API then you can unwrap it in order to get a Value | raise Exception type API

In the former case I can see it being as simple as decorating those functions like we can do with @contextmanager

The latter is more tricky and will require the libraries to start returning the Result types which could be breaking change for their users. But this can be something that maybe could be solved by having a decorator consume the non Result version of the API and handles the glue needing to turn it into a Result


# requests_results 
import requests
def wrap_requests(module):
   
  def get():
   result = module.get()
    # glue code goes here

   module.get = get
   return module

requests_results = wrap_requests(requests)

import requests_results as requests
requests.get()
type or paste code here

That’s true but here is never any guarantee libraries will adopt any number of features python introduced, as not everyone agrees what is the most pythonic way to implement a feature.

That’s not a good basis for deciding on a python feature, especially something like this that could easily be retrofitted to these applications using decorators.

Not all libraries will choose to adopt asyncio for example but people have stepped in to provide a layer that allows these libraries to be used in an asyncio context.

The burden of proof here is pretty high to add something that’s meant to replace exceptions, especially when it doesn’t need to be in the standard library to work. And to be clear, for standard library inclusion, things should be generally useful. We just had a whole bunch of libraries finally removed from the standard library in 3.13 to cut down on maintainer work for things that did not need to be there.

If it’s actually that simple and not a misrepresentation, then you shouldn’t need standard library adoption or even adoption by libraries, you should be able to opt into it for only your own use right now. You can use decorators functionally.

import some_library
import your_magic

use_this_instead = your_magic.decorator(some_library.function)
1 Like

You should not catch all exception types; I deliberately handle only IOError. If we were to create a result decorator for every possible exception combination, we would end up back at just using try/except again. It feels like going in circles.

except Exception as e:  # wrong
    return Result.Error(e)
2 Likes

You keep saying it’s mean’t to replace Exceptions but I’ve been trying to say that this is something that will live alongside it. Results don’t replace Exceptions anymore than Future replaces it.

It’s solely a container type with a consistent Future like API that provides easy to understand semantics around when an API call may have errored in a way that may have errored that you probably should handle but for whatever reason the library developer felt is more ergonomic to return as a failed result type rather than raise an Exceptioon.

It’s supposed to just as with a Future allow you to pass this object around which contains the result of this operation or unwrap it and then consume the exception in that moment. Therefore allowing those who prefer to unwrap immediately vs those who prefer to store the result then handle it to pick which suits them best without writing their own code to get it to behave like this.

Yes true it could be something like from requesttools import request the only reason I think it should be considered for being in a python library (Such as functools) is the social element of encouraging a common standard for this pattern. Part of reason Javascript added Promises was because I remember we had Bluebird.js, Promises A++ etc etc all implementing similar concept but with different implementations

in my view al ot of python libraries who are returning a result rather than throwing an error are doing a similar thing but on a smaller scale. It cannot be underestimated how much adoption can be driven purely just by who sanctions it. Result library build by python vs Result built by JimmyMcNoName

Even if there is a process under which we could build an experimental library which could eventually be built in, then happy to do that

It’s catching it for the purposes of storing the fact it failed, so this result can be passed around. If you try to read the value without checking, then just like with Future, it will re-raise the error.

Just like with Future you can check the reason for the error. In this case it won’t re-raise and you can decide to handle it.

If we want to distinguish between Errors and Exceptions then perhaps Result could have a exception and a error state. Meaning that if you tried to read a value or an error but the code had in fact reached an exception then it could raise the unhandled cases such as MemoryError

Some languages like Swift this can be done through their error handling by declaring what errors your function handles So the decorator could be:

@result(IOError) # IOErrors will stored, anything else is unexpected and will continue to be raised.

The two serve the exact same purpose. Both indicate a disjoint success/fail state. If you’re not intending to replace exceptions entirely, then this doesn’t belong in python at all, we don’t need another way of doing it.

All of the problems you claimed to have with other libraries stem from them mixing both exceptions and bespoke results instead of just consistently using exceptions.

This is you right?

And as for this…

exceptions are already for errors, and you can distinguish between them by simply choosing which ones you handle and letting the rest propogate.

So are you proposing to use this new result class in the stdlib or not? You keep posting very long answers which unfortunately don’t clearly answer the questions you’re being asked.

Can you please resist the temptation to expand on what you mean and simply answer my question here - is the proposal to use results in the stdlib or not? Yes or no is sufficient.

Yes and the reason they mix them is that it’s:

  • Easier to store and pass around if an operation failed
  • The code is easier to read and more ergonomic

i.e To avoid this

try:
  make.request()
except APIError:
  print('could not fetch user')
  doSomething()

try:
  make.request()
except APIError:
  print('could not fetch messages')
  doSoemthing()

By having this

  resp = make.request()

 if resp.status = 500:
  print('could not fetch user')
  doSomething()

 resp = make.request()

if resp.status = 500:
  print('could not fetch messages')
  doSoemthing()

And as I said there are loads of libraries that do this, they can’t all have chosen this pattern accidentally.

The fact is these already exist in the ecosystem, so we either can just hope one day they see the light and start adopting Exceptions. Or we can hand them a shovel and provide a way to at least make it easier to handle these errors.

If it was as easy as making everyone adopt Exceptions I wonder why other languages have adopted both having Exceptions and a way for a function to return a Result container which can then be upgraded to an Exception.

Part of one of the core libraries such as functools but no changes to any stdlib APIs (i.e dict.get for exmaple).

It would be adopted as needed.

I’m gonna bow out then. you used that api as an example of a problem, but are now saying that that api and mixing ways of doing it is a good thing within the same discussion context.

1 Like

Functools is part of the stdlib, so I still don’t understand your answer. It sounds like you’re saying “yes”.

Who defines “needed”? I say it’s not needed anywhere.

If you intend anything that currently exists and is shipped with Python to change to use this new class, you’ll need to defend the fact that you will have broken existing, working code. And I’ll tell you now, there’s basically no chance that such a proposal will be accepted.

I see no point in continuing this discussion, sorry.

2 Likes

So to recap to make more clear:

Abstract

In Python, exceptions are the standard error-handling mechanism, but many modern languages (e.g., Rust, Swift) also provide a Result type that encapsulates both success and failure in a single return value. This proposal introduces a synchronous Result type, providing explicit error handling for cases where returning error values is preferred over raising exceptions. Additionally, a decorator will allow retrofitting existing APIs to return Result objects without disrupting their original behavior. This feature will be introduced in Python’s functools module, ensuring backward compatibility and seamless integration.


Updated Proposal Sections

Motivation

While exceptions are an effective and widely-used error-handling mechanism in Python, certain use cases may benefit from explicit error propagation through return values. Some modern languages offer a Result type that can handle both success (Ok) and failure (Error) explicitly, making it easier to chain operations or manage errors without relying on try/except blocks at every step.

A Result type can complement, rather than replace, Python’s exceptions. This approach is especially beneficial for developers coming from languages like Rust or Swift, where Result patterns are common, or for scenarios where error handling needs to be explicit (e.g., functional programming or integration with external systems). This is particularly useful when:

  • You want to avoid the cost of raising and catching exceptions.
  • You are interacting with systems (such as Rust or C++) that use Result-like constructs for error handling.
  • You need to compose several functions that could fail without cluttering the control flow with exception handling.

By introducing a Result type alongside Python’s exception system, developers gain more control and flexibility in how they handle errors.


Goals

  • Introduce a Result type that encapsulates success (Ok) and failure (Error) in a unified way, exposing .value and .error attributes for accessing results.
  • Provide decorators that allow seamless integration with existing exception-based APIs, enabling these APIs to return Result types without breaking existing behavior.
  • Ensure backward compatibility by shipping the Result type in functools and keeping it fully optional for developers who prefer to continue using exceptions.

Design Overview

The Result type will feature two main states:

  • Ok(value): Represents a successful result.
  • Error(error): Represents a failed result, encapsulating an error.

The Result object exposes two attributes:

  • value: Accesses the result if the state is Ok. Raises an exception if the state is Error.
  • error: Accesses the error if the state is Error. Raises an exception if the state is Ok.

Here’s an implementation that aligns with Python’s conventions:

python

Copy code

class Result:
    def __init__(self, value=None, error=None):
        self._value = value
        self._error = error

    @classmethod
    def Ok(cls, value):
        return cls(value=value)

    @classmethod
    def Error(cls, error):
        return cls(error=error)

    @property
    def value(self):
        if self._error is not None:
            raise Exception(f"Tried to access value, but error occurred: {self._error}")
        return self._value

    @property
    def error(self):
        if self._value is not None:
            raise Exception("Tried to access error, but value is present.")
        return self._error

    def is_ok(self):
        return self._error is None

    def is_error(self):
        return self._error is not None

Example Usage

Handling Success and Failure Explicitly:

python

Copy code

def divide(a, b):
    if b == 0:
        return Result.Error("Division by zero")
    return Result.Ok(a / b)

result = divide(10, 2)
if result.is_ok():
    print(f"Success: {result.value}")
else:
    print(f"Error: {result.error}")

Auto-Unwrapping with Decorators

To ease the transition from exception-based error handling to a Result-based approach, decorators can be used to automatically wrap return values in Result objects. This enables existing APIs to switch to Result handling with minimal code changes.

python

Copy code

import functools

def resultify(*exception_types):
    """Decorator to wrap specified exceptions in Result.Error."""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            try:
                return Result.Ok(func(*args, **kwargs))
            except exception_types as e:
                return Result.Error(e)
        return wrapper
    return decorator

Using the Decorator:

python

Copy code

@resultify(ValueError)
def divide(a, b):
    if b == 0:
        raise ValueError("Division by zero")
    return a / b

result = divide(10, 0)
print(result.error)  # Outputs: Division by zero

This approach makes the integration with Result types straightforward without requiring major refactoring.


Backward Compatibility and Integration with Existing Code

Backward Compatibility:

  • Optional use of Result: The Result type will not replace exceptions. It provides an additional, explicit way to handle errors when needed, leaving Python’s current exception-based approach fully intact.
  • No breaking changes: The introduction of Result as an opt-in feature ensures no impact on existing libraries or codebases. Developers can continue using exceptions without modification, and legacy code can be enhanced to use Result via decorators like resultify.

Shipping Strategy:

  • Inclusion in functools: The Result type and related utilities, such as resultify, will be added to Python’s functools module. This provides an official, well-established location for optional tools that enhance Python’s functionality without changing core language features.

Integration Examples:

  1. Retrofitting existing libraries: Libraries can adopt the Result type in a non-breaking way by wrapping existing functions using the resultify decorator. This allows for the controlled introduction of Result types where explicit error handling is preferred.
  2. Hybrid exception and Result use: Developers can use both patterns together. In cases where a function returns a Result, errors can still raise exceptions when desired by simply calling the .value attribute, and exceptions will propagate as usual.

Advantages

  • Clarity: Explicit handling of success and error states leads to clearer, more readable code.
  • Flexibility: Developers can opt for either the Result type or exceptions, depending on the use case, without forcing a specific paradigm.
  • Error Propagation: Allows error handling to be more functional and structured, which is useful when chaining operations (e.g., .map() or .flatmap() could be added in future extensions).
  • Compatibility: Works alongside Python’s existing exception system, maintaining backward compatibility with libraries that expect exceptions to be raised.

Not a Replacement for Exceptions

While the Result type offers a way to handle success and failure in a more functional style, it is not intended to replace exceptions. Python’s exception model remains a key part of the language and is particularly suited for error handling when errors are exceptional and should interrupt the control flow.

The Result type is designed for situations where:

  • Errors are expected, and handling them as part of the normal control flow is important.
  • You need to compose several functions that may fail without cluttering the code with try/except blocks.
  • Explicit error propagation is preferred, especially when working with APIs or libraries that return errors as values rather than raising exceptions.

This hybrid approach of using both exceptions and Result allows Python to maintain its simplicity while offering more advanced error-handling tools for cases where they are needed.


Conclusion

Introducing a synchronous Result type to Python allows developers to handle errors explicitly, improving code clarity and composability without breaking existing functionality. By shipping the Result type and related tools in functools, Python retains its core exception-based error-handling model while giving developers the option to use a more functional, value-based approach when needed.

This proposal enhances error handling in Python by offering flexibility, providing backward compatibility, and ensuring developers can adopt the new pattern as gradually or comprehensively as they choose.

Sorry familiar with other args where stdlib is how types are implemented rather than libraries.

In case of stdlib APIs, then rather than breaking existing APIs. A result version would be introduce alongside them

For example with File IO:

file_handler = open("hey.txt")
result = open?("hey.txt")

You could always decorate the functions with a result decorator and use one of the functionality imports python had with things like enums to ask APIs to still return their unwrapped values like before