Skip to content

Commit

Permalink
Split result type into Ok and Err classes (#27)
Browse files Browse the repository at this point in the history
This allows for much better type safety in code that uses results, since code can now use `isinstance` as a replacement for matching on the type of a result. See #17 for details. A migration guide has been provided.

Fixes #17, replaces #18.

Co-authored-by: Emerentius <emerentius@arcor.de>
Co-authored-by: francium <francium@users.noreply.github.com>
Co-authored-by: Danilo Bargen <mail@dbrgn.ch>
  • Loading branch information
4 people authored Apr 15, 2020
1 parent 25a3507 commit 1dd8dd4
Show file tree
Hide file tree
Showing 7 changed files with 302 additions and 185 deletions.
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ source =
omit =
setup.py
result/typetests.py
venv/*

[report]
# Regexes for lines to exclude from consideration
Expand Down
60 changes: 60 additions & 0 deletions MIGRATING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Migration guides

## 0.5 -> 0.6 migration

The 0.6 migration includes two breaking changes and some useful new functionality:

1\. The `Result.Ok()` and `Result.Err()` class methods have been removed.
These should be replaced by direct use of `Ok()` and `Err()`. As an example, the following code:

```python
from result import Result
res1 = Result.Ok('yay')
res2 = Result.Err('nay')
```

should be replaced by:

```python
from result import Ok, Err
res1 = Ok('yay')
res2 = Err('nay')
```

2\. Result is now a Union type between `Ok[T]` and `Err[E]`. As such, you cannot use `isinstance(res, Result)` anymore.
These should be replaced by `isinstance(res, Result)`. As an example, the following code:

```python
from result import Ok, Result
res = Ok('yay')
if isinstance(res, Result):
print("Result type!")
```

should be replaced with:

```python
from result import Ok, OkErr
res = Ok('yay')
if isinstance(res, OkErr):
print("Result type!")
```

3\. Because `Result` is now a union type MyPy can statically check the Result type.
In previous versions MyPy saw the following types:

```python
r: Result[int, str] = Ok(2)
if r.is_ok():
reveal_type(r.value) # returns Union[int, str]
```

but now, by using `isinstance`:

```python
r: Result[int, str] = Ok(2) # == Union[Ok[int], Err[str]]
if isinstance(r, Ok):
reveal_type(r.value) # returns int
```

This allows for better type checking, but requires the use of `isinstance` instead of `is_ok()` or `is_err()`.
72 changes: 52 additions & 20 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ Result
A simple Result type for Python 3 `inspired by Rust
<https://doc.rust-lang.org/std/result/>`__, fully type annotated.

The idea is that a ``Result`` value can be either ``Ok(value)`` or ``Err(error)``,
with a way to differentiate between the two. It will change code like this:
The idea is that a result value can be either ``Ok(value)`` or ``Err(error)``,
with a way to differentiate between the two. ``Ok`` and ``Err`` are both classes
encapsulating an arbitrary value. ``Result[T, E]`` is a generic type alias for
``typing.Union[Ok[T], Err[E]]``. It will change code like this:

.. sourcecode:: python

def get_user_by_email(email):
def get_user_by_email(email: str) -> Tuple[Optional[User], Optional[str]]:
"""
Return the user instance or an error message.
"""
Expand All @@ -38,9 +40,9 @@ To something like this:

.. sourcecode:: python

from result import Ok, Err
from result import Ok, Err, Result

def get_user_by_email(email):
def get_user_by_email(email: str) -> Result[User, str]:
"""
Return the user instance or an error message.
"""
Expand All @@ -52,18 +54,22 @@ To something like this:
return Ok(user)

user_result = get_user_by_email(email)
if user_result.is_ok():
if isinstance(user_result, Ok):
# type(user_result.value) == User
do_something(user_result.value)
else:
raise RuntimeError('Could not fetch user: %s' user_result.value)
# type(user_result.value) == str
raise RuntimeError('Could not fetch user: %s' % user_result.value)

As this is Python and not Rust, you will lose some of the advantages that it
brings, like elegant combinations with the ``match`` statement. On the other
side, you don't have to return semantically unclear tuples anymore.

Not all methods (https://doc.rust-lang.org/std/result/enum.Result.html) have
been implemented, only the ones that make sense in the Python context. You still
don't get any type safety at runtime, but some easier handling of types that can
been implemented, only the ones that make sense in the Python context. By using
``isinstance`` to check for ``Ok`` or ``Err`` you get type safe access to the
contained value when using `MyPy <https://mypy.readthedocs.io/>`__ to typecheck
your code. All of this in a package allowing easier handling of values that can
be OK or not, without resorting to custom exceptions.


Expand All @@ -76,20 +82,35 @@ Creating an instance::
>>> res1 = Ok('yay')
>>> res2 = Err('nay')

Or through the class methods::

>>> from result import Result
>>> res1 = Result.Ok('yay')
>>> res2 = Result.Err('nay')

Checking whether a result is ``Ok`` or not::
Checking whether a result is ``Ok`` or ``Err``. With ``isinstance`` you get type safe
access that can be checked with MyPy. The ``is_ok()`` or ``is_err()`` methods can be
used if you don't need the type safety with MyPy::

>>> res = Ok('yay')
>>> isinstance(res, Ok)
True
>>> isinstance(res, Err)
False
>>> res.is_ok()
True
>>> res.is_err()
False

You can also check if an object is ``Ok`` or ``Err`` by using the ``OkErr`` type.
Please note that this type is designed purely for convenience, and should not be used
for anything else. Using ``(Ok, Err)`` also works fine::

>>> res1 = Ok('yay')
>>> res2 = Err('nay')
>>> isinstance(res1, OkErr)
True
>>> isinstance(res2, OkErr)
True
>>> isinstance(1, OkErr)
False
>>> isinstance(res1, (Ok, Err))
True

Convert a ``Result`` to the value or ``None``::

>>> res1 = Ok('yay')
Expand Down Expand Up @@ -121,12 +142,9 @@ Note that this is a property, you cannot assign to it. Results are immutable.

For your convenience, simply creating an ``Ok`` result without value is the same as using ``True``::

>>> res1 = Result.Ok()
>>> res1 = Ok()
>>> res1.value
True
>>> res2 = Ok()
>>> res2.value
True

The ``unwrap`` method returns the value if ``Ok`` and ``unwrap_err`` method
returns the error value if ``Err``, otherwise it raises an ``UnwrapError``::
Expand Down Expand Up @@ -198,6 +216,20 @@ Values and errors can be mapped using ``map``, ``map_or``, ``map_or_else`` and
Err(2)


FAQ
-------


- **Why do I get the "Cannot infer type argument" error with MyPy?**

There is `a bug in MyPy
<https://github.com/python/mypy/issues/230>`_ which can be triggered in some scenarios.
Using ``if isinstance(res, Ok)`` instead of ``if res.is_ok()`` will help in some cases.
Otherwise using `one of these workarounds
<https://github.com/python/mypy/issues/3889#issuecomment-325997911>`_ can help.



License
-------

Expand Down
4 changes: 2 additions & 2 deletions result/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
from .result import Result, Ok, Err, UnwrapError
__all__ = ['Result', 'Ok', 'Err', 'UnwrapError']
from .result import Result, Ok, Err, UnwrapError, OkErr
__all__ = ['Result', 'Ok', 'Err', 'UnwrapError', 'OkErr']
Loading

0 comments on commit 1dd8dd4

Please sign in to comment.