Posted by sebg 2 days ago
Another fun one is List[Foo]. Runtime Python has lists but you can’t ask a list what type of list it is, and you can’t write isinstance(a, List[Foo]).
- You have an inner value and the inner value is something non-null (an integer, a string, another optional, etc.)
- You have an inner value and the inner value is null
- You do not have an inner value
The problem, from what I understand, is that Option[T] in Python is equivalent to the union type T | None, and union types are flattened (e.g., (T | U) | V is equivalent to T | U | V) and duplicates are ignored (e.g., T | U | T is equivalent to T | U). As a result, Option[Option[T]] is equivalent to (T | None) | None, which is equivalent to T | None | None, which is finally equivalent to T | None, which means you lose the distinction between "No inner value" and "Inner value is present but is None".
As a more concrete (albeit somewhat contrived) example, say there's some API you can query for the owner of a particular document ID where the document ID may or may not refer to an actual document (e.g., maybe you got it from a cache and the document has since been deleted). One way to express this would be (using Rust syntax)
fn get_owner(id: DocumentId) -> Option<Option<UserId>> { ... }
So if you get None, the document does not exist; if you get Some(None), the document exists but it has no owner; and if you get Some(<id>) the document exists and it has an owner.(This can probably be done better by creating new enums, but it's an example so give me a bit of slack)
A naive port of this signature to Python would be
def get_owner(id: DocumentId) -> Option[Option[UserId]]: pass
But to type checkers this is equivalent to def get_owner(id: DocumentId) -> UserId | None: pass
So now you're in the pickle the OP describes: you can end up with a None, but you no longer knonw whether it means "no document" or "no owner". The Python solution is to basically create a different name for either the outer None (e.g., MISSING as in the OP) or the inner one (e.g., NO_OWNER or the like).Depends on what exactly you plan on doing with the information, I'd imagine. I don't think it's necessarily a given that "no document" and "no owner" should be handled identically (e.g., searching for the updated document to try again vs. returning an operation-not-allowed error).
> Why are you using get_owner to try and figure out if the document exists?
Potential TOCTOU issues, perhaps? As I mentioned the document could have vanished out from under you so detecting this situation could let handle things appropriately (though to be fair that argument could probably extend to future operations as well).
Maybe a versioned filesystem would have made a bit more sense, where None would indicate a new version is available? Another example I've seen mentioned is trying to get a key-value pair from a JSON where the key may be missing.
But in any case, I did say it was a bit of a contrived example, so cut me a bit of slack :P It was just the first thing that came to mind.
The examples sibling comments give are probably more practical.
If you care whether a document exists or not, you should use an API that exclusively tells you that instead of expecting everything related to documents telling you if the document exists or not. Otherwise you’re overloading the meaning. A non existent document has no owner. An API to fetch the owner of the document should not care if it exists or not, it does not matter.
But again, this was an off-the-cuff example that I explicitly described as contrived from the start. I could probably flesh it out to make more sense, but I think it'd be easier just to refer to sibling comments rather than bikeshed this to death.
I'd like nested Nullable in ClickHouse to use CoalescingMergeTree to support postgres unchanged TOAST values easily without getting mixed up on nulls
I bet this would be a good use of vibe coding. Experiment with what-ifs. I can imagine being able to “show not tell” and reduce the signal-to-noise of comment opinions.
> MISSING = sentinel("MISSING")
> def update_email(user_id: int, email: str | None | MISSING = MISSING) -> None:
> if email is MISSING:
> return # nothing to update
> elif email is None:
> db.delete_email(user_id)
> else:
> db.set_email(user_id, email.lower()) # narrowed to str
I see this style frequently from long time Python programmers. The language is so dynamic that people begin to think this is a good way to program. Hot take: It is not. Why not two different functions called delete_email() and set_email()? That would be more clear.This distinction is something that I think is actually handy about JavaScript, and distinguishing between 'null' and 'undefined' is sometimes useful for similar reasons.
No, it returns KeyError. Or at least it should.
cache = {“key”: “value”, “some input”: None}
cache.get(“missing_key”)
Vs cache[“missing_key”]
There are better examples that could have been highlighted.with match now being available, we even return explicit error objects and the ergonomics are pretty good (reminds me of rust).
The problem I see is there's no way to enforce the check that you actually handled/bubbled up the error. With exceptions, you just crash if you don't handle it, which is much preferred if determinism is the goal. Is there some way to use the type system to do this? Maybe incoming are UnhandledResult outgoing are some HandledResult or something like that?