balu
balu

Reputation: 3831

What is the inferred type of a function argument with a default value but no type annotation? How about a variable initialized as 'None'?

What is the type (in the sense of type annotations, not type()) of variables and parameters that come without a type annotation but with an initial value? E.g.,

foo = 2
bar = False
baz = MyClass()
bazz = None

Judging from the Mypy docs and example code in the Python docs, foo, bar, baz would currently be assigned the types int, False and MyClass. Has this been standardized anywhere, though? And what about bazz? And what about

def my_func(param1 = 2, param2 = False): 
    ...

? Would a type checker enforce arguments passed to my_func to be of type int and bool, respectively?

Note that I'm not that much interested in the status quo, e.g. the current implementation of type checkers like Mypy. Rather, I'm wondering whether the answer to my questions has been standardized anywhere. Unfortunately, PEP-484 does not seem to say anything about this, apart from:

Type checkers are expected to attempt to infer as much information as necessary.

…but it is unclear what this really means in the above situation. (After all, param1 = 2 might only be the default value and its actual type might be more complex in reality.)

Upvotes: 3

Views: 2034

Answers (2)

Christopher Peisert
Christopher Peisert

Reputation: 24154

Question 1: What is the inferred type of a function argument with a default value but no type annotation?

In 2017, Guido van Rossum, the creator of the Python language, suggested changing PEP 484 to specify that function arguments would infer their types from default values.

However, as of October 2020, PEP 484 states in section "The Any type:

A function parameter without an annotation is assumed to be annotated with Any.

In the discussion points below from mypy issue #3090 (Infer argument type from default value), Jukka reinforces the fact that default values do not change the default behavior of inferring unannotated parameters as having type Any.

Jukka Lehtosalo wrote:

Mypy follows PEP 484. A function parameter without an annotation is the same as having an Any annotation, and there is no exception for default values. Here is some rationale for this:

  • Often the default argument is not enough to infer a type. For example, None doesn't give enough context. With the current rules this doesn't pose a difficulty.
  • The default argument may not be enough to infer the correct type. For example, if the default is '' in Python 2, the correct type could well be Union[str, unicode]. If the default is 0, the correct type might well be float. If the programmer understands the rules for type inference they could override the default type with an annotation as needed, but this would add some extra complexity.
  • The default value could be a complex expression such as a function call, and thus the type may not [be] obvious from just the default value. Having an annotation makes code like this easier to read.
  • If a function has no annotation at all, then the argument type would have to be Any anyway. The current rule makes this consistent across functions with no annotations and functions with partial annotations.

It all boils down to the current rule being simple and obvious, and in practice it doesn't feel much of a burden to add a few extra : int annotations. There is no deep technical reason for the current rule, though it makes type inference easier for mypy.

Then Guido responded:

OTOH most of those also apply to regular assignments, and there the rule is that

x = 0

infers type int for x. In practice this is very common and useful and only occasionally needs help. So I think we might use the same rule for default values and the complexity of explaining things wouldn't really change. I'm willing to make this a PEP 484 change if enough people care.

Question 2: What is the inferred type of a variable initialized as None?

When a variable is initialized as None, it is directly inferred that the variable is allowed to be explicitly assigned to None. If the variable is then later assigned a value, such as:

bazz = None
bazz = 42  # type: Optional[int]

then the type is inferred to be Optional[int]. Since the inferred type of bazz is Optional[int], it can later be reassigned None without error.

bazz = None
bazz = 42
bazz = None  # Okay

However, if bazz had not been initialized as None, then the following would be an error:

bazz = 42
bazz = None  # Error: expression has type "None", variable has type "int"

Inferred types of variables initialized without type annotations

PEP 484 does not explicitly discuss inferring the types of unannotated variables based on the types of their assigned values. However, it can be inferred from the comments in the PEP 484 examples, that the types of unannotated variables are indeed inferred based on assignment.

Example from PEP 484 section "Scoping rules for type variables"

T = TypeVar('T')
S = TypeVar('S')
class Foo(Generic[T]):
    def method(self, x: T, y: S) -> S:
        ...

x = Foo()               # type: Foo[int]
y = x.method(0, "abc")  # inferred type of y is str

Example from PEP 484 section "Instantiating generic classes and type erasure"

from typing import TypeVar, Generic

T = TypeVar('T')

class Node(Generic[T]):
    x = None  # type: T # Instance attribute (see below)
    def __init__(self, label: T = None) -> None:
        ...

x = Node('')  # Inferred type is Node[str]
y = Node(0)   # Inferred type is Node[int]
z = Node()    # Inferred type is Node[Any]

Upvotes: 4

Ignacio Vergara Kausel
Ignacio Vergara Kausel

Reputation: 6026

Concerning the types situation of this block

foo = 2
bar = False
baz = MyClass()
bazz = None

they do have types that you could annotate if you want them to be checked. In my experience, there are certain cases where mypy manages to infer some types, but usually is because they're being annotated somewhere else "earlier"like in function signatures of attributes in classes or instances.

Thus, the block

foo: int = 2
bar: bool = False
baz: MyClass = MyClass()
bazz: Optional[?] = None

In the case of baz the class itself is a type, so you annotate it with the name of the class. For bazz I left a ? since you should have an expectation of the type it'll have. Maybe it can be a couple of possible types and use Union[int, float] or maybe you don't really know/care and use Any as type.

Now, about the function

def my_func(param1 = 2, param2 = False): 
    ...

If the signature of that function would be annotated, the type checker would tell you if it's being called correctly.

def my_func(param1: int = 2, param2: bool = False): 
    ...

The type checker is not an enforcer, python continues being a dynamic language. Although there could be tools/libraries that exploit the annotations to do some runtime enforcement of types.

Upvotes: 0

Related Questions