maybe-missing is a tiny typed helper for one annoyingly real distinction:
Nonemeans “the caller explicitly sent null”MISSINGmeans “the caller did not send this value at all”
That difference matters in patch APIs, dataclasses, settings layers, and anywhere a database column is nullable but an omitted field should mean “leave it alone”.
Python’s Optional[T] answers only one question: can the value be None?
It does not answer the other question: was there a value in the first place?
That becomes awkward fast:
- JSON
nullis a valid payload value - a nullable Postgres column may need to be set to
NULLintentionally - omitting a field in a PATCH request should usually mean “don’t update it”
- a dataclass default should sometimes mean “missing”, not “defaulted to null”
So this package gives you exactly two public names:
Maybe[T]MISSING
And then gets out of your way.
pip install maybe-missingfrom maybe_missing import Maybe, MISSING
def update_display_name(display_name: Maybe[str | None] = MISSING) -> None:
if display_name is MISSING:
print("leave the existing value alone")
elif display_name is None:
print("explicitly store NULL")
else:
print(f"store {display_name!r}")from dataclasses import dataclass
from maybe_missing import Maybe, MISSING
@dataclass(slots=True)
class UserPatch:
nickname: Maybe[str | None] = MISSING
bio: Maybe[str | None] = MISSING
def apply_patch(patch: UserPatch) -> None:
if patch.nickname is not MISSING:
# update nickname to a string or to NULL
...
if patch.bio is not MISSING:
# update bio to a string or to NULL
...Imagine a request body for partial updates:
{
"nickname": null
}This should mean:
nicknamewas provided- its value is explicitly
null - the server should write
NULL
While this body:
{}should mean:
nicknamewas not provided- keep the current value as is
Maybe[T] helps model that cleanly in Python code.
Yes.
There are only a couple of lines.
And that’s exactly why it’s more pleasant not to duplicate them across your projects and just do:
from maybe_missing import Maybe, MISSINGIsn’t it?
The package includes py.typed, so type checkers and IDEs can treat it as a typed distribution.
This package currently targets Python 3.10+.