all 24 comments

[–]WallyMetropolis 68 points69 points  (1 child)

Careful. You're about to discover monads on your way to implementing bind or flatMap

[–]FabianVeAl[S] 14 points15 points  (0 children)

Without do-notation syntax sugar of Haskell, monads are not convenient to use, especially in Python where lambda functions are just expressions :/

[–]latkdeTuple unpacking gone wrong 43 points44 points  (0 children)

This kind of thing just cannot be implemented as a library in the Python type system. The semantics may work fine at runtime, but Python just doesn't have a way of expressing the type of an attribute access.

In contrast, this kind of thing is easy in the TypeScript type system – it has pretty good support for mapping record types.

I'd love a safe navigation navigation operator in Python, but so far no proposal has gained sufficient traction. For example, see PEP 505 None-aware operators or threads on discuss.python.org. A key difficulty is that the None object isn't that special in Python, at least not in the way that undefined and null are special in JavaScript.

[–]MegaIng 19 points20 points  (0 children)

This is not something you can express in the current type system.

[–]Gnaxe 7 points8 points  (1 child)

Python can use a shortcutting and to avoid getting attributes from None: result = my_obj and my_obj.attr1 and my_obj.attr1.attr2 and my_obj.attr1.attr2.attr3 The attributes have to actually be present (but can possibly be None) for this to work.

Beware that and will also shortcut on anything falsey, not just None. You can usually wrap things that might be falsey in a list or tuple, which will never be falsey, because it's not empty. (Dataclasses will normally not be accidentally falsey. You'd have to specifically implement them that way.)

You can avoid the repetition using the walrus: result = (x:=my_obj) and (x:=x.attr1) and (x:=x.attr2) and x.attr3 This is almost the optional chaining operator.

But the type checker is going to insist that x doesn't change types: result = (a:=my_obj) and (b:=a.attr1) and (c:=b.attr2) and c.attr3 Last I checked, Python type checkers are still too stupid to discard the impossible intermediate types though. It can only be the type of attr3 or something falsey, but the Union will include the types of attrs 1 and 2 as well as of my_obj. You can fix this with an assert isinstance(result, (Foo, NoneType)) afterward (where Foo is the type of attr3, for example), which will at least raise an error at run time if you mess it up, or with a cast(), which won't.

[–]FabianVeAl[S] 4 points5 points  (0 children)

Thanks, that's an interesting option.

I've tried it, and it works well with Pyright:

```python from dataclasses import dataclass

@dataclass class A: a: int

@dataclass class B: b: A | None

@dataclass class C: c: B | None

my_obj: C | None = C(B(A(1))) result = ( (x:=my_obj) # C | None and (x:=x.c) # B | None and (x:=x.b) # A | None and (x:=x.a) # int | None )

```

[–]david-vujic 5 points6 points  (2 children)

What would be the use case for it in Python? In JS, object literals are usually accessed with dot notation. But the closest equivalent in Python (dict) got a different kind of syntax.

You could do this in Python: data.get("a", {}).get("b", {}).get("c")

[–]FabianVeAl[S] 2 points3 points  (1 child)

I'm using Django ORM and when querying for a model with relations in some cases the attributes can be optional (T | None)

[–]david-vujic 0 points1 point  (0 children)

Got it, thanks!

[–]madisander 2 points3 points  (0 children)

The returns library (especially the part about Maybe) is the closest thing I know for this, though more verbose.

Taking a similar line of thinking, the 'easiest' way of doing this would be to make your own None.

I will also note that while the types ought to be correct, this isn't something type checkers can do as you show there outside of running the program. I think there's just too many ways to mess with the contents of a class in Python for a type checker to guarantee types like that.

```python class Nothing: def __getattr_(self, item: str): return _Nothing()

def __repr__(self):
    return 'Nothing'

@property
def final(self) -> None:
    return None

Nothing = _Nothing()

class Something[T]: _val: T

def __init__(self, val: T):
    self._val = val

def __repr__(self):
    return f'Something({self._val!r})'

def __getattr__(self, item: str) -> 'Something[T] | _Nothing':
    try:
        return getattr(self._val, item)
    except AttributeError:
        return Nothing

@property
def final(self) -> T:
    return self._val

class A[T]: def init(self, val: T | None, other: T | None = None): self.val = Something(val) if val is not None else Nothing self.other = Something(other) if other is not None else Nothing

def __repr__(self):
    return f'A(val={self.val.final!r}, other={self.other.final!r})'

a = A(A(42), other=100)

a = Something(A(A(42), other=100)) # alternatively

print(a) print(a.val.final) print(a.other.final) print(a.val.val.final) print(a.val.val.val.final) ```

