Repository: scipy/scipy

Issue:
ENH: concatenating sparse arrays should return sparse arrays
### Is your feature request related to a problem? Please describe.

Concatenating sparse arrays (via `hstack`, `vstack`, `bmat`) returns a sparse matrix.

### Describe the solution you'd like.

I would like them to maintain the input classes.

### Describe alternatives you've considered.

`hstack` and `vstack` could be replaced with something more `concat`, which is in the `array-api`, unlike `hstack`, `vstack`.

cc: @dschult

### Additional context (e.g. screenshots, GIFs)

_No response_


Discussion:
The alternative you describe sounds like a good idea in any case. Moving towards supporting the Array API is something we might want to do in general.
The specification would be around what the outcome would be if I supply CSR, CSC and DIA arrays. Also note that, `np.hstack([a, b])` is order of magnitude more readable than `np.concat([a, b], axis=1)` so not sure that we should get rid of them that quick. 
> Also note that, np.hstack([a, b]) is order of magnitude more readable than np.concat([a, b], axis=1) so not sure that we should get rid of them that quick.

I was not suggesting a removal, just moving towards supporting the Array API. That does not necessarily imply that we would remove methods not part of the standard.

Resolving patch:
diff --git a/scipy/sparse/__init__.py b/scipy/sparse/__init__.py
--- a/scipy/sparse/__init__.py
+++ b/scipy/sparse/__init__.py
@@ -95,7 +95,8 @@
    block_diag - Build a block diagonal sparse matrix
    tril - Lower triangular portion of a matrix in sparse format
    triu - Upper triangular portion of a matrix in sparse format
-   bmat - Build a sparse matrix from sparse sub-blocks
+   block - Build a sparse array from sub-blocks
+   bmat - Build a sparse matrix from sub-blocks
    hstack - Stack sparse matrices horizontally (column wise)
    vstack - Stack sparse matrices vertically (row wise)
    rand - Random values in a given shape
diff --git a/scipy/sparse/_construct.py b/scipy/sparse/_construct.py
--- a/scipy/sparse/_construct.py
+++ b/scipy/sparse/_construct.py
@@ -5,7 +5,7 @@
 
 __all__ = ['spdiags', 'eye', 'identity', 'kron', 'kronsum',
            'hstack', 'vstack', 'bmat', 'rand', 'random', 'diags', 'block_diag',
-           'diags_array']
+           'diags_array', 'block']
 
 import numbers
 from functools import partial
@@ -16,12 +16,12 @@
 
 from ._sparsetools import csr_hstack
 from ._bsr import bsr_matrix
-from ._coo import coo_matrix
-from ._csc import csc_matrix
-from ._csr import csr_matrix
+from ._coo import coo_matrix, coo_array
+from ._csc import csc_matrix, csc_array
+from ._csr import csr_matrix, csr_array
 from ._dia import dia_matrix, dia_array
 
-from ._base import issparse
+from ._base import issparse, sparray
 
 
 def spdiags(data, diags, m=None, n=None, format=None):
@@ -481,9 +481,9 @@ def kronsum(A, B, format=None):
     return (L+R).asformat(format)  # since L + R is not always same format
 
 
-def _compressed_sparse_stack(blocks, axis):
+def _compressed_sparse_stack(blocks, axis, return_spmatrix):
     """
-    Stacking fast path for CSR/CSC matrices
+    Stacking fast path for CSR/CSC matrices or arrays
     (i) vstack for CSR, (ii) hstack for CSC.
     """
     other_axis = 1 if axis == 0 else 0
@@ -507,11 +507,20 @@ def _compressed_sparse_stack(blocks, axis):
         sum_dim += b.shape[axis]
         last_indptr += b.indptr[-1]
     indptr[-1] = last_indptr
+    # TODO remove this if-structure when sparse matrices removed
+    if return_spmatrix:
+        if axis == 0:
+            return csr_matrix((data, indices, indptr),
+                              shape=(sum_dim, constant_dim))
+        else:
+            return csc_matrix((data, indices, indptr),
+                              shape=(constant_dim, sum_dim))
+
     if axis == 0:
