BUG: MutliIndex variable length tuples
State: closed

 - [ ] closes #14794 
 - [ ] tests added / passed
 - [ ] passes ``git diff upstream/master | flake8 --diff``
 - [ ] whatsnew entry


Labels:
name: Error Reporting
description: Incorrect or improved errors from pandas
name: MultiIndex

Comments:
this also could impact performance
so i am inclined to raise an error (may need to modify some cython to detect though)
Are unbalanced indexes invalid?  When passing in an unbalanced list, the holes get filled with NaN.  My goal was trying to make the behavior consistent between lists and other containers.  I'm fine with raising an error if unbalanced input is invalid.
@groutr unbalanced are invalid, its not normal to do this and causes all kinds of indexing issues, they are barely supported, so a helpful error up front is useful. 
can you rebase / update
Yeah, I can rebase and update to throw an error.

I went through the test suite as well, and saw that a couple of tests construct these invalid multi-indexes.  I will try to update those tests too.
@groutr thanks!
@jreback I was working on updating this PR the other day and before changing too much like I did in #14806, I wanted to get your ideas on a particular issue.

The distinction in my mind between `from_tuples` and `from_arrays` is vanishing very quickly.  There seems to be very little distinction between `from_arrays` and `from_tuples` almost to the point where I think they could literally be the same function.  The only difference before seemed that `from_tuples` didn't enforce a uniform length requirement whereas `from_arrays` does.  I can't see the justification for casting the tuples to numpy object arrays in `from_tuples`.  Is that for performance reasons?  `from_arrays` seems to only require iterable containers of equal length.  

The `from_tuples` and `from_arrays` functions seem to be two special cases of a more general `from_sequences` idea.  The issue I'm running into is `from_tuples` calls `from_arrays`, but for ideal input, a relatively expensive check would be performed twice, once in `from_tuples` and then again in `from_arrays`.  `from_tuples` doesn't seem to be performing any major meaningful work on top of `from_arrays`, or am I missing something here?

Of course, we don't want to change an API willy-nilly.  My thought is to have a new "private" `_from_even_sequences` (or similar named) function that combines the work of `from_tuples` and `from_arrays`.   `from_tuples` and `from_arrays` would simply have their first len() check then call  `_from_even_sequences` to do the remainder of the work in `from_arrays` for generating the MultiIndex.  The API would be intact, but then performance of `_from_even_sequences` could be addressed more directly. 

@groutr actually ``.from_tuples`` is quite useful, its exactly what it says, take a list-of-tuples and create a MI. while ``.from_arrays`` is almost lower level, take a list of arrays that are already essentially the levels (dtyped) and create a MI, its is *much* more efficient.

``.from_tuples`` is a high-level interface, so of course it should be implemented in as ``.from_arrays``.

the check in ``.from_arrays`` is pretty cheap. ``.from_tuples`` *needs* to do a check (as seen in the issue).
# [Codecov](https://codecov.io/gh/pandas-dev/pandas/pull/14823?src=pr&el=h1) Report
> Merging [#14823](https://codecov.io/gh/pandas-dev/pandas/pull/14823?src=pr&el=desc) into [master](https://codecov.io/gh/pandas-dev/pandas/commit/3955261c04d5b838488a45fe7b186399bcdca137?src=pr&el=desc) will **decrease** coverage by `5.7%`.
> The diff coverage is `60%`.

[![Impacted file tree graph](https://codecov.io/gh/pandas-dev/pandas/pull/14823/graphs/tree.svg?src=pr&token=eZ4WkYLtcO&width=650&height=150)](https://codecov.io/gh/pandas-dev/pandas/pull/14823?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master   #14823      +/-   ##
==========================================
- Coverage   90.98%   85.28%   -5.71%     
==========================================
  Files         161      144      -17     
  Lines       49288    50972    +1684     
==========================================
- Hits        44846    43469    -1377     
- Misses       4442     7503    +3061
```

| Flag | Coverage Δ | |
|---|---|---|
| #multiple | `?` | |
| #single | `?` | |

| [Impacted Files](https://codecov.io/gh/pandas-dev/pandas/pull/14823?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pandas/indexes/multi.py](https://codecov.io/gh/pandas-dev/pandas/pull/14823?src=pr&el=tree#diff-cGFuZGFzL2luZGV4ZXMvbXVsdGkucHk=) | `96.28% <60%> (ø)` | |
| [pandas/io/sas/sas\_constants.py](https://codecov.io/gh/pandas-dev/pandas/pull/14823?src=pr&el=tree#diff-cGFuZGFzL2lvL3Nhcy9zYXNfY29uc3RhbnRzLnB5) | `0% <0%> (-100%)` | :arrow_down: |
| [pandas/io/sas/sas7bdat.py](https://codecov.io/gh/pandas-dev/pandas/pull/14823?src=pr&el=tree#diff-cGFuZGFzL2lvL3Nhcy9zYXM3YmRhdC5weQ==) | `0% <0%> (-90.67%)` | :arrow_down: |
| [pandas/io/sas/sas\_xport.py](https://codecov.io/gh/pandas-dev/pandas/pull/14823?src=pr&el=tree#diff-cGFuZGFzL2lvL3Nhcy9zYXNfeHBvcnQucHk=) | `0% <0%> (-90.28%)` | :arrow_down: |
| [pandas/io/sas/sasreader.py](https://codecov.io/gh/pandas-dev/pandas/pull/14823?src=pr&el=tree#diff-cGFuZGFzL2lvL3Nhcy9zYXNyZWFkZXIucHk=) | `4.34% <0%> (-81.86%)` | :arrow_down: |
| [pandas/io/gbq.py](https://codecov.io/gh/pandas-dev/pandas/pull/14823?src=pr&el=tree#diff-cGFuZGFzL2lvL2dicS5weQ==) | `17.77% <0%> (-65.57%)` | :arrow_down: |
| [pandas/io/html.py](https://codecov.io/gh/pandas-dev/pandas/pull/14823?src=pr&el=tree#diff-cGFuZGFzL2lvL2h0bWwucHk=) | `64.35% <0%> (-20.51%)` | :arrow_down: |
| [pandas/util/testing.py](https://codecov.io/gh/pandas-dev/pandas/pull/14823?src=pr&el=tree#diff-cGFuZGFzL3V0aWwvdGVzdGluZy5weQ==) | `82.5% <0%> (-17.5%)` | :arrow_down: |
| [pandas/tools/plotting.py](https://codecov.io/gh/pandas-dev/pandas/pull/14823?src=pr&el=tree#diff-cGFuZGFzL3Rvb2xzL3Bsb3R0aW5nLnB5) | `68.8% <0%> (-13.01%)` | :arrow_down: |
| [pandas/io/pickle.py](https://codecov.io/gh/pandas-dev/pandas/pull/14823?src=pr&el=tree#diff-cGFuZGFzL2lvL3BpY2tsZS5weQ==) | `74.28% <0%> (-6.97%)` | :arrow_down: |
| ... and [220 more](https://codecov.io/gh/pandas-dev/pandas/pull/14823?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pandas-dev/pandas/pull/14823?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/pandas-dev/pandas/pull/14823?src=pr&el=footer). Last update [3955261...6cf5286](https://codecov.io/gh/pandas-dev/pandas/pull/14823?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

Tests coming soon.
We should be raising on variable length tuples. have the whatsnew reflect that.
can you rebase / update
closing as stale. would like to have this fixed though :>
try something like this

```
diff --git a/pandas/core/frame.py b/pandas/core/frame.py
index 6559fc4..a64bf61 100644
--- a/pandas/core/frame.py
+++ b/pandas/core/frame.py
@@ -5828,7 +5828,7 @@ def _reorder_arrays(arrays, arr_columns, columns):
 
 def _list_to_arrays(data, columns, coerce_float=False, dtype=None):
     if len(data) > 0 and isinstance(data[0], tuple):
-        content = list(lib.to_object_array_tuples(data).T)
+        content = list(lib.tuples_to_object_array(np.array(data, dtype=object)).T)
     else:
         # list of lists
         content = list(lib.to_object_array(data).T)
diff --git a/pandas/core/indexes/multi.py b/pandas/core/indexes/multi.py
index 81eac0a..4b4a820 100644
--- a/pandas/core/indexes/multi.py
+++ b/pandas/core/indexes/multi.py
@@ -1128,20 +1128,21 @@ class MultiIndex(Index):
         MultiIndex.from_product : Make a MultiIndex from cartesian product
                                   of iterables
         """
+        if not is_list_like(tuples):
+            raise
+
         if len(tuples) == 0:
             if names is None:
                 msg = 'Cannot infer number of levels from empty list'
                 raise TypeError(msg)
             arrays = [[]] * len(names)
-        elif isinstance(tuples, (np.ndarray, Index)):
+
+        if isinstance(tuples, (list, tuple)):
+            arrays = list(lib.to_object_array_tuples(list(tuples)).T)
+        else:
             if isinstance(tuples, Index):
                 tuples = tuples._values
-
             arrays = list(lib.tuples_to_object_array(tuples).T)
-        elif isinstance(tuples, list):
-            arrays = list(lib.to_object_array_tuples(tuples).T)
-        else:
-            arrays = lzip(*tuples)
 
         return MultiIndex.from_arrays(arrays, sortorder=sortorder, names=names)
```
Hello @groutr! Thanks for updating the PR.



 - In the file [`pandas/tests/indexes/test_base.py`](https://github.com/pandas-dev/pandas/blob/6cf528630d72306504d6b9dc7817331b7e9679e0/pandas/tests/indexes/test_base.py), following are the PEP8 issues :

> [Line 1463:80](https://github.com/pandas-dev/pandas/blob/6cf528630d72306504d6b9dc7817331b7e9679e0/pandas/tests/indexes/test_base.py#L1463): [E501](https://duckduckgo.com/?q=pep8%20E501) line too long (83 > 79 characters)
> [Line 1464:45](https://github.com/pandas-dev/pandas/blob/6cf528630d72306504d6b9dc7817331b7e9679e0/pandas/tests/indexes/test_base.py#L1464): [E128](https://duckduckgo.com/?q=pep8%20E128) continuation line under-indented for visual indent



 - In the file [`pandas/tests/test_strings.py`](https://github.com/pandas-dev/pandas/blob/6cf528630d72306504d6b9dc7817331b7e9679e0/pandas/tests/test_strings.py), following are the PEP8 issues :

> [Line 2031:80](https://github.com/pandas-dev/pandas/blob/6cf528630d72306504d6b9dc7817331b7e9679e0/pandas/tests/test_strings.py#L2031): [E501](https://duckduckgo.com/?q=pep8%20E501) line too long (82 > 79 characters)
> [Line 2032:41](https://github.com/pandas-dev/pandas/blob/6cf528630d72306504d6b9dc7817331b7e9679e0/pandas/tests/test_strings.py#L2032): [E124](https://duckduckgo.com/?q=pep8%20E124) closing bracket does not match visual indentation


closing as stale, but would still like the fix. ping if you want to reopen.