diff --git a/src/sage/graphs/matching.py b/src/sage/graphs/matching.py
index c8eea2bb005..66e87683be8 100644
--- a/src/sage/graphs/matching.py
+++ b/src/sage/graphs/matching.py
@@ -877,7 +877,7 @@ def is_matching_covered(G, matching=None, algorithm='Edmonds', coNP_certificate=
         sage: H = graphs.PathGraph(20)
         sage: M = H.matching()
         sage: H.is_matching_covered(matching=M, coNP_certificate=True)
-        (False, (1, 2, None))
+        (False, (2, 1, None))

     TESTS:

@@ -1019,44 +1019,33 @@ def is_matching_covered(G, matching=None, algorithm='Edmonds', coNP_certificate=
             if color[u]:
                 u, v = v, u

-            if M.has_edge(u, v):
-                H.add_edge(u, v)
-            else:
-                H.add_edge(v, u)
+            H.add_edge((u, v))
+            if next(M.neighbor_iterator(u)) == v:
+                H.add_edge((v, u))

         # Check if H is strongly connected using Kosaraju's algorithm
-        def dfs(J, v, visited, orientation):
+        def dfs(v, visited, neighbor_iterator):
             stack = [v]  # a stack of vertices

             while stack:
                 v = stack.pop()
+                visited.add(v)

-                if v not in visited:
-                    visited[v] = True
-
-                if orientation == 'in':
-                    for u in J.neighbors_out(v):
-                        if u not in visited:
-                            stack.append(u)
-
-                elif orientation == 'out':
-                    for u in J.neighbors_in(v):
-                        if u not in visited:
-                            stack.append(u)
-                else:
-                    raise ValueError('Unknown orientation')
+                for u in neighbor_iterator(v):
+                    if u not in visited:
+                        stack.append(u)

         root = next(H.vertex_iterator())

-        visited_in = {}
-        dfs(H, root, visited_in, 'in')
+        visited_in = set()
+        dfs(root, visited_in, H.neighbor_in_iterator)

-        visited_out = {}
-        dfs(H, root, visited_out, 'out')
+        visited_out = set()
+        dfs(root, visited_out, H.neighbor_out_iterator)

         for edge in H.edge_iterator():
             u, v, _ = edge
-            if (u not in visited_in) or (v not in visited_out):
+            if (u not in visited_out) or (v not in visited_in):
                 if not M.has_edge(edge):
                     return (False, edge) if coNP_certificate else False

diff --git a/src/sage/graphs/matching_covered_graph.py b/src/sage/graphs/matching_covered_graph.py
index 370fc1f9cad..b52333443a9 100644
--- a/src/sage/graphs/matching_covered_graph.py
+++ b/src/sage/graphs/matching_covered_graph.py
@@ -69,6 +69,7 @@
         ``lexicographic_product()`` | Return the lexicographic product of ``self`` and ``other``.
         ``load_afile()`` | Load the matching covered graph specified in the given file into the current object.
         ``merge_vertices()`` | Merge vertices.
+        ``minor()`` | Return the vertices of a minor isomorphic to `H` in the current graph.
         ``random_subgraph()`` | Return a random matching covered subgraph containing each vertex with probability ``p``.
         ``save_afile()`` | Save the graph to file in alist format.
         ``strong_product()`` | Return the strong product of ``self`` and ``other``.
@@ -80,6 +81,7 @@
         ``subgraph_search_iterator()`` | Return an iterator over the labelled copies of (matching covered) ``G`` in ``self``.
         ``tensor_product()`` | Return the tensor product of ``self`` and ``other``.
         ``to_undirected()`` | Return an undirected Graph instance of the matching covered graph.
+        ``topological_minor()`` | Return a topological `H`-minor from ``self`` if one exists.
         ``transitive_closure()`` | Return the transitive closure of the matching covered graph.
         ``transitive_reduction()`` | Return a transitive reduction of the matching covered graph.
         ``union()`` | Return the union of ``self`` and ``other``.
@@ -92,12 +94,10 @@
         :delim: |

         ``bricks_and_braces()`` | Return the list of (underlying simple graph of) the bricks and braces of the (matching covered) graph.
-        ``is_brace()`` | Check if the (matching covered) graph is a brace.
-        ``is_brick()`` | Check if the (matching covered) graph is a brick.
         ``number_of_braces()`` | Return the number of braces.
         ``number_of_bricks()`` | Return the number of bricks.
         ``number_of_petersen_bricks()`` | Return the number of Petersen bricks.
-        ``tight_cut_decomposition()`` | Return a tight cut decomposition.
+        ``tight_cut_decomposition()`` | Return a maximal set of laminar nontrivial tight cuts and a corresponding vertex set partition.

     **Removability and ear decomposition**

@@ -1616,13 +1616,13 @@ def allow_loops(self, new, check=True):

         .. SEEALSO::

-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.allows_loops`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.has_loops`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loop_edges`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loop_vertices`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loops`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.number_of_loops`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.remove_loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.allows_loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.has_loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loop_edges`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loop_vertices`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.number_of_loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.remove_loops`
         """
         if new:
             raise ValueError('loops are not allowed in '
@@ -1657,13 +1657,13 @@ def allows_loops(self):

         .. SEEALSO::

-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.allow_loops`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.has_loops`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loop_edges`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loop_vertices`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loops`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.number_of_loops`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.remove_loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.allow_loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.has_loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loop_edges`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loop_vertices`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.number_of_loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.remove_loops`
         """
         return False

@@ -2057,8 +2057,7 @@ def maximal_barrier(self, vertex):

             sage: J = G.copy()
             sage: J.delete_vertices(B)
-            sage: all(len(K)%2 != 0 for K in J.connected_components())
-            ...
+            sage: all(len(K)%2 != 0 for K in J.connected_components(sort=True))
             True

         Let `B` be a maximal barrier in a matching covered graph `G` and let
@@ -2067,7 +2066,7 @@ def maximal_barrier(self, vertex):
         `v` is the end of that edge in `V(K)`, then `M \cap E(K)` is a perfect
         matching of `K - v`::

-            sage: K = J.subgraph(vertices=(J.connected_components())[0])
+            sage: K = J.subgraph(vertices=(J.connected_components(sort=True))[0])
             sage: # Let F := \partial_G(K) and T := M \cap F
             sage: F = [edge for edge in G.edge_iterator()
             ....:      if (edge[0] in K and edge[1] not in K)
@@ -2091,7 +2090,7 @@ def maximal_barrier(self, vertex):
         `G - B` is factor critical::

             sage: all((K.subgraph(vertices=connected_component)).is_factor_critical()
-            ....:     for connected_component in K.connected_components()
+            ....:     for connected_component in K.connected_components(sort=True)
             ....: )
             True

@@ -2251,13 +2250,13 @@ def has_loops(self):

         .. SEEALSO::

-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.allow_loops`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.allows_loops`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loop_edges`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loop_vertices`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loops`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.number_of_loops`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.remove_loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.allow_loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.allows_loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loop_edges`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loop_vertices`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.number_of_loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.remove_loops`
         """
         return False

@@ -2346,6 +2345,820 @@ def has_perfect_matching(G, algorithm='Edmonds', solver=None, verbose=0,
         raise ValueError('algorithm must be set to \'Edmonds\', '
                          '\'LP_matching\' or \'LP\'')

+    @doc_index('Bricks, braces and tight cut decomposition')
+    def is_brace(self, coNP_certificate=False):
+        r"""
+        Check if the (matching covered) graph is a brace.
+
+        A matching covered graph which is free of nontrivial tight cuts is
+        called a *brace* if it is bipartite. Let `G := (A \cup B, E)` be a
+        bipartite matching covered graph on six or more vertices. The
+        following statements are equivalent [LM2024]_:
+
+        1. `G` is a brace (aka free of nontrivial tight cuts).
+        2. `G - a_1 - a_2 - b_1 - b_2` has a perfect matching for any two
+           distinct vertices `a_1` and `a_2` in `A` and any two distinct
+           vertices `b_1` and `b_2` in `B`.
+        3. `G` is two extendable (any two nonadjacent distinct edges can be
+           extended to some perfect matching of `G`).
+        4. `|N(X)| \geq |X| + 2`, for all `X ⊂ A` such that `0 < |X| <
+           |A| - 1`, where `N(S) := \{b \mid (a, b) \in E ∧ a \in S\}` is called
+           the neighboring set of `S`.
+        5. `G - a - b` is matching covered, for some perfect matching `M` of
+           `G` and for each edge `ab` in `M`.
+
+        We shall be using the 5th characterization mentioned above in order
+        to determine whether the provided bipartite matching covered graph
+        is a brace or not using *M*-alternating tree search [LZ2001]_.
+
+        INPUT:
+
+        - ``coNP_certificate`` -- boolean (default: ``False``)
+
+        OUTPUT:
+
+        - If the input matching covered graph is not bipartite, a
+          :exc:`ValueError` is returned.
+
+        - If the input bipartite matching covered graph is a brace, a boolean
+          ``True`` is returned if ``coNP_certificate`` is set to ``False``
+          otherwise a 5-tuple ``(True, None, None, None, None)`` is returned.
+
+        - If the input bipartite matching covered graph is not a brace, a
+          boolean ``False`` is returned if ``coNP_certificate`` is set to
+          ``False`` otherwise a 5-tuple of
+
+          1. a boolean ``False``,
+
+          2. a list of edges constituting a nontrivial tight cut (which is a
+             nontrivial barrier cut)
+
+          3. a set of vertices of one of the shores of the nontrivial tight cut
+
+          4. a string 'nontrivial tight cut'
+
+          5. a set of vertices showing the respective barrier
+
+          is returned.
+
+        EXAMPLES:
+
+        The complete graph on two vertices `K_2` is the smallest brace::
+
+            sage: K = graphs.CompleteGraph(2)
+            sage: G = MatchingCoveredGraph(K)
+            sage: G.is_brace()
+            True
+
+        The cycle graph on four vertices `C_4` is a brace::
+
+            sage: C = graphs.CycleGraph(4)
+            sage: G = MatchingCoveredGraph(C)
+            sage: G.is_brace()
+            True
+
+        Each graph that is isomorphic to a biwheel is a brace::
+
+            sage: B = graphs.BiwheelGraph(15)
+            sage: G = MatchingCoveredGraph(B)
+            sage: G.is_brace()
+            True
+
+        A circular ladder graph of order eight or more on `2n` vertices for
+        an even `n` is a brace::
+
+            sage: n = 10
+            sage: CL = graphs.CircularLadderGraph(n)
+            sage: G = MatchingCoveredGraph(CL)
+            sage: G.is_brace()
+            True
+
+        A moebius ladder graph of order six or more on `2n` vertices for an odd
+        `n` is a brace::
+
+            sage: n = 11
+            sage: ML = graphs.MoebiusLadderGraph(n)
+            sage: G = MatchingCoveredGraph(ML)
+            sage: G.is_brace()
+            True
+
+        Note that the union of the above mentioned four families of braces,
+        that are:
+
+        1. the biwheel graph ``BiwheelGraph(n)``,
+        2. the circular ladder graph ``CircularLadderGraph(n)`` for even ``n``,
+        3. the moebius ladder graph ``MoebiusLadderGraph(n)`` for odd ``n``,
+
+        is referred to as the *McCuaig* *family* *of* *braces.*
+
+        The only simple brace of order six is the complete graph of the same
+        order, that is `K_{3, 3}`::
+
+            sage: L = list(graphs(6,
+            ....:          lambda G: G.size() <= 15 and
+            ....:                    G.is_bipartite())
+            ....: )
+            sage: L = list(G for G in L if G.is_connected() and
+            ....:                          G.is_matching_covered()
+            ....: )
+            sage: M = list(MatchingCoveredGraph(G) for G in L)
+            sage: B = list(G for G in M if G.is_brace())
+            sage: K = graphs.CompleteBipartiteGraph(3, 3)
+            sage: G = MatchingCoveredGraph(K)
+            sage: next(iter(B)).is_isomorphic(G)
+            True
+
+        The nonplanar `K_{3, 3}`-free brace Heawood graph is the unique cubic
+        graph of girth six with the fewest number of vertices (that is 14).
+        Note that by `K_{3, 3}`-free, it shows that the Heawood graph does not
+        contain a subgraph that is isomophic to a graph obtained by
+        bisubdivision of `K_{3, 3}`::
+
+            sage: K = graphs.CompleteBipartiteGraph(3, 3)
+            sage: J = graphs.HeawoodGraph()
+            sage: H = MatchingCoveredGraph(J)
+            sage: H.is_brace() and not H.is_planar() and \
+            ....: H.is_regular(k=3) and H.girth() == 6
+            True
+
+        Braces of order six or more are 3-connected::
+
+            sage: H = graphs.HexahedralGraph()
+            sage: G = MatchingCoveredGraph(H)
+            sage: G.is_brace() and G.is_triconnected()
+            True
+
+        Braces of order four or more are 2-extendable::
+
+            sage: H = graphs.EllinghamHorton54Graph()
+            sage: G = MatchingCoveredGraph(H)
+            sage: G.is_brace()
+            True
+            sage: e = next(G.edge_iterator(labels=False)); f = None
+            sage: for f in G.edge_iterator(labels=False):
+            ....:     if not (set(e) & set(f)):
+            ....:          break
+            sage: S = [u for x in [e, f] for u in set(x)]
+            sage: J = H.copy(); J.delete_vertices(S)
+            sage: M = Graph(J.matching())
+            sage: M.add_edges([e, f])
+            sage: if all(d == 1 for d in M.degree()) and \
+            ....:    G.order() == M.order() and \
+            ....:    G.order() == 2*M.size():
+            ....:      print(f'graph {G} is 2-extendable')
+            graph Ellingham-Horton 54-graph is 2-extendable
+
+        Every edge in a brace of order at least six is removable::
+
+            sage: H = graphs.CircularLadderGraph(8)
+            sage: G = MatchingCoveredGraph(H)
+            sage: # len(G.removble_edges()) == G.size()
+            # True
+
+        Every brace of order eight has the hexahedral graph as a spanning
+        subgraph::
+
+            sage: H = graphs.HexahedralGraph()
+            sage: L = list(graphs(8,
+            ....:          lambda G: G.size() <= 28 and
+            ....:                    G.is_bipartite())
+            ....: )
+            sage: L = list(G for G in L if G.is_connected() and
+            ....:                          G.is_matching_covered()
+            ....: )
+            sage: M = list(MatchingCoveredGraph(G) for G in L)
+            sage: B = list(G for G in M if G.is_brace())
+            sage: C = list(G for G in M if Graph(G).subgraph_search(H) is not None)
+            sage: B == C
+            True
+
+        For every brace `G[A, B]` of order at least six, the graph
+        `G - a_1 - a_2 - b_1 - b_2` has a perfect matching for any two distinct
+        vertices `a_1` and `a_2` in `A` and any two distinct vertices `b_1` and
+        `b_2` in `B`::
+
+            sage: H =  graphs.CompleteBipartiteGraph(10, 10)
+            sage: G = MatchingCoveredGraph(H)
+            sage: G.is_brace()
+            True
+            sage: S = [0, 1, 10, 12]
+            sage: G.delete_vertices(S)
+            sage: G.has_perfect_matching()
+            True
+
+        For a brace `G[A, B]` of order six or more, `|N(X)| \geq |X| + 2`, for
+        all `X \subset A` such that `0 < |X| <|A| - 1`, where
+        `N(S) := \{b | (a, b) \in E \^ a \in S\}` is called the neighboring set
+        of `S`::
+
+            sage: H = graphs.MoebiusLadderGraph(15)
+            sage: G = MatchingCoveredGraph(H)
+            sage: G.is_brace()
+            True
+            sage: A, _ = G.bipartite_sets()
+            sage: # needs random
+            sage: X = random.sample(list(A), random.randint(1, len(A) - 1))
+            sage: N = {v for u in X for v in G.neighbor_iterator(u)}
+            sage: len(N) >= len(X) + 2
+            True
+
+        For a brace `G` of order four or more with a perfect matching `M`, the
+        graph `G - a - b` is matching covered for each edge `(a, b)` in `M`::
+
+            sage: H = graphs.HeawoodGraph()
+            sage: G = MatchingCoveredGraph(H)
+            sage: G.is_brace()
+            True
+            sage: M = G.get_matching()
+            sage: L = []
+            sage: for a, b, *_ in M:
+            ....:     J = G.copy(); J.delete_vertices([a, b])
+            ....:     if J.is_matching_covered():
+            ....:          L.append(J)
+            sage: len(L) == len(M)
+            True
+
+        A cycle graph of order six or more is a bipartite matching covered
+        graph, but is not a brace::
+
+            sage: C = graphs.CycleGraph(10)
+            sage: G = MatchingCoveredGraph(C)
+            sage: G.is_brace()
+            False
+
+        A ladder graph of order six or more is a bipartite matching covered
+        graph, that is not a brace. The tight cut decomposition of a ladder
+        graph produces a list graphs the underlying graph of each of which
+        is isomorphic to a 4-cycle::
+
+            sage: L = graphs.LadderGraph(10)
+            sage: G = MatchingCoveredGraph(L)
+            sage: G.is_brace()
+            False
+
+        One may set the ``coNP_certificate`` to be ``True``::
+
+            sage: H = graphs.HexahedralGraph()
+            sage: G = MatchingCoveredGraph(H)
+            sage: G.is_brace(coNP_certificate=True)
+            (True, None, None, None, None)
+            sage: C = graphs.CycleGraph(6)
+            sage: D = MatchingCoveredGraph(C)
+            sage: is_brace, nontrivial_tight_cut, nontrivial_odd_component, \
+            ....: nontrivial_tight_cut_variant, cut_identifier = \
+            ....: D.is_brace(coNP_certificate=True)
+            sage: is_brace is False
+            True
+            sage: J = C.subgraph(vertices=nontrivial_odd_component)
+            sage: J.is_isomorphic(graphs.PathGraph(3))
+            True
+            sage: len(nontrivial_tight_cut) == 2
+            True
+            sage: nontrivial_tight_cut_variant
+            'nontrivial barrier cut'
+            sage: # Corresponding barrier
+            sage: cut_identifier == {a for u, v, *_ in nontrivial_tight_cut for a in [u, v] \
+            ....: if a not in nontrivial_odd_component}
+            True
+            sage: for u, v, *_ in nontrivial_tight_cut:
+            ....:     assert (u in nontrivial_odd_component and v not in nontrivial_odd_component)
+            sage: L = graphs.LadderGraph(3) # A ladder graph with two constituent braces
+            sage: G = MatchingCoveredGraph(L)
+            sage: is_brace, nontrivial_tight_cut, nontrivial_odd_component, cut_variant, cut_identifier = \
+            ....: G.is_brace(coNP_certificate=True)
+            sage: is_brace is False
+            True
+            sage: G1 = L.copy()
+            sage: G1.merge_vertices(list(nontrivial_odd_component))
+            sage: G1.to_simple().is_isomorphic(graphs.CycleGraph(4))
+            True
+            sage: G2 = L.copy()
+            sage: G2.merge_vertices([v for v in G if v not in nontrivial_odd_component])
+            sage: G2.to_simple().is_isomorphic(graphs.CycleGraph(4))
+            True
+            sage: cut_variant
+            'nontrivial barrier cut'
+            sage: cut_identifier == {a for u, v, *_ in nontrivial_tight_cut for a in [u, v] \
+            ....: if a not in nontrivial_odd_component}
+            True
+            sage: H = graphs.CompleteBipartiteGraph(3, 3)
+            sage: H.delete_edge(0, 3)
+            sage: G = MatchingCoveredGraph(H)
+            sage: G.is_brace(coNP_certificate=True)
+            (False,
+             [(4, 1, None), (5, 1, None), (4, 2, None), (5, 2, None)],
+             {0, 4, 5},
+             'nontrivial barrier cut',
+             {1, 2})
+
+        If the input matching covered graph is nonbipartite, a
+        :exc:`ValueError` is thrown::
+
+            sage: K4 = graphs.CompleteGraph(4)
+            sage: G = MatchingCoveredGraph(K4)
+            sage: G.is_brace()
+            Traceback (most recent call last):
+            ...
+            ValueError: the input graph is not bipartite
+            sage: P = graphs.PetersenGraph()
+            sage: H = MatchingCoveredGraph(P)
+            sage: H.is_brace(coNP_certificate=True)
+            Traceback (most recent call last):
+            ...
+            ValueError: the input graph is not bipartite
+
+        .. SEEALSO::
+
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.is_brick`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.bricks_and_braces`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.number_of_braces`
+        """
+        if not self.is_bipartite():
+            raise ValueError('the input graph is not bipartite')
+
+        if self.order() < 6:
+            return (True, None, None, None, None) if coNP_certificate else True
+
+        A, B = self.bipartite_sets()
+        matching = set(self.get_matching())
+        matching_neighbor = {x: y for u, v, *_ in matching for x, y in [(u, v), (v, u)]}
+
+        for e in matching:
+            u, v, *_ = e
+
+            # Let G denote the undirected graph self, and
+            # let the graph H(e) := G — u — v
+            H = Graph(self, multiedges=False)
+            H.delete_vertices([u, v])
+
+            if not H.is_connected() or not H.is_matching_covered(list(matching - set([e]))):
+                if not coNP_certificate:
+                    return False
+
+                # Construct the digraph D(e)(A ∪ B, F) defined as follows:
+                from sage.graphs.digraph import DiGraph
+                D = DiGraph()
+
+                # For each edge (a, b) in E(H(e)) ∩ M with a in A, b —> a in D(e).
+                # For each edge (a, b) in E(H(e)) with a in A, a —> b in D(e).
+                for a, b in H.edge_iterator(labels=False, sort_vertices=True):
+
+                    if a in B:
+                        a, b = b, a
+
+                    D.add_edge((a, b))
+                    if matching_neighbor[a] == b:
+                        D.add_edge((b, a))
+
+                # H(e) is matching covered iff D(e) is strongly connected.
+                # Check if D(e) is strongly connected using Kosaraju's algorithm
+                def dfs(x, visited, neighbor_iterator):
+                    stack = [x]  # a stack of xertices
+
+                    while stack:
+                        x = stack.pop()
+                        visited.add(x)
+
+                        for y in neighbor_iterator(x):
+                            if y not in visited:
+                                stack.append(y)
+
+                root = next(D.vertex_iterator())
+
+                visited_in = set()
+                dfs(root, visited_in, D.neighbor_in_iterator)
+
+                # Since D(e) is not strongly connected, it has a directed cut T(e).
+                # Note that by definition of D(e), it follows that T(e) ⊆ E(H(e)) — M.
+                # Thus, T(e) is a cut of H(e), which has a shore X such that every edge of T(e) is
+                # incident with a vertex in X ∩ B.
+
+                # Moreover, M — e is a perfect matching of H(e), and thus, |X ∩ A| = |X ∩ B|
+                # Consequently, Y := X + v is a shore of a nontrivial tight cut T of G
+
+                if len(visited_in) != D.order():
+                    X = visited_in
+                else:
+                    X = set()
+                    dfs(root, X, D.neighbor_out_iterator)
+
+                color_class = None
+
+                for a, b in H.edge_iterator(labels=False, sort_vertices=True):
+                    if (a in X) ^ (b in X):
+                        x = a if a in A else b
+                        color_class = x not in X
+                        break
+
+                # Obtain the color class Z ∈ {A, B} such that X ∩ Z is a vertex cover for T(e)
+                # Thus, obtain Y := X + v
+                X.add(u if (not color_class and u in A) or (color_class and u in B) or (color_class is None) else v)
+
+                # Compute the nontrivial tight cut C := ∂(Y)
+                C = [(x, y, w) if x in X else (y, x, w)
+                    for x, y, w in self.edge_iterator(sort_vertices=True)
+                    if (x in X) ^ (y in X)]
+
+                # Obtain the barrier Z
+                Z = None
+
+                if (u in X and u in A) or (v in X and v in A):
+                    Z = {b for b in B if b not in X}
+                else:
+                    Z = {a for a in A if a not in X}
+
+                return (False, C, set(X), 'nontrivial barrier cut', Z)
+
+        return (True, None, None, None, None) if coNP_certificate else True
+
+    @doc_index('Bricks, braces and tight cut decomposition')
+    def is_brick(self, coNP_certificate=False):
+        r"""
+        Check if the (matching covered) graph is a brick.
+
+        A matching covered graph which is free of nontrivial tight cuts is
+        called a *brick* if it is nonbipartite. A nonbipartite matching covered
+        graph is a brick if and only if it is 3-connected and bicritical
+        [LM2024]_.
+
+        INPUT:
+
+        - ``coNP_certificate`` -- boolean (default: ``False``)
+
+        OUTPUT:
+
+        - If the input matching covered graph is bipartite, a :exc:`ValueError`
+          is returned.
+
+        - If the input nonbipartite matching covered graph is a brick, a
+          boolean ``True`` is returned if ``coNP_certificate`` is set to
+          ``False``, otherwise a 5-tuple ``(True, None, None, None, None)`` is
+          returned.
+
+        - If the input nonbipartite matching covered graph is not a brick, a
+          boolean ``False`` is returned if ``coNP_certificate`` is set to
+          ``False``.
+
+        - If ``coNP_certificate`` is set to ``True`` and the input nonbipartite
+          graph is not a brick, a 5-tuple of
+
+          1. a boolean ``False``,
+
+          2. a list of lists of edges, each list constituting a nontrivial
+             tight cut collectively representing a laminar tight cut,
+
+          3. a list of set of vertices of one of the shores of those respective
+             nontrivial tight cuts:
+
+             #. In case of nontrivial barrier cuts, each of the shores is a
+                nontrivial odd component with respect to a nontrivial barrier,
+                thus the returned list forms mutually exclusive collection of
+                (odd) sets.
+
+             #. Otherwise each of the nontrivial tight cuts is a 2-separation
+                cut, each of the shores form a subset sequence, with the
+                `i` th shore being a proper subset of the `i + 1` th shore.
+
+          4. a string showing whether the nontrivial tight cuts are barrier
+             cuts (if the string is 'nontrivial barrier cut'), or 2-separation
+             cuts (if the string is 'nontrivial 2-separation cut')
+
+          5. a set of vertices showing the respective barrier if the
+             nontrivial tight cuts are barrier cuts, or otherwise
+             a set of two vertices constituting the corresponding
+             two vertex cut (in this case the nontrivial tight cuts are
+             2-separation cuts)
+
+          is returned.
+
+        EXAMPLES:
+
+        The complete graph on four vertices `K_4` is the smallest brick::
+
+            sage: K = graphs.CompleteGraph(4)
+            sage: G = MatchingCoveredGraph(K)
+            sage: G.is_brick()
+            True
+
+        The triangular cicular ladder (a graph on six vertices), aka
+        `\overline{C_6}` is a brick::
+
+            sage: C6Bar = graphs.CircularLadderGraph(3)
+            sage: G = MatchingCoveredGraph(C6Bar)
+            sage: G.is_brick()
+            True
+
+        Each of Petersen graph, Bicorn graph, Tricorn graph, Cubeplex graph,
+        Twinplex graph, Wagner graph is a brick::
+
+            sage: MatchingCoveredGraph(graphs.PetersenGraph()).is_brick() and \
+            ....: MatchingCoveredGraph(graphs.StaircaseGraph(4)).is_brick() and \
+            ....: MatchingCoveredGraph(graphs.TricornGraph()).is_brick() and \
+            ....: MatchingCoveredGraph(graphs.CubeplexGraph()).is_brick() and \
+            ....: MatchingCoveredGraph(graphs.TwinplexGraph()).is_brick() and \
+            ....: MatchingCoveredGraph(graphs.WagnerGraph()).is_brick()
+            True
+
+        The Murty graph is the smallest simple brick that is not odd-intercyclic::
+
+            sage: M = graphs.MurtyGraph()
+            sage: G = MatchingCoveredGraph(M)
+            sage: G.is_brick()
+            True
+
+        A circular ladder graph of order six or more on `2n` vertices for an
+        odd `n` is a brick::
+
+            sage: n = 11
+            sage: CL = graphs.CircularLadderGraph(n)
+            sage: G = MatchingCoveredGraph(CL)
+            sage: G.is_brick()
+            True
+
+        A moebius ladder graph of order eight or more on `2n` vertices for an
+        even `n` is a brick::
+
+            sage: n = 10
+            sage: ML = graphs.MoebiusLadderGraph(n)
+            sage: G = MatchingCoveredGraph(ML)
+            sage: G.is_brick()
+            True
+
+        A wheel graph of an even order is a brick::
+
+            sage: W = graphs.WheelGraph(10)
+            sage: G = MatchingCoveredGraph(W)
+            sage: G.is_brick()
+            True
+
+        A graph that is isomorphic to a truncated biwheel graph is a brick::
+
+            sage: TB = graphs.TruncatedBiwheelGraph(15)
+            sage: G = MatchingCoveredGraph(TB)
+            sage: G.is_brick()
+            True
+
+        Each of the graphs in the staircase graph family with order eight or
+        more is a brick::
+
+            sage: ST = graphs.StaircaseGraph(9)
+            sage: G = MatchingCoveredGraph(ST)
+            sage: G.is_brick()
+            True
+
+        Bricks are 3-connected::
+
+            sage: P = graphs.PetersenGraph()
+            sage: G = MatchingCoveredGraph(P)
+            sage: G.is_brick()
+            True
+            sage: G.is_triconnected()
+            True
+
+        Bricks are bicritical::
+
+            sage: P = graphs.PetersenGraph()
+            sage: G = MatchingCoveredGraph(P)
+            sage: G.is_brick()
+            True
+            sage: G.is_bicritical()
+            True
+
+        Examples of nonbipartite matching covered graphs that are not
+        bricks::
+
+            sage: H = Graph([
+            ....:     (0, 3), (0, 4), (0, 7),
+            ....:     (1, 3), (1, 5), (1, 7),
+            ....:     (2, 3), (2, 6), (2, 7),
+            ....:     (4, 5), (4, 6), (5, 6)
+            ....: ])
+            sage: G = MatchingCoveredGraph(H)
+            sage: G.is_bipartite()
+            False
+            sage: G.is_bicritical()
+            False
+            sage: G.is_triconnected()
+            True
+            sage: G.is_brick()
+            False
+            sage: H = Graph([
+            ....:     (0, 1), (0, 2), (0, 3), (0, 4), (1, 2),
+            ....:     (1, 5), (2, 5), (3, 4), (3, 5), (4, 5)
+            ....: ])
+            sage: G = MatchingCoveredGraph(H)
+            sage: G.is_bipartite()
+            False
+            sage: G.is_bicritical()
+            True
+            sage: G.is_triconnected()
+            False
+            sage: G.is_brick()
+            False
+
+        One may set the ``coNP_certificate`` to be ``True``::
+
+            sage: K4 = graphs.CompleteGraph(4)
+            sage: G = MatchingCoveredGraph(K4)
+            sage: G.is_brick(coNP_certificate=True)
+            (True, None, None, None, None)
+            sage: # K(4) ⊙ K(3, 3) is nonbipartite but not a brick
+            sage: H = graphs.MurtyGraph(); H.delete_edge(0, 1)
+            sage: G = MatchingCoveredGraph(H)
+            sage: G.is_brick(coNP_certificate=True)
+            (False, [[(5, 2, None), (6, 3, None), (7, 4, None)]], [{5, 6, 7}],
+             'nontrivial barrier cut', {2, 3, 4})
+            sage: H = Graph([
+            ....:     (0, 12), (0, 13), (0, 15), (1, 4), (1, 13), (1, 14),
+            ....:     (1, 19), (2, 4), (2, 13), (2, 14), (2, 17), (3, 9),
+            ....:     (3, 13), (3, 16), (3, 21), (4, 6), (4, 7), (5, 7),
+            ....:     (5, 8), (5, 12), (6, 8), (6, 11), (7, 10), (8, 9),
+            ....:     (9, 10), (10, 11), (11, 12), (14, 15), (14, 16), (15, 16),
+            ....:     (17, 18), (17, 21), (18, 19), (18, 20), (19, 20), (20, 21)
+            ....: ])
+            sage: G = MatchingCoveredGraph(H)
+            sage: G.is_brick(coNP_certificate=True)
+            (False,
+             [[(12, 0, None), (4, 1, None), (4, 2, None), (9, 3, None)],
+              [(19, 1, None), (17, 2, None), (21, 3, None)],
+              [(15, 0, None), (14, 1, None), (14, 2, None), (16, 3, None)]],
+             [{4, 5, 6, 7, 8, 9, 10, 11, 12}, {17, 18, 19, 20, 21}, {14, 15, 16}],
+             'nontrivial barrier cut', {0, 1, 2, 3})
+            sage: J = Graph([
+            ....:     (0, 1), (0, 2), (0, 3), (0, 4), (0, 5),
+            ....:     (0, 6), (0, 7), (0, 8), (0, 9), (0, 10),
+            ....:     (1, 2), (1, 11), (2, 11), (3, 4), (3, 11),
+            ....:     (4, 11), (5, 6), (5, 11), (6, 11), (7, 8),
+            ....:     (7, 11), (8, 11), (9, 10), (9, 11), (10, 11)
+            ....: ])
+            sage: G = MatchingCoveredGraph(J)
+            sage: G.is_brick(coNP_certificate=True)
+            (False,
+             [[(0, 3, None),
+               (0, 4, None),
+               (0, 5, None),
+               (0, 6, None),
+               (0, 7, None),
+               (0, 8, None),
+               (0, 9, None),
+               (0, 10, None),
+               (1, 11, None),
+               (2, 11, None)],
+              [(0, 5, None),
+               (0, 6, None),
+               (0, 7, None),
+               (0, 8, None),
+               (0, 9, None),
+               (0, 10, None),
+               (1, 11, None),
+               (2, 11, None),
+               (3, 11, None),
+               (4, 11, None)],
+              [(0, 7, None),
+               (0, 8, None),
+               (0, 9, None),
+               (0, 10, None),
+               (1, 11, None),
+               (2, 11, None),
+               (3, 11, None),
+               (4, 11, None),
+               (5, 11, None),
+               (6, 11, None)],
+              [(0, 9, None),
+               (0, 10, None),
+               (1, 11, None),
+               (2, 11, None),
+               (3, 11, None),
+               (4, 11, None),
+               (5, 11, None),
+               (6, 11, None),
+               (7, 11, None),
+               (8, 11, None)]],
+             [{0, 1, 2},
+              {0, 1, 2, 3, 4},
+              {0, 1, 2, 3, 4, 5, 6},
+              {0, 1, 2, 3, 4, 5, 6, 7, 8}],
+             'nontrivial 2-separation cut',
+             {0, 11})
+
+        If the input matching covered graph is bipartite, a
+        :exc:`ValueError` is thrown::
+
+            sage: H = graphs.HexahedralGraph()
+            sage: G = MatchingCoveredGraph(H)
+            sage: G.is_brick()
+            Traceback (most recent call last):
+            ...
+            ValueError: the input graph is bipartite
+            sage: J = graphs.HeawoodGraph()
+            sage: G = MatchingCoveredGraph(J)
+            sage: G.is_brick(coNP_certificate=True)
+            Traceback (most recent call last):
+            ...
+            ValueError: the input graph is bipartite
+
+        .. SEEALSO::
+
+            - :meth:`~sage.graphs.graph.Graph.is_bicritical`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.is_brace`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.bricks_and_braces`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.number_of_bricks`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.number_of_petersen_bricks`
+        """
+        if self.is_bipartite():
+            raise ValueError('the input graph is bipartite')
+
+        # Check if G is bicritical
+        bicritical, certificate = self.is_bicritical(coNP_certificate=True)
+
+        if not bicritical:
+            if not coNP_certificate:
+                return False
+
+            # G has a pair of vertices u, v such that G - u - v is not matching
+            # covered, thus has a nontrivial barrier B containing both u and v.
+            u, _ = certificate
+            B = self.maximal_barrier(u)
+
+            H = Graph(self)
+            H.delete_vertices(B)
+
+            # Let K be a nontrivial odd component of H := G - B. Note that
+            # there exists at least one such K since G is nonbipartite
+            nontrivial_odd_components = [
+                set(component) for component in H.connected_components(sort=True)
+                if len(component) % 2 and len(component) > 1
+            ]
+
+            # Find a laminar set of nontrivial barrier cuts
+            C = [[(u, v, w) if u in nontrivial_odd_component else (v, u, w)
+                  for u, v, w in self.edge_iterator()
+                  if (u in nontrivial_odd_component) ^ (v in nontrivial_odd_component)]
+                 for nontrivial_odd_component in nontrivial_odd_components]
+
+            return (False, C, nontrivial_odd_components, 'nontrivial barrier cut', B)
+
+        # Check if G is 3-connected
+        if self.is_triconnected():
+            return (True, None, None, None, None) if coNP_certificate else True
+
+        # G has a 2-vertex cut
+        # Compute the SPQR-tree decomposition
+        spqr_tree = self.spqr_tree()
+        two_vertex_cut = []
+
+        # Check for 2-vertex cuts in a P node
+        # Since the graph is matching covered, it is free of cut vertices
+        # It can be shown using counting arguments that the spqr tree
+        # decomposition for a bicritical graph, that is 2-connected but not
+        # 3-connected, is free of 'S' nodes
+        for u in spqr_tree:
+            if u[0] == 'P':
+                two_vertex_cut.extend(u[1])
+                break
+
+        # If no 2-vertex cut found, look for R nodes
+        if not two_vertex_cut:
+            from collections import Counter
+            R_frequency = Counter()
+
+            for t, g in spqr_tree:
+                if t == 'R':
+                    R_frequency.update(g)
+
+            # R frequency must be at least 2,
+            # since the graph is 2-connected but not 3-connected
+            two_vertex_cut = [u for u, f in R_frequency.items() if f >= 2][:2]
+
+        # We obtain a 2-vertex cut (u, v)
+        H = Graph(self)
+        H.delete_vertices(two_vertex_cut)
+
+        # Check if all components of H are odd
+        components = H.connected_components(sort=True)
+
+        # Find a nontrivial odd component
+        nontrivial_tight_cut_variation = 'nontrivial 2-separation cut'
+        nontrivial_odd_components = []
+
+        for index, component in enumerate(components):
+            if index == len(components) - 1:
+                continue
+            elif not index:
+                nontrivial_odd_components.append(set(components[0] + [two_vertex_cut[0]]))
+            else:
+                nontrivial_odd_component = nontrivial_odd_components[-1].copy()
+                nontrivial_odd_component.update(component)
+                nontrivial_odd_components.append(nontrivial_odd_component)
+
+        C = [[(u, v, w) if u in nontrivial_odd_component else (v, u, w)
+              for u, v, w in self.edge_iterator()
+              if (u in nontrivial_odd_component) ^ (v in nontrivial_odd_component)]
+             for nontrivial_odd_component in nontrivial_odd_components]
+
+        # Edge (u, v, w) in C are formatted so that u is in a nontrivial odd component
+        return (False, C, nontrivial_odd_components, nontrivial_tight_cut_variation, set(two_vertex_cut)) if coNP_certificate else False
+
     @doc_index('Overwritten methods')
     def loop_edges(self, labels=True):
         r"""
@@ -2416,13 +3229,13 @@ def loop_edges(self, labels=True):

         .. SEEALSO::

-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.allow_loops`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.allows_loops`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.has_loops`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loop_vertices`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loops`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.number_of_loops`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.remove_loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.allow_loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.allows_loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.has_loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loop_vertices`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.number_of_loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.remove_loops`
         """
         return []

@@ -2483,13 +3296,13 @@ def loop_vertices(self):

         .. SEEALSO::

-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.allow_loops`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.allows_loops`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.has_loops`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loop_edges`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loops`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.number_of_loops`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.remove_loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.allow_loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.allows_loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.has_loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loop_edges`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.number_of_loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.remove_loops`
         """
         return []

@@ -2555,13 +3368,13 @@ def number_of_loops(self):

         .. SEEALSO::

-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.allow_loops`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.allows_loops`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.has_loops`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loop_edges`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loop_vertices`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loops`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.remove_loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.allow_loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.allows_loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.has_loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loop_edges`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loop_vertices`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.remove_loops`
         """
         return 0

@@ -2647,13 +3460,13 @@ def remove_loops(self, vertices=None):

         .. SEEALSO::

-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.allow_loops`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.allows_loops`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.has_loops`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loop_edges`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loop_vertices`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loops`,
-            :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.number_of_loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.allow_loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.allows_loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.has_loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loop_edges`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loop_vertices`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.loops`
+            - :meth:`~sage.graphs.matching_covered_graph.MatchingCoveredGraph.number_of_loops`
         """
         from collections.abc import Iterable

On Decompositions, Generation Methods and related concepts in the theory of Matching Covered Graphs
### Problem Description

Matchings and perfect matchings have received considerable attention in graph theory as well as in other related domains (such as, but not limited to, algorithms and optimization). There still remain many open problems — such as Barnette’s conjecture, Berge-Fulkerson conjecture, and so on — due to which it continues to remain an active area of research. For problems concerning perfect matchings, it is well-known that it suffices to solve them for matching covered graphs (that is, those connected graphs wherein each edge belongs to some perfect matching).

The objective of this issue is to implement efficient algorithms pertaining to the canonical partition, tight cut decomposition, dependency relations, (optimal) ear decomposition, brick and brace generation methods and related concepts in the theory of matching covered graphs, and to make all of these available freely to students, educators as well as researchers all across the world. This issue has been inspired by the book of C.L. Lucchesi and U.S.R. Murty: *PERFECT MATCHINGS: A THEORY OF MATCHING COVERED GRAPHS* 1.

### Proposed Solution

To create a new class: `MatchingCoveredGraph`,  in the module `sage.graphs.matching_covered_graph` and to implement the following functions:

- `maximal_barrier()`: Return the (unique) maximal barrier of the (matching covered) graph containing the (provided) vertex.
- `canonical_partition()`: Return the canonical partition of the (matching covered) graph.
- `tight_cut_decomposition()`: Return  of the (matching covered) graph.
- `bricks_and_braces()`: Return the list of (underlying simple graph of) the bricks and braces of the (matching covered) graph.
- `number_of_bricks()`: Return the number of bricks.
- `number_of_braces()`: Return the number of braces.
- `number_of_petersen_bricks()`: Return the number of Petersen bricks (the bricks the underlying simple graph of which is isomorphic to `graphs.PetersenGraph()`).
- `is_brick()`: Check if the (matching covered) graph is a brick.
- `is_brace()`: Check if the (matching covered) graph is a brace.
- `is_removable_edge()`: Check if the edge of the (matching covered) graph is removable.
- `removable_edges()`: Return the list of all removable edges of the (matching covered) graph.
- `is_removable_doubleton()`: Check if the pair of edges constitute a removable doubleton in the (matching covered) graph.
- `removable_doubletons()`: Return the list of all removable doubletons.
- `retract()`: Compute the retract of the (matching covered) graph.
- `ear_decomposition()`: Return an efficient or an optimal ear decomposition (aka a list of ears/ a list of graphs).
- `is_b_invariant_edge()`: Check if the edge is *b*-invariant.
- `is_thin_edge()`: Check if the edge is thin.
- `is_strictly_thin_edge()`: Check if the edge is strictly thin.
- `is_mccuaig_brace()`: Check if the brace is a McCuaig brace.
- `is_norine_thomas_brick()`: Check if the brick is a Norine-Thomas brick.
- `brick_generation_sequence()`: Return a Norine-Thomas brick generation sequence of the (given) brick.
- `brace_generation_sequence()`: Return a McCuaig brace generation sequence of the (given) brace.

The following two functions are proposed to be implemented under the class: `Graph` in `sage.graphs.graph`:
- `is_matching_covered()`: Check if the graph is matching covered.
- `is_bicritical()`: Check if the graph is bicritical.

We adopt the notations and definitions as described in the aforementioned book of C.L. Lucchesi and U.S.R. Murty 1.

### Alternatives Considered

NA

### Additional Information

This implementation is a part of the project: [link](https://summerofcode.withgoogle.com/programs/2024/projects/iY5gx4Ls).

cc: @dcoudert.

### Is there an existing issue for this?

- [X] I have searched the existing issues for a bug report that matches the one I want to file, without success.

---
### References
1. C.L. Lucchesi, U.S.R. Murty. *Perfect Matchings: A Theory of Matching Covered Graphs*. Algorithms and Computation in Mathematics. Springer Cham, 1st edition, 2024, doi: [10.1007/978-3-031-47504-7](https://doi.org/10.1007/978-3-031-47504-7).


diff --git a/Lib/idlelib/macosx.py b/Lib/idlelib/macosx.py
index 89b645702d334d..2ea02ec04d661a 100644
--- a/Lib/idlelib/macosx.py
+++ b/Lib/idlelib/macosx.py
@@ -174,9 +174,8 @@ def overrideRootMenu(root, flist):
     del mainmenu.menudefs[-3][1][0:2]
     menubar = Menu(root)
     root.configure(menu=menubar)
-    menudict = {}

-    menudict['window'] = menu = Menu(menubar, name='window', tearoff=0)
+    menu = Menu(menubar, name='window', tearoff=0)
     menubar.add_cascade(label='Window', menu=menu, underline=0)

     def postwindowsmenu(menu=menu):
@@ -226,8 +225,7 @@ def help_dialog(event=None):

     if isCarbonTk():
         # for Carbon AquaTk, replace the default Tk apple menu
-        menudict['application'] = menu = Menu(menubar, name='apple',
-                                              tearoff=0)
+        menu = Menu(menubar, name='apple', tearoff=0)
         menubar.add_cascade(label='IDLE', menu=menu)
         mainmenu.menudefs.insert(0,
             ('application', [

IDLE maxosc.overrideRootMenu: remove unused menudict
BPO | [33964](https://bugs.python.org/issue33964)
--- | :---
Nosy | @terryjreedy, @csabella
PRs |
python/cpython#7926

Dependencies |
bpo-33963: IDLE macosx: add tests.

*Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.*

Show more details

GitHub fields:
```python
assignee = 'https://github.com/terryjreedy'
closed_at = None
created_at = <Date 2018-06-26.04:01:56.869>
labels = ['expert-IDLE', '3.7', '3.8', 'performance']
title = 'IDLE maxosc.overrideRootMenu: remove unused menudict'
updated_at = <Date 2019-03-20.19:52:30.187>
user = 'https://github.com/terryjreedy'
```

bugs.python.org fields:
```python
activity = <Date 2019-03-20.19:52:30.187>
actor = 'cheryl.sabella'
assignee = 'terry.reedy'
closed = False
closed_date = None
closer = None
components = ['IDLE']
creation = <Date 2018-06-26.04:01:56.869>
creator = 'terry.reedy'
dependencies = ['33963']
files = []
hgrepos = []
issue_num = 33964
keywords = ['patch']
message_count = 2.0
messages = ['320470', '338509']
nosy_count = 2.0
nosy_names = ['terry.reedy', 'cheryl.sabella']
pr_nums = ['7926']
priority = 'normal'
resolution = None
stage = 'patch review'
status = 'open'
superseder = None
type = 'performance'
url = 'https://bugs.python.org/issue33964'
versions = ['Python 3.7', 'Python 3.8']
```

Function local name 'menudict' is initialized empty, two key value pairs are added, and it is never touched again.


diff --git a/doc/release/upcoming_changes/10615.deprecation.rst b/doc/release/upcoming_changes/10615.deprecation.rst
new file mode 100644
index 000000000000..7fa948ea85be
--- /dev/null
+++ b/doc/release/upcoming_changes/10615.deprecation.rst
@@ -0,0 +1,14 @@
+Only ndim-0 arrays are treated as scalars
+-----------------------------------------
+NumPy used to treat all arrays of size 1 (e.g., ``np.array([3.14])``) as scalars.
+In the future, this will be limited to arrays of ndim 0 (e.g., ``np.array(3.14)``).
+The following expressions will report a deprecation warning:
+
+.. code-block:: python
+
+    a = np.array([3.14])
+    float(a)  # better: a[0] to get the numpy.float or a.item()
+
+    b = np.array([[3.14]])
+    c = numpy.random.rand(10)
+    c[0] = b  # better: c[0] = b[0, 0]
diff --git a/numpy/core/function_base.py b/numpy/core/function_base.py
index c82b495b159e..00e4e6b0ea84 100644
--- a/numpy/core/function_base.py
+++ b/numpy/core/function_base.py
@@ -168,7 +168,7 @@ def linspace(start, stop, num=50, endpoint=True, retstep=False, dtype=None,
     y += start

     if endpoint and num > 1:
-        y[-1] = stop
+        y[-1, ...] = stop

     if axis != 0:
         y = _nx.moveaxis(y, 0, axis)
diff --git a/numpy/core/src/multiarray/common.c b/numpy/core/src/multiarray/common.c
index 001d299c7f11..573d0d6063c7 100644
--- a/numpy/core/src/multiarray/common.c
+++ b/numpy/core/src/multiarray/common.c
@@ -441,3 +441,32 @@ new_array_for_sum(PyArrayObject *ap1, PyArrayObject *ap2, PyArrayObject* out,
     }
 }

+NPY_NO_EXPORT int
+check_is_convertible_to_scalar(PyArrayObject *v)
+{
+    if (PyArray_NDIM(v) == 0) {
+        return 0;
+    }
+
+    /* Remove this if-else block when the deprecation expires */
+    if (PyArray_SIZE(v) == 1) {
+        /* Numpy 1.25.0, 2023-01-02 */
+        if (DEPRECATE(
+                "Conversion of an array with ndim > 0 to a scalar "
+                "is deprecated, and will error in future. "
+                "Ensure you extract a single element from your array "
+                "before performing this operation. "
+                "(Deprecated NumPy 1.25.)") < 0) {
+            return -1;
+        }
+        return 0;
+    } else {
+        PyErr_SetString(PyExc_TypeError,
+            "only length-1 arrays can be converted to Python scalars");
+        return -1;
+    }
+
+    PyErr_SetString(PyExc_TypeError,
+            "only 0-dimensional arrays can be converted to Python scalars");
+    return -1;
+}
diff --git a/numpy/core/src/multiarray/common.h b/numpy/core/src/multiarray/common.h
index 127c6250da80..cb9fadc4e9f6 100644
--- a/numpy/core/src/multiarray/common.h
+++ b/numpy/core/src/multiarray/common.h
@@ -332,6 +332,13 @@ PyArray_TupleFromItems(int n, PyObject *const *items, int make_null_none)
     return tuple;
 }

+/*
+ * Returns 0 if the array has rank 0, -1 otherwise. Prints a deprecation
+ * warning for arrays of _size_ 1.
+ */
+NPY_NO_EXPORT int
+check_is_convertible_to_scalar(PyArrayObject *v);
+

 #include "ucsnarrow.h"

diff --git a/numpy/core/src/multiarray/methods.c b/numpy/core/src/multiarray/methods.c
index 93b290020265..3b2e728200c7 100644
--- a/numpy/core/src/multiarray/methods.c
+++ b/numpy/core/src/multiarray/methods.c
@@ -2804,9 +2804,7 @@ array_complex(PyArrayObject *self, PyObject *NPY_UNUSED(args))
     PyArray_Descr *dtype;
     PyObject *c;

-    if (PyArray_SIZE(self) != 1) {
-        PyErr_SetString(PyExc_TypeError,
-                "only length-1 arrays can be converted to Python scalars");
+    if (check_is_convertible_to_scalar(self) < 0) {
         return NULL;
     }

diff --git a/numpy/core/src/multiarray/number.c b/numpy/core/src/multiarray/number.c
index c208fb203233..35e4af79c8d7 100644
--- a/numpy/core/src/multiarray/number.c
+++ b/numpy/core/src/multiarray/number.c
@@ -869,13 +869,11 @@ array_scalar_forward(PyArrayObject *v,
                      PyObject *(*builtin_func)(PyObject *),
                      const char *where)
 {
-    PyObject *scalar;
-    if (PyArray_SIZE(v) != 1) {
-        PyErr_SetString(PyExc_TypeError, "only size-1 arrays can be"\
-                        " converted to Python scalars");
+    if (check_is_convertible_to_scalar(v) < 0) {
         return NULL;
     }

+    PyObject *scalar;
     scalar = PyArray_GETITEM(v, PyArray_DATA(v));
     if (scalar == NULL) {
         return NULL;

diff --git a/numpy/core/tests/test_deprecations.py b/numpy/core/tests/test_deprecations.py
index b92a20a12cea..e47a24995067 100644
--- a/numpy/core/tests/test_deprecations.py
+++ b/numpy/core/tests/test_deprecations.py
@@ -822,6 +822,18 @@ def test_deprecated_raised(self, dtype):
                 assert isinstance(e.__cause__, DeprecationWarning)

+class TestScalarConversion(_DeprecationTestCase):
+    # 2023-01-02, 1.25.0
+    def test_float_conversion(self):
+        self.assert_deprecated(float, args=(np.array([3.14]),))
+
+    def test_behaviour(self):
+        b = np.array([[3.14]])
+        c = np.zeros(5)
+        with pytest.warns(DeprecationWarning):
+            c[0] = b
+
+
 class TestPyIntConversion(_DeprecationTestCase):
     message = r".*stop allowing conversion of out-of-bound.*"

diff --git a/numpy/core/tests/test_multiarray.py b/numpy/core/tests/test_multiarray.py
index 4a064827dd0f..984047c87961 100644
--- a/numpy/core/tests/test_multiarray.py
+++ b/numpy/core/tests/test_multiarray.py
@@ -3644,9 +3644,13 @@ def test__complex__(self):
             msg = 'dtype: {0}'.format(dt)
             ap = complex(a)
             assert_equal(ap, a, msg)
-            bp = complex(b)
+
+            with assert_warns(DeprecationWarning):
+                bp = complex(b)
             assert_equal(bp, b, msg)
-            cp = complex(c)
+
+            with assert_warns(DeprecationWarning):
+                cp = complex(c)
             assert_equal(cp, c, msg)

     def test__complex__should_not_work(self):
@@ -3669,7 +3673,8 @@ def test__complex__should_not_work(self):
         assert_raises(TypeError, complex, d)

         e = np.array(['1+1j'], 'U')
-        assert_raises(TypeError, complex, e)
+        with assert_warns(DeprecationWarning):
+            assert_raises(TypeError, complex, e)

 class TestCequenceMethods:
     def test_array_contains(self):
@@ -8756,8 +8761,10 @@ def test_to_int_scalar(self):
         int_funcs = (int, lambda x: x.__int__())
         for int_func in int_funcs:
             assert_equal(int_func(np.array(0)), 0)
-            assert_equal(int_func(np.array([1])), 1)
-            assert_equal(int_func(np.array([[42]])), 42)
+            with assert_warns(DeprecationWarning):
+                assert_equal(int_func(np.array([1])), 1)
+            with assert_warns(DeprecationWarning):
+                assert_equal(int_func(np.array([[42]])), 42)
             assert_raises(TypeError, int_func, np.array([1, 2]))

             # gh-9972
@@ -8772,7 +8779,8 @@ class HasTrunc:
                     def __trunc__(self):
                         return 3
                 assert_equal(3, int_func(np.array(HasTrunc())))
-                assert_equal(3, int_func(np.array([HasTrunc()])))
+                with assert_warns(DeprecationWarning):
+                    assert_equal(3, int_func(np.array([HasTrunc()])))
             else:
                 pass

@@ -8781,8 +8789,9 @@ def __int__(self):
                     raise NotImplementedError
             assert_raises(NotImplementedError,
                 int_func, np.array(NotConvertible()))
-            assert_raises(NotImplementedError,
-                int_func, np.array([NotConvertible()]))
+            with assert_warns(DeprecationWarning):
+                assert_raises(NotImplementedError,
+                    int_func, np.array([NotConvertible()]))

 class TestWhere:

deprecate scalar conversions for rank>0 arrays
Numpy allows for conversion of arrays into scalars if they are  size-1, i.e.,
```python
float(numpy.array(1.0))  # 1.0
float(numpy.array([1.0]))  # 1.0
float(numpy.array([[[[1.0]]]]))  # 1.0

# TypeError: only size-1 arrays can be converted to Python scalars
float(numpy.array([1.0, 1.0]))
```
I'm fine with rank-0 conversions, but I found that discrimination of conversion based on array size can easily lead to inconsistencies downstream. See, for example, https://github.com/sympy/sympy/issues/13924 where suddenly you have a different behavior of multiplication depending on the length of the input array.

Moreover, this feature is actually redundant for rank>0 arrays since the value can simply be retrieved from the array via `x[0]`.

When grepping for `"converted to Python scalar"` in the numpy sources, one finds that `numpy/lib/user_array.py` has
```
"only rank-0 arrays can be converted to Python scalars."
```
which seems to be the more consistent solution.

I would hence like to suggest to deprecate scalar conversion for rank>0 size-1 arrays.

Personally agree with you (there is a reason we got rid of the `operator.index` equivalent thing, where it was causing obvious problems) and would encourage anyone to try it.
But… I expect there are some annoying cases to solve and it might have a pretty large downstream impact which would make it a very slow change indeed (I am not sure, I think I might have tried it at some time way back, heh).
> or rank-1 and size-1

Not even - `size == 1` is all that is checked:

```
>>> float(np.array([[[1]]]))
1.0
```

> Personally agree with you

Me too. The deprecation warning could stick around for a very long time, and making noise downstream seems pretty reasonable if that noise identifies their bugs.
I agree with @seberg -- this change would indeed make the API cleaner, but I'm dubious about whether we can actually pull it off or the pain would be worth the benefit. The next step would be to try making the change and see how much stuff breaks (e.g. by running the test suites for big downstream projects like scipy, scikit-learn, astropy, ...). That'd at least give us some data to discuss.
Thanks for the comments. I've adapted to original post to reflect the fact that arrays of larger rank can be converted to floats as well.

I understand that it can be painful to deprecate stuff, particularly if its not clear how much a functionality is actually used downstream. I would suggest that I make the change locally and compile scipy, sympy, pandas, scikit-learn, and astropy (any other suggestions?) against it and report back with the experience.
Alright, I've now dug into scipy and this is what I found:

  * The use of explicit int/float conversions on size-1 arrays is not that common. In replacing it with the proper expressions, I've even discovered a small bug in scipy.

  * When disallowing float conversions on size-1 arrays, the following code is illegal:
    ```python
    a = numpy.random.rand(10, 1)
    a[0] = numpy.array([1.0])
    # ValueError: setting an array element with a sequence.
    ```
    While arguably unclean (you should just set `a[0] = 1.0`), the use of that is more common. This probably constitutes a roadblock for the suggested change.

I'll leave it up to you to comment and/or close this issue.
@nschloe I don't follow why that second case becomes illegal. It seems like that should be well defined, since the array `a[0, :]` has shape `(1,)`. Maybe that was a bug in your implementation, possibly due to preexisting assumptions in Numpy?
> I don't follow why that second case becomes illegal.

@shoyer I'll try to explain why I think it failed. The array element `a[0]` in
```python
a = numpy.random.rand(10, 1)
a[0] = numpy.array([1.0])
```
is a scalar (i.e., empty shape `(,)`), and you're trying to set it with an object of shape `(1,)`. For this to work, the rhs needs to be converted to a scalar (automatically). If scalar conversion is disabled, you'll get an error.
@nschloe: That might be considered a sort of reverse broadcasting, which currently succeeds in some cases but not others:
```python
a = np.zeros((1, 3))
b = np.empty((3,))
b[...] = a # leading 1 dimensions are removed
b[...] += a  # but this fails!
```

Thinking again, allowing a `size == 1` array decay to a scalar seems pretty consistent with broadcasting rules in general, to me - if `()` can magically promote to `(1, 1, 1, 1)`, then is it any worse to allow `(1, 1, 1, 1)` to decay to `()`?
@nschloe: in your branch, does `a[0, ...] = np.array([0])` succeed?
Yeah… well the lhs broadcasting for singleton assignments was what I
was thinking of. Not sure if there is more, but if it is the main
point, it should not be all that bad….

Eric, hehe, that should succeed, but without checking I am not actually
sure anymore if it sidesteps and just does item assignment currently,
though I doubt it.

There may be a bit annoyance with object arrays, where you never do
this for item assignment, since you assume that the array should be the
object. But that wouldn't even have to change.
> There may be a bit annoyance with object arrays

I think that collectively, we've hit the nail on the head there - reverse broadcasting has no place in scalar assignments, because it just doesn't generalize to object arrays. I think that

```
a[0] = some_1d_len1_array
```
ought to be forbidden, since its semantics are dtype dependent - code written this way for numeric arrays will already fail in unexpected ways for object arrays. If the reverse-broadcasting is desired by the user, then they should be forced to write
```
a[0, ...] = some_1d_len1_array
```
which does the same thing for all dtypes. Making them write it this way eliminates a class of bug, which is exactly what we should be aiming to do with `DeprecationWarning`s

---

Whether reverse broadcasting is actually desirable is also perhaps questionable, but you've made it clear to me that it's an orthogonal issue.
Yeah, its orthogonal. If you attempt this, you basically have to put into place the stuff that allows to deprecate the other, too. But that is all there is to it probably.

EDIT: OK it is not quite orthogonal, since for a bit the behaviour between some array likes would differ.
I think @shoyer's point was that if `a` has shape (10, 1), then `a[0]` should expand to `a[0, ...]` and have shape (1,), so it's *not* a scalar.
@njsmith: I mistakenly read that as shape `(10,)`
I've finally gotten around to setting up a branch for this, https://github.com/numpy/numpy/compare/master...nschloe:bug-10404. Here are the results:
```python
import numpy

a = numpy.random.rand(10, 1)
a[0] = numpy.array([1.0])       # okay
a[0] = numpy.array(1.0)         # okay
a[0] = 1.0                      # okay

b = numpy.random.rand(10)
b[0] = numpy.array([1.0])       # ValueError: setting an array element with a sequence.
b[0, ...] = numpy.array([1.0])  # okay
b[0] = numpy.array(1.0)         # okay
b[0] = 1.0                      # okay
```
This all makes sense, and the error message is accurate too. Instead of recommending to insert `...` (as @eric-wieser suggests), I find the simple `b[0] = 1.0` more appropriate. Perhaps there are cases where `...` is actually needed though.

Yeah, so this is a breaking change that would need a long way of deprecation first. Should I make the ValueError a deprecation and submit a PR where we can discuss further, or are there other suggestions?
Nothing wrong with PRs in any case, we can always close them ;). It might be a very long and possibly painful deprecation, unfortunately. About the assignment thing, currently that type of broadcasting is also supported for non-scalar LHS, which may or may not be something to look into/synchronize.

The problem with these things is that often you only know how bad this kind of change is for downstream until they start seriously using it.
If you make a PR we can at least identify where we rely on this internally
:arrow_right: PR #10615.
As another pain point, this behavior is a tad confusing when combined with `pybind11`, where you can define overloads.
When `pybind11` evaluates its overloads, it first does a "no convert" pass, where it attempts to convert from Python to C++ with no (or very little) implicit conversion, passing over each overload. If this fails, then it passes over the same overload set, this time permitting implicit conversions. This overload behavior makes things great when you want to distinguish between `int` and `float` overloads, but it makes things painful when NumPy can implicitly convert its arrays to scalars, but lists themselves to do not get collapsed to scalars.

I ran into this when trying to write overloads for a function, say `func`, for argument types `np.array[double]` (returns "Vector") and `int` (returns "Int"). (Was actually trying to use this where one call constructed from an array, while another was sugar for creating a fresh array of a given size). Some example runs:
```
func(0) -> "Int"  # Good.
func([0.])  -> "Vector"  # Great.
func(np.array([0.]))  -> "Int"  # What???
```

I originally ran into this when writing a custom dtype, but found this was the behavior for floats too.

I will file a `pybind11` issue as well, with a workaround that is specific to `pybind11`, but it would have been nice if NumPy were a tad more strict on how it treats scalars vs. single-item arrays.
The scalar conversion of size-1 arrays I think is a part of Numpy semantics that is quite central, and I believe it's not a small amount of code that assumes it (https://github.com/scipy/scipy/pull/11147 also seems to suggest). IMO, this would be more like Numpy 2.0 stuff.
> IMO, this would be more like Numpy 2.0 stuff.

Perhaps there's a compromise where we can still deprecate it in 1.x, but it's a far longer deprecation period than normal - perhaps `PendingDeprecationWarning` would be appropriate here

@eric-wieser how are we going to deprecate/futurewarn though? Return new scalars, which warn when in-place operations happen, or when someone tries to hash them? I suppose that is actually possible, but I guess it might be very noisy. OTOH, considering how much I dislike this behaviour, maybe there is a point in trying it out. Even if we would hide it behind some env variable for now.
Isn't it as simple as replacing the valueerror in the linked PR with a warning?

I don't think the length of the deprecation period matters here --- either the API is kept stable or not. The arguments what is gained by this seem not so clearly considered above (or at least I didn't get them by reading it twice :): Unlike with strict-casting and float indices, it's not as clear presence of this coercion in code would usually be a bug (only sympy and pybind11 corner cases are mentioned above, not sure if the object array assignment is the same).
The way I see it is:

* The current behavior hides potential bugs (which I agree we need to expand upon before actually deprecating this, preferably in the release notes)
* It is always possible to write code that does not rely on this behavior, in all remotely recent versions of numpy
* Even if libraries like scipy do not have any bugs caused by this coercion, it is likely that they expose a similar class of pitfall to their callers
* Users can always silence warnings

> either the API is kept stable or not

Perhaps then this should be a `UserWarning` instead - indicating that the api is unlikely to change, but the user is nonetheless advised not to do what they're doing.
> Perhaps then this should be a UserWarning instead -

:+1:

I think there no need for hard breaking people's code so you're right, a deprecation is perhaps a bit harsh. However, when using float conversion of rank>0 arrays, you're almost always making a mistake in how you treat your arrays. A typical case would be returning `[val]` from an objective function for optimization which really should be a scalar. Right now, numpy irons out that mistake for you, but it is my experience that your code quality improves when dealing with it (e.g., https://github.com/scipy/scipy/pull/11147).

More typical anti-patterns caught by this change:
```python
import numpy
a = numpy.array([1.0e-1], dtype=numpy.float16)
# ....
val = float(a)   # you want a[0]
```
```python
import numpy
p = numpy.array([3.14])
a = numpy.array([1.0, 2*p, 1/p])  # whoops: a is of type obj
#
b = numpy.zeros((10, 3))
b[0] = a   # !
```

`float(a)` and `a[0]` mean different things --- the former is an explicit scalar and type cast, whereas the latter extracts (a potentially >0-dim) sub-array from an array, and these are not exchangeable. Safe scalar coercion can be done with `a.reshape([])` ... but that's quite ugly.
> float(a) and a[0] mean different thing

Indeed. And until now, in many cases where the user _should_ have used `a[0]`, you'll find `float(a)`/`int(a)` (even scipy). Now you get a warning for that.
@nschloe, I think your scipy PR might be fixing the symptom and not the cause - for anyone else reading along, I've been trying a different approach in https://github.com/scipy/scipy/pull/11158.
In https://github.com/scipy/scipy/pull/11147, majority of the changes do not appear to be actually bugs --- most of them are extracting a Python scalar of given type from an array known to have a size 1 (with exception if not), i.e. `float(x)` as short for `float(x.item())`.
In principle `arr.item()` means something very similar. Although, I think it always defaults to `.item(0)` and will not notify that the array has additional dimensions. The main issue with `float()` is that it uses reversed broadcasting rules. Also we already deprecated this for `__nonzero__` due to the problems of inconsistent indexing.
I agree that there is lots of correct usage out there though. Similar to `squeeze`, although it has problems, it is often used correctly probably.

component: numpy._core
07 - Deprecation
62 - Python API


diff --git a/doc/source/whatsnew/v1.5.0.rst b/doc/source/whatsnew/v1.5.0.rst
index 45c32d689bd5b..fccfbb98f2591 100644
--- a/doc/source/whatsnew/v1.5.0.rst
+++ b/doc/source/whatsnew/v1.5.0.rst
@@ -133,6 +133,7 @@ Other enhancements
 - :meth:`MultiIndex.to_frame` now supports the argument ``allow_duplicates`` and raises on duplicate labels if it is missing or False (:issue:`45245`)
 - :class:`StringArray` now accepts array-likes containing nan-likes (``None``, ``np.nan``) for the ``values`` parameter in its constructor in addition to strings and :attr:`pandas.NA`. (:issue:`40839`)
 - Improved the rendering of ``categories`` in :class:`CategoricalIndex` (:issue:`45218`)
+- :meth:`DataFrame.plot` will now allow the ``subplots`` parameter to be a list of iterables specifying column groups, so that columns may be grouped together in the same subplot (:issue:`29688`).
 - :meth:`to_numeric` now preserves float64 arrays when downcasting would generate values not representable in float32 (:issue:`43693`)
 - :meth:`Series.reset_index` and :meth:`DataFrame.reset_index` now support the argument ``allow_duplicates`` (:issue:`44410`)
 - :meth:`.GroupBy.min` and :meth:`.GroupBy.max` now supports `Numba <https://numba.pydata.org/>`_ execution with the ``engine`` keyword (:issue:`45428`)
diff --git a/pandas/plotting/_core.py b/pandas/plotting/_core.py
index 29110f63787a6..929ddb52aea6d 100644
--- a/pandas/plotting/_core.py
+++ b/pandas/plotting/_core.py
@@ -649,8 +649,18 @@ class PlotAccessor(PandasObject):
         - 'hexbin' : hexbin plot (DataFrame only)
     ax : matplotlib axes object, default None
         An axes of the current figure.
-    subplots : bool, default False
-        Make separate subplots for each column.
+    subplots : bool or sequence of iterables, default False
+        Whether to group columns into subplots:
+
+        - ``False`` : No subplots will be used
+        - ``True`` : Make separate subplots for each column.
+        - sequence of iterables of column labels: Create a subplot for each
+          group of columns. For example `[('a', 'c'), ('b', 'd')]` will
+          create 2 subplots: one with columns 'a' and 'c', and one
+          with columns 'b' and 'd'. Remaining columns that aren't specified
+          will be plotted in additional subplots (one per column).
+          .. versionadded:: 1.5.0
+
     sharex : bool, default True if ax is None else False
         In case ``subplots=True``, share x axis and set some x axis labels
         to invisible; defaults to True if ax is None otherwise False if
diff --git a/pandas/plotting/_matplotlib/core.py b/pandas/plotting/_matplotlib/core.py
index 3899885bd95f1..5fceb14b9d1cc 100644
--- a/pandas/plotting/_matplotlib/core.py
+++ b/pandas/plotting/_matplotlib/core.py
@@ -3,6 +3,8 @@
 from typing import (
     TYPE_CHECKING,
     Hashable,
+    Iterable,
+    Sequence,
 )
 import warnings

@@ -102,7 +104,7 @@ def __init__(
         data,
         kind=None,
         by: IndexLabel | None = None,
-        subplots=False,
+        subplots: bool | Sequence[Sequence[str]] = False,
         sharex=None,
         sharey=False,
         use_index=True,
@@ -166,8 +168,7 @@ def __init__(
         self.kind = kind

         self.sort_columns = sort_columns
-
-        self.subplots = subplots
+        self.subplots = self._validate_subplots_kwarg(subplots)

         if sharex is None:

@@ -253,6 +254,112 @@ def __init__(

         self._validate_color_args()

+    def _validate_subplots_kwarg(
+        self, subplots: bool | Sequence[Sequence[str]]
+    ) -> bool | list[tuple[int, ...]]:
+        """
+        Validate the subplots parameter
+
+        - check type and content
+        - check for duplicate columns
+        - check for invalid column names
+        - convert column names into indices
+        - add missing columns in a group of their own
+        See comments in code below for more details.
+
+        Parameters
+        ----------
+        subplots : subplots parameters as passed to PlotAccessor
+
+        Returns
+        -------
+        validated subplots : a bool or a list of tuples of column indices. Columns
+        in the same tuple will be grouped together in the resulting plot.
+        """
+
+        if isinstance(subplots, bool):
+            return subplots
+        elif not isinstance(subplots, Iterable):
+            raise ValueError("subplots should be a bool or an iterable")
+
+        supported_kinds = (
+            "line",
+            "bar",
+            "barh",
+            "hist",
+            "kde",
+            "density",
+            "area",
+            "pie",
+        )
+        if self._kind not in supported_kinds:
+            raise ValueError(
+                "When subplots is an iterable, kind must be "
+                f"one of {', '.join(supported_kinds)}. Got {self._kind}."
+            )
+
+        if isinstance(self.data, ABCSeries):
+            raise NotImplementedError(
+                "An iterable subplots for a Series is not supported."
+            )
+
+        columns = self.data.columns
+        if isinstance(columns, ABCMultiIndex):
+            raise NotImplementedError(
+                "An iterable subplots for a DataFrame with a MultiIndex column "
+                "is not supported."
+            )
+
+        if columns.nunique() != len(columns):
+            raise NotImplementedError(
+                "An iterable subplots for a DataFrame with non-unique column "
+                "labels is not supported."
+            )
+
+        # subplots is a list of tuples where each tuple is a group of
+        # columns to be grouped together (one ax per group).
+        # we consolidate the subplots list such that:
+        # - the tuples contain indices instead of column names
+        # - the columns that aren't yet in the list are added in a group
+        #   of their own.
+        # For example with columns from a to g, and
+        # subplots = [(a, c), (b, f, e)],
+        # we end up with [(ai, ci), (bi, fi, ei), (di,), (gi,)]
+        # This way, we can handle self.subplots in a homogeneous manner
+        # later.
+        # TODO: also accept indices instead of just names?
+
+        out = []
+        seen_columns: set[Hashable] = set()
+        for group in subplots:
+            if not is_list_like(group):
+                raise ValueError(
+                    "When subplots is an iterable, each entry "
+                    "should be a list/tuple of column names."
+                )
+            idx_locs = columns.get_indexer_for(group)
+            if (idx_locs == -1).any():
+                bad_labels = np.extract(idx_locs == -1, group)
+                raise ValueError(
+                    f"Column label(s) {list(bad_labels)} not found in the DataFrame."
+                )
+            else:
+                unique_columns = set(group)
+                duplicates = seen_columns.intersection(unique_columns)
+                if duplicates:
+                    raise ValueError(
+                        "Each column should be in only one subplot. "
+                        f"Columns {duplicates} were found in multiple subplots."
+                    )
+                seen_columns = seen_columns.union(unique_columns)
+                out.append(tuple(idx_locs))
+
+        unseen_columns = columns.difference(seen_columns)
+        for column in unseen_columns:
+            idx_loc = columns.get_loc(column)
+            out.append((idx_loc,))
+        return out
+
     def _validate_color_args(self):
         if (
             "color" in self.kwds
@@ -371,8 +478,11 @@ def _maybe_right_yaxis(self, ax: Axes, axes_num):

     def _setup_subplots(self):
         if self.subplots:
+            naxes = (
+                self.nseries if isinstance(self.subplots, bool) else len(self.subplots)
+            )
             fig, axes = create_subplots(
-                naxes=self.nseries,
+                naxes=naxes,
                 sharex=self.sharex,
                 sharey=self.sharey,
                 figsize=self.figsize,
@@ -784,9 +894,23 @@ def _get_ax_layer(cls, ax, primary=True):
         else:
             return getattr(ax, "right_ax", ax)

+    def _col_idx_to_axis_idx(self, col_idx: int) -> int:
+        """Return the index of the axis where the column at col_idx should be plotted"""
+        if isinstance(self.subplots, list):
+            # Subplots is a list: some columns will be grouped together in the same ax
+            return next(
+                group_idx
+                for (group_idx, group) in enumerate(self.subplots)
+                if col_idx in group
+            )
+        else:
+            # subplots is True: one ax per column
+            return col_idx
+
     def _get_ax(self, i: int):
         # get the twinx ax if appropriate
         if self.subplots:
+            i = self._col_idx_to_axis_idx(i)
             ax = self.axes[i]
             ax = self._maybe_right_yaxis(ax, i)
             self.axes[i] = ax

diff --git a/pandas/tests/plotting/frame/test_frame.py b/pandas/tests/plotting/frame/test_frame.py
index c4ce0b256cd41..3ec3744e43653 100644
--- a/pandas/tests/plotting/frame/test_frame.py
+++ b/pandas/tests/plotting/frame/test_frame.py
@@ -2071,6 +2071,90 @@ def test_plot_no_numeric_data(self):
         with pytest.raises(TypeError, match="no numeric data to plot"):
             df.plot()

+    @td.skip_if_no_scipy
+    @pytest.mark.parametrize(
+        "kind", ("line", "bar", "barh", "hist", "kde", "density", "area", "pie")
+    )
+    def test_group_subplot(self, kind):
+        d = {
+            "a": np.arange(10),
+            "b": np.arange(10) + 1,
+            "c": np.arange(10) + 1,
+            "d": np.arange(10),
+            "e": np.arange(10),
+        }
+        df = DataFrame(d)
+
+        axes = df.plot(subplots=[("b", "e"), ("c", "d")], kind=kind)
+        assert len(axes) == 3  # 2 groups + single column a
+
+        expected_labels = (["b", "e"], ["c", "d"], ["a"])
+        for ax, labels in zip(axes, expected_labels):
+            if kind != "pie":
+                self._check_legend_labels(ax, labels=labels)
+            if kind == "line":
+                assert len(ax.lines) == len(labels)
+
+    def test_group_subplot_series_notimplemented(self):
+        ser = Series(range(1))
+        msg = "An iterable subplots for a Series"
+        with pytest.raises(NotImplementedError, match=msg):
+            ser.plot(subplots=[("a",)])
+
+    def test_group_subplot_multiindex_notimplemented(self):
+        df = DataFrame(np.eye(2), columns=MultiIndex.from_tuples([(0, 1), (1, 2)]))
+        msg = "An iterable subplots for a DataFrame with a MultiIndex"
+        with pytest.raises(NotImplementedError, match=msg):
+            df.plot(subplots=[(0, 1)])
+
+    def test_group_subplot_nonunique_cols_notimplemented(self):
+        df = DataFrame(np.eye(2), columns=["a", "a"])
+        msg = "An iterable subplots for a DataFrame with non-unique"
+        with pytest.raises(NotImplementedError, match=msg):
+            df.plot(subplots=[("a",)])
+
+    @pytest.mark.parametrize(
+        "subplots, expected_msg",
+        [
+            (123, "subplots should be a bool or an iterable"),
+            ("a", "each entry should be a list/tuple"),  # iterable of non-iterable
+            ((1,), "each entry should be a list/tuple"),  # iterable of non-iterable
+            (("a",), "each entry should be a list/tuple"),  # iterable of strings
+        ],
+    )
+    def test_group_subplot_bad_input(self, subplots, expected_msg):
+        # Make sure error is raised when subplots is not a properly
+        # formatted iterable. Only iterables of iterables are permitted, and
+        # entries should not be strings.
+        d = {"a": np.arange(10), "b": np.arange(10)}
+        df = DataFrame(d)
+
+        with pytest.raises(ValueError, match=expected_msg):
+            df.plot(subplots=subplots)
+
+    def test_group_subplot_invalid_column_name(self):
+        d = {"a": np.arange(10), "b": np.arange(10)}
+        df = DataFrame(d)
+
+        with pytest.raises(ValueError, match=r"Column label\(s\) \['bad_name'\]"):
+            df.plot(subplots=[("a", "bad_name")])
+
+    def test_group_subplot_duplicated_column(self):
+        d = {"a": np.arange(10), "b": np.arange(10), "c": np.arange(10)}
+        df = DataFrame(d)
+
+        with pytest.raises(ValueError, match="should be in only one subplot"):
+            df.plot(subplots=[("a", "b"), ("a", "c")])
+
+    @pytest.mark.parametrize("kind", ("box", "scatter", "hexbin"))
+    def test_group_subplot_invalid_kind(self, kind):
+        d = {"a": np.arange(10), "b": np.arange(10)}
+        df = DataFrame(d)
+        with pytest.raises(
+            ValueError, match="When subplots is an iterable, kind must be one of"
+        ):
+            df.plot(subplots=[("a", "b")], kind=kind)
+
     @pytest.mark.parametrize(
         "index_name, old_label, new_label",
         [

ENH: Group some columns in subplots with DataFrame.plot
`df.plot(subplots=True)`  will create one subplot per column. Is there a way to group multiple columns on the same subplot (and leave the rest of the column separated)?

I'd be happy to submit a PR if that's something you'd consider? In terms of API  `subplot` could accept a list of tuples where each tuple indicates which columns should be grouped together.

Just to make sure I understand, `df.plot(subplots=[('a', 'b'), ('d', 'e', 'f')])` would have two axes?

I'm not sure, but I'm guessing it'd be somewhat complicated to work that into our current `subplots` logic. At glance, it looks like we assume `subplots=True` -> one axes per column in several places.
> `df.plot(subplots=[('a', 'b'), ('d', 'e', 'f')])` would have two axes?

Yes that's the idea. My motivation is that sometimes some columns are comparable, in which case it would make sense to plot them together.