-        return csr_matrix((data, indices, indptr),
+        return csr_array((data, indices, indptr),
                           shape=(sum_dim, constant_dim))
     else:
-        return csc_matrix((data, indices, indptr),
+        return csc_array((data, indices, indptr),
                           shape=(constant_dim, sum_dim))
 
 
@@ -565,10 +574,10 @@ def _stack_along_minor_axis(blocks, axis):
         data = np.empty(0, dtype=data_cat.dtype)
 
     if axis == 0:
-        return csc_matrix((data, indices, indptr),
+        return blocks[0]._csc_container((data, indices, indptr),
                           shape=(sum_dim, constant_dim))
     else:
-        return csr_matrix((data, indices, indptr),
+        return blocks[0]._csr_container((data, indices, indptr),
                           shape=(constant_dim, sum_dim))
 
 
@@ -588,6 +597,16 @@ def hstack(blocks, format=None, dtype=None):
         The data-type of the output matrix. If not given, the dtype is
         determined from that of `blocks`.
 
+    Returns
+    -------
+    new_array : sparse matrix or array
+        If any block in blocks is a sparse array, return a sparse array.
+        Otherwise return a sparse matrix.
+
+        If you want a sparse array built from blocks that are not sparse
+        arrays, use `block(hstack(blocks))` or convert one block
+        e.g. `blocks[0] = csr_array(blocks[0])`.
+
     See Also
     --------
     vstack : stack sparse matrices vertically (row wise)
@@ -602,46 +621,68 @@ def hstack(blocks, format=None, dtype=None):
            [3, 4, 6]])
 
     """
-    return bmat([blocks], format=format, dtype=dtype)
+    blocks = np.asarray(blocks, dtype='object')
+    if any(isinstance(b, sparray) for b in blocks.flat):
+        return _block([blocks], format, dtype)
+    else:
+        return _block([blocks], format, dtype, return_spmatrix=True)
 
 
 def vstack(blocks, format=None, dtype=None):
     """
-    Stack sparse matrices vertically (row wise)
+    Stack sparse arrays vertically (row wise)
 
     Parameters
     ----------
     blocks
-        sequence of sparse matrices with compatible shapes
+        sequence of sparse arrays with compatible shapes
     format : str, optional
         sparse format of the result (e.g., "csr")
-        by default an appropriate sparse matrix format is returned.
+        by default an appropriate sparse array format is returned.
         This choice is subject to change.
     dtype : dtype, optional
-        The data-type of the output matrix. If not given, the dtype is
+        The data-type of the output array. If not given, the dtype is
         determined from that of `blocks`.
 
+    Returns
+    -------
+    new_array : sparse matrix or array
+        If any block in blocks is a sparse array, return a sparse array.
+        Otherwise return a sparse matrix.
+
+        If you want a sparse array built from blocks that are not sparse
+        arrays, use `block(vstack(blocks))` or convert one block
+        e.g. `blocks[0] = csr_array(blocks[0])`.
+
     See Also
     --------
     hstack : stack sparse matrices horizontally (column wise)
 
     Examples
     --------
-    >>> from scipy.sparse import coo_matrix, vstack
-    >>> A = coo_matrix([[1, 2], [3, 4]])
-    >>> B = coo_matrix([[5, 6]])
+    >>> from scipy.sparse import coo_array, vstack
+    >>> A = coo_array([[1, 2], [3, 4]])
+    >>> B = coo_array([[5, 6]])
     >>> vstack([A, B]).toarray()
     array([[1, 2],
            [3, 4],
            [5, 6]])
 
     """
-    return bmat([[b] for b in blocks], format=format, dtype=dtype)
+    blocks = np.asarray(blocks, dtype='object')
+    if any(isinstance(b, sparray) for b in blocks.flat):
+        return _block([[b] for b in blocks], format, dtype)
+    else:
+        return _block([[b] for b in blocks], format, dtype, return_spmatrix=True)
 
 
 def bmat(blocks, format=None, dtype=None):
     """
-    Build a sparse matrix from sparse sub-blocks
+    Build a sparse array or matrix from sparse sub-blocks
+
+    Note: `block` is preferred over `bmat`. They are the same function
+    except that `bmat` can return a deprecated sparse matrix.
+    `bmat` returns a coo_matrix if none of the inputs are a sparse array.
 
     Parameters
     ----------
@@ -658,18 +699,23 @@ def bmat(blocks, format=None, dtype=None):
 
     Returns
     -------
-    bmat : sparse matrix
+    bmat : sparse matrix or array
+        If any block in blocks is a sparse array, return a sparse array.
+        Otherwise return a sparse matrix.
+
+        If you want a sparse array built from blocks that are not sparse
+        arrays, use `block()`.
 
     See Also
     --------
-    block_diag, diags
+    block
 
     Examples
     --------
-    >>> from scipy.sparse import coo_matrix, bmat
-    >>> A = coo_matrix([[1, 2], [3, 4]])
-    >>> B = coo_matrix([[5], [6]])
-    >>> C = coo_matrix([[7]])
+    >>> from scipy.sparse import coo_array, bmat
+    >>> A = coo_array([[1, 2], [3, 4]])
+    >>> B = coo_array([[5], [6]])
+    >>> C = coo_array([[7]])
     >>> bmat([[A, B], [None, C]]).toarray()
     array([[1, 2, 5],
            [3, 4, 6],
@@ -681,7 +727,60 @@ def bmat(blocks, format=None, dtype=None):
            [0, 0, 7]])
 
     """
+    blocks = np.asarray(blocks, dtype='object')
+    if any(isinstance(b, sparray) for b in blocks.flat):
+        return _block(blocks, format, dtype)
+    else:
+        return _block(blocks, format, dtype, return_spmatrix=True)
 
+
+def block(blocks, *, format=None, dtype=None):
+    """
+    Build a sparse array from sparse sub-blocks
+
+    Parameters
+    ----------
+    blocks : array_like
+        Grid of sparse arrays with compatible shapes.
+        An entry of None implies an all-zero array.
+    format : {'bsr', 'coo', 'csc', 'csr', 'dia', 'dok', 'lil'}, optional
+        The sparse format of the result (e.g. "csr"). By default an
+        appropriate sparse array format is returned.
+        This choice is subject to change.
+    dtype : dtype, optional
+        The data-type of the output array. If not given, the dtype is
+        determined from that of `blocks`.
+
+    Returns
+    -------
+    block : sparse array
+
+    See Also
+    --------
+    block_diag : specify blocks along the main diagonals
+    diags : specify (possibly offset) diagonals
+
+    Examples
+    --------
+    >>> from scipy.sparse import coo_array, block
+    >>> A = coo_array([[1, 2], [3, 4]])
+    >>> B = coo_array([[5], [6]])
+    >>> C = coo_array([[7]])
+    >>> block([[A, B], [None, C]]).toarray()
+    array([[1, 2, 5],
+           [3, 4, 6],
+           [0, 0, 7]])
+
+    >>> block([[A, None], [None, C]]).toarray()
+    array([[1, 2, 0],
+           [3, 4, 0],
+           [0, 0, 7]])
+
+    """
+    return _block(blocks, format, dtype)
+
+
+def _block(blocks, format, dtype, return_spmatrix=False):
     blocks = np.asarray(blocks, dtype='object')
 
     if blocks.ndim != 2:
@@ -690,29 +789,29 @@ def bmat(blocks, format=None, dtype=None):
     M,N = blocks.shape
 
     # check for fast path cases
-    if (format in (None, 'csr') and all(isinstance(b, csr_matrix)
-                                        for b in blocks.flat)):
+    if (format in (None, 'csr') and
+        all(issparse(b) and b.format == 'csr' for b in blocks.flat)
+    ):
         if N > 1:
-            # stack along columns (axis 1):
-            blocks = [[_stack_along_minor_axis(blocks[b, :], 1)]
-                      for b in range(M)]   # must have shape: (M, 1)
+            # stack along columns (axis 1): must have shape (M, 1)
+            blocks = [[_stack_along_minor_axis(blocks[b, :], 1)] for b in range(M)]
             blocks = np.asarray(blocks, dtype='object')
 
         # stack along rows (axis 0):
-        A = _compressed_sparse_stack(blocks[:, 0], 0)
+        A = _compressed_sparse_stack(blocks[:, 0], 0, return_spmatrix)
         if dtype is not None:
             A = A.astype(dtype)
         return A
-    elif (format in (None, 'csc') and all(isinstance(b, csc_matrix)
-                                          for b in blocks.flat)):
+    elif (format in (None, 'csc') and
+          all(issparse(b) and b.format == 'csc' for b in blocks.flat)
+    ):
         if M > 1:
-            # stack along rows (axis 0):
-            blocks = [[_stack_along_minor_axis(blocks[:, b], 0)
-                       for b in range(N)]]   # must have shape: (1, N)
+            # stack along rows (axis 0): must have shape (1, N)
+            blocks = [[_stack_along_minor_axis(blocks[:, b], 0) for b in range(N)]]
             blocks = np.asarray(blocks, dtype='object')
 
         # stack along columns (axis 1):
-        A = _compressed_sparse_stack(blocks[0, :], 1)
+        A = _compressed_sparse_stack(blocks[0, :], 1, return_spmatrix)
         if dtype is not None:
             A = A.astype(dtype)
         return A
@@ -725,7 +824,7 @@ def bmat(blocks, format=None, dtype=None):
     for i in range(M):
         for j in range(N):
             if blocks[i,j] is not None:
-                A = coo_matrix(blocks[i,j])
+                A = coo_array(blocks[i,j])
                 blocks[i,j] = A
                 block_mask[i,j] = True
 
@@ -771,27 +870,31 @@ def bmat(blocks, format=None, dtype=None):
         np.add(B.col, col_offsets[j], out=col[idx], dtype=idx_dtype)
         nnz += B.nnz
 
-    return coo_matrix((data, (row, col)), shape=shape).asformat(format)
+    if return_spmatrix:
+        return coo_matrix((data, (row, col)), shape=shape).asformat(format)
+    return coo_array((data, (row, col)), shape=shape).asformat(format)
 
 
 def block_diag(mats, format=None, dtype=None):
     """
-    Build a block diagonal sparse matrix from provided matrices.
+    Build a block diagonal sparse matrix or array from provided matrices.
 
     Parameters
     ----------
-    mats : sequence of matrices
-        Input matrices.
+    mats : sequence of matrices or arrays
+        Input matrices or arrays.
     format : str, optional
-        The sparse format of the result (e.g., "csr"). If not given, the matrix
+        The sparse format of the result (e.g., "csr"). If not given, the result
         is returned in "coo" format.
     dtype : dtype specifier, optional
-        The data-type of the output matrix. If not given, the dtype is
+        The data-type of the output. If not given, the dtype is
         determined from that of `blocks`.
 
     Returns
     -------
-    res : sparse matrix
+    res : sparse matrix or array
+        If at least one input is a sparse array, the output is a sparse array.
+        Otherwise the output is a sparse matrix.
 
     Notes
     -----
@@ -800,14 +903,14 @@ def block_diag(mats, format=None, dtype=None):
 
     See Also
     --------
-    bmat, diags
+    block, diags
 
     Examples
     --------
-    >>> from scipy.sparse import coo_matrix, block_diag
-    >>> A = coo_matrix([[1, 2], [3, 4]])
-    >>> B = coo_matrix([[5], [6]])
-    >>> C = coo_matrix([[7]])
+    >>> from scipy.sparse import coo_array, block_diag
+    >>> A = coo_array([[1, 2], [3, 4]])
+    >>> B = coo_array([[5], [6]])
+    >>> C = coo_array([[7]])
     >>> block_diag((A, B, C)).toarray()
     array([[1, 2, 0, 0],
            [3, 4, 0, 0],
@@ -816,6 +919,11 @@ def block_diag(mats, format=None, dtype=None):
            [0, 0, 0, 7]])
 
     """
+    if any(isinstance(a, sparray) for a in mats):
+        container = coo_array
+    else:
+        container = coo_matrix
+
     row = []
     col = []
     data = []
@@ -823,7 +931,7 @@ def block_diag(mats, format=None, dtype=None):
     c_idx = 0
     for a in mats:
         if isinstance(a, (list, numbers.Number)):
-            a = coo_matrix(a)
+            a = coo_array(a)
         nrows, ncols = a.shape
         if issparse(a):
             a = a.tocoo()
@@ -840,7 +948,7 @@ def block_diag(mats, format=None, dtype=None):
     row = np.concatenate(row)
     col = np.concatenate(col)
     data = np.concatenate(data)
-    return coo_matrix((data, (row, col)),
+    return container((data, (row, col)),
                       shape=(r_idx, c_idx),
                       dtype=dtype).asformat(format)
 


Test patch:
diff --git a/scipy/sparse/tests/test_construct.py b/scipy/sparse/tests/test_construct.py
--- a/scipy/sparse/tests/test_construct.py
+++ b/scipy/sparse/tests/test_construct.py
@@ -10,9 +10,10 @@
 from scipy._lib._util import check_random_state
 
 from scipy.sparse import (csr_matrix, coo_matrix,
+                          csr_array, coo_array,
+                          sparray, spmatrix,
                           _construct as construct)
 from scipy.sparse._construct import rand as sprand
-from scipy.sparse._sputils import matrix
 
 sparse_formats = ['csr','csc','coo','bsr','dia','lil','dok']
 
@@ -306,7 +307,7 @@ def test_kron(self):
             for b in cases:
                 expected = np.kron(a, b)
                 for fmt in sparse_formats:
-                    result = construct.kron(csr_matrix(a), csr_matrix(b), format=fmt) 
+                    result = construct.kron(csr_matrix(a), csr_matrix(b), format=fmt)
                     assert_equal(result.format, fmt)
                     assert_array_equal(result.toarray(), expected)
 
@@ -338,10 +339,10 @@ def test_kronsum(self):
                         np.kron(b, np.eye(len(a)))
                 assert_array_equal(result,expected)
 
-    def test_vstack(self):
-
-        A = coo_matrix([[1,2],[3,4]])
-        B = coo_matrix([[5,6]])
+    @pytest.mark.parametrize("coo_cls", [coo_matrix, coo_array])
+    def test_vstack(self, coo_cls):
+        A = coo_cls([[1,2],[3,4]])
+        B = coo_cls([[5,6]])
 
         expected = array([[1, 2],
                           [3, 4],
@@ -364,10 +365,18 @@ def test_vstack(self):
         assert_equal(result.indices.dtype, np.int32)
         assert_equal(result.indptr.dtype, np.int32)
 
-    def test_hstack(self):
+    def test_vstack_matrix_or_array(self):
+        A = [[1,2],[3,4]]
+        B = [[5,6]]
+        assert isinstance(construct.vstack([coo_array(A), coo_array(B)]), sparray)
+        assert isinstance(construct.vstack([coo_array(A), coo_matrix(B)]), sparray)
+        assert isinstance(construct.vstack([coo_matrix(A), coo_array(B)]), sparray)
+        assert isinstance(construct.vstack([coo_matrix(A), coo_matrix(B)]), spmatrix)
 
-        A = coo_matrix([[1,2],[3,4]])
-        B = coo_matrix([[5],[6]])
+    @pytest.mark.parametrize("coo_cls", [coo_matrix, coo_array])
+    def test_hstack(self,coo_cls):
+        A = coo_cls([[1,2],[3,4]])
+        B = coo_cls([[5],[6]])
 
         expected = array([[1, 2, 5],
                           [3, 4, 6]])
@@ -385,85 +394,142 @@ def test_hstack(self):
                                       dtype=np.float32).dtype,
                      np.float32)
 
-    def test_bmat(self):
+    def test_hstack_matrix_or_array(self):
+        A = [[1,2],[3,4]]
+        B = [[5],[6]]
+        assert isinstance(construct.hstack([coo_array(A), coo_array(B)]), sparray)
+        assert isinstance(construct.hstack([coo_array(A), coo_matrix(B)]), sparray)
+        assert isinstance(construct.hstack([coo_matrix(A), coo_array(B)]), sparray)
+        assert isinstance(construct.hstack([coo_matrix(A), coo_matrix(B)]), spmatrix)
+
+    @pytest.mark.parametrize("block", (construct.bmat, construct.block))
+    def test_block_creation(self, block):
 
-        A = coo_matrix([[1, 2], [3, 4]])
-        B = coo_matrix([[5],[6]])
-        C = coo_matrix([[7]])
-        D = coo_matrix((0, 0))
+        A = coo_array([[1, 2], [3, 4]])
+        B = coo_array([[5],[6]])
+        C = coo_array([[7]])
+        D = coo_array((0, 0))
 
         expected = array([[1, 2, 5],
                           [3, 4, 6],
                           [0, 0, 7]])
-        assert_equal(construct.bmat([[A, B], [None, C]]).toarray(), expected)
-        E = csr_matrix((1, 2), dtype=np.int32)
-        assert_equal(construct.bmat([[A.tocsr(), B.tocsr()],
+        assert_equal(block([[A, B], [None, C]]).toarray(), expected)
+        E = csr_array((1, 2), dtype=np.int32)
+        assert_equal(block([[A.tocsr(), B.tocsr()],
                                      [E, C.tocsr()]]).toarray(),
                      expected)
-        assert_equal(construct.bmat([[A.tocsc(), B.tocsc()],
+        assert_equal(block([[A.tocsc(), B.tocsc()],
                                      [E.tocsc(), C.tocsc()]]).toarray(),
                      expected)
 
         expected = array([[1, 2, 0],
                           [3, 4, 0],
                           [0, 0, 7]])
-        assert_equal(construct.bmat([[A, None], [None, C]]).toarray(),
+        assert_equal(block([[A, None], [None, C]]).toarray(),
                      expected)
-        assert_equal(construct.bmat([[A.tocsr(), E.T.tocsr()],
+        assert_equal(block([[A.tocsr(), E.T.tocsr()],
                                      [E, C.tocsr()]]).toarray(),
                      expected)
-        assert_equal(construct.bmat([[A.tocsc(), E.T.tocsc()],
+        assert_equal(block([[A.tocsc(), E.T.tocsc()],
                                      [E.tocsc(), C.tocsc()]]).toarray(),
                      expected)
 
-        Z = csr_matrix((1, 1), dtype=np.int32)
+        Z = csr_array((1, 1), dtype=np.int32)
         expected = array([[0, 5],
                           [0, 6],
                           [7, 0]])
-        assert_equal(construct.bmat([[None, B], [C, None]]).toarray(),
+        assert_equal(block([[None, B], [C, None]]).toarray(),
                      expected)
-        assert_equal(construct.bmat([[E.T.tocsr(), B.tocsr()],
+        assert_equal(block([[E.T.tocsr(), B.tocsr()],
                                      [C.tocsr(), Z]]).toarray(),
                      expected)
-        assert_equal(construct.bmat([[E.T.tocsc(), B.tocsc()],
+        assert_equal(block([[E.T.tocsc(), B.tocsc()],
                                      [C.tocsc(), Z.tocsc()]]).toarray(),
                      expected)
 
-        expected = matrix(np.empty((0, 0)))
-        assert_equal(construct.bmat([[None, None]]).toarray(), expected)
-        assert_equal(construct.bmat([[None, D], [D, None]]).toarray(),
+        expected = np.empty((0, 0))
+        assert_equal(block([[None, None]]).toarray(), expected)
+        assert_equal(block([[None, D], [D, None]]).toarray(),
                      expected)
 
         # test bug reported in gh-5976
         expected = array([[7]])
-        assert_equal(construct.bmat([[None, D], [C, None]]).toarray(),
+        assert_equal(block([[None, D], [C, None]]).toarray(),
                      expected)
 
         # test failure cases
         with assert_raises(ValueError) as excinfo:
-            construct.bmat([[A], [B]])
+            block([[A], [B]])
         excinfo.match(r'Got blocks\[1,0\]\.shape\[1\] == 1, expected 2')
 
         with assert_raises(ValueError) as excinfo:
-            construct.bmat([[A.tocsr()], [B.tocsr()]])
+            block([[A.tocsr()], [B.tocsr()]])
         excinfo.match(r'incompatible dimensions for axis 1')
 
         with assert_raises(ValueError) as excinfo:
-            construct.bmat([[A.tocsc()], [B.tocsc()]])
+            block([[A.tocsc()], [B.tocsc()]])
         excinfo.match(r'Mismatching dimensions along axis 1: ({1, 2}|{2, 1})')
 
         with assert_raises(ValueError) as excinfo:
-            construct.bmat([[A, C]])
+            block([[A, C]])
         excinfo.match(r'Got blocks\[0,1\]\.shape\[0\] == 1, expected 2')
 
         with assert_raises(ValueError) as excinfo:
-            construct.bmat([[A.tocsr(), C.tocsr()]])
+            block([[A.tocsr(), C.tocsr()]])
         excinfo.match(r'Mismatching dimensions along axis 0: ({1, 2}|{2, 1})')
 
         with assert_raises(ValueError) as excinfo:
-            construct.bmat([[A.tocsc(), C.tocsc()]])
+            block([[A.tocsc(), C.tocsc()]])
         excinfo.match(r'incompatible dimensions for axis 0')
 
+    def test_block_return_type(self):
+        block = construct.block
+
+        # csr format ensures we hit _compressed_sparse_stack
+        # shape of F,G ensure we hit _stack_along_minor_axis
+        # list version ensure we hit the path with neither helper function
+        Fl, Gl = [[1, 2],[3, 4]], [[7], [5]]
+        Fm, Gm = csr_matrix(Fl), csr_matrix(Gl)
+        assert isinstance(block([[None, Fl], [Gl, None]], format="csr"), sparray)
+        assert isinstance(block([[None, Fm], [Gm, None]], format="csr"), sparray)
+        assert isinstance(block([[Fm, Gm]], format="csr"), sparray)
+
+    def test_bmat_return_type(self):
+        """This can be removed after sparse matrix is removed"""
+        bmat = construct.bmat
+        # check return type. if any input _is_array output array, else matrix
+        Fl, Gl = [[1, 2],[3, 4]], [[7], [5]]
+        Fm, Gm = csr_matrix(Fl), csr_matrix(Gl)
+        Fa, Ga = csr_array(Fl), csr_array(Gl)
+        assert isinstance(bmat([[Fa, Ga]], format="csr"), sparray)
+        assert isinstance(bmat([[Fm, Gm]], format="csr"), spmatrix)
+        assert isinstance(bmat([[None, Fa], [Ga, None]], format="csr"), sparray)
+        assert isinstance(bmat([[None, Fm], [Ga, None]], format="csr"), sparray)
+        assert isinstance(bmat([[None, Fm], [Gm, None]], format="csr"), spmatrix)
+        assert isinstance(bmat([[None, Fl], [Gl, None]], format="csr"), spmatrix)
+
+        # type returned by _compressed_sparse_stack (all csr)
+        assert isinstance(bmat([[Ga, Ga]], format="csr"), sparray)
+        assert isinstance(bmat([[Gm, Ga]], format="csr"), sparray)
+        assert isinstance(bmat([[Ga, Gm]], format="csr"), sparray)
+        assert isinstance(bmat([[Gm, Gm]], format="csr"), spmatrix)
+        # shape is 2x2 so no _stack_along_minor_axis
+        assert isinstance(bmat([[Fa, Fm]], format="csr"), sparray)
+        assert isinstance(bmat([[Fm, Fm]], format="csr"), spmatrix)
+
+        # type returned by _compressed_sparse_stack (all csc)
+        assert isinstance(bmat([[Gm.tocsc(), Ga.tocsc()]], format="csc"), sparray)
+        assert isinstance(bmat([[Gm.tocsc(), Gm.tocsc()]], format="csc"), spmatrix)
+        # shape is 2x2 so no _stack_along_minor_axis
+        assert isinstance(bmat([[Fa.tocsc(), Fm.tocsc()]], format="csr"), sparray)
+        assert isinstance(bmat([[Fm.tocsc(), Fm.tocsc()]], format="csr"), spmatrix)
+
+        # type returned when mixed input
+        assert isinstance(bmat([[Gl, Ga]], format="csr"), sparray)
+        assert isinstance(bmat([[Gm.tocsc(), Ga]], format="csr"), sparray)
+        assert isinstance(bmat([[Gm.tocsc(), Gm]], format="csr"), spmatrix)
+        assert isinstance(bmat([[Gm, Gm]], format="csc"), spmatrix)
+
     @pytest.mark.slow
     @pytest.mark.xfail_on_32bit("Can't create large array for test")
     def test_concatenate_int32_overflow(self):
@@ -471,7 +537,7 @@ def test_concatenate_int32_overflow(self):
         check_free_memory(30000)
 
         n = 33000
-        A = csr_matrix(np.ones((n, n), dtype=bool))
+        A = csr_array(np.ones((n, n), dtype=bool))
         B = A.copy()
         C = construct._compressed_sparse_stack((A,B), 0)
 
@@ -481,9 +547,9 @@ def test_concatenate_int32_overflow(self):
 
     def test_block_diag_basic(self):
         """ basic test for block_diag """
-        A = coo_matrix([[1,2],[3,4]])
-        B = coo_matrix([[5],[6]])
-        C = coo_matrix([[7]])
+        A = coo_array([[1,2],[3,4]])
+        B = coo_array([[5],[6]])
+        C = coo_array([[7]])
 
         expected = array([[1, 2, 0, 0],
                           [3, 4, 0, 0],
@@ -511,21 +577,26 @@ def test_block_diag_1(self):
         assert_equal(construct.block_diag([1]).toarray(),
                      array([[1]]))
 
-    def test_block_diag_sparse_matrices(self):
-        """ block_diag with sparse matrices """
+    def test_block_diag_sparse_arrays(self):
+        """ block_diag with sparse arrays """
 
-        sparse_col_matrices = [coo_matrix(([[1, 2, 3]]), shape=(1, 3)),
-                               coo_matrix(([[4, 5]]), shape=(1, 2))]
-        block_sparse_cols_matrices = construct.block_diag(sparse_col_matrices)
-        assert_equal(block_sparse_cols_matrices.toarray(),
+        A = coo_array([[1, 2, 3]], shape=(1, 3))
+        B = coo_array([[4, 5]], shape=(1, 2))
+        assert_equal(construct.block_diag([A, B]).toarray(),
                      array([[1, 2, 3, 0, 0], [0, 0, 0, 4, 5]]))
 
-        sparse_row_matrices = [coo_matrix(([[1], [2], [3]]), shape=(3, 1)),
-                               coo_matrix(([[4], [5]]), shape=(2, 1))]
-        block_sparse_row_matrices = construct.block_diag(sparse_row_matrices)
-        assert_equal(block_sparse_row_matrices.toarray(),
+        A = coo_array([[1], [2], [3]], shape=(3, 1))
+        B = coo_array([[4], [5]], shape=(2, 1))
+        assert_equal(construct.block_diag([A, B]).toarray(),
                      array([[1, 0], [2, 0], [3, 0], [0, 4], [0, 5]]))
 
+    def test_block_diag_return_type(self):
+        A, B = coo_array([[1, 2, 3]]), coo_matrix([[2, 3, 4]])
+        assert isinstance(construct.block_diag([A, A]), sparray)
+        assert isinstance(construct.block_diag([A, B]), sparray)
+        assert isinstance(construct.block_diag([B, A]), sparray)
+        assert isinstance(construct.block_diag([B, B]), spmatrix)
+
     def test_random_sampling(self):
         # Simple sanity checks for sparse random sampling.
         for f in sprand, _sprandn: