Rust's Option type... in Python
Tags: rust, python, discuss, null
Published: 2023-10-13 14:14:18

---
title: Rust's Option type... in Python
published: true
description: Rust's void-safety mechanism can be part-expressed in Python.... but should it ??
tags: rust, python, discuss, null
cover_image: https://live.staticflickr.com/6134/5975823310_4297735fb1_k.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2023-10-13 13:06 +0000
---

<small>[Cover Image (C) Tai Kedzierski](https://www.flickr.com/photos/ducakedhare/5975823310/)</small>

How many times have you written/seen code like this:

```python
data = get_data()
print(data.strip())
```

Looks reasonable right?

How about if `data == None`?

It is so easy to assume _thingness_ and forget to handle _nullness_. So frequent. In Python, Java, JavaScript, and many other languages, this is prone to happening.

Then some languages, like Rust, have a semantic way of dealing with this... and I pondered, what if we tried to intorduce this to Python? How would it look?

Just for a laugh, and in between waiting for integration tests to finish running, I threw this together: [a quick Python implementation to mimic Rust's `Option` type](https://gist.github.com/taikedz/44e3af7d30d124f10974ca45f9780021)

In Rust, the `Option` enumeration type is used with the `match` operator to force handling the concept of null-ness, without treating it as a first-order value, unlike in languages that have a null representation.

Does that sound weird? Let me digress a moment

## Nullity as a typed value, not a stand-in

In languages where there can be a `None` or `null` _placeholder_-concept, it works like this:

`functionA()` returns a value, say of _type_ `String` (or other, it is immaterial). If `functionA()` returns `null`, it effectively says it returned `null` _instead of_ a String. Trying to treat this `null` as String (by calling its methods, concatenating it, etc) leads to errors.

In Rust, this cannot happen.

In Rust if `functionA()` says returns a String, it _always returns a valid string_. If we want to express that a non-valid value can be returned, then `functionA()` must flip: it returns an `Option` instead.

We must then handle the `Option` enumeration type - check if it is the variant known as `None` , or check if it is the variant known as `Some`.

We can then `.unwrap()` the result explicitly, or through pattern matching implicitly:

```rust
# Panics if we get a None variant
let value = functionA().unwrap();

# vs

match functionA() {
    None => println!("Fail!"),
    Some(value) => println!("Success: {}", value),
}
```

## In Python

So how does this experiment look in Python ?

Well, we're not replacing the native `None` type. We'll use a `Null` class (a regular class), subclassed to `Option`. I didn't include a `Some` type, but it would be simple enough - just `class Some(Option): pass` and it's done. Everything is in fact implemented on `Option`

We are then able to wrap this around any return result we care to add to our script to make it return an `Option`.

```python
def some_func() -> Option:
    some_value = None
    
    # ... implementation ...

    if valid:
        return Option("some value")

    elif not_valid:
        # Explicit null-return
        return Null

    # or even
    return Option(some_value)
    # (will behave like Null if some_value is still None)

res = some_func()
if res.is_null():
    ... # handle it
value = res.unwrap()
```

This forces whoever uses `some_function()` to consider that nullity can come of it, before gaining access to the value itself:

```python
# Without a check, this may "panic" (borrowing from rust's terminology)
#  the program exits systematically with an error message
value = some_func().unwrap("failed to get data from some_func")

# On occurrence of nullity, a default value is substituted instead.
value = some_func().or_default("default data")
```

This makes for explicit force-handling of `None` , where before it could easily pass under the review radar silently.

An interesting side-effect is that managing default-substitution is now done outside of `some_func`'s logic; the design of `some_func` can stay unencumbered with specifically dealing with alternate eventualities, just as the caller code can declare the default value without multiple lines of if/else checking.

We also don't have a `match` keyword to use, but this doesn't impede the usability in any significant way - merely having to acknowledge unwrapping brings gains, and the `or_raise()` method allows some additional flow control.

## Should this even be used in Python ?

I am actually tempted to use it in my projects... as a way of forcing awareness of null possibility.

It ensures that nullness cannot be ignored/forgotten by the caller, promoting more conscientious coding, and helping avoid some common pitfalls.

It is succinct enough to allow minimal changes in code flow, but explicit so that it cannot be ignored.

But it's very un-idiomatic, literally "trying to write Rust style code in Python."

And yet.

:coin: A penny for you thoughts?