Categories: utility library
There are often situations in code where a required value doesn't exist. A typical way of dealing with this is to return
None but optionals provide a cleaner way, and the optional.py module provides an easy-to-use implementation of optionals.
For example, suppose we had a customer database, and we needed a function to find the value of a customer's most recent transaction:
def getLastTransactionAmount(customerId): ... return value
This is all fine except for the edge case of a customer who is registered but never bought anything. What should we return in that case?
- We could return zero. But maybe our shop sometimes gives away free offers that get recorded as a transaction of value zero. We would then have no way to distinguish between a customer who has never bought anything, as opposed to a customer who has purchased many times before but their most recent transaction happens to be a freebie.
- We could return
None. That is fairly common practice but isn't necessarily good practice. It means the calling code has to check the type of the return value, which is all a little bit hacky. In addition, if we use truthy values to check for zero transactions, we might accidentally conflate
Nonewith zero, and they are both falsy.
- Or we could raise an exception. Again this complicates the calling code. Not to mention that a registered customer who hasn't bought anything yet is hardly an exceptional situation.
What we really need is a clean way to return either a value or an unambiguous indication that the value doesn't exist.
To use the optional.py module, you must first install it. The easiest way is to use
pip install to install it from the PyPi repository, or if you prefer it is available via Conda. Be sure to choose "optional.py" (there is a different module named "optional" that appears to be unmaintained).
Optional class is a container class, It can either contain the value you wish to return, or it can be empty. Here is how to use the class
Optional in your code:
from optional import Optional def getLastTransactionAmount(customerId): ... if value_exists: return Optional.of(value) else: return Optional.empty()
We first import
Optional. Then we use it to return a suitable value:
- If the
valueexists, we return
Optional.of(value), which is an
Optionalobject with the value contained inside it.
- If there is no value, we return
Optional.empty(), which is an
Optionalobject that contains nothing.
In both cases, the function returns an
Optional object, so there is no ambiguity:
result = getLastTransactionAmount()
There are several ways for the caller to handle an optional. First, we could check if the result is present, and only handle it if it is there:
f = Optional.of(100) e = Optional.empty() if f.is_present(): print(f.get()) # 100 if e.is_present(): print(e.get()) # Never called, the if statement is false.
For illustration, we have created
Optional with a value 100) and
e (an empty
f.is_present() returns true because
f has a value, so the if body executes. We use
f.get() to get the value 100 and print it.
e.is_present() returns false because
e has an empty, so the if body is skipped.
An alternative is to supply a default to handle the empty case, If the value is present, its value is used, but if there is no value, the default is used:
print(f.get_or_default(10)) # 100 print(e.get_or_default(10)) # 10 (the default)
Another option is to raise an exception if the value isn't present. There are two ways to do this, but the second is best practice as the intent is more clear:
# Get without checking (bad practice) print(f.get()) # 100 print(e.get()) # Raises an exception # get_or_raise (best practice) print(f.get_or_raise(SomeException)) # 100 print(e.get_or_raise(SomeException)) # Raises SomeException
Chaining and mapping
Optionals have their origins in functional programming, specifically the Maybe monad. So they fit very well with other functional techniques such as chaining and mapping.
Put simply, chaining and mapping allow us to pass values into functions without having to explicitly deal with getting the value or handling empties.
if_present method checks if the Optional is not empty, and then calls the function, passing the value from the Optional. If the Optional is empty, the function is not called at all. Here it is in action:
def do_something(val): print("If present example", val) return -1 x = f.if_present(do_something) x = e.if_present(do_something)
do_something is a function that does something with the value. Whatever function you use must take exactly one argument. In this case, it prints the value. So this bit of code runs
do_something with the value of
f (which is 100, see above):
x = f.if_present(do_something)
If present example 100
There are a couple of important things to note here:
- When we pass in
do_somethingwe are passing in the function object itself (strictly speaking it has to be a callable object, which usually means a function). We could also use a lambda here, see later.
- The return value of
do_somethingis completely ignored.
do_somethingto perform an action based on the value, but not to return a value. To do that you must use the
mapfunction as we will see. After executing the line above,
xwill be set equal to
If we repeat this with an empty optional, the
do_something won't be called at all, and
x will be set equal to
x = e.if_present(do_something)
or_else method calls a function only if the optional is empty:
def supplier(): print("In supplier") return -2 x = f.or_else(supplier) x = e.or_else(supplier)
In this case, when we call
f.or_else, the supplier function will not be called at all, and
x will be set equal to
When we call
e.or_else, the supplier function will be called. The supplier function returns -2, so
or_else will return an
Optional object with a value of -2.
x will be set to that optional object. Notice also that the supplier function has no parameters.
This is a little bit like
get_or_default except that it uses a supplier function. This is a subtle but important difference. Consider the following:
a = f.get_or_default(time_consuming_function()) b = f.or_else(time_consuming_function)
In the first case,
time_consuming_function is always called, because its result is needed to pass into
get_or_default. So even though
f has content so the default isn't needed, it is still calculated. Which is a waste of time.
In the second case,
time_consuming_function isn't called until
or_else is called. But because
f has content,
or_else won't be called so neither will
The other difference, of course, is that
a will be set to 100 (the value of
b will be set equal to
f itself (but you can easily get its value by calling
Finally, mapping provides a way to easily call a function on a value wrapped in an optional. Here is how it works:
def add_1(val): return val + 1 def mul_2(val): return val*2 x = f.map(add_1).map(mul_2) x = e.map(add_1).map(mul_2)
mul_2 are both simple functions that operate on numbers. The
map function does three things:
- Checks if the optional is empty. If it is, the empty value is returned immediately.
- Gets the value from the optional (which at this stage we know is not empty).
- Calls the function and returns the value as an optional.
So for the first call (involving
f), the code first calls
add_1 with the value of
f (which is 100), so the result is an optional with a value of 101. It then calls
mul_2 with this new optional. This results in an optional with value 202, which is returned and assigned to
In the second case, where we run the same code using the empty value
map method doesn't call
mul_2, and the value of
x is an empty optional.
This provides a transparent way to run one or more functions on an optional value, that also handles empty optionals by simply setting the result to an empty optional.
There is also a variant of this, called
flat_map. If you are writing code that uses optionals, you might sometimes have a useful function that does exactly what you need, but it just happens to have been written to return an optional rather than a raw value:
def add_2_opt(val): return Optional.of(val + 2) x = f.map(add_2_opt) x = f.flat_map(add_2_opt)
In this case
add_2_opt returns an optional of
val + 2 rather than just
val + 2. This means that if we use
map with this function we will get an optional of an optional of 102. Now that is perfectly valid but it almost certainly isn't what you want.
You could duplicate the function to create a similar function that returns a raw value, but code duplication is always best avoided. A better solution is to use
flat_map. This is similar to
map, except that it removes the extra layer of optionals.
The optional module provides a clean and simple alternative to the ambiguous use of
None to represent optional values.
Join the PythonInformer Newsletter
Sign up using this form to receive an email when new content is added:
2d arrays abstract data type alignment and angle animation arc array arrays bar chart bar style behavioural pattern bezier curve built-in function callable object chain circle classes clipping close closure cmyk colour combinations comparison operator comprehension context context manager conversion count creational pattern data science data types decorator design pattern device space dictionary drawing duck typing efficiency ellipse else encryption enumerate fill filter font font style for loop formula function function composition function plot functools game development generativepy tutorial generator geometry gif global variable gradient greyscale higher order function hsl html image image processing imagesurface immutable object in operator index inner function input installing iter iterable iterator itertools join l system lambda function len lerp line line plot line style linear gradient linspace list list comprehension logical operator lru_cache magic method mandelbrot mandelbrot set map marker style matplotlib monad mutability named parameter numeric python numpy object open operator optimisation optional parameter or pandas partial application path pattern permutations pie chart pil pillow polygon pong positional parameter print product programming paradigms programming techniques pure function python standard library radial gradient range recipes rectangle recursion reduce regular polygon repeat rgb rotation roundrect scaling scatter plot scipy sector segment sequence setup shape singleton slice slicing sound spirograph sprite square str stream string stroke structural pattern subpath symmetric encryption template text text metrics tinkerbell fractal transform translation transparency triangle truthy value tuple turtle unpacking user space vectorisation webserver website while loop zip zip_longest