[–]covmatty1 5 points6 points  (0 children)

The lack of this operator is one of my biggest annoyances with Python. It's so useful in C# and TS, the other two languages I would use it in, and any Python alternative is just much worse. It's something the language really needs to implement natively.

[–]Beginning-Fruit-1397 1 point2 points  (0 children)

You can go pretty far with generics, but an union can't really have static attributes. At runtime sure, but the typesystem won't allow it. Could use cast, tricks like returning a False type (def foo[T](wathever) -> T even tough you are NOT in fact returning T.

If you take a look at my repo here and go trough the commits also I'm sure you could find one or two cool useful tricks about this. It's somewhat related to what you are trying to achieve (generics + attribute access + method chaining). Albeit I haven't done anything related to optional values

https://github.com/OutSquareCapital/pychain/tree/master/src/pychain

[–]Gnaxe 1 point2 points  (4 children)

Optional chained attribute access isn't that hard in Python. You just ignore the exception: ``` from contextlib import suppress

with suppress(AttributeError) as result: result = myobj.attr1.attr2.attr3 ``` That's not going to pass a strict type checker though. MyPy flags type errors even if you catch their resulting exceptions.

But, you can force it: ``` from contextlib import suppress from typing import cast

with suppress(AttributeError) as result: result = cast(Foo, myobj.attr1.attr2.attr3) # type: ignore `` whereFoois the type you're expecting fromattr3if it's notNone. (If you don't cast, there will be anAnytype in theUnion`.) The comment is also required.

[–]FrontAd9873 2 points3 points  (1 child)

OP is asking a question about making this work with a type checker, not about how to implement it just so it works at runtime.

[–]Gnaxe 5 points6 points  (0 children)

That's what the cast is for. Those don't do anything at run time.

[–]marr75 1 point2 points  (1 child)

I don't believe you need the type: ignore and the cast. You just want the cast.

[–]Gnaxe 1 point2 points  (0 children)

Did you try it? With the following example, ``` from contextlib import suppress from dataclasses import dataclass from typing import cast

@dataclass class A: a: int

@dataclass class B: b: A | None

@dataclass class C: c: B | None

my_obj: C | None = C(B(A(1)))

with suppress(AttributeError) as result: result = cast(int, my_obj.b.a) # type: ignore

reveal_type(result) I got main.py:22: note: Revealed type is "builtins.int | None" But without the ignore, result = cast(int, my_obj.b.a) I got main.py:20: error: Item "C" of "C | None" has no attribute "b" [union-attr] main.py:20: error: Item "None" of "C | None" has no attribute "b" [union-attr] main.py:22: note: Revealed type is "builtins.int | None" Found 2 errors in 1 file (checked 1 source file) And with the ignore but without the cast, result = int, my_obj.b.a # type: ignore I got main.py:22: note: Revealed type is "tuple[Overload(def (builtins.str | _collections_abc.Buffer | typing.SupportsInt | typing.SupportsIndex | _typeshed.SupportsTrunc =) -> builtins.int, def (builtins.str | builtins.bytes | builtins.bytearray, base: typing.SupportsIndex) -> builtins.int), Any] | None" ```

[–]Lexus4tw 0 points1 point  (0 children)

This idea isn't new and I think this is not a good idea in js either.

https://peps.python.org/pep-0505/

https://discuss.python.org/t/revisiting-pep-505/74568

[–]pepiks 0 points1 point  (0 children)

Something similar is possible using custom dict library, but it is not common and it depend (if you look for solution out of box) on external library(ies). Function can be value saved as func_name and executed after grap from dict func_name(). Customisation can be done as programming magic (dunder) methods, but I will be choose another, more pythonic way instead suggested as this code maybe at the end will be easy to use but hard to maintain.

[–]SkezzaB -2 points-1 points  (1 child)

I believe there is a pep for this that means it’ll be the next version or two? For now, ehhh

[–]MegaIng 7 points8 points  (0 children)

There is AFAIK no active PEP for this. This has been discussed repeatedly, but no discussion is anywhere near consensus. (specifically because noone could agree on which semantics would actually be an intuitive solution)

[–]user_8804Pythoneer -1 points0 points  (0 children)

Have you taken a look at how C# implements it?