Skip to content

Commit 8df6adc

Browse files
authored
Allow returning inferred None from functions (#5663)
Fixes #4214 As discussed in the issue, assigning an _inferred_ (from generics, overloads, or lambdas) `None` type should be always allowed, while prohibiting assigning function calls that have an explicit `None` return type may have some value (at least people don't complain much about this). Note: this error is quite nasty for dataclasses, where it flags a legitimate typical use case (see tests).
1 parent 391cbd3 commit 8df6adc

10 files changed

+141
-16
lines changed

mypy/checkexpr.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
ConditionalExpr, ComparisonExpr, TempNode, SetComprehension,
3232
DictionaryComprehension, ComplexExpr, EllipsisExpr, StarExpr, AwaitExpr, YieldExpr,
3333
YieldFromExpr, TypedDictExpr, PromoteExpr, NewTypeExpr, NamedTupleExpr, TypeVarExpr,
34-
TypeAliasExpr, BackquoteExpr, EnumCallExpr, TypeAlias, ClassDef, Block, SymbolTable,
34+
TypeAliasExpr, BackquoteExpr, EnumCallExpr, TypeAlias, ClassDef, Block, SymbolNode,
3535
ARG_POS, ARG_OPT, ARG_NAMED, ARG_STAR, ARG_STAR2, MODULE_REF, LITERAL_TYPE, REVEAL_TYPE
3636
)
3737
from mypy.literals import literal
@@ -329,11 +329,48 @@ def visit_call_expr_inner(self, e: CallExpr, allow_none_return: bool = False) ->
329329
self.check_protocol_issubclass(e)
330330
if isinstance(ret_type, UninhabitedType) and not ret_type.ambiguous:
331331
self.chk.binder.unreachable()
332-
if not allow_none_return and isinstance(ret_type, NoneTyp):
332+
if not allow_none_return and self.always_returns_none(e.callee):
333333
self.chk.msg.does_not_return_value(callee_type, e)
334334
return AnyType(TypeOfAny.from_error)
335335
return ret_type
336336

