I'm trying to implement an optional chaining operator similar to JavaScript's `?.` in Python. The idea is to create an `Optional` class that wraps a type T and lets you safely access attributes. When accessing an attribute from the wrapped object, the expected result should either be the type of the attribute or `None`. Here's what I'm envisioning:
1. For a scenario where the wrapped object is `None`:
```python
my_obj = Optional(None)
result = my_obj.attr1.attr2.attr3.value # This should return None
```
2. For nested objects:
```python
@dataclass
class A:
attr3: int
@dataclass
class B:
attr2: A
@dataclass
class C:
attr1: B
my_obj = Optional(C(B(A(1))))
result = my_obj.attr1.attr2.attr3.value # This should return 5
```
3. For cases with nested objects that might have `None` values:
```python
@dataclass
class X:
attr1: int
@dataclass
class Y:
attr2: X | None
@dataclass
class Z:
attr1: Y
my_obj = Optional(Z(Y(None)))
result = my_obj.attr1.attr2.attr3.value # This should return None
```
I started off with this implementation:
```python
from dataclasses import dataclass
@dataclass
class Optional[T]:
value: T | None
def __getattr__[V](self, name: str) -> "Optional[V | None]":
return Optional(getattr(self.value, name, None))
```
However, Pyright and Ty don't seem to recognize the subtypes properly. Any ideas on how to implement this effectively?
2 Answers
You're on the right track thinking about optional chaining a la JS `?.`. However, the current Python typing ecosystem doesn't lend itself to this easily. Here’s a couple of ideas:
1. Use `__getattr__` carefully with actual runtime checks.
2. Manually short-circuit with `getattr` or `if obj is not None` to avoid exceptions.
3. A workaround is to use a chaining function like `functools.reduce` to handle the attribute checks:
```python
from functools import reduce
def optional_chain(obj, *attrs):
try:
return reduce(getattr, attrs, obj)
except AttributeError:
return None
```
This gives you a more Pythonic way to safely access nested attributes without having to deal with potential exceptions all over the place, until we get native support for optional chaining.
While this isn't directly supported, you can achieve optional chained access by using a try-except approach. For example:
```python
from contextlib import suppress
with suppress(AttributeError):
result = my_obj.attr1.attr2.attr3
```
This won't satisfy strict type checking, though. However, casting can help refine the type expectations if you specify the expected output type.
That sounds like a lot of extra work. Can we expect this to change in future Python versions?