Search code examples
pythontype-hinting

Should Optional type annotation be used when an initialized input parameter is immediately set in the function body?


In Python we often have a situation where the default argument for some input parameter is None and, if that is the case, we immediately initialize that variable at the top of the function body for use in the rest of the function. One common use case is for mutable default arguments:

def foo(input_list = None):
    if input_list is None:
        input_list = []
    input_list.append('bar')
    print(input_list)

What would be the appropriate way to type hint this?

from typing import List

def foo(input_list: List = None):
    if input_list is None:
        input_list = []
    input_list.append('bar')
    print(input_list)

or

from typing import List, Optional

def foo(input_list: Optional[List] = None):
    if input_list is None:
        input_list = []
    input_list.append('bar')
    print(input_list)

The core of the question is this: From the caller's perspective it is ok to pass in either a list or nothing. So from the callers perspective the input_list parameter is Optional. But within the body of the function, it is necessary that input_list is indeed a List and not None. So is the Optional flag meant to be an indicator to the caller that it is optional to pass that parameter in (in which case the second code block would be correct and the type hint in the first block would be too restrictive), or is it actually a signal to the body of the function that input_list may be either a List or None (in which case the second code block would be wrong since None is not acceptable)?

I lean towards not including Optional because users aren't really supposed to pass anything other than a list into input_list. It's true that they can pass in None, and nothing bad will happen. But that's not really the intent.

Note that I am aware that in at least some cases param: type = None will be parsed as if it was equal to param: Optional[type] = None. That doesn't actually give me clarity on how I should use the Optional annotation.


Solution

  • Depending on the type checker you're using and how it's configured, assigning a parameter a default value of None implicitly makes it Optional without you having to say so.

    Within the function body the type should automatically be narrowed by the if and the resulting assignment.

    Both of these behaviors can be demonstrated by adding reveal_type statements and running mypy:

    def foo(input_list: list[str] = None):
        reveal_type(input_list)  # Revealed type is "Union[builtins.list[builtins.str], None]"
        if input_list is None:
            input_list = []
        reveal_type(input_list)  # Revealed type is "builtins.list[builtins.str]"