Skip to content

Usage

The purpose of MayPy is to provide a type-level solution for representing and manipulating optional values instead of None reference. This away around, no need to worry about the optional value and testing it, before applying process on it.

There are two types of Maybe container, either it contains a value or it is empty.

A Maybe object become empty when the value it should contain is None.

Create Maybe objects

Empty

There are 3 ways to have a empty Maybe.

from maypy import EMPTY, Empty, maybe

assert maybe(None) == Empty() == EMPTY

Note

maybe(None) is here as an example, for readability use the others to instantiate an empty Maybe.

Valuated

Valuated Maybe or Some, is quite straightforward, provide some value whatever it is, as long as is not None to of .

assert maybe(12)

Checking value presence

There are three methods to check if the value is present or not.

Either by using is_present to know if the container has a value or use is_empty to verify the absence of value. Another approach is to examine the "truthiness" of Maybe.

name = maybe("name")
assert name.is_present()

name = Empty()
assert not name.is_present()
name = maybe("name")
assert not name.is_empty()

name = Empty()
assert name.is_empty()
assert maybe("name")

assert not Empty()

Get wrapped value

To retrieve the value contained inside a Maybe, the method get returns it or raises an EmptyMaybeException.

assert maybe("Mathieu").get() == "Mathieu"
assert Empty().get()
>>> EmptyMaybeException

Handle Emptiness

Default value

When Maybe is empty, it's possible to provide a default value using or_else. Taking either a value or a Supplier (with mypy the value provide as default should be the same type as the hypothetical value wrapped).

assert Maybe.empty().or_else(12) == 12

assert maybe("present").or_else("absent") == "present"

Warning

It is indeed possible to do this way in pur python.

assert ("present" or "absent) == "present"

assert (None or 12) == 12

However, it is using the "truthiness" of the first element, it does not mean that the element is None!

>>> assert (0 or 12) == 0
>>> AssertionError
assert Maybe.empty().or_else(lambda: 12) == 12

assert maybe("present").or_else(lambda: "absent") == "present"

Tips

We may wonder what is the different between passing the function and calling it as the default value. The function is used only if Maybe is empty, whereas in the other hands, it will be invoked no matter what.

def populate_data() -> list[str]:
    print("invocation of populate_data")
    return ["python", "c++", "c", "java"]

assert Maybe.empty().or_else(populate_data) == ["python", "c++", "c", "java"]
>>> "invocation of populate_data"
assert maybe(["ruby", "kotlin"]).or_else(populate_data) == ["ruby", "kotlin"]
assert maybe(["ruby", "kotlin"]).or_else(populate_data()) == ["ruby", "kotlin"]
>>> "invocation of populate_data"

Raise error

Another approach for handling value absence, is to raise a custom exception by or_else_raise when Maybe is empty.

class CustomError(Exception):
    pass

assert Maybe.empty().or_else_raise(CustomError())
>>> CustomError
assert maybe(12).or_else_raise(CustomError()) == 12

Manipulating the value

Note

In the further examples, I keep using lambda function to keep it simple and easy to read. It totally possible to use named function, no matter what you use as long as it respects the API contract.

By the way mypy will infer types even with lambda 🎉!!!

Filtering

It is possible to perform inline condition on our wrapped value with filter. Taking a Predicate, it will check if the value matches the predicate and returning Maybe itself when passing, otherwise an empty Maybe is returned.

Built-in Predicates

1.1.0

Some built-in predicates have been added.

price = 999.99

assert maybe(price).filter(lambda x: x <= 1000).is_present()
assert maybe(price).filter(lambda x: x >= 1000).is_empty()

You may wonder why using it and what is the gain. Let's dive on a more concrete example!

You want to watch a movie, and you only care about its release date. It should be in certain interval.

from dataclasses import dataclass, field

@dataclass
class Movie:
    director: str
    title: str
    year: int
    genre: list[str] = field(default_factory=list)
    oscars: list[str] | None = field(default=None)
from typing import Callable

def interval_checker(start_year: int, end_year: int) -> Callable[[Movie], bool]:
    def is_in_range(movie: Movie | None) -> bool:
        if movie is not None:
            return start_year <= movie.year <= end_year
        return False

    return is_in_range


movie = Movie("Luc Besson", "Taxi", 1998, ["comedy"])

assert interval_checker(1990, 2005)(movie)
assert not interval_checker(2000, 2020)(movie)
assert not interval_checker(2000, 2020)(None)
from maypy import Maybe

assert maybe(movie).filter(lambda film: 1990 <= film.year <= 2005).is_present()

Mapping

With a similar syntax, we can transform the value inside Maybe using map.

Reusing last example, getting the number of oscars rewarding the movie.

movie = Movie("Luc Besson", "Taxi", 1998, ["comedy"])

assert (
        maybe(movie)
        .map(lambda film: movie.oscars)
        .map(lambda oscars: len(oscars))
        .or_else(0) == 0
)

It is powerful to chain filter and map together. Like checking the correctness of an input by a user.

from maypy.predicates import one_of

VALID_BOOLS = ("y", "n", "yes", "no")

user_input = "y "

assert (
    maybe(user_input)
    .map(lambda input_: input_.strip())
    .filter(one_of(VALID_BOOLS))
    .is_present()
)

Conditional action

The last method is if_present, it allows to perform some code, using a Consumer function (Callable[[VALUE], None]), on the wrapped value if present, otherwise nothing will happen.

maybe("name").if_present(lambda val: print(val))

Warning

To keep it functional, the function passed should not modify the value but only use it. Please use chaining of map and get the value instead.