
Okay, I'll let myself get sucked into responding ONE TIME, but only because you gave me such a nice API to work with :) On 10/18/2021 9:11 PM, Piotr Waszkiewicz wrote:
class User(DBModel): phone: str | None
class Publisher(DBModel): owner: ForeignKey[User] | None
class Book(DBModel) publisher: ForeignKey[Publisher] | None
Imagine wanting to get the phone number of the person that published a certain book from the database. In this situation, with maybe-dot operator I can write:
phone_number = book.publisher?.owner?.phone
Consider today, you wrote this as "book.publisher.owner.phone". You would potentially get AttributeError, from any one of the elements - no way to tell which, and no way to react. Generally, AttributeError indicates that you've provided a value to an API which doesn't fit its pattern. In other words, it's an error about the *type* rather than the value. But in this case, the (semantic, not implementation) *type* is known and correct - it's a publisher! It just happens that the API designed it such that when the *value* is unknown, the *type* no longer matches. This is PRECISELY the kind of (IMHO, bad) API design that None-aware operators will encourage. Consider an alternative: class ForeignKey: ... def __bool__(self): return not self.is_dbnull def value(self): if self.is_dbnull: return self.Type.empty() # that is, DBModel.empty() return self._value class DBModel: @classmethod def empty(cls): return cls(__secret_is_empty_flag=True) def __bool__(self): return not self._is_empty def __getattr__(self, key): if not self: t = self._get_model_type(key) return t.empty() if isinstance(t, DBModel) else None ... class User(DBModel): phone: str | None class Publisher(DBModel): owner: ForeignKey[User] class Book(DBModel) publisher: ForeignKey[Publisher] Okay, so as the API implementer, I've had to do a tonne more work. That's fine - *that's my job*. The user hasn't had to stick "| None" everywhere (and when we eventually get around to allowing named arguments in indexing then they could use "ForeignKey[User, non_nullable=True]", but I guess for now that would be some subclass of ForeignKey). But now here's the example again:
book.publisher.owner.phone
If there is no publisher, it'll return None. If there is no owner, it'll return None. If the owner has no phone number, it'll return None. BUT, if you misspell "owner", it will raise AttributeError, because you referenced something that is not part of the *type*. And that error will be raised EVERY time, not just in the cases where 'publisher' is non-null. It takes away the random value-based errors we've come to love from poorly coded web sites and makes them reliably based on the value's type (and doesn't even require a type checker ;) ). Additionally, if you want to explicitly check whether a FK is null, you can do everything with regular checks: if book.publisher.owner: # we know the owner! else: # we don't know # Get all owner names - including where the name is None - but only if # Mrs. None actually published a book (and not just because we don't # know a book's publisher or a publisher's owner) owners = {book.id: book.publisher.owner.name for book in all_books if book.publisher.owner} # Update a null FK with a lazy lookup book.publisher = book.publisher or publishers.get(...) You can't do anything useful with a native None here besides test for it, and there are better ways to do that test. So None is not a useful value compared to a rich DBModel subclass that *knows* it is empty. --- So to summarise my core concern - allowing an API designer to "just use None" is a cop out, and it lets people write lazy/bad APIs rather than coming up with good ones. Cheers, Steve