Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python typehint for os.getenv causes downstream incompatible type errors

When using os.getenv to retrieve environment variables, the default behavior returns a type of Optional[str]. This is problematic as any downstream methods/functions that utilize these variables will likely be defined to accept a str type explicitly. Is there an accepted usage to get around this issue or enforce the str return type?

In the stub file definition for getenv in typeshed you can find that getenv can have a return type of Optional[str] or Union[str, T_] depending on the usage of the default kwarg.

The four options I can see as yet are:

  1. Define any downstream operations to accept Optional[str] types as arguments. This doesn't feel particularly right as a function/method may not be structured in a way that the Optional type makes sense. i.e. the operation has no reason for a particular argument to be None.
  2. Use the default kwarg for getenv and provide a str default value. This seems more correct, but requires that one set a default value for every usage of getenv. The only problem I can see with this is that doing so may be confounding for testing or usage in different environments.
  3. Define some kind of variable checking function. This could be a function that accepts the name of an environment variable to load, explicitly returns a string, and raises an error if the environment variable doesn't exist.
  4. Explicitly set the type of the value returned by getenv to be a str. I really don't like this as it expects the environment to always be properly configured which, in my experience, is not a good assumption.

Find below an example that raises a mypy error.

import os

SOME_VAR = os.getenv("SOME_VAR")


def some_func(val: str) -> None:
    print(f"Loaded env var: {val}")


some_func(SOME_VAR)

The above raises the mypy error:

error: Argument 1 to "some_func" has incompatible type "Optional[str]"; expected "str"

like image 300
Grr Avatar asked Feb 18 '20 16:02

Grr


1 Answers

tl;dr Use os.environ['SOME_VAR'] if you're sure it's always there


os.getenv can and does return None -- mypy is being helpful in showing you have a bug there:

>>> repr(os.getenv('DOES_NOT_EXIST'))
'None'
>>> repr(os.getenv('USER'))
"'asottile'"

Alternatively, you can convince mypy that it is of the type you expect in two different ways:

  1. utilizing assertions:
x = os.getenv('SOME_VAR')
assert x is not None, x
# mypy will believe that it is non-None after this point
  1. utilizing a cast:

    from typing import cast
    
    x = cast(str, os.getenv('SOME_VAR'))
    # mypy will believe that it is a `str` after this point
    

(the cast has some downsides in that it is never checked, whereas the assertion will hopefully lead to a test failure)

I would suggest not ignoring this error / working around it and instead use os.environ['SOME_VAR'] for things you expect to always be there, or write a condition to check for the error case when it is missing

like image 51
Anthony Sottile Avatar answered Oct 21 '22 01:10

Anthony Sottile