Repository: python/mypy

Issue:
Turn off implicit-optional by default
We'd require an explicit optional argument type in cases like this:

```py
def f(x: int = None) -> None:
    ...
```

Currently this is accepted by mypy, unless `--no-implicit-optional` is used. The idea would be to make `--no-implicit-optional` the default, and `--implicit-optional` can be used to get back to the current default behavior.


Discussion:
It might be a good idea to flip the switch sooner rather than later. If I understand things correctly, annotations relying on "implicit optional" are no longer PEP 484 compliant after https://github.com/python/peps/pull/689 and the number of these non-compliant annotations will keep on increasing as long as mypy allows them. 
@hukkinj1 A good point. We could try to get this included in the next public release as a breaking change, perhaps, if there no good arguments against this.

cc @ilevkivskyi 
I was wondering if we could change the behavior to mean, "the argument is optional, but explicit `None` is not allowed". Here's a simple, contrived use-case:
```python

class Point:
  ...

  @overload
  def update(self, other_point: Point) -> None: ...

  @overload
  def update(self, *, x: int = None, y: int = None) -> None: ...  # Keyword-only arguments that can't explicitly be `None`

  def update(self, *args: Any, **kwargs: Any) -> None:
    if args:
      self.x = args[0].x
      self.y = args[0].y
    else:
      self.x = kwargs.get("x", self.x)  # Can't be `None` because explicit `.update(x=None)` is not allowed
      self.y = kwargs.get("y", self.y)
```
PEP 484 is clear that the subexpression `x: int = None` should be flagged as an error going forward.

An ellipsis is already the standard way to indicate this behavior within an overload signature: `x: int = ...`. Or you can use the actual default value (`x: int = 0`).
What is the status of this almost 2 years in? Clearly `foo(x: int = None)` is more concise than `foo(x: Optional[int] = None)`. While the latter may seem _more correct_ than the former, the intent of the former is transparent for anyone understanding type hints. There are other situation in which brevity has been chosen over "correctness". For example `None` is allowed to be used instead of `NoneType` in unions.