337+
def always_returns_none(self, node: Expression) -> bool:
338+
"""Check if `node` refers to something explicitly annotated as only returning None."""
339+
if isinstance(node, RefExpr):
340+
if self.defn_returns_none(node.node):
341+
return True
342+
if isinstance(node, MemberExpr) and node.node is None: # instance or class attribute
343+
typ = self.chk.type_map.get(node.expr)
344+
if isinstance(typ, Instance):
345+
info = typ.type
346+
elif (isinstance(typ, CallableType) and typ.is_type_obj() and
347+
isinstance(typ.ret_type, Instance)):
348+
info = typ.ret_type.type
349+
else:
350+
return False
351+
sym = info.get(node.name)
352+
if sym and self.defn_returns_none(sym.node):
353+
return True
354+
return False
355+
356+
def defn_returns_none(self, defn: Optional[SymbolNode]) -> bool:
357+
"""Check if `defn` can _only_ return None."""
358+
if isinstance(defn, FuncDef):
359+
return (isinstance(defn.type, CallableType) and
360+
isinstance(defn.type.ret_type, NoneTyp))
361+
if isinstance(defn, OverloadedFuncDef):
362+
return all(isinstance(item.type, CallableType) and
363+
isinstance(item.type.ret_type, NoneTyp) for item in defn.items)
364+
if isinstance(defn, Var):
365+
if (not defn.is_inferred and isinstance(defn.type, CallableType) and
366+
isinstance(defn.type.ret_type, NoneTyp)):
367+
return True
368+
if isinstance(defn.type, Instance):
369+
sym = defn.type.type.get('__call__')
370+
if sym and self.defn_returns_none(sym.node):
371+
return True
372+
return False
373+
337374
def check_runtime_protocol_test(self, e: CallExpr) -> None:
338375
for expr in mypy.checker.flatten(e.args[1]):
339376
tp = self.chk.type_map[expr]
@@ -3150,6 +3187,7 @@ def visit_yield_from_expr(self, e: YieldFromExpr, allow_none_return: bool = Fals
31503187
if isinstance(actual_item_type, AnyType):
31513188
expr_type = AnyType(TypeOfAny.from_another_any, source_any=actual_item_type)
31523189
else:
3190+
# Treat `Iterator[X]` as a shorthand for `Generator[X, None, Any]`.
31533191
expr_type = NoneTyp()
31543192

31553193
if not allow_none_return and isinstance(expr_type, NoneTyp):

mypy/semanal.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2017,7 +2017,7 @@ def analyze_name_lvalue(self,
20172017
not self.type)
20182018
if (add_global or nested_global) and lval.name not in self.globals:
20192019
# Define new global name.
2020-
v = self.make_name_lvalue_var(lval, GDEF)
2020+
v = self.make_name_lvalue_var(lval, GDEF, not explicit_type)
20212021
self.globals[lval.name] = SymbolTableNode(GDEF, v)
20222022
elif isinstance(lval.node, Var) and lval.is_new_def:
20232023
if lval.kind == GDEF:
@@ -2029,7 +2029,7 @@ def analyze_name_lvalue(self,
20292029
lval.name not in self.global_decls[-1] and
20302030
lval.name not in self.nonlocal_decls[-1]):
20312031
# Define new local name.
2032-
v = self.make_name_lvalue_var(lval, LDEF)
2032+
v = self.make_name_lvalue_var(lval, LDEF, not explicit_type)
20332033
self.add_local(v, lval)
20342034
if lval.name == '_':
20352035
# Special case for assignment to local named '_': always infer 'Any'.
@@ -2038,16 +2038,16 @@ def analyze_name_lvalue(self,
20382038
elif not self.is_func_scope() and (self.type and
20392039
lval.name not in self.type.names):
20402040
# Define a new attribute within class body.
2041-
v = self.make_name_lvalue_var(lval, MDEF)
2042-
v.is_inferred = not explicit_type
2041+
v = self.make_name_lvalue_var(lval, MDEF, not explicit_type)
20432042
self.type.names[lval.name] = SymbolTableNode(MDEF, v)
20442043
else:
20452044
self.make_name_lvalue_point_to_existing_def(lval, explicit_type, final_cb)
20462045

2047-
def make_name_lvalue_var(self, lvalue: NameExpr, kind: int) -> Var:
2046+
def make_name_lvalue_var(self, lvalue: NameExpr, kind: int, inferred: bool) -> Var:
20482047
"""Return a Var node for an lvalue that is a name expression."""
20492048
v = Var(lvalue.name)
20502049
v.set_line(lvalue)
2050+
v.is_inferred = inferred
20512051
if kind == MDEF:
20522052
assert self.type is not None
20532053
v.info = self.type

test-data/unit/check-bound.test

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ class C(Generic[T]):
5656
return self.t
5757
c1 = None # type: C[None]
5858
c1.get()
59-
d = c1.get() # E: "get" of "C" does not return a value
59+
d = c1.get()
60+
reveal_type(d) # E: Revealed type is 'None'
6061

6162

6263
[case testBoundAny]
@@ -82,7 +83,8 @@ def f(g: Callable[[], T]) -> T:
8283
return g()
8384
def h() -> None: pass
8485
f(h)
85-
a = f(h) # E: "f" does not return a value
86+
a = f(h)
87+
reveal_type(a) # E: Revealed type is 'None'
8688

8789

8890
[case testBoundInheritance]

test-data/unit/check-dataclasses.test

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,3 +500,27 @@ class A:
500500
reveal_type(cls()) # E: Revealed type is 'T`-1'
501501
return cls()
502502
[builtins fixtures/classmethod.pyi]
503+
504+
[case testNoComplainFieldNone]
505+
# flags: --python-version 3.6
506+
# flags: --no-strict-optional
507+
from dataclasses import dataclass, field
508+
from typing import Optional
509+
510+
@dataclass
511+
class Foo:
512+
bar: Optional[int] = field(default=None)
513+
[builtins fixtures/list.pyi]
514+
[out]
515+
516+
[case testNoComplainFieldNoneStrict]
517+
# flags: --python-version 3.6
518+
# flags: --strict-optional
519+
from dataclasses import dataclass, field
520+
from typing import Optional
521+
522+
@dataclass
523+
class Foo:
524+
bar: Optional[int] = field(default=None)
525+
[builtins fixtures/list.pyi]
526+
[out]

test-data/unit/check-expressions.test

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -938,6 +938,7 @@ a, o = None, None # type: (A, object)
938938
a = f() # E: "f" does not return a value
939939
o = a() # E: Function does not return a value
940940
o = A().g(a) # E: "g" of "A" does not return a value
941+
o = A.g(a, a) # E: "g" of "A" does not return a value
941942
A().g(f()) # E: "f" does not return a value
942943
x: A = f() # E: "f" does not return a value
943944
f()

test-data/unit/check-functions.test

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2246,8 +2246,7 @@ def h() -> Dict[Union[str, int], str]:
22462246

22472247
def i() -> List[Union[int, float]]:
22482248
x: List[int] = [1]
2249-
return x # E: Incompatible return value type (got "List[int]", expected "List[Union[int, float]]") \
2250-
# N: Perhaps you need a type annotation for "x"? Suggestion: "List[Union[int, float]]"
2249+
return x # E: Incompatible return value type (got "List[int]", expected "List[Union[int, float]]")
22512250

22522251
[builtins fixtures/dict.pyi]
22532252

@@ -2319,3 +2318,61 @@ main:6: error: b: builtins.int
23192318
main:6: error: c: builtins.int
23202319
main:9: error: Revealed local types are:
23212320
main:9: error: a: builtins.float
2321+
2322+
[case testNoComplainOverloadNone]
2323+
# flags: --no-strict-optional
2324+
from typing import overload, Optional
2325+
@overload
2326+
def bar(x: None) -> None:
2327+
...
2328+
@overload
2329+
def bar(x: int) -> str:
2330+
...
2331+
def bar(x: Optional[int]) -> Optional[str]:
2332+
if x is None:
2333+
return None
2334+
return "number"
2335+
2336+
reveal_type(bar(None)) # E: Revealed type is 'None'
2337+
[builtins fixtures/isinstance.pyi]
2338+
[out]
2339+
2340+
[case testNoComplainOverloadNoneStrict]
2341+
# flags: --strict-optional
2342+
from typing import overload, Optional
2343+
@overload
2344+
def bar(x: None) -> None:
2345+
...
2346+
@overload
2347+
def bar(x: int) -> str:
2348+
...
2349+
def bar(x: Optional[int]) -> Optional[str]:
2350+
if x is None:
2351+
return None
2352+
return "number"
2353+
2354+
reveal_type(bar(None)) # E: Revealed type is 'None'
2355+
[builtins fixtures/isinstance.pyi]
2356+
[out]
2357+
2358+
[case testNoComplainInferredNone]
2359+
# flags: --no-strict-optional
2360+
from typing import TypeVar, Optional
2361+
T = TypeVar('T')
2362+
def X(val: T) -> T: ...
2363+
x_in = None
2364+
def Y(x: Optional[str] = X(x_in)): ...
2365+
2366+
xx: Optional[int] = X(x_in)
2367+
[out]
2368+
2369+
[case testNoComplainInferredNoneStrict]
2370+
# flags: --strict-optional
2371+
from typing import TypeVar, Optional
2372+
T = TypeVar('T')
2373+
def X(val: T) -> T: ...
2374+
x_in = None
2375+
def Y(x: Optional[str] = X(x_in)): ...
2376+
2377+
xx: Optional[int] = X(x_in)
2378+
[out]

test-data/unit/check-inference-context.test

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -887,6 +887,7 @@ reveal_type(c.f(None)) # E: Revealed type is 'builtins.list[builtins.int]'
887887
[builtins fixtures/list.pyi]
888888

889889
[case testGenericMethodCalledInGenericContext]
890+
# flags: --strict-optional
890891
from typing import TypeVar, Generic
891892

892893
_KT = TypeVar('_KT')
@@ -897,7 +898,7 @@ class M(Generic[_KT, _VT]):
897898
def get(self, k: _KT, default: _T) -> _T: ...
898899

899900
def f(d: M[_KT, _VT], k: _KT) -> _VT:
900-
return d.get(k, None) # E: "get" of "M" does not return a value
901+
return d.get(k, None) # E: Incompatible return value type (got "None", expected "_VT")
901902

902903
[case testGenericMethodCalledInGenericContext2]
903904
from typing import TypeVar, Generic, Union

test-data/unit/check-optional.test

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ reveal_type(z2) # E: Revealed type is 'Union[builtins.int, builtins.str, None]'
119119

120120
[case testLambdaReturningNone]
121121
f = lambda: None
122-
x = f() # E: Function does not return a value
122+
x = f()
123+
reveal_type(x) # E: Revealed type is 'None'
123124

124125
[case testNoneArgumentType]
125126
def f(x: None) -> None: pass

test-data/unit/check-overloading.test

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1211,7 +1211,8 @@ def g(x: U, y: V) -> None:
12111211
# N: Possible overload variants: \
12121212
# N: def [T <: str] f(x: T) -> T \
12131213
# N: def [T <: str] f(x: List[T]) -> None
1214-
a = f([x]) # E: "f" does not return a value
1214+
a = f([x])
1215+
reveal_type(a) # E: Revealed type is 'None'
12151216
f([y]) # E: Value of type variable "T" of "f" cannot be "V"
12161217
f([x, y]) # E: Value of type variable "T" of "f" cannot be "object"
12171218
[builtins fixtures/list.pyi]

test-data/unit/check-protocols.test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1238,8 +1238,8 @@ class P2(Protocol):
12381238
T = TypeVar('T')
12391239
def f(x: Callable[[T, T], None]) -> T: pass
12401240
def g(x: P, y: P2) -> None: pass
1241-
x = f(g) # E: "f" does not return a value
1242-
1241+
x = f(g)
1242+
reveal_type(x) # E: Revealed type is 'None'
12431243
[case testMeetProtocolWithNormal]
12441244
from typing import Protocol, Callable, TypeVar
12451245

0 commit comments

Comments
 (0)