11

The following code is rejected by mypy as expected:

def foo(value: int) -> None:
    print(value, type(value))
foo(None)

output:

error: Argument 1 to "foo" has incompatible type "None"; expected "int"

But after introducing a default parameter of None, there is no error anymore:

def foo(value: int=None) -> None:
    print(value, type(value))
foo(None)

I would expect mypy to only allow None (as argument and as the default) if we change value from int to Optional[int], but it seems like this is not needed. Why?

bad_coder
  • 11,289
  • 20
  • 44
  • 72
Tobias Hermann
  • 9,936
  • 6
  • 61
  • 134

2 Answers2

15

When you make a keyword argument accept None, mypy will implicitly make that argument be of type Optional[Blah] if it isn't already. You can see this by adding the reveal_type(...) function to your code and running mypy:

def foo(value: int = None) -> None:
    print(value, type(value))

reveal_type(foo)
foo(None)

The output would be:

test.py:4: error: Revealed type is 'def (value: Union[builtins.int, None] =)'

(Be sure to delete reveal_type before actually running your code though, since the function doesn't actually exist at runtime -- it's just special-cased by mypy to help with debugging.)

This behavior exists mostly because it helps makes the signatures of functions less noisy. After all, if value, at some point, is allowed to be None, clearly it must accept both ints and None. In that case, why not just infer the type to be Optional[int] (which is equivalent to Union[int, None], btw) so the user doesn't need to repeat the same info twice?

Of course, not everybody likes this behavior: some people prefer being more explicit. In that case, run mypy with the --no-implicit-optional flag. That will produce the following output:

test.py:1: error: Incompatible default for argument "value" (default has type "None", argument has type "int")
test.py:4: error: Revealed type is 'def (value: builtins.int =)'
test.py:5: error: Argument 1 to "foo" has incompatible type "None"; expected "int"

You'd need to change your function signature though, of course.

If you'd like to raise the strictness of mypy in various other ways, try passing the --strict flag. That will automatically enable --no-implicit-optional and several other strictness flags. For more details, run mypy --help.

Michael0x2a
  • 58,192
  • 30
  • 175
  • 224
  • 1
    Wow, perfect answer. Thank you very much. Yes, I'm the kind of guy who prefers strict behavior regarding types, so I've just enabled this flag for our whole code base and now am working through the errors. ;) – Tobias Hermann Jun 28 '18 at 16:24
3

Adding some references with historical depth to @Michael0x2a's excellent answer. The recommend inference rule that a default value of None on a signature parameter should make its hinted type implicitly be considered as Optional[type] was initially established in PEP 484 but has in the meanwhile changed.

Union types - PEP 484

A past version of this PEP allowed type checkers to assume an optional type when the default value is None, as in this code:

def handle_employee(e: Employee = None): ...

This would have been treated as equivalent to:

def handle_employee(e: Optional[Employee] = None) -> None: ...

This is no longer the recommended behavior. Type checkers should move towards requiring the optional type to be made explicit.

If we look at PEP 484's revision history we arrive at "GitHub's blame" which in turn gives its reasoning in Pull request #689 and references back to typing issue #275.

bad_coder
  • 11,289
  • 20
  • 44
  • 72