title: Python, MyPy: how can I do an "exclusive OR" of types in a generic's restrictions?
body: <p>I want to use generics so that I can avoid the &quot;invariant Xyz&quot; problem, and have a single source of truth for a couple of large type structure implementations, without having to Union the internals of a generic class and the externals as well. My code has a heavily nested series of aliases for a custom data structure, which is basically a big JSON of dicts and lists where the &quot;leaves&quot; are of one of two possible types (call them <code>A</code> and <code>B</code>), and only ever &quot;<code>A</code> exclusively&quot;, or &quot;<code>B</code> exclusively&quot; for a given tree. However, some substructures of the overall data model (like <code>List[A]</code> or <code>List[B]</code>) can happen in different layers of the dictionary tree, and I don't want to have a to maintain a specific type for each layer, when their logic is exactly the same.</p>
  <p>I'll be using the invariant <code>List</code> as an example. I want to have an alias <code>C = List[A | B]</code> recognize <code>List[A]</code> and <code>List[B]</code> as valid subtypes, without having to do <code>C = List[A] | List[B]</code> everywhere this happens, since Unions would need discrimination to function properly in my utility functions. I want <code>A</code> and <code>B</code> to be replaced by a <code>GenericAorB</code> when I can, to auto-infer if I have a <code>List[A]</code> or <code>List[B]</code> in my utilities.</p>
  <p>I took inspiration from here, since it seemed like what I needed:
  <a href="https://mypy.readthedocs.io/en/stable/generics.html#type-variables-with-value-restriction" rel="nofollow noreferrer">https://mypy.readthedocs.io/en/stable/generics.html#type-variables-with-value-restriction</a></p>
  <p>However it doesn't seem to work.</p>
  <pre class="lang-py prettyprint-override"><code>from typing import TypeVar, List
  
  GenericStringOrFloat = TypeVar(&quot;GenericStringOrFloat&quot;, str, float)
  ListStringOrFloat    = List[GenericStringOrFloat]
  my_list : ListStringOrFloat
  my_list = [12.0, 13.0]       # Fine
  my_list = [&quot;hello&quot;, &quot;world&quot;] # Fine
  my_list = [&quot;hello&quot;, 13.0]    # Also fine... what ? shouldn't mypy return an error for this ?
  </code></pre>
  <p>I don't understand why the last line doesn't return an error. To me it looks like</p>
  <pre class="lang-py prettyprint-override"><code>GenericStringOrFloat = TypeVar(&quot;GenericStringOrFloat&quot;, str, float)
  </code></pre>
  <p>is synonymous to</p>
  <pre class="lang-py prettyprint-override"><code>GenericStringOrFloat = TypeVar(&quot;GenericStringOrFloat&quot;, str | float)
  </code></pre>
  <p>which is thus completely pointless. Is this maybe a MyPy bug ? the documentation looks like this syntax is precisely meant to avoid this issue, and have the generic not be a dumb Union.</p>
  <p>I want an <em>exclusive OR</em>, not inclusive, between the types given to my generic type restrictions. Phrased mathematically (though not rigorously), I want a sort of functor which can do something like <code>ListUnionMapper : (A | B) -&gt; List[A] | List[B]</code> (and for Dicts as well).</p>
  <p>Any idea how to do that ?</p>
  <p>=========</p>
  <p><strong>EDIT:</strong></p>
  <p>After some more experimentation, I found an almost workable solution, but it's still far from perfect. The TLDR is that you need to give the generic variable a second time to allow mypy to do the automatic inference/discrimination; but there is still a bunch of weirdness with generic type restrictions.</p>
  <p>When declaring a variable, give it the specific type being used (ie, <code>str</code> or <code>float</code>), and when declaring a function signature, give it the generic type variable.</p>
  <p>The below code exemplifies what I mean by this.</p>
  <pre class="lang-py prettyprint-override"><code>from typing import TypeVar, List
  
  GenericStringOrFloat  = TypeVar(&quot;GenericStringOrFloat&quot;, str, float)
  ListFloat             = List[float]
  ListString            = List[str]
  ListStringOrFloat     = List[GenericStringOrFloat]
  ListStringOrListFloat = ListString | ListFloat
  my_list1A : ListStringOrFloat[      float] = [12.0,    13.0   ]  # Good as expected
  my_list1B : ListStringOrFloat[      float] = [&quot;hello&quot;, &quot;world&quot;]  # Error as expected: not float
  my_list2A : ListStringOrFloat[str        ] = [&quot;hello&quot;, &quot;world&quot;]  # Good as expected
  my_list2B : ListStringOrFloat[str        ] = [12.0,    13.0   ]  # Error as expected: not str
  my_list3  : ListStringOrFloat[str | float] = [&quot;hello&quot;, 13.0   ]  # Good as expected
  my_list4A : ListStringOrListFloat          = [&quot;hello&quot;, &quot;world&quot;]  # Good as expected
  my_list4B : ListStringOrListFloat          = [12.0,    13.0   ]  # Good as expected
  my_list4C : ListStringOrListFloat          = [&quot;hello&quot;, 13.0   ]  # Error as expected: not List[str] or List[float]
  my_list4B                                  = [&quot;hello&quot;, &quot;world&quot;]  # NB: doesn't infer/discriminate between List[str] and List[float], since union, so can be reassigned
  my_list5A  : ListStringOrFloat[int]        = [&quot;hello&quot;, 13.0   ]  # Error as expected: neither List[str} nor List[float]
  my_list5B  : ListStringOrFloat[int]        = [1,       2      ]  # UNEXPECTED GOOD: this means that the type restriction basically doesn't work...
  
  my_list_extra : ListStringOrFloat[GenericStringOrFloat] = [&quot;hello&quot;, &quot;world&quot;]  # Error: Type variable GenericStringOrFloat is unbound
  
  
  def return_first_element(lst: ListStringOrFloat[GenericStringOrFloat]) -&gt; GenericStringOrFloat:
      assert len(lst) &gt; 0
      return lst[0]
  
  
  my_float_A : float = return_first_element(my_list1A)  # Good as expected
  my_float_B : str   = return_first_element(my_list1A)  # Error: correctly infers that the function's return type is float
  my_str_A   : float = return_first_element(my_list2A)  # Error: correctly infers that the function's return type is str
  my_str_B   : str   = return_first_element(my_list2A)  # Good as expected
  my_int_A   : float = return_first_element(my_list5B)  # Error: expects List[float] (and not List[Generic_Value]...)
  my_int_B   : str   = return_first_element(my_list5B)  # Error: expression has type float + expects List[float] (and not List[Generic_Value]...)
  my_int_C   : int   = return_first_element(my_list5B)  # Error: expression has type float + expects List[float] (and not List[Generic_Value]...)
  </code></pre>