10

I would like to allow Mypy' strict_optional flag. However, consider this:

emails = [get_user(uuid).email for uuid in user_uuids]

where get_user could return None in theory, but in this use case, I know it can't (and am fine with getting an exception if it did). This would have to become:

emails = []
for uuid in user_uuids:
    user = get_user(uuid)
    assert user is not None
    emails.append(user.email)

In TypeScript, there's a non-null assertion operator which would allows you to just add a ! (as in getUser(uuid)!.email).

Is there any better or more elegant way to handle this problem?

Garrett
  • 4,007
  • 2
  • 41
  • 59
  • I suppose one option if the function `get_user` is able to be modified would be: instead of returning `Optional[str]`, it could return `str` (and throw an exception when a user is not found). Not ideal though... – Garrett Feb 03 '21 at 15:46
  • A trivial modification makes this work: `emails = [get_user(uuid).email for uuid in user_uuids if uuid]` – Tim Roberts Apr 08 '21 at 00:52
  • 1
    I don't think that works since `if uuid` will necessarily return True in my case, but `get_user(user_uuid)` may return `None` (even when `uuid` is not `None`). – Garrett Apr 08 '21 at 01:08
  • 1
    Yes, you're right, I flubbed that. – Tim Roberts Apr 08 '21 at 03:07
  • If you're okay with getting an exception on None return then can't you just keep it as is? – Keverly Apr 14 '21 at 22:40
  • Not if you're using `mypy` as a pre-commit hook since it fails – Garrett Apr 14 '21 at 23:21

5 Answers5

6

I found two ways that I think get close to a non-null assertion operator and therefore cast type Optional[User] to User:

1) Use typing.cast

from typing import cast

emails = [cast(User, get_user(uuid)).email for uuid in user_uuids]

2) Imitate non-null assertion with function

from typing import TypeVar

T = TypeVar('T')

def not_none(obj: Optional[T]) -> T:
    assert obj is not None
    return obj

emails = [not_none(get_user(uuid)).email for uuid in user_uuids]
Garrett
  • 4,007
  • 2
  • 41
  • 59
1

There is no reason you can't use the same call in the conditional, so

emails = [get_user(uuid).email for uuid in user_uuids if get_user(uuid)]

will work

Garrett
  • 4,007
  • 2
  • 41
  • 59
labroid
  • 452
  • 4
  • 14
  • That's true, that does work and might be the best option. Not as nice as a null-assertion operator for 2 reasons: (1) if you don't want to call `get_user` twice for some reason (like that it makes a roundtrip to the DB); (2) it makes the reader think that sometimes `get_user` is None (it technically has return type `Optional[User]`, but in this specific case, it can't be None which a non-null assertion operator nicely communicates to the reader). – Garrett Apr 08 '21 at 01:31
  • If you are OK with an exception as your question implies, perhaps would you be happier with [(get_user(uuid).email or raise someerror) for uuid in user_uuids] – labroid Apr 08 '21 at 01:50
  • I don't think that's valid Python syntax. `raise` must only appear as the first token on a line, right? – Garrett Apr 08 '21 at 05:14
  • 1
    You are right. Apparently both `raise` and `assert` are simple statements, so raising an error this way is difficult. That leaves defining a function external to the comprehension, which is uglier than your original workaround. I'll stick with my original answer :-). – labroid Apr 08 '21 at 12:34
1

@labroid has a good answer. One comment mentions that it's not ideal to call get_user twice, so I'll just build on labroid's answer to create a statement that only calls get_user once:

users = [
  {"uuid":"abc", "email":"email"}
]

def get_user(uuid):
  for user in users:
    if user["uuid"] == uuid:
      return user
    return None

user_uuids = ["abc", "def"]

emails = [user["email"] for user in [get_user(uuid) for uuid in user_uuids] if user != None]

print(emails)
Airistotal
  • 95
  • 8
1

Since I wouldn't want to call get_user twice. I would do something like this:

users = map(get_user, user_uuids)
emails = [user.email for user in users if user]

This feels pythonic more than anything else I've seen, to me.

RatherBKnitting
  • 411
  • 3
  • 14
0

Python and some other langauges feature short-circuiting so the following statement is perfectly fine

TestArr = [None, None, None, None]
ArrTest = [x.testFunc() for x in TestArr if x != None]
print(ArrTest)

and will return a blank list.

Although I'm unfamiliar with strict_optional.

Garrett
  • 4,007
  • 2
  • 41
  • 59
  • 3
    Generally if you want to check for None you should used "is" and "not is" instead of "==" and "!=", otherwise, this is how I'd do it – Keverly Apr 14 '21 at 22:32