Repository: python/mypy

Issue:
Type checking of None values
A common pattern in alot of functions is to use None to signify something alternative behavior.
It might be beneficial to include a convenience Maybe type that is a partial application of Union with None. 

I'm still familiarizing myself with the code and am not 100% what to do yet



Discussion:
I'd like to have Optional[x]  which means Union[x, None]. As long as None is compatible with every type (as it is now), Optional[x] would be equivalent with just x and would basically be just for documentation, which is not optimal.

More strict type checking of None values (i.e., turning None to an ordinary type) probably requires at least better type inference of conditional None checks. For example, consider this code:

```
def f(x: Optional[str]) -> bool:
    return x and x.startswith('a')
```

The code is valid, but inferring that the second reference to x is never None is not entirely trivial, and there are more complex cases that will probably have to be handled as well. The current implementation of union types supports some common idioms but not all of them.

In addition to isinstance guards, could we utilize something like single dispatch (https://docs.python.org/3/library/functools.html#functools.singledispatch) to handle these kind of assertions?

Ultimately this comes down to a pattern matching issue. I think using a dispatch decorator, we could shoehorn pattern matching in.

Mypy already has something like single dispatch (`@overload` -- it's actually multiple dispatch), but the problem is that most existing Python code doesn't use anything like it. Also, often you have None values in lists, for example, and I don't see how to easily dispatch on items in lists using a decorator-based approach without some pretty ugly code. I want to support common Python idioms without complex refactoring, and splitting a function into multiple variants feels pretty complex.

Of course, for new code, single/multiple dispatch function decorators may often make a lot of sense.

General pattern matching is tricky without new syntax. Repeated isinstance tests + type inference that knows about them is about as good as I've been able to come up with.

Nullable types is one of the most important features we'd like in a type checker for Python at Facebook. For Hack (our typed PHP variant), assuming that arguments don't accept "null" by default catches many simple bugs before they get committed.

So, all in all this is a crucial feature. That's not enough, though. Nullable types will be common in the wild. There are thousands of cases of optional ints in just the Facebook codebase. What that means is, optionality must be easy to express. Arguably `Optional[int]` is quite verbose. Hack uses `?int`, pytypedecl uses `int or None`, which is still shorter and reads like English.

As a side note: single dispatch was intentionally kept simple. The worry was that making it too powerful would possibly skew how future code is structured in Python to a form that is harder to debug and read. This is why we didn't even add method support (e.g. "skip the first argument"), let alone multiple dispatch. By just introducing single dispatch we single-handedly got rid of most problems with conflict resolution. I had to anyway re-implement C3 linearization to support abstract base classes, which is in my opinion the biggest win from the PEP.

I agree that nullable types are important for static type checking. Also, even though supporting them is non-trivial, it seems quite feasible. Much of the needed machinery is already implemented for union types.

I have some ideas for making syntax verbosity less of a problem. First, for arguments with a default value of `None`, nullability could be inferred:

```
def f(x: int = None) -> None: ...   # type of x is implicitly Optional[int]
```

Also, we could infer an optional type from assigning a `None` value:

```
x = None
if c:
    x = 1
# type of x is Optional[int]
```

Currently the type of `x` above would be inferred from the first assignment only, and thus an explicit annotation would be needed. The above idiom would only work for straightforward cases where the value of the variable cannot be read between two assignments (i.e., there are conceptually two or more initializers for the variable), and it would probably not work for two unrelated types, in order to catch programming errors early. The following code would thus be invalid:

```
x = 1
if c:
    x = 'x'  # Won't infer type Union[int, str] for x
```

Verbosity could still be an issue. `int or None` has the issue that the runtime value of the type would just be `int`, and we could not use it for runtime dispatch.

Most of the syntax decisions in mypy are still up for debate. I'm not completely against the syntax `int or None`, for example.

About your side note: Overloading in in mypy is actually pretty simple -- the variants are always checked in the order they are introduced in a source file, and the first match is chosen. There is no attempt to pick the most precise one. Actually, many forms of overlapping overload variants are disallowed by the type checker.

I'm confused how `Union[int, str]` would be technically any different than `Optional[int]`.

Isn't `Optional[int]` just sugar for `Union[NoneType]` ?

I feel that without a proof that `bool(c) != False`, the type inference engine should always union the types.

As for syntax, `int or None` is nice!
In fact rewriting it as `int | None` makes it look like Sum Types from ML Style languages.

Is the type `None` the same as the type `Union[]` i.e. Union of no types?

No, when used as a type, None is a shorthand for type(None). Union [] is
invalid , and so is Union [()].
On Apr 7, 2015 11:06 AM, "Dedoig" notifications@github.com wrote:

> Is the type None the same as the type Union[] i.e. Union of no types?
> 
> —
> Reply to this email directly or view it on GitHub.

Thanks. 

It seems Union[…] is already implemented and working, but type None (as disjoint from every other type) is not yet implemented. I was wondering of treating type None as Union[] might make it easier to implement…

On Apr 7, 2015, at 2:36 PM, Guido van Rossum notifications@github.com wrote:

> No, when used as a type, None is a shorthand for type(None). Union [] is
> invalid , and so is Union [()].
> On Apr 7, 2015 11:06 AM, "Dedoig" notifications@github.com wrote:
> 
> > Is the type None the same as the type Union[] i.e. Union of no types?
> > 
> > —
> > Reply to this email directly or view it on GitHub.
> > —
> > Reply to this email directly or view it on GitHub.

Type None does not mean "not any type". It means "must be the value None". This is primarily useful to be explicit about the return value of a function that has no result (because it still returns None):

```
def greeting() -> None:
    print('Hello, world')
```

Without the `-> None` the body of the function won't be type-checked and its return value will be assumed to be `Any`.

Technically, `Union[()]` _could_ be [a thing](http://en.wikipedia.org/wiki/Bottom_type), but I'm not sure how useful it would be in practice.  Maybe if you want to indicate that a function can raise exceptions?  E.g. `sys.exit()` could hypothetically look something like this:

```
def exit(value: Any) -> Union[()]:
    raise SystemExit(value)
```

See the dupe issue #875 for more discussion. 

The flag `--strict-optional` now exists. Is it useful to keep this ticket open?

IMO the ticket should remain open until strict optional checking is the default.


Resolving patch:
diff --git a/mypy/main.py b/mypy/main.py
--- a/mypy/main.py
+++ b/mypy/main.py
@@ -424,8 +424,10 @@ def add_invertible_flag(flag: str,
                         help="include fine-grained dependency information in the cache")
     parser.add_argument('--skip-version-check', action='store_true',
                         help="allow using cache written by older mypy version")
-    add_invertible_flag('--strict-optional', default=False, strict_flag=True,
-                        help="enable experimental strict Optional checks")
+    parser.add_argument('--strict-optional', action='store_true',
+                        help=argparse.SUPPRESS)
+    parser.add_argument('--no-strict-optional', action='store_false', dest='strict_optional',
+                        help="disable strict Optional checks (inverse: --no-strict-optional)")
     parser.add_argument('--strict-optional-whitelist', metavar='GLOB', nargs='*',
                         help="suppress strict Optional errors in all but the provided files "
                         "(experimental -- read documentation before using!).  "
diff --git a/mypy/options.py b/mypy/options.py
--- a/mypy/options.py
+++ b/mypy/options.py
@@ -120,7 +120,7 @@ def __init__(self) -> None:
         self.strict_boolean = False
 
         # Apply strict None checking
-        self.strict_optional = False
+        self.strict_optional = True
 
         # Show "note: In function "foo":" messages.
         self.show_error_context = False
diff --git a/runtests.py b/runtests.py
--- a/runtests.py
+++ b/runtests.py
@@ -274,16 +274,17 @@ def add_stdlibsamples(driver: Driver) -> None:
                 modules.append(module)
         if modules:
             driver.add_mypy_modules('stdlibsamples (%s)' % (version,), modules,
-                                    cwd=stdlibsamples_dir)
+                                    cwd=stdlibsamples_dir, extra_args=['--no-strict-optional'])
 
 
 def add_samples(driver: Driver) -> None:
     for f in find_files(os.path.join('test-data', 'samples'), suffix='.py'):
         if f == os.path.join('test-data', 'samples', 'crawl2.py'):
             # This test requires 3.5 for async functions
-            driver.add_mypy_cmd('file {}'.format(f), ['--python-version=3.5', f])
+            driver.add_mypy_cmd('file {}'.format(f), ['--python-version=3.5',
+                                                      '--no-strict-optional', f])
         else:
-            driver.add_mypy('file %s' % f, f)
+            driver.add_mypy('file %s' % f, '--no-strict-optional', f)
 
 
 def usage(status: int) -> None:


Test patch:
diff --git a/mypy/test/helpers.py b/mypy/test/helpers.py
--- a/mypy/test/helpers.py
+++ b/mypy/test/helpers.py
@@ -339,6 +339,8 @@ def parse_options(program_text: str, testcase: DataDrivenTestCase,
             raise RuntimeError('Specifying targets via the flags pragma is not supported.')
     else:
         options = Options()
+        # TODO: Enable strict optional in test cases by default (requires *many* test case changes)
+        options.strict_optional = False
 
     # Allow custom python version to override testcase_pyversion
     if (not flag_list or
diff --git a/mypy/test/testpythoneval.py b/mypy/test/testpythoneval.py
--- a/mypy/test/testpythoneval.py
+++ b/mypy/test/testpythoneval.py
@@ -49,7 +49,8 @@ def test_python_evaluation(testcase: DataDrivenTestCase) -> None:
     version.
     """
     assert testcase.old_cwd is not None, "test was not properly set up"
-    mypy_cmdline = ['--show-traceback', '--no-site-packages']
+    # TODO: Enable strict optional for these tests
+    mypy_cmdline = ['--show-traceback', '--no-site-packages', '--no-strict-optional']
     py2 = testcase.name.lower().endswith('python2')
     if py2:
         mypy_cmdline.append('--py2')
diff --git a/mypy/test/testtypegen.py b/mypy/test/testtypegen.py
--- a/mypy/test/testtypegen.py
+++ b/mypy/test/testtypegen.py
@@ -31,6 +31,7 @@ def run_case(self, testcase: DataDrivenTestCase) -> None:
 
             src = '\n'.join(testcase.input)
             options = Options()
+            options.strict_optional = False  # TODO: Enable strict optional checking
             options.use_builtins_fixtures = True
             options.show_traceback = True
             result = build.build(sources=[BuildSource('main', None, src)],
diff --git a/test-data/unit/check-generics.test b/test-data/unit/check-generics.test
--- a/test-data/unit/check-generics.test
+++ b/test-data/unit/check-generics.test
@@ -586,7 +586,7 @@ reveal_type(wrap(z)) # E: Revealed type is '__main__.Node[builtins.int, builtins
 main:13: error: Argument 2 to "Node" has incompatible type "int"; expected "str"
 
 [case testGenericTypeAliasesWrongAliases]
-# flags: --show-column-numbers --python-version 3.6
+# flags: --show-column-numbers --python-version 3.6 --no-strict-optional
 from typing import TypeVar, Generic, List, Callable, Tuple, Union
 T = TypeVar('T')
 S = TypeVar('S')
@@ -1052,7 +1052,7 @@ reveal_type(C[str]().b) # E: Revealed type is '__main__.B[builtins.str*, builtin
 [builtins fixtures/list.pyi]
 
 [case testGenericTypeAliasesTypeVarConstraints]
-# flags: --show-column-numbers
+# flags: --show-column-numbers --no-strict-optional
 from typing import TypeVar, Generic
 T = TypeVar('T', int, list)
 S = TypeVar('S', int, list)
diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test
--- a/test-data/unit/check-inference.test
+++ b/test-data/unit/check-inference.test
@@ -2056,7 +2056,7 @@ def f():
 reveal_type(x) # E: Revealed type is 'builtins.None'
 
 [case testLocalPartialTypesWithGlobalInitializedToNone3]
-# flags: --local-partial-types
+# flags: --local-partial-types --no-strict-optional
 x = None
 
 def f() -> None:
@@ -2079,7 +2079,7 @@ def g() -> None:
     reveal_type(x) # E: Revealed type is 'Union[builtins.str, builtins.None]'
 
 [case testLocalPartialTypesWithGlobalInitializedToNone4]
-# flags: --local-partial-types
+# flags: --local-partial-types --no-strict-optional
 a = None
 
 def f() -> None:
@@ -2197,7 +2197,7 @@ def f() -> None:
 [builtins fixtures/list.pyi]
 
 [case testLocalPartialTypesWithNestedFunction3]
-# flags: --local-partial-types
+# flags: --local-partial-types --no-strict-optional
 def f() -> None:
     a = None
     def g() -> None:
diff --git a/test-data/unit/check-tuples.test b/test-data/unit/check-tuples.test
--- a/test-data/unit/check-tuples.test
+++ b/test-data/unit/check-tuples.test
@@ -271,7 +271,7 @@ class B: pass
 [builtins fixtures/tuple.pyi]
 
 [case testMultipleAssignmentWithSquareBracketTuplesPython2]
-# flags: --python-version 2.7
+# flags: --python-version 2.7 --no-strict-optional
 from typing import Tuple
 
 def avoid_confusing_test_parser():