Skip to content

Instantly share code, notes, and snippets.

@oliverlambson
Last active June 27, 2024 16:35
Show Gist options
  • Save oliverlambson/6c33ec5f278aa2e68152e1bdaf76b815 to your computer and use it in GitHub Desktop.
Save oliverlambson/6c33ec5f278aa2e68152e1bdaf76b815 to your computer and use it in GitHub Desktop.
Rust & Go-style return types in Python

Sometimes you just want errors as values.

(With type safety)

Rust-style Result type in Python

Lightweight, generic result type à la Rust.

I think this is way nicer, given the limitations of the Go implementation below.

Note: There is a full-on package that does this, with all the other Rust niceties of .unwrap_or(), etc. But if all you need is the basics, this is a zero-dependency approach in like 11 lines of actual code.

from dataclasses import dataclass
from typing import TypeVar

T = TypeVar("T")
E = TypeVar("E")


@dataclass
class Ok(Generic[T]):
    value: T


@dataclass
class Err(Generic[E]):
    value: E


Result = Ok[T] | Err[E]

# --- Example -----------------------------------------------------------------
def fn(x: bool) -> Result[int, str]:
    if x:
        return Ok(1)
    else:
        return Err("woah")


y = fn(True)
match y:
    case Ok(value):
        print(value + 1)
    case Err(err):
        print(err.upper())

Go-style result, err return type in Python

Lightweight, generic result type à la golang.

mypy isn't able to infer without requiring a redundant assert1. I can't figure out how to get a type safe exact go-style implementation, but there's an option below that delays unpacking of the result tuple.

from typing import TypeVar

from typing_extensions import TypeIs

T = TypeVar("T")
E = TypeVar("E")

Ok = tuple[T, None]
Err = tuple[None, E]
Result = Ok[T] | Err[E]


# --- Example -----------------------------------------------------------------
def fn(x: bool) -> Result[int, str]:
    if x:
        return 1, None
    else:
        return None, "woah"


# unfortunately mypy can't figure this out:
def main_fail() -> None:
    result, err = fn(True)
    if err is not None:
        print(err.upper())
        return
    print(result + 1)  # error: Operator "+" is not supported for "None"


# --- Example (with assert) ---------------------------------------------------
# so we have to assert, which kind of defeats the point...
def main_pass_with_assert() -> None:
    result, err = fn(True)
    if err is not None:
        print(err.upper())
        return
    assert result is not None
    print(result + 1)


# --- Example (with TypeIs) ---------------------------------------------------
# we could delay unpacking and use a TypeIs function, but then we have to unpack
# the tuple in both branches...
#
# (I think it's up to personal preference if you prefer this or the assert approach)
def is_err(result: Result[T, E]) -> TypeIs[Err[E]]:
    _, err = result
    return err is not None


def main_pass_with_typeis() -> None:
    result = fn(True)  # can't unpack or we lose result <-> err type relationship
    if is_err(result):
        _, err = result
        print(err.upper())
        return
    value, _ = result
    print(value + 1)

Footnotes

  1. I think this is due to the limitations of Python's current type system, as described in PEP-724. That PEP was withdrawn, but there's a new one, PEP-742, which has been accepted and created TypeIs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment