Repository: sandialabs/pyttb

Issue:
Sptensor constructor breaks with int overflow for large tensor
# Issue
The constructor for `sptensor` fails an assertion when the tensor size is large. It's important to note that the `shape` isn't allocated, simply checked for consistency.

# Example
For `size = (50000000, 50000000, 100, 50000)` and `nnzs = 1000000000`, we get:
```
    X = ttb.sptenrand(sz, nonzeros=nnz)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/jermyer/pyttb/pyttb/sptensor.py", line 3709, in sptenrand
    return ttb.sptensor.from_function(unit_uniform, shape, valid_nonzeros)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/jermyer/pyttb/pyttb/sptensor.py", line 214, in from_function
    assert False, (
AssertionError: Requested number of nonzeros must be positive and less than the total size
```

Checking `np.prod(size)` used in the assertion, `np.prod(size) = -6892481975075995648` .

# Solution
Use `prod` from the `math` module to get the correct result, i.e.,
```
>>> from math import prod
>>> sz = (50000000,50000000,100,50000)
>>> prod(sz)
12500000000000000000000
```
This has the added bonus that `sz` is a python `tuple`, not a `numpy` object, as pointed out by @dmdunla.

Discussion:

Solution patch:
diff --git a/docs/source/tutorial/class_sptensor.ipynb b/docs/source/tutorial/class_sptensor.ipynb
index 00f21ec..bd2b072 100644
--- a/docs/source/tutorial/class_sptensor.ipynb
+++ b/docs/source/tutorial/class_sptensor.ipynb
@@ -271,7 +271,7 @@
    "outputs": [],
    "source": [
     "indices = np.array([[0, 0, 0], [1, 0, 0]])\n",
-    "values = np.ones((2,))\n",
+    "values = np.ones((2, 1))\n",
     "\n",
     "Y = ttb.sptensor.from_aggregator(indices, values)  # Create a sparse tensor.\n",
     "Y"
@@ -300,7 +300,7 @@
    "outputs": [],
    "source": [
     "indices = np.array([[0, 0, 0], [2, 2, 2]])\n",
-    "values = np.array([1, 3])\n",
+    "values = np.array([[1], [3]])\n",
     "\n",
     "Y = ttb.sptensor.from_aggregator(indices, values)  # Create a sparse tensor.\n",
     "Y"
@@ -555,7 +555,7 @@
    "outputs": [],
    "source": [
     "indices = np.array([[0], [2], [4]])\n",
-    "values = np.array([1, 1, 1])\n",
+    "values = np.array([[1], [1], [1]])\n",
     "X = ttb.sptensor.from_aggregator(indices, values)\n",
     "X"
    ]
diff --git a/pyttb/cp_als.py b/pyttb/cp_als.py
index 4554137..86d5585 100644
--- a/pyttb/cp_als.py
+++ b/pyttb/cp_als.py
@@ -6,11 +6,12 @@
 
 from __future__ import annotations
 
-from typing import Dict, List, Literal, Optional, Tuple, Union
+from typing import Dict, Literal, Optional, Tuple, Union
 
 import numpy as np
 
 import pyttb as ttb
+from pyttb.pyttb_utils import OneDArray, parse_one_d
 
 
 def cp_als(  # noqa: PLR0912,PLR0913,PLR0915
@@ -18,8 +19,8 @@ def cp_als(  # noqa: PLR0912,PLR0913,PLR0915
     rank: int,
     stoptol: float = 1e-4,
     maxiters: int = 1000,
-    dimorder: Optional[List[int]] = None,
-    optdims: Optional[List[int]] = None,
+    dimorder: Optional[OneDArray] = None,
+    optdims: Optional[OneDArray] = None,
     init: Union[Literal["random"], Literal["nvecs"], ttb.ktensor] = "random",
     printitn: int = 1,
     fixsigns: bool = True,
@@ -109,8 +110,8 @@ def cp_als(  # noqa: PLR0912,PLR0913,PLR0915
     [[0.1467... 0.0923...]
      [0.1862... 0.3455...]]
     >>> print(output["params"]) # doctest: +NORMALIZE_WHITESPACE
-    {'stoptol': 0.0001, 'maxiters': 1000, 'dimorder': [0, 1],\
-     'optdims': [0, 1], 'printitn': 1, 'fixsigns': True}
+    {'stoptol': 0.0001, 'maxiters': 1000, 'dimorder': array([0, 1]),\
+     'optdims': array([0, 1]), 'printitn': 1, 'fixsigns': True}
 
     Example using "nvecs" initialization:
 
@@ -135,15 +136,17 @@ def cp_als(  # noqa: PLR0912,PLR0913,PLR0915
 
     # Set up dimorder if not specified
     if dimorder is None:
-        dimorder = list(range(N))
-    elif not isinstance(dimorder, list):
-        assert False, "Dimorder must be a list"
-    elif tuple(range(N)) != tuple(sorted(dimorder)):
+        dimorder = np.arange(N)
+    else:
+        dimorder = parse_one_d(dimorder)
+    if tuple(range(N)) != tuple(sorted(dimorder)):
         assert False, "Dimorder must be a list or permutation of range(tensor.ndims)"
 
     # Set up optdims if not specified
     if optdims is None:
-        optdims = list(range(N))
+        optdims = np.arange(N)
+    else:
+        optdims = parse_one_d(optdims)
 
     # Error checking
     assert rank > 0, "Number of components requested must be positive"
diff --git a/pyttb/export_data.py b/pyttb/export_data.py
index 0f54d61..7f8f56e 100644
--- a/pyttb/export_data.py
+++ b/pyttb/export_data.py
@@ -6,11 +6,12 @@
 
 from __future__ import annotations
 
-from typing import Optional, TextIO, Tuple, Union
+from typing import Optional, TextIO, Union
 
 import numpy as np
 
 import pyttb as ttb
+from pyttb.pyttb_utils import Shape, parse_shape
 
 
 def export_data(
@@ -56,8 +57,9 @@ def export_data(
             export_array(fp, data, fmt_data)
 
 
-def export_size(fp: TextIO, shape: Tuple[int, ...]):
+def export_size(fp: TextIO, shape: Shape):
     """Export the size of something to a file"""
+    shape = parse_shape(shape)
     print(f"{len(shape)}", file=fp)  # # of dimensions on one line
     shape_str = " ".join([str(d) for d in shape])
     print(f"{shape_str}", file=fp)  # size of each dimensions on the next line
diff --git a/pyttb/gcp_opt.py b/pyttb/gcp_opt.py
index 1ac112a..e9a9bcf 100644
--- a/pyttb/gcp_opt.py
+++ b/pyttb/gcp_opt.py
@@ -8,7 +8,8 @@ from __future__ import annotations
 
 import logging
 import time
-from typing import Dict, List, Literal, Optional, Tuple, Union
+from math import prod
+from typing import Dict, Literal, Optional, Sequence, Tuple, Union
 
 import numpy as np
 
@@ -24,7 +25,7 @@ def gcp_opt(  # noqa:  PLR0912,PLR0913
     rank: int,
     objective: Union[Objectives, Tuple[function_type, function_type, float]],
     optimizer: Union[StochasticSolver, LBFGSB],
-    init: Union[Literal["random"], ttb.ktensor, List[np.ndarray]] = "random",
+    init: Union[Literal["random"], ttb.ktensor, Sequence[np.ndarray]] = "random",
     mask: Optional[Union[ttb.tensor, np.ndarray]] = None,
     sampler: Optional[GCPSampler] = None,
     printitn: int = 1,
@@ -74,7 +75,7 @@ def gcp_opt(  # noqa:  PLR0912,PLR0913
     if not isinstance(data, (ttb.tensor, ttb.sptensor)):
         raise ValueError("Input data must be tensor or sptensor.")
 
-    tensor_size = int(np.prod(data.shape))
+    tensor_size = prod(data.shape)
 
     if isinstance(data, ttb.tensor) and isinstance(mask, ttb.tensor):
         data *= mask
@@ -134,7 +135,7 @@ def gcp_opt(  # noqa:  PLR0912,PLR0913
 def _get_initial_guess(
     data: Union[ttb.tensor, ttb.sptensor],
     rank: int,
-    init: Union[Literal["random"], ttb.ktensor, List[np.ndarray]],
+    init: Union[Literal["random"], ttb.ktensor, Sequence[np.ndarray]],
 ) -> ttb.ktensor:
     """Get initial guess for gcp_opt
 
@@ -143,7 +144,7 @@ def _get_initial_guess(
         Normalized ktensor.
     """
     # TODO might be nice to merge with ALS/other CP methods
-    if isinstance(init, list):
+    if isinstance(init, Sequence) and not isinstance(init, str):
         return ttb.ktensor(init).normalize("all")
     if isinstance(init, ttb.ktensor):
         init.normalize("all")
diff --git a/pyttb/hosvd.py b/pyttb/hosvd.py
index c26bca0..4243e22 100644
--- a/pyttb/hosvd.py
+++ b/pyttb/hosvd.py
@@ -7,21 +7,22 @@
 from __future__ import annotations
 
 import warnings
-from typing import List, Optional
+from typing import Optional
 
 import numpy as np
 import scipy
 
 import pyttb as ttb
+from pyttb.pyttb_utils import OneDArray, parse_one_d
 
 
 def hosvd(  # noqa: PLR0912,PLR0913,PLR0915
     input_tensor: ttb.tensor,
     tol: float,
     verbosity: float = 1,
-    dimorder: Optional[List[int]] = None,
+    dimorder: Optional[OneDArray] = None,
     sequential: bool = True,
-    ranks: Optional[List[int]] = None,
+    ranks: Optional[OneDArray] = None,
 ) -> ttb.ttensor:
     """Compute sequentially-truncated higher-order SVD (Tucker).
 
@@ -57,21 +58,22 @@ def hosvd(  # noqa: PLR0912,PLR0913,PLR0915
     # In tucker als this is N
     d = input_tensor.ndims
 
-    if ranks is not None:
-        if len(ranks) != d:
-            raise ValueError(
-                f"Ranks must be a list of length tensor ndims. Ndims: {d} but got "
-                f"ranks: {ranks}."
-            )
+    if ranks is None:
+        ranks = np.zeros((d,), dtype=int)
     else:
-        ranks = [0] * d
+        ranks = parse_one_d(ranks)
+
+    if len(ranks) != d:
+        raise ValueError(
+            "Ranks must be a sequence of length tensor ndims."
+            f" Ndims: {d} but got ranks: {ranks}."
+        )
 
     # Set up dimorder if not specified (this is copy past from tucker_als
-    if not dimorder:
-        dimorder = list(range(d))
+    if dimorder is None:
+        dimorder = np.arange(d)
     else:
-        if not isinstance(dimorder, list):
-            raise ValueError("Dimorder must be a list")
+        dimorder = parse_one_d(dimorder)
         if tuple(range(d)) != tuple(sorted(dimorder)):
             raise ValueError(
                 "Dimorder must be a list or permutation of range(tensor.ndims)"
diff --git a/pyttb/ktensor.py b/pyttb/ktensor.py
index 5ae1f05..94b52a7 100644
--- a/pyttb/ktensor.py
+++ b/pyttb/ktensor.py
@@ -8,12 +8,13 @@ from __future__ import annotations
 
 import logging
 import warnings
+from math import prod
 from typing import (
     Callable,
-    Iterable,
     List,
     Literal,
     Optional,
+    Sequence,
     Tuple,
     Union,
     cast,
@@ -28,10 +29,14 @@ from matplotlib.figure import Figure
 
 import pyttb as ttb
 from pyttb.pyttb_utils import (
+    OneDArray,
+    Shape,
     get_mttkrp_factors,
     isrow,
     isvector,
     np_to_python,
+    parse_one_d,
+    parse_shape,
     tt_dimscheck,
     tt_ind2sub,
 )
@@ -71,7 +76,7 @@ class ktensor:
 
     def __init__(
         self,
-        factor_matrices: Optional[List[np.ndarray]] = None,
+        factor_matrices: Optional[Sequence[np.ndarray]] = None,
         weights: Optional[np.ndarray] = None,
         copy: bool = True,
     ):
@@ -147,8 +152,8 @@ class ktensor:
             return
 
         # 'factor_matrices' must be a list
-        if not isinstance(factor_matrices, list):
-            assert False, "Input 'factor_matrices' must be a list."
+        if not isinstance(factor_matrices, Sequence):
+            assert False, "Input 'factor_matrices' must be a sequence."
         # each factor matrix should be a np.ndarray
         if not (
             all(isinstance(fm, np.ndarray) for fm in factor_matrices)
@@ -189,13 +194,16 @@ class ktensor:
         if copy:
             self.factor_matrices = [fm.copy() for fm in factor_matrices]
         else:
+            if not isinstance(factor_matrices, list):
+                logging.warning("Must provide factor matrices as list to avoid copy")
+                factor_matrices = list(factor_matrices)
             self.factor_matrices = factor_matrices
 
     @classmethod
     def from_function(
         cls,
         function_handle: Callable[[Tuple[int, ...]], np.ndarray],
-        shape: Tuple[int, ...],
+        shape: Shape,
         num_components: int,
     ):
         """
@@ -282,7 +290,7 @@ class ktensor:
         """
         # CONSTRUCTOR FROM FUNCTION HANDLE
         assert callable(function_handle), "Input parameter 'fun' must be a function."
-        assert isinstance(shape, tuple), "Input parameter 'shape' must be a tuple."
+        shape = parse_shape(shape)
         assert isinstance(
             num_components, int
         ), "Input parameter 'num_components' must be an int."
@@ -294,9 +302,7 @@ class ktensor:
         return cls(factor_matrices, weights, copy=False)
 
     @classmethod
-    def from_vector(
-        cls, data: np.ndarray, shape: Tuple[int, ...], contains_weights: bool
-    ):
+    def from_vector(cls, data: np.ndarray, shape: Shape, contains_weights: bool):
         """
         Construct a :class:`pyttb.ktensor` from a vector and shape. The rank of the
         :class:`pyttb.ktensor` is inferred from the shape and length of the vector.
@@ -366,7 +372,7 @@ class ktensor:
          [14. 18.]]
         """
         assert isvector(data), "Input parameter 'data' must be a numpy.array vector."
-        assert isinstance(shape, tuple), "Input parameter 'shape' must be a tuple."
+        shape = parse_shape(shape)
         assert isinstance(
             contains_weights, bool
         ), "Input parameter 'contains_weights' must be a bool."
@@ -893,14 +899,14 @@ class ktensor:
         <BLANKLINE>
         """
 
-        def min_split_dims(dims):
+        def min_split_dims(dims: Tuple[int, ...]):
             """
             solve
               min_{i in range(1,d)}  product(dims[:i]) + product(dims[i:])
             to minimize the memory footprint of the intermediate matrix
             """
             sum_of_prods = [
-                np.prod(dims[:i]) + np.prod(dims[i:]) for i in range(1, len(dims))
+                prod(dims[:i]) + prod(dims[i:]) for i in range(1, len(dims))
             ]
             i_min = np.argmin(sum_of_prods) + 1  # note range above starts at 1
             return i_min
@@ -1181,7 +1187,7 @@ class ktensor:
             vals = vals + tmpvals
         return vals
 
-    def mttkrp(self, U: Union[ktensor, List[np.ndarray]], n: int) -> np.ndarray:
+    def mttkrp(self, U: Union[ktensor, Sequence[np.ndarray]], n: int) -> np.ndarray:
         """
         Matricized tensor times Khatri-Rao product for :class:`pyttb.ktensor`.
 
@@ -1443,7 +1449,7 @@ class ktensor:
                     v[:, i] *= -1
         return v
 
-    def permute(self, order: np.ndarray) -> ktensor:
+    def permute(self, order: OneDArray) -> ktensor:
         """
         Permute :class:`pyttb.ktensor` dimensions.
 
@@ -1491,6 +1497,7 @@ class ktensor:
         [[1. 2.]
          [3. 4.]]
         """
+        order = parse_one_d(order)
         # Check that the permutation is valid
         if tuple(range(self.ndims)) != tuple(sorted(order.tolist())):
             assert False, "Invalid permutation"
@@ -1702,7 +1709,7 @@ class ktensor:
             best_perm = -1 * np.ones((RA), dtype=int)
             best_score = 0.0
             for _ in range(RB):
-                idx = np.argmax(C.reshape(np.prod(C.shape), order="F"))
+                idx = np.argmax(C.reshape(prod(C.shape), order="F"))
                 ij = tt_ind2sub((RA, RB), np.array(idx))
                 best_score = best_score + C[ij[0], ij[1]]
                 C[ij[0], :] = -10
@@ -1951,9 +1958,9 @@ class ktensor:
 
     def ttv(
         self,
-        vector: Union[List[np.ndarray], np.ndarray],
-        dims: Optional[Union[int, np.ndarray]] = None,
-        exclude_dims: Optional[Union[int, np.ndarray]] = None,
+        vector: Union[Sequence[np.ndarray], np.ndarray],
+        dims: Optional[OneDArray] = None,
+        exclude_dims: Optional[OneDArray] = None,
     ) -> Union[float, ktensor]:
         """
         Tensor times vector for a :class:`pyttb.ktensor`.
@@ -2044,15 +2051,6 @@ class ktensor:
         [[1. 3.]
          [2. 4.]]
         """
-
-        if dims is None and exclude_dims is None:
-            dims = np.array([])
-        elif isinstance(dims, (float, int)):
-            dims = np.array([dims])
-
-        if isinstance(exclude_dims, (float, int)):
-            exclude_dims = np.array([exclude_dims])
-
         # Check vector is a list of vectors
         # if not place single vector as element in list
         if (
@@ -2093,7 +2091,7 @@ class ktensor:
             factor_matrices.append(self.factor_matrices[i])
         return ttb.ktensor(factor_matrices, new_weights, copy=False)
 
-    def update(self, modes: Union[int, Iterable[int]], data: np.ndarray) -> ktensor:
+    def update(self, modes: OneDArray, data: np.ndarray) -> ktensor:
         """
         Updates a :class:`pyttb.ktensor` in the specific dimensions with the
         values in `data` (in vector or matrix form). The value of `modes` must
@@ -2191,13 +2189,10 @@ class ktensor:
          [4. 4.]]
 
         """
-        if not isinstance(modes, int):
-            modes = np.array(modes)
-            assert np.all(
-                modes[:-1] <= modes[1:]
-            ), "Modes must be sorted in ascending order"
-        else:
-            modes = np.array([modes])
+        modes = parse_one_d(modes)
+        assert np.all(
+            modes[:-1] <= modes[1:]
+        ), "Modes must be sorted in ascending order"
 
         loc = 0  # Location in data array
         for k in modes:
@@ -2299,7 +2294,7 @@ class ktensor:
 
         Use plot K using default behavior K.vis()
 
-        >>> fig, axs = K.vis()  # doctest: +ELLIPSIS
+        >>> fig, axs = K.vis(show_figure=False)  # doctest: +ELLIPSIS
         >>> plt.close(fig)
 
         Define a more realistic plot fuctions with x labels,
@@ -2317,6 +2312,7 @@ class ktensor:
         ...     ax.set_xlabel("$E$, [kJ]")
         >>> plots = [mode_1_plot, mode_2_plot, mode_3_plot]
         >>> fig, axs = K.vis(
+        ...     show_figure=False,
         ...     plots=plots,
         ...     rel_widths=[1, 2, 3],
         ...     horz_space=0.4,
diff --git a/pyttb/pyttb_utils.py b/pyttb/pyttb_utils.py
index 3fb2fcd..d96d9d2 100644
--- a/pyttb/pyttb_utils.py
+++ b/pyttb/pyttb_utils.py
@@ -8,11 +8,12 @@ from __future__ import annotations
 
 from enum import Enum
 from inspect import signature
+from math import prod
 from typing import (
     Iterable,
-    List,
     Literal,
     Optional,
+    Sequence,
     Tuple,
     Union,
     get_args,
@@ -23,6 +24,9 @@ import numpy as np
 
 import pyttb as ttb
 
+Shape = Union[int, Iterable[int]]
+OneDArray = Union[int, float, Iterable[int], Iterable[float], np.ndarray]
+
 
 def tt_union_rows(MatrixA: np.ndarray, MatrixB: np.ndarray) -> np.ndarray:
     """
@@ -73,8 +77,8 @@ def tt_union_rows(MatrixA: np.ndarray, MatrixB: np.ndarray) -> np.ndarray:
 def tt_dimscheck(
     N: int,
     M: None = None,
-    dims: Optional[np.ndarray] = None,
-    exclude_dims: Optional[np.ndarray] = None,
+    dims: Optional[OneDArray] = None,
+    exclude_dims: Optional[OneDArray] = None,
 ) -> Tuple[np.ndarray, None]: ...  # pragma: no cover see coveragepy/issues/970
 
 
@@ -82,16 +86,16 @@ def tt_dimscheck(
 def tt_dimscheck(
     N: int,
     M: int,
-    dims: Optional[np.ndarray] = None,
-    exclude_dims: Optional[np.ndarray] = None,
+    dims: Optional[OneDArray] = None,
+    exclude_dims: Optional[OneDArray] = None,
 ) -> Tuple[np.ndarray, np.ndarray]: ...  # pragma: no cover see coveragepy/issues/970
 
 
-def tt_dimscheck(
+def tt_dimscheck(  # noqa: PLR0912
     N: int,
     M: Optional[int] = None,
-    dims: Optional[np.ndarray] = None,
-    exclude_dims: Optional[np.ndarray] = None,
+    dims: Optional[OneDArray] = None,
+    exclude_dims: Optional[OneDArray] = None,
 ) -> Tuple[np.ndarray, Optional[np.ndarray]]:
     """
     Used to preprocess dimensions for tensor dimensions
@@ -105,6 +109,10 @@ def tt_dimscheck(
     """
     if dims is not None and exclude_dims is not None:
         raise ValueError("Either specify dims to include or exclude, but not both")
+    if dims is not None:
+        dims = parse_one_d(dims)
+    if exclude_dims is not None:
+        exclude_dims = parse_one_d(exclude_dims)
 
     dim_array: np.ndarray = np.empty((1,))
 
@@ -122,7 +130,8 @@ def tt_dimscheck(
         dim_array = np.setdiff1d(np.arange(0, N), exclude_dims)
 
     # Fix empty case
-    if (dims is None or dims.size == 0) and exclude_dims is None:
+    # if (dims is None or dims.size == 0) and exclude_dims is None:
+    if dims is None and exclude_dims is None:
         dim_array = np.arange(0, N)
     elif isinstance(dims, np.ndarray):
         dim_array = dims
@@ -255,9 +264,9 @@ def tt_tenfun(function_handle, *inputs):  # noqa: PLR0912
         X = inputs[0].data
         X = np.reshape(X, (1, -1))
     else:
-        X = np.zeros((len(inputs), np.prod(sz)))
+        X = np.zeros((len(inputs), prod(sz)))
         for i, an_input in enumerate(inputs):
-            X[i, :] = np.reshape(an_input.data, (np.prod(sz)))
+            X[i, :] = np.reshape(an_input.data, (prod(sz)))
     data = function_handle(X)
     data = np.reshape(data, sz)
     Z = ttb.tensor(data, copy=False)
@@ -516,7 +525,7 @@ def tt_ind2sub(shape: Tuple[int, ...], idx: np.ndarray) -> np.ndarray:
     """
     if idx.size == 0:
         return np.empty(shape=(0, len(shape)), dtype=int)
-    idx[idx < 0] += np.prod(shape)  # Handle negative indexing as simply as possible
+    idx[idx < 0] += prod(shape)  # Handle negative indexing as simply as possible
     return np.array(np.unravel_index(idx, shape, order="F")).transpose()
 
 
@@ -698,7 +707,7 @@ def tt_valscheck(vals: np.ndarray, nargout: bool = True) -> bool:
     else:
         ok = False
     if not ok and not nargout:
-        assert False, "Values must be in array"
+        assert False, f"Values must be in array but got {vals}"
     return ok
 
 
@@ -797,8 +806,8 @@ def get_index_variant(indices: IndexType) -> IndexVariant:
 
 
 def get_mttkrp_factors(
-    U: Union[ttb.ktensor, List[np.ndarray]], n: int, ndims: int
-) -> List[np.ndarray]:
+    U: Union[ttb.ktensor, Sequence[np.ndarray]], n: int, ndims: int
+) -> Sequence[np.ndarray]:
     """Apply standard checks and type conversions for mttkrp factors"""
     if isinstance(U, ttb.ktensor):
         U = U.copy()
@@ -812,8 +821,8 @@ def get_mttkrp_factors(
         U = U.factor_matrices
 
     assert isinstance(
-        U, (list, np.ndarray)
-    ), "Second argument must be list of numpy.ndarray's or a ktensor"
+        U, (Sequence, np.ndarray)
+    ), "Second argument must be a sequence of numpy.ndarray's or a ktensor"
 
     assert len(U) == ndims, "List of factor matrices is the wrong length"
 
@@ -879,3 +888,85 @@ def np_to_python(
         element.item() if isinstance(element, np.generic) else element
         for element in iterable
     )
+
+
+def parse_shape(shape: Shape) -> Tuple[int, ...]:
+    """Provides more flexible shape support
+
+
+    Examples
+    --------
+    >>> integer_shape = 4
+    >>> parse_shape(integer_shape)
+    (4,)
+    >>> flat_numpy_shape = np.ones((4,), dtype=int)
+    >>> parse_shape(flat_numpy_shape)
+    (1, 1, 1, 1)
+    >>> stacked_numpy_shape = np.ones((4, 1, 1), dtype=int)
+    >>> parse_shape(stacked_numpy_shape)
+    (1, 1, 1, 1)
+    >>> list_shape = [1, 1, 1, 1]
+    >>> parse_shape(list_shape)
+    (1, 1, 1, 1)
+    """
+    # FIXME do we care to map numpy ints to python ints?
+    if isinstance(shape, (int, np.integer)):
+        return (shape,)
+    if isinstance(shape, np.ndarray):
+        if not np.issubdtype(shape.dtype, np.integer):
+            raise ValueError("Numpy arrays used as shapes must be integer valued")
+        squeezed_shape = shape.squeeze()
+        if squeezed_shape.ndim == 0:
+            # If it's an array containing a single scalar
+            return (int(squeezed_shape),)
+        if squeezed_shape.ndim > 1:
+            raise ValueError(
+                "Numpy arrays used as shapes can only have one non-trivial dimension"
+            )
+        return tuple(map(int, squeezed_shape))
+
+    shape = tuple(shape)
+    if not all(isinstance(ele, (int, np.integer)) for ele in shape):
+        raise ValueError("Shapes entries must be integers")
+    return shape
+
+
+def parse_one_d(maybe_vector: OneDArray) -> np.ndarray:
+    """Provides more flexible vector support
+
+    Examples
+    --------
+    >>> int_scalar = 1
+    >>> parse_one_d(int_scalar)
+    array([1])
+    >>> np_int_scalar = np.int8(1)
+    >>> parse_one_d(np_int_scalar)
+    array([1], dtype=int8)
+    >>> float_scalar = 1.0
+    >>> parse_one_d(float_scalar)
+    array([1.])
+    >>> np_float_scalar = 1.0
+    >>> parse_one_d(np_float_scalar)
+    array([1.])
+    >>> example_list = [1.0, 1.0]
+    >>> parse_one_d(example_list)
+    array([1., 1.])
+    >>> extra_dims = np.array([[1, 1]])
+    >>> parse_one_d(extra_dims)
+    array([1, 1])
+    """
+    if isinstance(maybe_vector, (int, float, np.integer, np.floating)):
+        return np.array([maybe_vector])
+    if isinstance(maybe_vector, np.ndarray):
+        squeezed_vector = maybe_vector.squeeze()
+        if squeezed_vector.ndim == 1:
+            return squeezed_vector
+        elif squeezed_vector.ndim == 0:
+            # Squeezed to scalar so force vector
+            return squeezed_vector[None]
+        else:
+            raise ValueError(
+                "Vector can have at most one non-trivial dimension but "
+                f"had shape {maybe_vector.shape}"
+            )
+    return np.array(maybe_vector)
diff --git a/pyttb/sptensor.py b/pyttb/sptensor.py
index 3e5ad2d..a75ab7d 100644
--- a/pyttb/sptensor.py
+++ b/pyttb/sptensor.py
@@ -9,12 +9,12 @@ from __future__ import annotations
 import logging
 import warnings
 from collections.abc import Iterable, Sequence
+from math import prod
 from operator import ge, gt, le, lt
 from typing import (
     Any,
     Callable,
     Dict,
-    List,
     Literal,
     Optional,
     Tuple,
@@ -31,10 +31,14 @@ from scipy import sparse
 import pyttb as ttb
 from pyttb.pyttb_utils import (
     IndexVariant,
+    OneDArray,
+    Shape,
     gather_wrap_dims,
     get_index_variant,
     get_mttkrp_factors,
     np_to_python,
+    parse_one_d,
+    parse_shape,
     tt_dimscheck,
     tt_ind2sub,
     tt_intersect_rows,
@@ -85,7 +89,7 @@ class sptensor:
         self,
         subs: Optional[np.ndarray] = None,
         vals: Optional[np.ndarray] = None,
-        shape: Optional[Tuple[int, ...]] = None,
+        shape: Optional[Shape] = None,
         copy: bool = True,
     ):
         """
@@ -129,6 +133,8 @@ class sptensor:
             self.vals = np.array([], ndmin=2)
             self.shape: Union[Tuple[()], Tuple[int, ...]] = ()
             if shape is not None:
+                shape = parse_shape(shape)
+                # TODO do we need sizecheck or should that wrap in our share parser?
                 if not tt_sizecheck(shape):
                     raise ValueError(f"Invalid shape provided: {shape}")
                 self.shape = tuple(shape)
@@ -137,7 +143,9 @@ class sptensor:
             raise ValueError("If subs or vals are provided they must both be provided.")
 
         if shape is None:
-            shape = tuple(np.max(subs, axis=0) + 1)
+            shape = parse_shape(np.max(subs, axis=0) + 1)
+        else:
+            shape = parse_shape(shape)
 
         if subs.size > 0:
             assert subs.shape[1] == len(shape) and np.all(
@@ -169,7 +177,7 @@ class sptensor:
     def from_function(
         cls,
         function_handle: Callable[[Tuple[int, ...]], np.ndarray],
-        shape: Tuple[int, ...],
+        shape: Shape,
         nonzeros: float,
     ) -> sptensor:
         """
@@ -219,13 +227,14 @@ class sptensor:
         """
         assert callable(function_handle), "function_handle must be callable"
 
-        if (nonzeros < 0) or (nonzeros >= np.prod(shape)):
+        shape = parse_shape(shape)
+        if (nonzeros < 0) or (nonzeros >= prod(shape)):
             assert False, (
                 "Requested number of nonzeros must be positive "
                 "and less than the total size"
             )
         elif nonzeros < 1:
-            nonzeros = int(np.ceil(np.prod(shape) * nonzeros))
+            nonzeros = int(np.ceil(prod(shape) * nonzeros))
         else:
             nonzeros = int(np.floor(nonzeros))
         nonzeros = int(nonzeros)
@@ -252,7 +261,7 @@ class sptensor:
         cls,
         subs: np.ndarray,
         vals: np.ndarray,
-        shape: Optional[Tuple[int, ...]] = None,
+        shape: Optional[Shape] = None,
         function_handle: Union[str, Callable[[Any], Union[float, np.ndarray]]] = "sum",
     ) -> sptensor:
         """
@@ -307,16 +316,17 @@ class sptensor:
         [1, 2] = 6.0
         [1, 3] = 7.5
         """
-        tt_subscheck(subs)
-        tt_valscheck(vals)
+        tt_subscheck(subs, False)
+        tt_valscheck(vals, False)
         if subs.size > 1 and vals.shape[0] != subs.shape[0]:
             assert False, "Number of subscripts and values must be equal"
 
         # Extract the shape
         if shape is not None:
-            tt_sizecheck(shape)
+            shape = parse_shape(shape)
+            tt_sizecheck(shape, False)
         else:
-            shape = tuple(np.max(subs, axis=0) + 1)
+            shape = parse_shape(np.max(subs, axis=0) + 1)
 
         # Check for wrong input
         if subs.size > 0 and subs.shape[1] > len(shape):
@@ -396,7 +406,7 @@ class sptensor:
         if len(self.shape) == 0:
             return np.empty(shape=(1, 0), dtype=int)
 
-        s = np.zeros(shape=(np.prod(self.shape), self.ndims))
+        s = np.zeros(shape=(prod(self.shape), self.ndims))
 
         # Generate appropriately sized ones vectors
         o = []
@@ -413,7 +423,7 @@ class sptensor:
 
     def collapse(
         self,
-        dims: Optional[np.ndarray] = None,
+        dims: Optional[OneDArray] = None,
         function_handle: Callable[[np.ndarray], Union[float, np.ndarray]] = sum,
     ) -> Union[float, np.ndarray, sptensor]:
         """
@@ -454,9 +464,6 @@ class sptensor:
         >>> S.collapse(dims=np.array([0, 2]))
         array([6., 7.])
         """
-        if dims is None:
-            dims = np.arange(0, self.ndims)
-
         dims, _ = tt_dimscheck(self.ndims, dims=dims)
         remdims = np.setdiff1d(np.arange(0, self.ndims), dims)
 
@@ -558,7 +565,7 @@ class sptensor:
             )
 
         # Check if result should be dense
-        if y.nnz > 0.5 * np.prod(y.shape):
+        if y.nnz > 0.5 * prod(y.shape):
             # Final result is a dense tensor
             return y.to_tensor()
         return y
@@ -1236,7 +1243,7 @@ class sptensor:
         vals[matching_indices] = self.vals[matching_indices]
         return vals
 
-    def mttkrp(self, U: Union[ttb.ktensor, List[np.ndarray]], n: int) -> np.ndarray:
+    def mttkrp(self, U: Union[ttb.ktensor, Sequence[np.ndarray]], n: int) -> np.ndarray:
         """
         Matricized tensor times Khatri-Rao product using the
         :class:`pyttb.sptensor`. This is an efficient form of the matrix
@@ -1477,7 +1484,7 @@ class sptensor:
         oneVals.fill(1)
         return ttb.sptensor(self.subs, oneVals, self.shape)
 
-    def permute(self, order: np.ndarray) -> sptensor:
+    def permute(self, order: OneDArray) -> sptensor:
         """
         Permute the :class:`pyttb.sptensor` dimensions. The result is a new
         sparse tensor that has the same values, but the order of the
@@ -1512,6 +1519,7 @@ class sptensor:
         [1, 0] = 2.0
         [0, 1] = 3.0
         """
+        order = parse_one_d(order)
         # Error check
         if self.ndims != order.size or np.any(
             np.sort(order) != np.arange(0, self.ndims)
@@ -1527,7 +1535,7 @@ class sptensor:
 
     def reshape(
         self,
-        new_shape: Tuple[int, ...],
+        new_shape: Shape,
         old_modes: Optional[Union[np.ndarray, int]] = None,
     ) -> sptensor:
         """
@@ -1600,15 +1608,16 @@ class sptensor:
         shapeArray = np.array(self.shape)
         old_shape = shapeArray[old_modes]
         keep_shape = shapeArray[keep_modes]
+        new_shape = parse_shape(new_shape)
 
-        if np.prod(new_shape) != np.prod(old_shape):
+        if prod(new_shape) != prod(old_shape):
             assert False, "Reshape must maintain tensor size"
 
         if self.subs.size == 0:
             return ttb.sptensor(
                 np.array([]),
                 np.array([]),
-                tuple(np.concatenate((keep_shape, new_shape))),
+                np.concatenate((keep_shape, new_shape)),
                 copy=False,
             )
         if np.isscalar(old_shape):
@@ -1620,13 +1629,13 @@ class sptensor:
         return ttb.sptensor(
             np.concatenate((self.subs[:, keep_modes], new_subs), axis=1),
             self.vals,
-            tuple(np.concatenate((keep_shape, new_shape))),
+            np.concatenate((keep_shape, new_shape)),
         )
 
     def scale(
         self,
         factor: Union[np.ndarray, ttb.tensor, ttb.sptensor],
-        dims: Union[float, np.ndarray],
+        dims: OneDArray,
     ) -> sptensor:
         """
         Scale the :class:`pyttb.sptensor` along specified dimensions.
@@ -1671,8 +1680,6 @@ class sptensor:
         [0, 1, 2] = 24
         [0, 2, 2] = 27
         """
-        if isinstance(dims, (float, int)):
-            dims = np.array([dims])
         dims, _ = tt_dimscheck(self.ndims, dims=dims)
 
         if isinstance(factor, ttb.tensor):
@@ -1861,11 +1868,11 @@ class sptensor:
 
         return loc
 
-    def ttv(  # noqa: PLR0912
+    def ttv(
         self,
-        vector: Union[np.ndarray, List[np.ndarray]],
-        dims: Optional[Union[int, np.ndarray]] = None,
-        exclude_dims: Optional[Union[int, np.ndarray]] = None,
+        vector: Union[np.ndarray, Sequence[np.ndarray]],
+        dims: Optional[OneDArray] = None,
+        exclude_dims: Optional[OneDArray] = None,
     ) -> Union[sptensor, ttb.tensor, float]:
         """
         Multiplication of the :class:`pyttb.sptensor` with a vector.
@@ -1939,14 +1946,6 @@ class sptensor:
         >>> S1.ttv(vectors)
         36.0
         """
-        if dims is None and exclude_dims is None:
-            dims = np.array([])
-        elif isinstance(dims, (float, int)):
-            dims = np.array([dims])
-
-        if isinstance(exclude_dims, (float, int)):
-            exclude_dims = np.array([exclude_dims])
-
         # Check that vector is a list of vectors,
         # if not place single vector as element in list
         if len(vector) > 0 and isinstance(vector[0], (int, float, np.int_, np.float64)):
@@ -1990,7 +1989,7 @@ class sptensor:
             )
             if np.count_nonzero(c) <= 0.5 * newsiz:
                 return ttb.sptensor.from_aggregator(
-                    np.arange(0, newsiz)[:, None], c, tuple(newsiz)
+                    np.arange(0, newsiz)[:, None], c.reshape((len(c), 1)), tuple(newsiz)
                 )
             return ttb.tensor(c, tuple(newsiz), copy=False)
 
@@ -1998,7 +1997,7 @@ class sptensor:
         c = ttb.sptensor.from_aggregator(newsubs, newvals, tuple(newsiz))
 
         # Convert to a dense tensor if more than 50% of the result is nonzero.
-        if c.nnz > 0.5 * np.prod(newsiz):
+        if c.nnz > 0.5 * prod(newsiz):
             c = c.to_tensor()
 
         return c
@@ -2142,7 +2141,7 @@ class sptensor:
             if isinstance(item, (int, float, np.generic, slice)):
                 if isinstance(item, (int, float, np.generic)):
                     if item < 0:
-                        item = np.prod(self.shape) + item
+                        item = prod(self.shape) + item
                     idx = np.array([item])
                 elif isinstance(item, slice):
                     idx = np.array(range(np.prod(self.shape))[item])
@@ -2756,7 +2755,7 @@ class sptensor:
             unionSubs = tt_union_rows(
                 self.subs, np.array(np.where(other.data == 0)).transpose()
             )
-            if unionSubs.shape[0] != np.prod(self.shape):
+            if unionSubs.shape[0] != prod(self.shape):
                 subs1Idx = tt_setdiff_rows(self.allsubs(), unionSubs)
                 subs1 = self.allsubs()[subs1Idx]
             else:
@@ -3418,9 +3417,9 @@ class sptensor:
 
     def ttm(
         self,
-        matrices: Union[np.ndarray, List[np.ndarray]],
-        dims: Optional[Union[float, np.ndarray]] = None,
-        exclude_dims: Optional[Union[float, np.ndarray]] = None,
+        matrices: Union[np.ndarray, Sequence[np.ndarray]],
+        dims: Optional[OneDArray] = None,
+        exclude_dims: Optional[OneDArray] = None,
         transpose: bool = False,
     ) -> Union[ttb.tensor, sptensor]:
         """
@@ -3482,18 +3481,8 @@ class sptensor:
         data[1, 1, :, :] =
         [[0.]]
         """
-        if dims is None and exclude_dims is None:
-            dims = np.arange(self.ndims)
-        elif isinstance(dims, list):
-            dims = np.array(dims)
-        elif isinstance(dims, (float, int, np.generic)):
-            dims = np.array([dims])
-
-        if isinstance(exclude_dims, (float, int)):
-            exclude_dims = np.array([exclude_dims])
-
         # Handle list of matrices
-        if isinstance(matrices, list):
+        if isinstance(matrices, Sequence):
             # Check dimensions are valid
             [dims, vidx] = tt_dimscheck(self.ndims, len(matrices), dims, exclude_dims)
             # Calculate individual products
@@ -3510,9 +3499,9 @@ class sptensor:
         if transpose:
             matrices = matrices.transpose()
 
-        # FIXME: This made typing happy but shouldn't be possible
-        if not isinstance(dims, np.ndarray):  # pragma: no cover
-            raise ValueError("Dims should be an array here")
+        # This is slightly inefficient for the looping above
+        # consider short circuit
+        dims, _ = tt_dimscheck(self.ndims, None, dims, exclude_dims)
 
         # Ensure this is the terminal single dimension case
         if not (dims.size == 1 and np.isin(dims, np.arange(self.ndims))):
@@ -3538,7 +3527,7 @@ class sptensor:
         # Rearrange back into sparse tensor of correct shape
         Ynt = ttb.sptenmat.from_array(Z, Xnt.rdims, Xnt.cdims, tuple(siz)).to_sptensor()
 
-        if not isinstance(Z, np.ndarray) and Z.nnz <= 0.5 * np.prod(siz):
+        if not isinstance(Z, np.ndarray) and Z.nnz <= 0.5 * prod(siz):
             return Ynt
         # TODO evaluate performance loss by casting into sptensor then tensor.
         #  I assume minimal since we are already using sparse matrix representation
@@ -3608,7 +3597,7 @@ class sptensor:
 
 
 def sptenrand(
-    shape: Tuple[int, ...],
+    shape: Shape,
     density: Optional[float] = None,
     nonzeros: Optional[float] = None,
 ) -> sptensor:
@@ -3646,8 +3635,10 @@ def sptenrand(
     if density is not None and not 0 < density <= 1:
         raise ValueError(f"Density must be a fraction (0, 1] but received {density}")
 
+    shape = parse_shape(shape)
     if isinstance(density, float):
-        valid_nonzeros = float(np.prod(shape) * density)
+        # TODO this should be an int
+        valid_nonzeros = float(prod(shape) * density)
     elif isinstance(nonzeros, (int, float)):
         valid_nonzeros = nonzeros
     else:  # pragma: no cover
@@ -3663,9 +3654,7 @@ def sptenrand(
     return ttb.sptensor.from_function(unit_uniform, shape, valid_nonzeros)
 
 
-def sptendiag(
-    elements: np.ndarray, shape: Optional[Tuple[int, ...]] = None
-) -> sptensor:
+def sptendiag(elements: OneDArray, shape: Optional[Shape] = None) -> sptensor:
     """
     Creates a :class:`pyttb.sptensor` with elements along the super diagonal.
     If provided shape is too small the sparse tensor will be enlarged to
@@ -3695,11 +3684,12 @@ def sptendiag(
     True
     """
     # Flatten provided elements
-    elements = np.ravel(elements)
+    elements = parse_one_d(elements)
     N = len(elements)
     if shape is None:
         constructed_shape = (N,) * N
     else:
+        shape = parse_shape(shape)
         constructed_shape = tuple(max(N, dim) for dim in shape)
     subs = np.tile(np.arange(0, N).transpose(), (len(constructed_shape), 1)).transpose()
     return sptensor.from_aggregator(subs, elements.reshape((N, 1)), constructed_shape)
diff --git a/pyttb/tenmat.py b/pyttb/tenmat.py
index 683f0b6..d9f294b 100644
--- a/pyttb/tenmat.py
+++ b/pyttb/tenmat.py
@@ -6,12 +6,13 @@
 
 from __future__ import annotations
 
+from math import prod
 from typing import Optional, Tuple, Union
 
 import numpy as np
 
 import pyttb as ttb
-from pyttb.pyttb_utils import gather_wrap_dims, np_to_python
+from pyttb.pyttb_utils import Shape, gather_wrap_dims, np_to_python, parse_shape
 
 
 class tenmat:
@@ -27,7 +28,7 @@ class tenmat:
         data: Optional[np.ndarray] = None,
         rdims: Optional[np.ndarray] = None,
         cdims: Optional[np.ndarray] = None,
-        tshape: Optional[Tuple[int, ...]] = None,
+        tshape: Optional[Shape] = None,
         copy: bool = True,
     ):
         """
@@ -60,7 +61,7 @@ class tenmat:
         Create tensor shaped data.
 
         >>> tshape = (2, 2, 2)
-        >>> data = np.reshape(np.arange(np.prod(tshape), dtype=np.double), tshape)
+        >>> data = np.reshape(np.arange(prod(tshape), dtype=np.double), tshape)
         >>> data  # doctest: +NORMALIZE_WHITESPACE
         array([[[0., 1.],
                 [2., 3.]],
@@ -125,11 +126,10 @@ class tenmat:
         # use data.shape for tshape if not provided
         if tshape is None:
             tshape = data.shape
-        elif not isinstance(tshape, tuple):
-            assert False, "tshape must be a tuple."
+        tshape = parse_shape(tshape)
 
         # check that data.shape and tshape agree
-        if np.prod(data.shape) != np.prod(tshape):
+        if prod(data.shape) != prod(tshape):
             assert False, (
                 "Incorrect dimensions specified: products of data.shape and tuple do "
                 "not match"
@@ -142,7 +142,7 @@ class tenmat:
         # check that data.shape and product of dimensions agree
         if not np.prod(np.array(tshape)[rdims]) * np.prod(
             np.array(tshape)[cdims]
-        ) == np.prod(data.shape):
+        ) == prod(data.shape):
             assert (
                 False
             ), "data.shape does not match shape specified by rdims, cdims, and tshape."
diff --git a/pyttb/tensor.py b/pyttb/tensor.py
index fe729fb..80d1045 100644
--- a/pyttb/tensor.py
+++ b/pyttb/tensor.py
@@ -9,8 +9,18 @@ from __future__ import annotations
 import logging
 from collections.abc import Iterable
 from itertools import combinations_with_replacement, permutations
-from math import factorial
-from typing import Any, Callable, List, Literal, Optional, Tuple, Union, overload
+from math import factorial, prod
+from typing import (
+    Any,
+    Callable,
+    List,
+    Literal,
+    Optional,
+    Sequence,
+    Tuple,
+    Union,
+    overload,
+)
 
 import numpy as np
 import scipy.sparse.linalg
@@ -20,10 +30,14 @@ from scipy import sparse
 import pyttb as ttb
 from pyttb.pyttb_utils import (
     IndexVariant,
+    OneDArray,
+    Shape,
     gather_wrap_dims,
     get_index_variant,
     get_mttkrp_factors,
     np_to_python,
+    parse_one_d,
+    parse_shape,
     tt_dimscheck,
     tt_ind2sub,
     tt_sub2ind,
@@ -59,7 +73,7 @@ class tensor:
     def __init__(
         self,
         data: Optional[np.ndarray] = None,
-        shape: Optional[Tuple[int, ...]] = None,
+        shape: Optional[Shape] = None,
         copy: bool = True,
     ):
         """
@@ -110,15 +124,14 @@ class tensor:
         # Create or check second argument
         if shape is None:
             shape = data.shape
-        elif not isinstance(shape, tuple):
-            assert False, "Second argument must be a tuple."
+        shape = parse_shape(shape)
 
         # Make sure the number of elements matches what's been specified
         if len(shape) == 0:
             if data.size > 0:
                 assert False, "Empty tensor cannot contain any elements"
 
-        elif np.prod(shape) != data.size:
+        elif prod(shape) != data.size:
             assert (
                 False
             ), "TTB:WrongSize, Size of data does not match specified size of tensor"
@@ -140,7 +153,7 @@ class tensor:
     def from_function(
         cls,
         function_handle: Callable[[Tuple[int, ...]], np.ndarray],
-        shape: Tuple[int, ...],
+        shape: Shape,
     ) -> tensor:
         """
         Construct a :class:`pyttb.tensor` whose data entries are set using
@@ -176,8 +189,7 @@ class tensor:
          [1. 1. 1. 1.]]
         """
         # Check size
-        if not isinstance(shape, tuple):
-            assert False, "TTB:BadInput, Shape must be a tuple"
+        shape = parse_shape(shape)
 
         # Generate data
         data = function_handle(shape)
@@ -333,7 +345,7 @@ class tensor:
         newsize = tuple(np.array(self.shape)[remdims])
 
         # Total size of remainder
-        m = np.prod(newsize)
+        m = prod(newsize)
 
         # Number of items to add for trace
         n = self.shape[i1]
@@ -350,7 +362,7 @@ class tensor:
             newdata += data[:, idx, idx][:, None]
 
         # Reshape result
-        if np.prod(newsize) > 1:
+        if prod(newsize) > 1:
             newdata = np.reshape(newdata, newsize, order="F")
 
         return ttb.tensor(newdata, newsize, copy=False)
@@ -503,7 +515,7 @@ class tensor:
         Create a :class:`pyttb.tensor`.
 
         >>> tshape = (2, 2, 2)
-        >>> data = np.reshape(np.arange(np.prod(tshape)), tshape)
+        >>> data = np.reshape(np.arange(prod(tshape)), tshape)
         >>> T = ttb.tensor(data)
         >>> T  # doctest: +NORMALIZE_WHITESPACE
         tensor of shape (2, 2, 2)
@@ -869,7 +881,7 @@ class tensor:
         # Extract those non-zero values
         return self.data[tuple(wsubs.transpose())]
 
-    def mttkrp(self, U: Union[ttb.ktensor, List[np.ndarray]], n: int) -> np.ndarray:
+    def mttkrp(self, U: Union[ttb.ktensor, Sequence[np.ndarray]], n: int) -> np.ndarray:
         """
         Matricized tensor times Khatri-Rao product. The matrices used in the
         Khatri-Rao product are passed as a :class:`pyttb.ktensor` (where the
@@ -913,8 +925,8 @@ class tensor:
             if U[i].shape[0] != self.shape[i]:
                 assert False, f"Entry {i} of list of arrays is wrong size"
 
-        szl = int(np.prod(self.shape[0:n]))
-        szr = int(np.prod(self.shape[n + 1 :]))
+        szl = prod(self.shape[0:n])
+        szr = prod(self.shape[n + 1 :])
         szn = self.shape[n]
 
         if n == 0:
@@ -938,7 +950,7 @@ class tensor:
                 V[:, [r]] = Y[:, :, r].T @ Ur[:, :, r]
             return V
 
-    def mttkrps(self, U: Union[ttb.ktensor, List[np.ndarray]]) -> List[np.ndarray]:
+    def mttkrps(self, U: Union[ttb.ktensor, Sequence[np.ndarray]]) -> List[np.ndarray]:
         """
         Sequence of MTTKRP calculations for a tensor.
 
@@ -1085,7 +1097,7 @@ class tensor:
                     v[:, i] *= -1
         return v
 
-    def permute(self, order: np.ndarray) -> tensor:
+    def permute(self, order: OneDArray) -> tensor:
         """
         Permute tensor dimensions. The result is a tensor that has the
         same values, but the order of the subscripts needed to access
@@ -1114,6 +1126,7 @@ class tensor:
         [[1 3]
          [2 4]]
         """
+        order = parse_one_d(order)
         if self.ndims != order.size:
             assert False, "Invalid permutation order"
 
@@ -1128,7 +1141,7 @@ class tensor:
         # Np transpose does error checking on order, acts as permutation
         return ttb.tensor(np.transpose(self.data, order), copy=False)
 
-    def reshape(self, shape: Tuple[int, ...]) -> tensor:
+    def reshape(self, shape: Shape) -> tensor:
         """
         Reshape the tensor.
 
@@ -1146,7 +1159,8 @@ class tensor:
         >>> T2.shape
         (4, 1)
         """
-        if np.prod(self.shape) != np.prod(shape):
+        shape = parse_shape(shape)
+        if prod(self.shape) != prod(shape):
             assert False, "Reshaping a tensor cannot change number of elements"
 
         return ttb.tensor(np.reshape(self.data, shape, order="F"), shape)
@@ -1351,7 +1365,7 @@ class tensor:
 
             # Create all the permuations to be averaged
             combo_lengths = [len(perm) for perm in combos]
-            total_perms = int(np.prod(combo_lengths))
+            total_perms = prod(combo_lengths)
             sym_perms = np.tile(np.arange(0, n), [total_perms, 1])
             for i in range(0, ngrps):
                 ntimes = np.prod(combo_lengths[0:i], dtype=int)
@@ -1386,9 +1400,9 @@ class tensor:
 
     def ttm(
         self,
-        matrix: Union[np.ndarray, List[np.ndarray]],
-        dims: Optional[Union[float, np.ndarray]] = None,
-        exclude_dims: Optional[Union[int, np.ndarray]] = None,
+        matrix: Union[np.ndarray, Sequence[np.ndarray]],
+        dims: Optional[OneDArray] = None,
+        exclude_dims: Optional[OneDArray] = None,
         transpose: bool = False,
     ) -> tensor:
         """
@@ -1444,17 +1458,7 @@ class tensor:
         data[1, 1, :, :] =
         [[16.]]
         """
-        if dims is None and exclude_dims is None:
-            dims = np.arange(self.ndims)
-        elif isinstance(dims, list):
-            dims = np.array(dims)
-        elif isinstance(dims, (float, int, np.generic)):
-            dims = np.array([dims])
-
-        if isinstance(exclude_dims, (float, int)):
-            exclude_dims = np.array([exclude_dims])
-
-        if isinstance(matrix, list):
+        if isinstance(matrix, Sequence):
             # Check that the dimensions are valid
             dims, vidx = tt_dimscheck(self.ndims, len(matrix), dims, exclude_dims)
 
@@ -1587,9 +1591,9 @@ class tensor:
 
     def ttv(
         self,
-        vector: Union[np.ndarray, List[np.ndarray]],
-        dims: Optional[Union[int, np.ndarray]] = None,
-        exclude_dims: Optional[Union[int, np.ndarray]] = None,
+        vector: Union[np.ndarray, Sequence[np.ndarray]],
+        dims: Optional[OneDArray] = None,
+        exclude_dims: Optional[OneDArray] = None,
     ) -> Union[float, tensor]:
         """
         Tensor times vector.
@@ -1612,7 +1616,7 @@ class tensor:
         Parameters
         ----------
         vector:
-            Vector or vectors to multiple by.
+            Vector or vectors to multiply by.
         dims:
             Dimensions to multiply against.
         exclude_dims:
@@ -1636,14 +1640,6 @@ class tensor:
         >>> T.ttv([np.ones(2), np.ones(2)])
         10.0
         """
-        if dims is None and exclude_dims is None:
-            dims = np.array([])
-        elif isinstance(dims, (float, int)):
-            dims = np.array([dims])
-
-        if isinstance(exclude_dims, (float, int)):
-            exclude_dims = np.array([exclude_dims])
-
         # Check that vector is a list of vectors, if not place single vector as element
         # in list
         if len(vector) > 0 and isinstance(vector[0], (int, float, np.int_, np.float64)):
@@ -1681,7 +1677,7 @@ class tensor:
 
     def ttsv(
         self,
-        vector: np.ndarray,
+        vector: OneDArray,
         skip_dim: Optional[int] = None,
         version: Optional[int] = None,
     ) -> Union[float, np.ndarray, tensor]:
@@ -1710,6 +1706,7 @@ class tensor:
         array([[1, 2],
                [3, 4]])
         """
+        vector = parse_one_d(vector)
         # Only two simple cases are supported
         if skip_dim is None:
             exclude_dims = None
@@ -1814,7 +1811,7 @@ class tensor:
         if isinstance(key, (int, float, np.generic)):
             idx = np.array([key])
         elif isinstance(key, slice):
-            idx = np.array(range(np.prod(self.shape))[key])
+            idx = np.array(range(prod(self.shape))[key])
         idx = tt_ind2sub(self.shape, idx)
         if idx.shape[0] == 1:
             self.data[tuple(idx[0, :])] = value
@@ -1950,7 +1947,7 @@ class tensor:
             if isinstance(item, (int, float, np.generic)):
                 idx = np.array(item)
             elif isinstance(item, slice):
-                idx = np.array(range(np.prod(self.shape))[item])
+                idx = np.array(range(prod(self.shape))[item])
             a = np.squeeze(self.data[tuple(tt_ind2sub(self.shape, idx).transpose())])
             # Todo if row make column?
             return tt_subsubsref(a, idx)
@@ -2550,7 +2547,7 @@ class tensor:
     __str__ = __repr__
 
 
-def tenones(shape: Tuple[int, ...]) -> tensor:
+def tenones(shape: Shape) -> tensor:
     """
     Creates a tensor of all ones.
 
@@ -2581,7 +2578,7 @@ def tenones(shape: Tuple[int, ...]) -> tensor:
     return tensor.from_function(np.ones, shape)
 
 
-def tenzeros(shape: Tuple[int, ...]) -> tensor:
+def tenzeros(shape: Shape) -> tensor:
     """
     Creates a tensor of all zeros.
 
@@ -2612,7 +2609,7 @@ def tenzeros(shape: Tuple[int, ...]) -> tensor:
     return tensor.from_function(np.zeros, shape)
 
 
-def tenrand(shape: Tuple[int, ...]) -> tensor:
+def tenrand(shape: Shape) -> tensor:
     """
     Creates a tensor with entries drawn from a uniform
     distribution on the unit interval.
@@ -2644,7 +2641,7 @@ def tenrand(shape: Tuple[int, ...]) -> tensor:
     return tensor.from_function(unit_uniform, shape)
 
 
-def tendiag(elements: np.ndarray, shape: Optional[Tuple[int, ...]] = None) -> tensor:
+def tendiag(elements: OneDArray, shape: Optional[Shape] = None) -> tensor:
     """
     Creates a tensor with elements along super diagonal. If provided shape is too
     small the tensor will be enlarged to accomodate.
@@ -2670,11 +2667,12 @@ def tendiag(elements: np.ndarray, shape: Optional[Tuple[int, ...]] = None) -> te
     True
     """
     # Flatten provided elements
-    elements = np.ravel(elements)
+    elements = parse_one_d(elements)
     N = len(elements)
     if shape is None:
         constructed_shape = (N,) * N
     else:
+        shape = parse_shape(shape)
         constructed_shape = tuple(max(N, dim) for dim in shape)
     X = tenzeros(constructed_shape)
     subs = np.tile(np.arange(0, N)[:, None], (len(constructed_shape),))
@@ -2758,7 +2756,7 @@ def mttv_left(W_in: np.ndarray, U1: np.ndarray) -> np.ndarray:
     return W_out
 
 
-def mttv_mid(W_in: np.ndarray, U_mid: List[np.ndarray]) -> np.ndarray:
+def mttv_mid(W_in: np.ndarray, U_mid: Sequence[np.ndarray]) -> np.ndarray:
     """
     Contract intermediate modes in partial MTTKRP W_in using factor matrices U_mid.
 
@@ -2785,7 +2783,7 @@ def mttv_mid(W_in: np.ndarray, U_mid: List[np.ndarray]) -> np.ndarray:
     return V
 
 
-def min_split(shape: Tuple[int, ...]) -> int:
+def min_split(shape: Shape) -> int:
     """Scan for optimal splitting with minimal memory footprint.
 
     Parameters
@@ -2799,8 +2797,9 @@ def min_split(shape: Tuple[int, ...]) -> int:
         Modes 0:split will contract in left-partial computation and the
         rest will contract in right-partial.
     """
+    shape = parse_shape(shape)
     m_left = shape[0]
-    m_right = np.prod(shape[1:])
+    m_right = prod(shape[1:])
     idx_min = 0
 
     # Minimize m_left + m_right
diff --git a/pyttb/ttensor.py b/pyttb/ttensor.py
index cef017e..c33c8ae 100644
--- a/pyttb/ttensor.py
+++ b/pyttb/ttensor.py
@@ -9,7 +9,7 @@ from __future__ import annotations
 import logging
 import textwrap
 from copy import deepcopy
-from typing import List, Optional, Tuple, Union
+from typing import List, Optional, Sequence, Tuple, Union
 
 import numpy as np
 import scipy
@@ -17,6 +17,7 @@ from scipy import sparse
 
 import pyttb as ttb
 from pyttb import pyttb_utils as ttb_utils
+from pyttb.pyttb_utils import OneDArray, parse_one_d
 
 ALT_CORE_ERROR = "TTensor doesn't support non-tensor cores yet. Only tensor/sptensor."
 
@@ -31,7 +32,7 @@ class ttensor:
     def __init__(
         self,
         core: Optional[Union[ttb.tensor, ttb.sptensor]] = None,
-        factors: Optional[List[np.ndarray]] = None,
+        factors: Optional[Sequence[np.ndarray]] = None,
         copy: bool = True,
     ) -> None:
         """
@@ -80,10 +81,16 @@ class ttensor:
         if isinstance(core, (ttb.tensor, ttb.sptensor)):
             if copy:
                 self.core = core.copy()
-                self.factor_matrices = deepcopy(factors)
+                self.factor_matrices = list(deepcopy(factors))
             else:
                 self.core = core
-                self.factor_matrices = factors
+                if isinstance(factors, list):
+                    self.factor_matrices = factors
+                else:
+                    logging.warning(
+                        "Must provide factor matrices as list to avoid copy"
+                    )
+                    self.factor_matrices = list(factors)
         else:
             # TODO support any tensor type with supported ops
             raise ValueError(ALT_CORE_ERROR)
@@ -342,9 +349,9 @@ class ttensor:
 
     def ttv(
         self,
-        vector: Union[List[np.ndarray], np.ndarray],
-        dims: Optional[Union[int, np.ndarray]] = None,
-        exclude_dims: Optional[Union[int, np.ndarray]] = None,
+        vector: Union[Sequence[np.ndarray], np.ndarray],
+        dims: Optional[OneDArray] = None,
+        exclude_dims: Optional[OneDArray] = None,
     ) -> Union[float, ttensor]:
         """
         TTensor times vector
@@ -358,15 +365,6 @@ class ttensor:
         exclude_dims:
             Alternative multiply by all dimensions but these.
         """
-        if dims is None and exclude_dims is None:
-            dims = np.array([])
-        # TODO make helper function to check scalar since re-used many places
-        elif isinstance(dims, (float, int)):
-            dims = np.array([dims])
-
-        if isinstance(exclude_dims, (float, int)):
-            exclude_dims = np.array([exclude_dims])
-
         # Check that vector is a list of vectors,
         # if not place single vector as element in list
         if (
@@ -403,7 +401,7 @@ class ttensor:
         assert not isinstance(newcore, float)
         return ttensor(newcore, [self.factor_matrices[dim] for dim in remdims])
 
-    def mttkrp(self, U: Union[ttb.ktensor, List[np.ndarray]], n: int) -> np.ndarray:
+    def mttkrp(self, U: Union[ttb.ktensor, Sequence[np.ndarray]], n: int) -> np.ndarray:
         """
         Matricized tensor times Khatri-Rao product for ttensors.
 
@@ -449,7 +447,7 @@ class ttensor:
             return np.sqrt(tmp)
         return self.full().norm()
 
-    def permute(self, order: np.ndarray) -> ttensor:
+    def permute(self, order: OneDArray) -> ttensor:
         """
         Permute :class:`pyttb.ttensor` dimensions.
 
@@ -467,6 +465,7 @@ class ttensor:
         -------
         Permuted :class:`pyttb.ttensor`.
         """
+        order = parse_one_d(order)
         if not np.array_equal(np.arange(0, self.ndims), np.sort(order)):
             raise ValueError("Invalid permutation")
         new_core = self.core.permute(order)
@@ -475,7 +474,7 @@ class ttensor:
 
     def ttm(
         self,
-        matrix: Union[np.ndarray, List[np.ndarray]],
+        matrix: Union[np.ndarray, Sequence[np.ndarray]],
         dims: Optional[Union[float, np.ndarray]] = None,
         exclude_dims: Optional[Union[int, np.ndarray]] = None,
         transpose: bool = False,
@@ -504,7 +503,7 @@ class ttensor:
         if isinstance(exclude_dims, (float, int)):
             exclude_dims = np.array([exclude_dims])
 
-        if not isinstance(matrix, list):
+        if not isinstance(matrix, Sequence):
             return self.ttm([matrix], dims, exclude_dims, transpose)
 
         # Check that the dimensions are valid
@@ -530,8 +529,8 @@ class ttensor:
 
     def reconstruct(  # noqa: PLR0912
         self,
-        samples: Optional[Union[np.ndarray, List[np.ndarray]]] = None,
-        modes: Optional[Union[np.ndarray, List[np.ndarray]]] = None,
+        samples: Optional[Union[np.ndarray, Sequence[np.ndarray]]] = None,
+        modes: Optional[Union[np.ndarray, Sequence[np.ndarray]]] = None,
     ) -> ttb.tensor:
         """
         Reconstruct or partially reconstruct tensor from ttensor.
@@ -559,14 +558,14 @@ class ttensor:
 
         if modes is None:
             modes = np.arange(self.ndims)
-        elif isinstance(modes, list):
+        elif isinstance(modes, Sequence):
             modes = np.array(modes)
         elif np.isscalar(modes):
             modes = np.array([modes])
 
         if np.isscalar(samples):
             samples = [np.array([samples])]
-        elif not isinstance(samples, list):
+        elif not isinstance(samples, Sequence):
             samples = [samples]
 
         unequal_lengths = len(samples) > 0 and len(samples) != len(modes)
diff --git a/pyttb/tucker_als.py b/pyttb/tucker_als.py
index 452e1bd..430e404 100644
--- a/pyttb/tucker_als.py
+++ b/pyttb/tucker_als.py
@@ -7,20 +7,21 @@
 from __future__ import annotations
 
 from numbers import Real
-from typing import Dict, List, Literal, Optional, Tuple, Union
+from typing import Dict, Literal, Optional, Tuple, Union
 
 import numpy as np
 
 import pyttb as ttb
+from pyttb.pyttb_utils import OneDArray, parse_one_d
 from pyttb.ttensor import ttensor
 
 
-def tucker_als(  # noqa: PLR0912,PLR0913,PLR0915
+def tucker_als(  # noqa: PLR0912, PLR0913, PLR0915
     input_tensor: ttb.tensor,
-    rank: Union[int, List[int], np.ndarray],
+    rank: OneDArray,
     stoptol: float = 1e-4,
     maxiters: int = 1000,
-    dimorder: Optional[List[int]] = None,
+    dimorder: Optional[OneDArray] = None,
     init: Union[Literal["random"], Literal["nvecs"], ttb.ktensor] = "random",
     printitn: int = 1,
 ) -> Tuple[ttensor, ttensor, Dict]:
@@ -89,19 +90,17 @@ def tucker_als(  # noqa: PLR0912,PLR0913,PLR0915
             f"printitn must be a real valued scalar but received: {printitn}"
         )
 
-    if isinstance(rank, Real) or len(rank) == 1:
-        rank = rank * np.ones(N, dtype=int)
+    rank = parse_one_d(rank)
+    if len(rank) == 1:
+        rank = rank.repeat(N)
 
     # Set up dimorder if not specified
-    if not dimorder:
-        dimorder = list(range(N))
+    if dimorder is None:
+        dimorder = np.arange(N)
     else:
-        if not isinstance(dimorder, list):
-            raise ValueError("Dimorder must be a list")
+        dimorder = parse_one_d(dimorder)
         if tuple(range(N)) != tuple(sorted(dimorder)):
-            raise ValueError(
-                "Dimorder must be a list or permutation of range(tensor.ndims)"
-            )
+            raise ValueError("Dimorder must be a permutation of range(tensor.ndims)")
 
     if isinstance(init, list):
         Uinit = init


Test patch:
diff --git a/tests/test_ktensor.py b/tests/test_ktensor.py
index 2c5b810..30c0af8 100644
--- a/tests/test_ktensor.py
+++ b/tests/test_ktensor.py
@@ -111,7 +111,7 @@ def test_ktensor_init(sample_ktensor_2way):
     # 'factor_matrices' must be a list
     with pytest.raises(AssertionError) as excinfo:
         ttb.ktensor(np.ones((2, 2)), np.array([2.0]))
-    assert "Input 'factor_matrices' must be a list." in str(excinfo)
+    assert "Input 'factor_matrices' must be a sequence." in str(excinfo)
 
     # each factor matrix should be a np.ndarray
     with pytest.raises(AssertionError) as excinfo:
@@ -517,15 +517,12 @@ def test_ktensor_mttkrp(sample_ktensor_3way):
     with pytest.raises(AssertionError) as excinfo:
         K.mttkrp(fm_wrong_size, 0)
     assert "List of factor matrices is the wrong length" in str(excinfo)
-    # Wrong input type
-    fm_wrong_type = (
-        K1.factor_matrices[0],
-        K1.factor_matrices[0],
-        K1.factor_matrices[0],
-    )
+
     with pytest.raises(AssertionError) as excinfo:
-        K.mttkrp(fm_wrong_type, 0)
-    assert "Second argument must be list of numpy.ndarray's" in str(excinfo)
+        K.mttkrp(5, 0)
+    assert "Second argument must be a sequence of numpy.ndarray's or a ktensor" in str(
+        excinfo
+    )
 
 
 def test_ktensor_ncomponents(sample_ktensor_2way):
diff --git a/tests/test_pyttb_utils.py b/tests/test_pyttb_utils.py
index d5358e1..f58a428 100644
--- a/tests/test_pyttb_utils.py
+++ b/tests/test_pyttb_utils.py
@@ -9,6 +9,7 @@ import pytest
 
 import pyttb as ttb
 import pyttb.pyttb_utils as ttb_utils
+from pyttb.pyttb_utils import parse_one_d, parse_shape
 
 
 def test_tt_union_rows():
@@ -25,7 +26,7 @@ def test_tt_union_rows():
 
 def test_tt_dimscheck():
     #  Empty
-    rdims, ridx = ttb_utils.tt_dimscheck(6, dims=np.array([]))
+    rdims, ridx = ttb_utils.tt_dimscheck(6, dims=None)
     assert np.array_equal(rdims, np.array([0, 1, 2, 3, 4, 5]))
     assert ridx is None
 
@@ -492,3 +493,15 @@ def test_get_index_variant_subtensor():
 
 def test_get_index_variant_unknown():
     assert ttb_utils.get_index_variant("a") == ttb_utils.IndexVariant.UNKNOWN
+
+
+def test_parse_shape():
+    with pytest.raises(ValueError):
+        parse_shape(np.ones((4,), dtype=float))
+    with pytest.raises(ValueError):
+        parse_shape(np.ones((4, 2), dtype=int))
+
+
+def test_parse_one_d():
+    with pytest.raises(ValueError):
+        parse_one_d(np.ones((4, 2)))
diff --git a/tests/test_sptensor.py b/tests/test_sptensor.py
index 72360bf..2417eb0 100644
--- a/tests/test_sptensor.py
+++ b/tests/test_sptensor.py
@@ -142,7 +142,7 @@ def test_sptensor_initialization_from_aggregator(sample_sptensor):
 
     with pytest.raises(AssertionError) as excinfo:
         ttb.sptensor.from_aggregator(
-            np.concatenate((subs, np.ones((6, 1))), axis=1), vals, shape
+            np.concatenate((subs, np.ones((6, 1), dtype=int)), axis=1), vals, shape
         )
     assert "More subscripts than specified by shape" in str(excinfo)
 
@@ -1745,8 +1745,8 @@ def test_sptensor_mttkrp(sample_sptensor):
     assert "List of factor matrices is the wrong length" in str(excinfo)
 
     with pytest.raises(AssertionError) as excinfo:
-        sptensorInstance.mttkrp("string", 0)
-    assert "Second argument must be list of numpy.ndarray's or a ktensor" in str(
+        sptensorInstance.mttkrp(5, 0)
+    assert "Second argument must be a sequence of numpy.ndarray's or a ktensor" in str(
         excinfo
     )
 
diff --git a/tests/test_tenmat.py b/tests/test_tenmat.py
index 5451063..987b3eb 100644
--- a/tests/test_tenmat.py
+++ b/tests/test_tenmat.py
@@ -174,12 +174,6 @@ def test_tenmat_initialization_from_data(
         ttb.tenmat(ndarrayInstance1, rdims, cdims, None)
     assert exc in str(excinfo)
 
-    # tshape is not a tuple
-    exc = "tshape must be a tuple."
-    with pytest.raises(AssertionError) as excinfo:
-        ttb.tenmat(ndarrayInstance2, rdims, cdims, list(tshape))
-    assert exc in str(excinfo)
-
     # products of tshape and data.shape do not match
     exc = (
         "Incorrect dimensions specified: products of data.shape and tuple do not match"
diff --git a/tests/test_tensor.py b/tests/test_tensor.py
index 933afdb..638db35 100644
--- a/tests/test_tensor.py
+++ b/tests/test_tensor.py
@@ -62,10 +62,6 @@ def test_tensor_initialization_from_data(sample_tensor_2way):
         excinfo
     )
 
-    with pytest.raises(AssertionError) as excinfo:
-        ttb.tensor(params["data"], np.array([2, 3]))
-    assert "Second argument must be a tuple." in str(excinfo)
-
     # TODO how else to break this logical statement?
     data = np.array([["a", 2, 3], [4, 5, 6]])
     with pytest.raises(AssertionError) as excinfo:
@@ -115,10 +111,6 @@ def test_tensor_initialization_from_function():
     assert np.array_equal(a.data, data)
     assert a.shape == shape
 
-    with pytest.raises(AssertionError) as excinfo:
-        ttb.tensor.from_function(function_handle, [2, 3])
-    assert "TTB:BadInput, Shape must be a tuple" in str(excinfo)
-
 
 def test_tensor_copy(sample_tensor_2way):
     (params, tensor_instance) = sample_tensor_2way
@@ -1624,7 +1616,7 @@ def test_tensor_mttkrp(sample_tensor_2way):
     # second argument not a ktensor or list
     with pytest.raises(AssertionError) as excinfo:
         tensorInstance.mttkrp(5, 0)
-    assert "Second argument must be list of numpy.ndarray's or a ktensor" in str(
+    assert "Second argument must be a sequence of numpy.ndarray's or a ktensor" in str(
         excinfo
     )