I’m not sure if I exactly follow what you mean here, but I believe you’re asking about examples like these as comparisons to sensor.machine?.line?.department.engineer?.email
if (line := gettattr(sensor.machine, "line", None)) is not None
address = getattr(line.department.engineer, "email", None)
else:
address = None
if (line := sensor.machine?.line) is not None:
address = line.department.engineer?.email
else:
address = None
The “find an engineer for this machine” example could also then be
engineer = getattr(machine.maintenance_team, "enigneer", None)
if engineer is None and (line := machine.line) is not None:
engineer = line.engineer or line.department.engineer
engineer = machine.maintenance_team?.engineer
if engineer is None and (line := machine.line) is not None:
engineer = line.engineer or line.department.engineer
Or even
engineer = (
getattr(machine.maintenance_team, "engineer", None)
or getattr(machine.line, "engineer", None)
or getattr(getattr(machine.line, "department", None), "engineer", None)
)
I think that last line shows another benefit of ?.
Going from machine to department engineer requires just a single optional lookup machine.line?.department.engineer whereas using getattr means that every lookup after the optional one also needs to use getattr.
And to mention it again, getattr drops my type annotations
Speaking of problems with the getattr approach, I intentionally left a typo I made in one of the strings above so there’s actually a bug somewhere up there that would have been caught when using ?.
The semantics laid out in the PEP seem clear to me, but a ton of the discussion is talking about all kinds of other behavior, so I’ve also been confused. Maybe I’m misreading and the discussion is actually more about proposing changes for how it should work instead.
Quoting (most of) the example from grammar changes section of the PEP here (I’ve taken out the await parts since they’re only included to complete the grammar specification)
For example,
a.b(c).d[e]is currently parsed as['a', '.b', '(c)', '.d', '[e]']and evaluated:
_v = a
_v = _v.b
_v = _v(c)
_v = _v.d
_v = _v[e]
When a
None-aware operator is present, the left-to-right evaluation may be short-circuited. For example,a?.b(c).d?[e]is evaluated:
_v = a
if _v is not None:
_v = _v.b
_v = _v(c)
_v = _v.d
if _v is not None:
_v = _v[e]
The proposal is just to insert a check for None anytime you hit a ? and stop evaluating anymore if it is None
It’s incredibly straightforward, the output from running mypy on the file where I’ve been saving all the scratch work for these examples is
$ mypy better_pep505_example.py
better_pep505_example.py:60: error: Item "None" of "Machine | None" has no attribute "line" [union-attr]
better_pep505_example.py:60: error: Item "None" of "Line | Any | None" has no attribute "department" [union-attr]
better_pep505_example.py:60: error: Item "None" of "Person | Any | None" has no attribute "email" [union-attr]
Found 3 errors in 1 file (checked 1 source file)
So I need to add three ?'s: before line, department, and email
address = sensor.machine.line.department.engineer.email
becomes
address = sensor.machine?.line?.department.engineer?.email
Something that occurred to me in relation to this (prompted by the recent result class thread): what if the safe navigation operators didn’t return a regular
Nonereference when the left operand wasNone, but instead returned a newResulttype that contains a reference to the exception that would have been raised in the absence of the safe navigation operator?
I’ve been meaning to read that thread, but haven’t gotten a chance yet. Seems very similar to the ? Unary Postfix Operator in PEP 505’s Rejected Ideas.
I’ve looked at some of the implementations of Result and Maybe classes before and it seems quite noisy, so I would hope that any consideration of adding those also had nice ways to use them very simply, but I imagine that’s impossible to do in a type-safe way without syntax changes like what PEP 505 proposes.
For instance, using the Maybe class from the returns package, my example of
address = sensor.machine?.line?.department.engineer?.email
becomes
address = (
Maybe.from_optional(sensor.machine)
.bind_optional(lambda machine: machine.line)
.bind_optional(lambda line: line.department.engineer)
.map(lambda engineer: engineer.email)
.value_or(None)
)
Not sure if that should be split off into it’s own thread or if discussing it in context would be better to ensure those in favor of this syntax can directly compare it as a reason to not accept the syntax, for now I’ll discuss here, but if it needs to be split, please just move the messages.
Perhaps a function
def extract_value( obj: Any, path: str, type: type[T] = object, on_exception: Callable[[Context], T] | Literal[MISSING] = MISSING, default_value: T | Literal[MISSING] = MISSING, ) -> T: ...where MISSING is an internal sentinel.
would allow type safe handling and user defined behavior on access failure, but that if both default_value and an exception handler are omitted, that the path specified not existing should error.
For the “type safe” handling part of that, in my eyes, there’s not much point.
I pass in type = str | None and then a type checker says “Great! This function must return str | None”, but the type checker is not actually checking anything.