Resolving patch:
diff --git a/mypy/fastparse.py b/mypy/fastparse.py
--- a/mypy/fastparse.py
+++ b/mypy/fastparse.py
@@ -1003,7 +1003,7 @@ def do_func_def(
         return retval
 
     def set_type_optional(self, type: Type | None, initializer: Expression | None) -> None:
-        if self.options.no_implicit_optional:
+        if not self.options.implicit_optional:
             return
         # Indicate that type should be wrapped in an Optional if arg is initialized to None.
         optional = isinstance(initializer, NameExpr) and initializer.name == "None"
diff --git a/mypy/main.py b/mypy/main.py
--- a/mypy/main.py
+++ b/mypy/main.py
@@ -720,10 +720,9 @@ def add_invertible_flag(
         "https://mypy.readthedocs.io/en/stable/kinds_of_types.html#no-strict-optional",
     )
     add_invertible_flag(
-        "--no-implicit-optional",
+        "--implicit-optional",
         default=False,
-        strict_flag=True,
-        help="Don't assume arguments with default values of None are Optional",
+        help="Assume arguments with default values of None are Optional",
         group=none_group,
     )
     none_group.add_argument("--strict-optional", action="store_true", help=argparse.SUPPRESS)
diff --git a/mypy/options.py b/mypy/options.py
--- a/mypy/options.py
+++ b/mypy/options.py
@@ -39,14 +39,14 @@ class BuildType:
     "disallow_untyped_defs",
     "enable_error_code",
     "enabled_error_codes",
-    "follow_imports",
     "follow_imports_for_stubs",
+    "follow_imports",
     "ignore_errors",
     "ignore_missing_imports",
+    "implicit_optional",
     "implicit_reexport",
     "local_partial_types",
     "mypyc",
-    "no_implicit_optional",
     "strict_concatenate",
     "strict_equality",
     "strict_optional",
@@ -160,8 +160,8 @@ def __init__(self) -> None:
         self.color_output = True
         self.error_summary = True
 
-        # Don't assume arguments with default values of None are Optional
-        self.no_implicit_optional = False
+        # Assume arguments with default values of None are Optional
+        self.implicit_optional = False
 
         # Don't re-export names unless they are imported with `from ... as ...`
         self.implicit_reexport = True


Test patch:
diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test
--- a/test-data/unit/check-classes.test
+++ b/test-data/unit/check-classes.test
@@ -2933,8 +2933,9 @@ class B: pass
 
 
 [case testConstructInstanceWith__new__]
+from typing import Optional
 class C:
-    def __new__(cls, foo: int = None) -> 'C':
+    def __new__(cls, foo: Optional[int] = None) -> 'C':
         obj = object.__new__(cls)
         return obj
 
diff --git a/test-data/unit/check-fastparse.test b/test-data/unit/check-fastparse.test
--- a/test-data/unit/check-fastparse.test
+++ b/test-data/unit/check-fastparse.test
@@ -106,6 +106,7 @@ class C:
 [builtins fixtures/property.pyi]
 
 [case testFastParsePerArgumentAnnotations]
+# flags: --implicit-optional
 
 class A: pass
 class B: pass
@@ -130,6 +131,7 @@ def f(a,        # type: A
 [out]
 
 [case testFastParsePerArgumentAnnotationsWithReturn]
+# flags: --implicit-optional
 
 class A: pass
 class B: pass
diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test
--- a/test-data/unit/check-flags.test
+++ b/test-data/unit/check-flags.test
@@ -843,6 +843,7 @@ standard.f(None)
 [file mypy.ini]
 \[mypy]
 strict_optional = False
+implicit_optional = true
 \[mypy-optional]
 strict_optional = True
 
@@ -862,6 +863,7 @@ standard.f(None)
 [file pyproject.toml]
 \[tool.mypy]
 strict_optional = false
+implicit_optional = true
 \[[tool.mypy.overrides]]
 module = 'optional'
 strict_optional = true
diff --git a/test-data/unit/check-functions.test b/test-data/unit/check-functions.test
--- a/test-data/unit/check-functions.test
+++ b/test-data/unit/check-functions.test
@@ -465,6 +465,7 @@ if int():
 
 
 [case testCallingFunctionsWithDefaultArgumentValues]
+# flags: --implicit-optional --no-strict-optional
 
 a, b = None, None # type: (A, B)
 if int():
diff --git a/test-data/unit/check-kwargs.test b/test-data/unit/check-kwargs.test
--- a/test-data/unit/check-kwargs.test
+++ b/test-data/unit/check-kwargs.test
@@ -24,6 +24,7 @@ class A: pass
 class B: pass
 
 [case testOneOfSeveralOptionalKeywordArguments]
+# flags: --implicit-optional
 import typing
 def f(a: 'A' = None, b: 'B' = None, c: 'C' = None) -> None: pass
 f(a=A())
@@ -219,6 +220,7 @@ f(a, **b)
 [builtins fixtures/dict.pyi]
 
 [case testKeywordArgAfterVarArgs]
+# flags: --implicit-optional
 import typing
 def f(*a: 'A', b: 'B' = None) -> None: pass
 f()
@@ -235,6 +237,7 @@ class B: pass
 [builtins fixtures/list.pyi]
 
 [case testKeywordArgAfterVarArgsWithBothCallerAndCalleeVarArgs]
+# flags: --implicit-optional --no-strict-optional
 from typing import List
 def f(*a: 'A', b: 'B' = None) -> None: pass
 a = None # type: List[A]
diff --git a/test-data/unit/check-optional.test b/test-data/unit/check-optional.test
--- a/test-data/unit/check-optional.test
+++ b/test-data/unit/check-optional.test
@@ -127,6 +127,7 @@ def f(x: None) -> None: pass
 f(None)
 
 [case testInferOptionalFromDefaultNone]
+# flags: --implicit-optional
 def f(x: int = None) -> None:
   x + 1  # E: Unsupported left operand type for + ("None") \
          # N: Left operand is of type "Optional[int]"
@@ -140,6 +141,7 @@ def f(x: int = None) -> None:  # E: Incompatible default for argument "x" (defau
 [out]
 
 [case testInferOptionalFromDefaultNoneComment]
+# flags: --implicit-optional
 def f(x=None):
   # type: (int) -> None
   x + 1  # E: Unsupported left operand type for + ("None") \
diff --git a/test-data/unit/check-varargs.test b/test-data/unit/check-varargs.test
--- a/test-data/unit/check-varargs.test
+++ b/test-data/unit/check-varargs.test
@@ -82,6 +82,7 @@ class C: pass
 [builtins fixtures/list.pyi]
 
 [case testCallingVarArgsFunctionWithDefaultArgs]
+# flags: --implicit-optional --no-strict-optional
 
 a = None # type: A
 b = None # type: B
@@ -388,12 +389,14 @@ class B(A): pass
 [builtins fixtures/list.pyi]
 
 [case testCallerVarArgsAndDefaultArgs]
+# flags: --implicit-optional --no-strict-optional
 
 a, b = None, None # type: (A, B)
-f(*())        # Fail
-f(a, *[a])    # Fail
-f(a, b, *[a]) # Fail
-f(*(a, a, b)) # Fail
+f(*())        # E: Too few arguments for "f"
+f(a, *[a])    # E: Argument 2 to "f" has incompatible type "*List[A]"; expected "Optional[B]" \
+              # E: Argument 2 to "f" has incompatible type "*List[A]"; expected "B"
+f(a, b, *[a]) # E: Argument 3 to "f" has incompatible type "*List[A]"; expected "B"
+f(*(a, a, b)) # E: Argument 1 to "f" has incompatible type "*Tuple[A, A, B]"; expected "Optional[B]"
 f(*(a,))
 f(*(a, b))
 f(*(a, b, b, b))
@@ -407,12 +410,6 @@ def f(a: 'A', b: 'B' = None, *c: 'B') -> None:
 class A: pass
 class B: pass
 [builtins fixtures/list.pyi]
-[out]
-main:3: error: Too few arguments for "f"
-main:4: error: Argument 2 to "f" has incompatible type "*List[A]"; expected "Optional[B]"
-main:4: error: Argument 2 to "f" has incompatible type "*List[A]"; expected "B"
-main:5: error: Argument 3 to "f" has incompatible type "*List[A]"; expected "B"
-main:6: error: Argument 1 to "f" has incompatible type "*Tuple[A, A, B]"; expected "Optional[B]"
 
 [case testVarArgsAfterKeywordArgInCall1]
 # see: mypy issue #2729
diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test
--- a/test-data/unit/fine-grained.test
+++ b/test-data/unit/fine-grained.test
@@ -7942,7 +7942,7 @@ class Foo(a.I):
 ==
 
 [case testImplicitOptionalRefresh1]
-# flags: --strict-optional
+# flags: --strict-optional --implicit-optional
 from x import f
 def foo(x: int = None) -> None:
     f()