Repository: python/mypy

Issue:
Conditional function variants result in warning despite having identical types (only varying in parameter names)
These both result in the `All conditional function variants must have identical signatures` warning:

```python
try:
    from math import comb
except ImportError:
    def comb(n: int, k: int) -> int:  # All conditional function variants must have identical signatures
        return int(factorial(n) / factorial(k) / factorial(n - k))
```

```python
import sys
if sys.version_info >= (3, 8):
    from math import comb
else:
    def comb(n: int, k: int) -> int:  # All conditional function variants must have identical signatures
        return int(factorial(n) / factorial(k) / factorial(n - k))
```

Typeshed [conditionally defines](https://github.com/python/typeshed/blob/f4c5ae01c45df5009233ca1e3168c70db24e8caf/stdlib/math.pyi#L19-L20) `math.comb` as `def comb(__n: int, __k: int) -> int: ...`. Changing the parameter names in either of the above examples to match the typeshed signature silences the warning:

```python
try:
    from math import comb
except ImportError:
    def comb(__n: int, __k: int) -> int:  # <no more warning>
        return int(factorial(__n) / factorial(__k) / factorial(__n - __k))
```

I think this came up before in #1168 (and via #1801, but, given the above, I don't think #698 addresses this).


Discussion:
Additionally, the cause of this error is not very discoverable.  The error message doesn't tell you what the difference is, just that there is one.  The documentation doesn't seem to cover this case.  The only information I could find about what is happening here is this ticket itself.

Ok, I'll play devil's advocate. What should mypy do if you write:

```py
try:
    from math import comb
except ImportError:
    def comb(n: int, k: int) -> int:  # All conditional function variants must have identical signatures
        return int(factorial(n) / factorial(k) / factorial(n - k))
```

And then call:

```py
comb(n = 3, k = 5)
```

I agree that the error should be more descriptive. My question is: is this a bug? If the above situation comes up, what should happen? Should an error only be produced here? That seems like it would make type tracking much more painful.

Resolving patch:
diff --git a/mypy/checker.py b/mypy/checker.py
--- a/mypy/checker.py
+++ b/mypy/checker.py
@@ -950,8 +950,9 @@ def _visit_func_def(self, defn: FuncDef) -> None:
             new_type = self.function_type(defn)
             if isinstance(defn.original_def, FuncDef):
                 # Function definition overrides function definition.
-                if not is_same_type(new_type, self.function_type(defn.original_def)):
-                    self.msg.incompatible_conditional_function_def(defn)
+                old_type = self.function_type(defn.original_def)
+                if not is_same_type(new_type, old_type):
+                    self.msg.incompatible_conditional_function_def(defn, old_type, new_type)
             else:
                 # Function definition overrides a variable initialized via assignment or a
                 # decorated function.
diff --git a/mypy/messages.py b/mypy/messages.py
--- a/mypy/messages.py
+++ b/mypy/messages.py
@@ -1301,8 +1301,17 @@ def incompatible_self_argument(
             context,
         )
 
-    def incompatible_conditional_function_def(self, defn: FuncDef) -> None:
+    def incompatible_conditional_function_def(
+        self, defn: FuncDef, old_type: FunctionLike, new_type: FunctionLike
+    ) -> None:
         self.fail("All conditional function variants must have identical signatures", defn)
+        if isinstance(old_type, (CallableType, Overloaded)) and isinstance(
+            new_type, (CallableType, Overloaded)
+        ):
+            self.note("Original:", defn)
+            self.pretty_callable_or_overload(old_type, defn, offset=4)
+            self.note("Redefinition:", defn)
+            self.pretty_callable_or_overload(new_type, defn, offset=4)
 
     def cannot_instantiate_abstract_class(
         self, class_name: str, abstract_attributes: dict[str, bool], context: Context


Test patch:
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
@@ -1393,7 +1393,11 @@ x = None # type: Any
 if x:
     def f(x: int) -> None: pass
 else:
-    def f(x): pass # E: All conditional function variants must have identical signatures
+    def f(x): pass # E: All conditional function variants must have identical signatures \
+                   # N: Original: \
+                   # N:     def f(x: int) -> None \
+                   # N: Redefinition: \
+                   # N:     def f(x: Any) -> Any
 
 [case testIncompatibleConditionalFunctionDefinition2]
 from typing import Any
@@ -1401,7 +1405,11 @@ x = None # type: Any
 if x:
     def f(x: int) -> None: pass
 else:
-    def f(y: int) -> None: pass # E: All conditional function variants must have identical signatures
+    def f(y: int) -> None: pass # E: All conditional function variants must have identical signatures \
+                                # N: Original: \
+                                # N:     def f(x: int) -> None \
+                                # N: Redefinition: \
+                                # N:     def f(y: int) -> None
 
 [case testIncompatibleConditionalFunctionDefinition3]
 from typing import Any
@@ -1409,7 +1417,11 @@ x = None # type: Any
 if x:
     def f(x: int) -> None: pass
 else:
-    def f(x: int = 0) -> None: pass # E: All conditional function variants must have identical signatures
+    def f(x: int = 0) -> None: pass # E: All conditional function variants must have identical signatures \
+                                    # N: Original: \
+                                    # N:     def f(x: int) -> None \
+                                    # N: Redefinition: \
+                                    # N:     def f(x: int = ...) -> None
 
 [case testConditionalFunctionDefinitionUsingDecorator1]
 from typing import Callable
@@ -1467,14 +1479,22 @@ from typing import Any
 def f(x: str) -> None: pass
 x = None # type: Any
 if x:
-    def f(x: int) -> None: pass # E: All conditional function variants must have identical signatures
+    def f(x: int) -> None: pass # E: All conditional function variants must have identical signatures \
+                                # N: Original: \
+                                # N:     def f(x: str) -> None \
+                                # N: Redefinition: \
+                                # N:     def f(x: int) -> None
 
 [case testConditionalRedefinitionOfAnUnconditionalFunctionDefinition2]
 from typing import Any
 def f(x: int) -> None: pass # N: "f" defined here
 x = None # type: Any
 if x:
-    def f(y: int) -> None: pass # E: All conditional function variants must have identical signatures
+    def f(y: int) -> None: pass # E: All conditional function variants must have identical signatures \
+                                # N: Original: \
+                                # N:     def f(x: int) -> None \
+                                # N: Redefinition: \
+                                # N:     def f(y: int) -> None
 f(x=1) # The first definition takes precedence.
 f(y=1) # E: Unexpected keyword argument "y" for "f"
 
@@ -1640,7 +1660,11 @@ class A:
     if x:
         def f(self, x: int) -> None: pass
     else:
-        def f(self, x): pass # E: All conditional function variants must have identical signatures
+        def f(self, x): pass # E: All conditional function variants must have identical signatures \
+                             # N: Original: \
+                             # N:     def f(self: A, x: int) -> None \
+                             # N: Redefinition: \
+                             # N:     def f(self: A, x: Any) -> Any
 [out]
 
 [case testConditionalFunctionDefinitionInTry]
diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test
--- a/test-data/unit/check-modules.test
+++ b/test-data/unit/check-modules.test
@@ -625,7 +625,11 @@ try:
     from m import f, g
 except:
     def f(x): pass
-    def g(x): pass # E: All conditional function variants must have identical signatures
+    def g(x): pass # E: All conditional function variants must have identical signatures \
+                   # N: Original: \
+                   # N:     def g(x: Any, y: Any) -> Any \
+                   # N: Redefinition: \
+                   # N:     def g(x: Any) -> Any
 [file m.py]
 def f(x): pass
 def g(x, y): pass
diff --git a/test-data/unit/check-newsemanal.test b/test-data/unit/check-newsemanal.test
--- a/test-data/unit/check-newsemanal.test
+++ b/test-data/unit/check-newsemanal.test
@@ -1864,7 +1864,11 @@ if int():
 elif bool():
     def f(x: int) -> None:
         1()  # E: "int" not callable
-    def g(x: str) -> None:  # E: All conditional function variants must have identical signatures
+    def g(x: str) -> None:  # E: All conditional function variants must have identical signatures \
+                            # N: Original: \
+                            # N:     def g(x: int) -> None \
+                            # N: Redefinition: \
+                            # N:     def g(x: str) -> None
         pass
 else:
     def f(x: int) -> None:
@@ -1881,7 +1885,12 @@ if int():
 else:
     def f(x: A) -> None:
         1()  # E: "int" not callable
-    def g(x: str) -> None:  # E: All conditional function variants must have identical signatures
+    def g(x: str) -> None:  # E: All conditional function variants must have identical signatures \
+                            # N: Original: \
+                            # N:     def g(x: A) -> None \
+                            # N: Redefinition: \
+                            # N:     def g(x: str) -> None
+
         pass
 
 reveal_type(g) # N: Revealed type is "def (x: __main__.A)"
diff --git a/test-data/unit/check-unions.test b/test-data/unit/check-unions.test
--- a/test-data/unit/check-unions.test
+++ b/test-data/unit/check-unions.test
@@ -193,11 +193,19 @@ elif foo():
 elif foo():
     def f(x: Union[int, str, int, int, str]) -> None: pass
 elif foo():
-    def f(x: Union[int, str, float]) -> None: pass  # E: All conditional function variants must have identical signatures
+    def f(x: Union[int, str, float]) -> None: pass  # E: All conditional function variants must have identical signatures \
+                                                    # N: Original: \
+                                                    # N:     def f(x: Union[int, str]) -> None \
+                                                    # N: Redefinition: \
+                                                    # N:     def f(x: Union[int, str, float]) -> None
 elif foo():
     def f(x: Union[S, T]) -> None: pass
 elif foo():
-    def f(x: Union[str]) -> None: pass  # E: All conditional function variants must have identical signatures
+    def f(x: Union[str]) -> None: pass  # E: All conditional function variants must have identical signatures \
+                                        # N: Original: \
+                                        # N:     def f(x: Union[int, str]) -> None \
+                                        # N: Redefinition: \
+                                        # N:     def f(x: str) -> None
 else:
     def f(x: Union[Union[int, T], Union[S, T], str]) -> None: pass
 
@@ -206,7 +214,11 @@ else:
 if foo():
     def g(x: Union[int, str, bytes]) -> None: pass
 else:
-    def g(x: Union[int, str]) -> None: pass  # E: All conditional function variants must have identical signatures
+    def g(x: Union[int, str]) -> None: pass  # E: All conditional function variants must have identical signatures \
+                                             # N: Original: \
+                                             # N:     def g(x: Union[int, str, bytes]) -> None \
+                                             # N: Redefinition: \
+                                             # N:     def g(x: Union[int, str]) -> None
 
 [case testUnionSimplificationSpecialCases]
 from typing import Any, TypeVar, Union
diff --git a/test-data/unit/check-unreachable-code.test b/test-data/unit/check-unreachable-code.test
--- a/test-data/unit/check-unreachable-code.test
+++ b/test-data/unit/check-unreachable-code.test
@@ -242,7 +242,11 @@ import sys
 if sys.version_info >= (3, 5, 0):
     def foo() -> int: return 0
 else:
-    def foo() -> str: return ''  # E: All conditional function variants must have identical signatures
+    def foo() -> str: return ''  # E: All conditional function variants must have identical signatures \
+                                 # N: Original: \
+                                 # N:     def foo() -> int \
+                                 # N: Redefinition: \
+                                 # N:     def foo() -> str
 [builtins fixtures/ops.pyi]
 [out]
 
@@ -253,7 +257,11 @@ import sys
 if sys.version_info[1:] >= (5, 0):
     def foo() -> int: return 0
 else:
-    def foo() -> str: return ''  # E: All conditional function variants must have identical signatures
+    def foo() -> str: return ''  # E: All conditional function variants must have identical signatures \
+                                 # N: Original: \
+                                 # N:     def foo() -> int \
+                                 # N: Redefinition: \
+                                 # N:     def foo() -> str
 [builtins fixtures/ops.pyi]
 [out]