Repository: agronholm/exceptiongroup

Issue:
Document how to print an exceptiongroup traceback when the monkey patching doesn't work / is not applied.
First, thanks for working on this project.

I  updated to the latest `cattr 22.1.0` from @Tinche who started to use the `exceptiongroup` backport to report various errors occurring while "structuring" objects. All fine.

Unfortunately, I think I have an installed package that messes with the monkeypatching of the traceback formatting so when I run my buggy object structuring which fails, I get this console output:
```
Traceback (most recent call last):
  File "debug.py", line 40, in <module>
    main()
  File "debug.py", line 34, in main
    pressure_rule_structured = DATA_QUALITY_CONVERTER.structure(
  File "/opt/python3toolbox/.venv/lib/python3.8/site-packages/cattrs/converters.py", line 281, in structure
    return self._structure_func.dispatch(cl)(obj, cl)
  File "<cattrs generated structure DataQuality.rule_objects.RangeConfig>", line 44, in structure_RangeConfig
cattrs.errors.ClassValidationError: While structuring RangeConfig (4 sub-exceptions)
```

When I run this same code in an virtualenv with less packages, I get the traceback "trees" akin to the ones shown here: https://github.com/python-attrs/cattrs/issues/258#issue-1215069520

I tried for some time to try to force the `exceptiongroup` exception formatting and it resulted in different form attribute errors or the likes deep in the `exceptiongroup` code which makes me think there are 2 monkey patching at play.

My question: is there anyway to force the display of this exceptiongroup formatting even if someone else elsewhere monkeypatched `traceback.TracebackException`? All my incantations involving `traceback` stdlib module and `exceptiongroup._formatting` failed miserably.

Discussion:
coveralls: ## Pull Request Test Coverage Report for [Build 2884157288](https://coveralls.io/builds/51772267)

* **0** of **0**   changed or added relevant lines in **0** files are covered.
* **200** unchanged lines in **12** files lost coverage.
* Overall coverage decreased (**-0.1%**) to **74.533%**

---


|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [/opt/hostedtoolcache/PyPy/3.8.13/x64/lib/pypy3.8/site-packages/exceptiongroup/__init__.py](https://coveralls.io/builds/51772267/source?filename=%2Fopt%2Fhostedtoolcache%2FPyPy%2F3.8.13%2Fx64%2Flib%2Fpypy3.8%2Fsite-packages%2Fexceptiongroup%2F__init__.py#L25) | 3 | 80.0% |
| [/opt/hostedtoolcache/Python/3.10.6/x64/lib/python3.10/site-packages/exceptiongroup/__init__.py](https://coveralls.io/builds/51772267/source?filename=%2Fopt%2Fhostedtoolcache%2FPython%2F3.10.6%2Fx64%2Flib%2Fpython3.10%2Fsite-packages%2Fexceptiongroup%2F__init__.py#L25) | 3 | 80.0% |
| [/opt/hostedtoolcache/Python/3.7.13/x64/lib/python3.7/site-packages/exceptiongroup/__init__.py](https://coveralls.io/builds/51772267/source?filename=%2Fopt%2Fhostedtoolcache%2FPython%2F3.7.13%2Fx64%2Flib%2Fpython3.7%2Fsite-packages%2Fexceptiongroup%2F__init__.py#L25) | 3 | 80.0% |
| [/opt/hostedtoolcache/Python/3.8.13/x64/lib/python3.8/site-packages/exceptiongroup/__init__.py](https://coveralls.io/builds/51772267/source?filename=%2Fopt%2Fhostedtoolcache%2FPython%2F3.8.13%2Fx64%2Flib%2Fpython3.8%2Fsite-packages%2Fexceptiongroup%2F__init__.py#L25) | 3 | 80.0% |
| [/opt/hostedtoolcache/Python/3.9.13/x64/lib/python3.9/site-packages/exceptiongroup/__init__.py](https://coveralls.io/builds/51772267/source?filename=%2Fopt%2Fhostedtoolcache%2FPython%2F3.9.13%2Fx64%2Flib%2Fpython3.9%2Fsite-packages%2Fexceptiongroup%2F__init__.py#L25) | 3 | 80.0% |
| [/opt/hostedtoolcache/Python/3.11.0-rc.1/x64/lib/python3.11/site-packages/exceptiongroup/__init__.py](https://coveralls.io/builds/51772267/source?filename=%2Fopt%2Fhostedtoolcache%2FPython%2F3.11.0-rc.1%2Fx64%2Flib%2Fpython3.11%2Fsite-packages%2Fexceptiongroup%2F__init__.py#L17) | 5 | 60.0% |
| [/opt/hostedtoolcache/Python/3.10.6/x64/lib/python3.10/site-packages/exceptiongroup/_formatting.py](https://coveralls.io/builds/51772267/source?filename=%2Fopt%2Fhostedtoolcache%2FPython%2F3.10.6%2Fx64%2Flib%2Fpython3.10%2Fsite-packages%2Fexceptiongroup%2F_formatting.py#L31) | 14 | 87.65% |
| [/opt/hostedtoolcache/Python/3.7.13/x64/lib/python3.7/site-packages/exceptiongroup/_formatting.py](https://coveralls.io/builds/51772267/source?filename=%2Fopt%2Fhostedtoolcache%2FPython%2F3.7.13%2Fx64%2Flib%2Fpython3.7%2Fsite-packages%2Fexceptiongroup%2F_formatting.py#L31) | 15 | 86.9% |
| [/opt/hostedtoolcache/Python/3.8.13/x64/lib/python3.8/site-packages/exceptiongroup/_formatting.py](https://coveralls.io/builds/51772267/source?filename=%2Fopt%2Fhostedtoolcache%2FPython%2F3.8.13%2Fx64%2Flib%2Fpython3.8%2Fsite-packages%2Fexceptiongroup%2F_formatting.py#L31) | 15 | 87.06% |
| [/opt/hostedtoolcache/Python/3.9.13/x64/lib/python3.9/site-packages/exceptiongroup/_formatting.py](https://coveralls.io/builds/51772267/source?filename=%2Fopt%2Fhostedtoolcache%2FPython%2F3.9.13%2Fx64%2Flib%2Fpython3.9%2Fsite-packages%2Fexceptiongroup%2F_formatting.py#L31) | 15 | 87.06% |
<!-- | **Total:** | **200** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/51772267/badge)](https://coveralls.io/builds/51772267) |
| :-- | --: |
| Change from base [Build 2862927764](https://coveralls.io/builds/51673975): |  -0.1% |
| Covered Lines: | 1674 |
| Relevant Lines: | 2246 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 19143ad..55d6f93 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,5 +1,3 @@
-exclude: ^src/wheel/vendored
-
 repos:
 - repo: https://github.com/pre-commit/pre-commit-hooks
   rev: v4.3.0
@@ -26,7 +24,7 @@ repos:
   rev: v2.37.3
   hooks:
   - id: pyupgrade
-    args: ["--py37-plus"]
+    args: ["--py37-plus", "--keep-runtime-typing"]
 
 - repo: https://github.com/psf/black
   rev: 22.6.0
diff --git a/CHANGES.rst b/CHANGES.rst
index 655fd7e..f28691e 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -3,6 +3,11 @@ Version history
 
 This library adheres to `Semantic Versioning 2.0 <http://semver.org/>`_.
 
+**UNRELEASED**
+
+- Added custom versions of several ``traceback``  functions that work with exception
+  groups even if monkey patching was disabled or blocked
+
 **1.0.0rc8**
 
 - Don't monkey patch anything if ``sys.excepthook`` has been altered
diff --git a/README.rst b/README.rst
index 47f7e1c..48178d9 100644
--- a/README.rst
+++ b/README.rst
@@ -18,6 +18,14 @@ It contains the following:
   (installed on import)
 * An exception hook that handles formatting of exception groups through
   ``TracebackException`` (installed on import)
+* Special versions of some of the functions from the ``traceback`` module, modified to
+  correctly handle exception groups even when monkey patching is disabled, or blocked by
+  another custom exception hook:
+
+  * ``traceback.format_exception()``
+  * ``traceback.format_exception_only()``
+  * ``traceback.print_exception()``
+  * ``traceback.print_exc()``
 
 If this package is imported on Python 3.11 or later, the built-in implementations of the
 exception group classes are used instead, ``TracebackException`` is not monkey patched
@@ -90,7 +98,24 @@ earlier than 3.11:
    already present. This hook causes the exception to be formatted using
    ``traceback.TracebackException`` rather than the built-in rendered.
 
+If ``sys.exceptionhook`` is found to be set to something else than the default when
+``exceptiongroup`` is imported, no monkeypatching is done at all.
+
 To prevent the exception hook and patches from being installed, set the environment
 variable ``EXCEPTIONGROUP_NO_PATCH`` to ``1``.
 
+Formatting exception groups
+---------------------------
+
+Normally, the monkey patching applied by this library on import will cause exception
+groups to be printed properly in tracebacks. But in cases when the monkey patching is
+blocked by a third party exception hook, or monkey patching is explicitly disabled,
+you can still manually format exceptions using the special versions of the ``traceback``
+functions, like ``format_exception()``, listed at the top of this page. They work just
+like their counterparts in the ``traceback`` module, except that they use a separately
+patched subclass of ``TracebackException`` to perform the rendering.
+
+Particularly in cases where a library installs its own exception hook, it is recommended
+to use these special versions to do the actual formatting of exceptions/tracebacks.
+
 .. _PEP 654: https://www.python.org/dev/peps/pep-0654/
diff --git a/src/exceptiongroup/__init__.py b/src/exceptiongroup/__init__.py
index bc2ee9a..0e7e02b 100644
--- a/src/exceptiongroup/__init__.py
+++ b/src/exceptiongroup/__init__.py
@@ -1,4 +1,12 @@
-__all__ = ["BaseExceptionGroup", "ExceptionGroup", "catch"]
+__all__ = [
+    "BaseExceptionGroup",
+    "ExceptionGroup",
+    "catch",
+    "format_exception",
+    "format_exception_only",
+    "print_exception",
+    "print_exc",
+]
 
 import os
 import sys
@@ -8,6 +16,12 @@ from ._version import version as __version__  # noqa: F401
 
 if sys.version_info < (3, 11):
     from ._exceptions import BaseExceptionGroup, ExceptionGroup
+    from ._formatting import (
+        format_exception,
+        format_exception_only,
+        print_exc,
+        print_exception,
+    )
 
     if os.getenv("EXCEPTIONGROUP_NO_PATCH") != "1":
         from . import _formatting  # noqa: F401
@@ -15,5 +29,12 @@ if sys.version_info < (3, 11):
     BaseExceptionGroup.__module__ = __name__
     ExceptionGroup.__module__ = __name__
 else:
+    from traceback import (
+        format_exception,
+        format_exception_only,
+        print_exc,
+        print_exception,
+    )
+
     BaseExceptionGroup = BaseExceptionGroup
     ExceptionGroup = ExceptionGroup
diff --git a/src/exceptiongroup/_formatting.py b/src/exceptiongroup/_formatting.py
index 63eebf9..2c74162 100644
--- a/src/exceptiongroup/_formatting.py
+++ b/src/exceptiongroup/_formatting.py
@@ -8,8 +8,9 @@ import collections.abc
 import sys
 import textwrap
 import traceback
+from functools import singledispatch
 from types import TracebackType
-from typing import Any
+from typing import Any, List, Optional
 
 from ._exceptions import BaseExceptionGroup
 
@@ -41,64 +42,6 @@ def _safe_string(value, what, func=str):
         return f"<{what} {func.__name__}() failed>"
 
 
-def traceback_exception_init(
-    self,
-    exc_type: type[BaseException],
-    exc_value: BaseException,
-    exc_traceback: TracebackType,
-    *,
-    limit: int | None = None,
-    lookup_lines: bool = True,
-    capture_locals: bool = False,
-    compact: bool = False,
-    _seen: set[int] | None = None,
-) -> None:
-    kwargs: dict[str, Any] = {}
-    if sys.version_info >= (3, 10):
-        kwargs["compact"] = compact
-
-    # Capture the original exception and its cause and context as TracebackExceptions
-    traceback_exception_original_init(
-        self,
-        exc_type,
-        exc_value,
-        exc_traceback,
-        limit=limit,
-        lookup_lines=lookup_lines,
-        capture_locals=capture_locals,
-        _seen=_seen,
-        **kwargs,
-    )
-
-    seen_was_none = _seen is None
-
-    if _seen is None:
-        _seen = set()
-
-    # Capture each of the exceptions in the ExceptionGroup along with each of
-    # their causes and contexts
-    if isinstance(exc_value, BaseExceptionGroup):
-        embedded = []
-        for exc in exc_value.exceptions:
-            if id(exc) not in _seen:
-                embedded.append(
-                    traceback.TracebackException.from_exception(
-                        exc,
-                        limit=limit,
-                        lookup_lines=lookup_lines,
-                        capture_locals=capture_locals,
-                        # copy the set of _seen exceptions so that duplicates
-                        # shared between sub-exceptions are not omitted
-                        _seen=None if seen_was_none else set(_seen),
-                    )
-                )
-        self.exceptions = embedded
-        self.msg = exc_value.message
-    else:
-        self.exceptions = None
-    self.__notes__ = getattr(exc_value, "__notes__", ())
-
-
 class _ExceptionPrintContext:
     def __init__(self):
         self.seen = set()
@@ -122,152 +65,286 @@ class _ExceptionPrintContext:
                 yield textwrap.indent(text, indent_str, lambda line: True)
 
 
-def traceback_exception_format_exception_only(self):
-    """Format the exception part of the traceback.
-    The return value is a generator of strings, each ending in a newline.
-    Normally, the generator emits a single string; however, for
-    SyntaxError exceptions, it emits several lines that (when
-    printed) display detailed information about where the syntax
-    error occurred.
-    The message indicating which exception occurred is always the last
-    string in the output.
-    """
-    if self.exc_type is None:
-        yield traceback._format_final_exc_line(None, self._str)
-        return
-
-    stype = self.exc_type.__qualname__
-    smod = self.exc_type.__module__
-    if smod not in ("__main__", "builtins"):
-        if not isinstance(smod, str):
-            smod = "<unknown>"
-        stype = smod + "." + stype
-
-    if not issubclass(self.exc_type, SyntaxError):
-        yield _format_final_exc_line(stype, self._str)
-    elif traceback_exception_format_syntax_error is not None:
-        yield from traceback_exception_format_syntax_error(self, stype)
-    else:
-        yield from traceback_exception_original_format_exception_only(self)
-
-    if isinstance(self.__notes__, collections.abc.Sequence):
-        for note in self.__notes__:
-            note = _safe_string(note, "note")
-            yield from [line + "\n" for line in note.split("\n")]
-    elif self.__notes__ is not None:
-        yield _safe_string(self.__notes__, "__notes__", func=repr)
-
-
-def traceback_exception_format(self, *, chain=True, _ctx=None):
-    if _ctx is None:
-        _ctx = _ExceptionPrintContext()
-
-    output = []
-    exc = self
-    if chain:
-        while exc:
-            if exc.__cause__ is not None:
-                chained_msg = _cause_message
-                chained_exc = exc.__cause__
-            elif exc.__context__ is not None and not exc.__suppress_context__:
-                chained_msg = _context_message
-                chained_exc = exc.__context__
-            else:
-                chained_msg = None
-                chained_exc = None
+def exceptiongroup_excepthook(
+    etype: type[BaseException], value: BaseException, tb: TracebackType | None
+) -> None:
+    sys.stderr.write("".join(traceback.format_exception(etype, value, tb)))
 
-            output.append((chained_msg, exc))
-            exc = chained_exc
-    else:
-        output.append((None, exc))
-
-    for msg, exc in reversed(output):
-        if msg is not None:
-            yield from _ctx.emit(msg)
-        if exc.exceptions is None:
-            if exc.stack:
-                yield from _ctx.emit("Traceback (most recent call last):\n")
-                yield from _ctx.emit(exc.stack.format())
-            yield from _ctx.emit(exc.format_exception_only())
-        elif _ctx.exception_group_depth > max_group_depth:
-            # exception group, but depth exceeds limit
-            yield from _ctx.emit(f"... (max_group_depth is {max_group_depth})\n")
+
+class PatchedTracebackException(traceback.TracebackException):
+    def __init__(
+        self,
+        exc_type: type[BaseException],
+        exc_value: BaseException,
+        exc_traceback: TracebackType,
+        *,
+        limit: int | None = None,
+        lookup_lines: bool = True,
+        capture_locals: bool = False,
+        compact: bool = False,
+        _seen: set[int] | None = None,
+    ) -> None:
+        kwargs: dict[str, Any] = {}
+        if sys.version_info >= (3, 10):
+            kwargs["compact"] = compact
+
+        # Capture the original exception and its cause and context as
+        # TracebackExceptions
+        traceback_exception_original_init(
+            self,
+            exc_type,
+            exc_value,
+            exc_traceback,
+            limit=limit,
+            lookup_lines=lookup_lines,
+            capture_locals=capture_locals,
+            _seen=_seen,
+            **kwargs,
+        )
+
+        seen_was_none = _seen is None
+
+        if _seen is None:
+            _seen = set()
+
+        # Capture each of the exceptions in the ExceptionGroup along with each of
+        # their causes and contexts
+        if isinstance(exc_value, BaseExceptionGroup):
+            embedded = []
+            for exc in exc_value.exceptions:
+                if id(exc) not in _seen:
+                    embedded.append(
+                        PatchedTracebackException(
+                            type(exc),
+                            exc,
+                            exc.__traceback__,
+                            lookup_lines=lookup_lines,
+                            capture_locals=capture_locals,
+                            # copy the set of _seen exceptions so that duplicates
+                            # shared between sub-exceptions are not omitted
+                            _seen=None if seen_was_none else set(_seen),
+                        )
+                    )
+            self.exceptions = embedded
+            self.msg = exc_value.message
         else:
-            # format exception group
-            is_toplevel = _ctx.exception_group_depth == 0
-            if is_toplevel:
-                _ctx.exception_group_depth += 1
-
-            if exc.stack:
-                yield from _ctx.emit(
-                    "Exception Group Traceback (most recent call last):\n",
-                    margin_char="+" if is_toplevel else None,
-                )
-                yield from _ctx.emit(exc.stack.format())
-
-            yield from _ctx.emit(exc.format_exception_only())
-            num_excs = len(exc.exceptions)
-            if num_excs <= max_group_width:
-                n = num_excs
-            else:
-                n = max_group_width + 1
-            _ctx.need_close = False
-            for i in range(n):
-                last_exc = i == n - 1
-                if last_exc:
-                    # The closing frame may be added by a recursive call
-                    _ctx.need_close = True
-
-                if max_group_width is not None:
-                    truncated = i >= max_group_width
-                else:
-                    truncated = False
-                title = f"{i + 1}" if not truncated else "..."
-                yield (
-                    _ctx.indent()
-                    + ("+-" if i == 0 else "  ")
-                    + f"+---------------- {title} ----------------\n"
-                )
-                _ctx.exception_group_depth += 1
-                if not truncated:
-                    yield from exc.exceptions[i].format(chain=chain, _ctx=_ctx)
+            self.exceptions = None
+        self.__notes__ = getattr(exc_value, "__notes__", ())
+
+    def format(self, *, chain=True, _ctx=None):
+        if _ctx is None:
+            _ctx = _ExceptionPrintContext()
+
+        output = []
+        exc = self
+        if chain:
+            while exc:
+                if exc.__cause__ is not None:
+                    chained_msg = _cause_message
+                    chained_exc = exc.__cause__
+                elif exc.__context__ is not None and not exc.__suppress_context__:
+                    chained_msg = _context_message
+                    chained_exc = exc.__context__
                 else:
-                    remaining = num_excs - max_group_width
-                    plural = "s" if remaining > 1 else ""
-                    yield from _ctx.emit(f"and {remaining} more exception{plural}\n")
+                    chained_msg = None
+                    chained_exc = None
 
-                if last_exc and _ctx.need_close:
-                    yield (_ctx.indent() + "+------------------------------------\n")
-                    _ctx.need_close = False
-                _ctx.exception_group_depth -= 1
-
-            if is_toplevel:
-                assert _ctx.exception_group_depth == 1
-                _ctx.exception_group_depth = 0
+                output.append((chained_msg, exc))
+                exc = chained_exc
+        else:
+            output.append((None, exc))
+
+        for msg, exc in reversed(output):
+            if msg is not None:
+                yield from _ctx.emit(msg)
+            if exc.exceptions is None:
+                if exc.stack:
+                    yield from _ctx.emit("Traceback (most recent call last):\n")
+                    yield from _ctx.emit(exc.stack.format())
+                yield from _ctx.emit(exc.format_exception_only())
+            elif _ctx.exception_group_depth > max_group_depth:
+                # exception group, but depth exceeds limit
+                yield from _ctx.emit(f"... (max_group_depth is {max_group_depth})\n")
+            else:
+                # format exception group
+                is_toplevel = _ctx.exception_group_depth == 0
+                if is_toplevel:
+                    _ctx.exception_group_depth += 1
+
+                if exc.stack:
+                    yield from _ctx.emit(
+                        "Exception Group Traceback (most recent call last):\n",
+                        margin_char="+" if is_toplevel else None,
+                    )
+                    yield from _ctx.emit(exc.stack.format())
 
+                yield from _ctx.emit(exc.format_exception_only())
+                num_excs = len(exc.exceptions)
+                if num_excs <= max_group_width:
+                    n = num_excs
+                else:
+                    n = max_group_width + 1
+                _ctx.need_close = False
+                for i in range(n):
+                    last_exc = i == n - 1
+                    if last_exc:
+                        # The closing frame may be added by a recursive call
+                        _ctx.need_close = True
+
+                    if max_group_width is not None:
+                        truncated = i >= max_group_width
+                    else:
+                        truncated = False
+                    title = f"{i + 1}" if not truncated else "..."
+                    yield (
+                        _ctx.indent()
+                        + ("+-" if i == 0 else "  ")
+                        + f"+---------------- {title} ----------------\n"
+                    )
+                    _ctx.exception_group_depth += 1
+                    if not truncated:
+                        yield from exc.exceptions[i].format(chain=chain, _ctx=_ctx)
+                    else:
+                        remaining = num_excs - max_group_width
+                        plural = "s" if remaining > 1 else ""
+                        yield from _ctx.emit(
+                            f"and {remaining} more exception{plural}\n"
+                        )
+
+                    if last_exc and _ctx.need_close:
+                        yield _ctx.indent() + "+------------------------------------\n"
+                        _ctx.need_close = False
+                    _ctx.exception_group_depth -= 1
+
+                if is_toplevel:
+                    assert _ctx.exception_group_depth == 1
+                    _ctx.exception_group_depth = 0
+
+    def format_exception_only(self):
+        """Format the exception part of the traceback.
+        The return value is a generator of strings, each ending in a newline.
+        Normally, the generator emits a single string; however, for
+        SyntaxError exceptions, it emits several lines that (when
+        printed) display detailed information about where the syntax
+        error occurred.
+        The message indicating which exception occurred is always the last
+        string in the output.
+        """
+        if self.exc_type is None:
+            yield traceback._format_final_exc_line(None, self._str)
+            return
+
+        stype = self.exc_type.__qualname__
+        smod = self.exc_type.__module__
+        if smod not in ("__main__", "builtins"):
+            if not isinstance(smod, str):
+                smod = "<unknown>"
+            stype = smod + "." + stype
+
+        if not issubclass(self.exc_type, SyntaxError):
+            yield _format_final_exc_line(stype, self._str)
+        elif traceback_exception_format_syntax_error is not None:
+            yield from traceback_exception_format_syntax_error(self, stype)
+        else:
+            yield from traceback_exception_original_format_exception_only(self)
 
-def exceptiongroup_excepthook(
-    etype: type[BaseException], value: BaseException, tb: TracebackType | None
-) -> None:
-    sys.stderr.write("".join(traceback.format_exception(etype, value, tb)))
+        if isinstance(self.__notes__, collections.abc.Sequence):
+            for note in self.__notes__:
+                note = _safe_string(note, "note")
+                yield from [line + "\n" for line in note.split("\n")]
+        elif self.__notes__ is not None:
+            yield _safe_string(self.__notes__, "__notes__", func=repr)
 
 
+traceback_exception_original_init = traceback.TracebackException.__init__
+traceback_exception_original_format = traceback.TracebackException.format
+traceback_exception_original_format_exception_only = (
+    traceback.TracebackException.format_exception_only
+)
+traceback_exception_format_syntax_error = getattr(
+    traceback.TracebackException, "_format_syntax_error", None
+)
 if sys.excepthook is sys.__excepthook__:
-    traceback_exception_original_init = traceback.TracebackException.__init__
     traceback.TracebackException.__init__ = (  # type: ignore[assignment]
-        traceback_exception_init
+        PatchedTracebackException.__init__
     )
-    traceback_exception_original_format = traceback.TracebackException.format
     traceback.TracebackException.format = (  # type: ignore[assignment]
-        traceback_exception_format
-    )
-    traceback_exception_original_format_exception_only = (
-        traceback.TracebackException.format_exception_only
+        PatchedTracebackException.format
     )
     traceback.TracebackException.format_exception_only = (  # type: ignore[assignment]
-        traceback_exception_format_exception_only
-    )
-    traceback_exception_format_syntax_error = getattr(
-        traceback.TracebackException, "_format_syntax_error", None
+        PatchedTracebackException.format_exception_only
     )
     sys.excepthook = exceptiongroup_excepthook
+
+
+@singledispatch
+def format_exception_only(__exc: BaseException) -> List[str]:
+    return list(
+        PatchedTracebackException(type(__exc), __exc, None).format_exception_only()
+    )
+
+
+@format_exception_only.register
+def _(__exc: type, value: BaseException) -> List[str]:
+    return format_exception_only(value)
+
+
+@singledispatch
+def format_exception(
+    __exc: BaseException,
+    limit: Optional[int] = None,
+    chain: bool = True,
+) -> List[str]:
+    return list(
+        PatchedTracebackException(
+            type(__exc), __exc, __exc.__traceback__, limit=limit
+        ).format(chain=chain)
+    )
+
+
+@format_exception.register
+def _(
+    __exc: type,
+    value: BaseException,
+    tb: TracebackType,
+    limit: Optional[int] = None,
+    chain: bool = True,
+) -> List[str]:
+    return format_exception(value, limit, chain)
+
+
+@singledispatch
+def print_exception(
+    __exc: BaseException,
+    limit: Optional[int] = None,
+    file: Any = None,
+    chain: bool = True,
+) -> None:
+    if file is None:
+        file = sys.stderr
+
+    for line in PatchedTracebackException(
+        type(__exc), __exc, __exc.__traceback__, limit=limit
+    ).format(chain=chain):
+        print(line, file=file, end="")
+
+
+@print_exception.register
+def _(
+    __exc: type,
+    value: BaseException,
+    tb: TracebackType,
+    limit: Optional[int] = None,
+    file: Any = None,
+    chain: bool = True,
+) -> None:
+    print_exception(value, limit, file, chain)
+
+
+def print_exc(
+    limit: Optional[int] = None,
+    file: Any | None = None,
+    chain: bool = True,
+) -> None:
+    value = sys.exc_info()[1]
+    print_exception(value, limit, file, chain)


Test patch:
diff --git a/tests/test_formatting.py b/tests/test_formatting.py
index 8b58561..0270cb9 100644
--- a/tests/test_formatting.py
+++ b/tests/test_formatting.py
@@ -1,9 +1,15 @@
 import sys
+from typing import NoReturn
+
+import pytest
+from _pytest.capture import CaptureFixture
+from _pytest.fixtures import SubRequest
+from _pytest.monkeypatch import MonkeyPatch
 
 from exceptiongroup import ExceptionGroup
 
 
-def test_formatting(capsys):
+def raise_excgroup() -> NoReturn:
     exceptions = []
     try:
         raise ValueError("foo")
@@ -16,30 +22,64 @@ def test_formatting(capsys):
         exc.__notes__ = ["Note from bar handler"]
         exceptions.append(exc)
 
+    exc = ExceptionGroup("test message", exceptions)
+    exc.add_note("Displays notes attached to the group too")
+    raise exc
+
+
+@pytest.fixture(
+    params=[
+        pytest.param(True, id="patched"),
+        pytest.param(
+            False,
+            id="unpatched",
+            marks=[
+                pytest.mark.skipif(
+                    sys.version_info >= (3, 11),
+                    reason="No patching is done on Python >= 3.11",
+                )
+            ],
+        ),
+    ],
+)
+def patched(request: SubRequest) -> bool:
+    return request.param
+
+
+@pytest.fixture(
+    params=[pytest.param(False, id="newstyle"), pytest.param(True, id="oldstyle")]
+)
+def old_argstyle(request: SubRequest) -> bool:
+    return request.param
+
+
+def test_exceptionhook(capsys: CaptureFixture) -> None:
     try:
-        raise ExceptionGroup("test message", exceptions)
+        raise_excgroup()
     except ExceptionGroup as exc:
-        exc.add_note("Displays notes attached to the group too")
         sys.excepthook(type(exc), exc, exc.__traceback__)
 
-    lineno = test_formatting.__code__.co_firstlineno
+    local_lineno = test_exceptionhook.__code__.co_firstlineno
+    lineno = raise_excgroup.__code__.co_firstlineno
     module_prefix = "" if sys.version_info >= (3, 11) else "exceptiongroup."
     output = capsys.readouterr().err
     assert output == (
         f"""\
   + Exception Group Traceback (most recent call last):
-  |   File "{__file__}", line {lineno + 14}, in test_formatting
-  |     raise ExceptionGroup("test message", exceptions)
+  |   File "{__file__}", line {local_lineno + 2}, in test_exceptionhook
+  |     raise_excgroup()
+  |   File "{__file__}", line {lineno + 15}, in raise_excgroup
+  |     raise exc
   | {module_prefix}ExceptionGroup: test message (2 sub-exceptions)
   | Displays notes attached to the group too
   +-+---------------- 1 ----------------
     | Traceback (most recent call last):
-    |   File "{__file__}", line {lineno + 3}, in test_formatting
+    |   File "{__file__}", line {lineno + 3}, in raise_excgroup
     |     raise ValueError("foo")
     | ValueError: foo
     +---------------- 2 ----------------
     | Traceback (most recent call last):
-    |   File "{__file__}", line {lineno + 8}, in test_formatting
+    |   File "{__file__}", line {lineno + 8}, in raise_excgroup
     |     raise RuntimeError("bar")
     | RuntimeError: bar
     | Note from bar handler
@@ -48,43 +88,34 @@ def test_formatting(capsys):
     )
 
 
-def test_formatting_exception_only(capsys):
-    exceptions = []
-    try:
-        raise ValueError("foo")
-    except ValueError as exc:
-        exceptions.append(exc)
-
+def test_exceptionhook_format_exception_only(capsys: CaptureFixture) -> None:
     try:
-        raise RuntimeError("bar")
-    except RuntimeError as exc:
-        exc.__notes__ = ["Note from bar handler"]
-        exceptions.append(exc)
-
-    try:
-        raise ExceptionGroup("test message", exceptions)
+        raise_excgroup()
     except ExceptionGroup as exc:
-        exc.add_note("Displays notes attached to the group too")
         sys.excepthook(type(exc), exc, exc.__traceback__)
 
-    lineno = test_formatting_exception_only.__code__.co_firstlineno
+    local_lineno = test_exceptionhook_format_exception_only.__code__.co_firstlineno
+    lineno = raise_excgroup.__code__.co_firstlineno
     module_prefix = "" if sys.version_info >= (3, 11) else "exceptiongroup."
     output = capsys.readouterr().err
     assert output == (
         f"""\
   + Exception Group Traceback (most recent call last):
-  |   File "{__file__}", line {lineno + 14}, in test_formatting_exception_only
-  |     raise ExceptionGroup("test message", exceptions)
+  |   File "{__file__}", line {local_lineno + 2}, in \
+test_exceptionhook_format_exception_only
+  |     raise_excgroup()
+  |   File "{__file__}", line {lineno + 15}, in raise_excgroup
+  |     raise exc
   | {module_prefix}ExceptionGroup: test message (2 sub-exceptions)
   | Displays notes attached to the group too
   +-+---------------- 1 ----------------
     | Traceback (most recent call last):
-    |   File "{__file__}", line {lineno + 3}, in test_formatting_exception_only
+    |   File "{__file__}", line {lineno + 3}, in raise_excgroup
     |     raise ValueError("foo")
     | ValueError: foo
     +---------------- 2 ----------------
     | Traceback (most recent call last):
-    |   File "{__file__}", line {lineno + 8}, in test_formatting_exception_only
+    |   File "{__file__}", line {lineno + 8}, in raise_excgroup
     |     raise RuntimeError("bar")
     | RuntimeError: bar
     | Note from bar handler
@@ -93,7 +124,7 @@ def test_formatting_exception_only(capsys):
     )
 
 
-def test_formatting_syntax_error(capsys):
+def test_formatting_syntax_error(capsys: CaptureFixture) -> None:
     try:
         exec("//serser")
     except SyntaxError as exc:
@@ -119,3 +150,185 @@ in test_formatting_syntax_error
 SyntaxError: invalid syntax
 """
     )
+
+
+def test_format_exception(
+    patched: bool, old_argstyle: bool, monkeypatch: MonkeyPatch
+) -> None:
+    if not patched:
+        # Block monkey patching, then force the module to be re-imported
+        del sys.modules["traceback"]
+        del sys.modules["exceptiongroup"]
+        del sys.modules["exceptiongroup._formatting"]
+        monkeypatch.setattr(sys, "excepthook", lambda *args: sys.__excepthook__(*args))
+
+    from exceptiongroup import format_exception
+
+    exceptions = []
+    try:
+        raise ValueError("foo")
+    except ValueError as exc:
+        exceptions.append(exc)
+
+    try:
+        raise RuntimeError("bar")
+    except RuntimeError as exc:
+        exc.__notes__ = ["Note from bar handler"]
+        exceptions.append(exc)
+
+    try:
+        raise_excgroup()
+    except ExceptionGroup as exc:
+        if old_argstyle:
+            lines = format_exception(type(exc), exc, exc.__traceback__)
+        else:
+            lines = format_exception(exc)
+
+        local_lineno = test_format_exception.__code__.co_firstlineno
+        lineno = raise_excgroup.__code__.co_firstlineno
+        assert isinstance(lines, list)
+        module_prefix = "" if sys.version_info >= (3, 11) else "exceptiongroup."
+        assert "".join(lines) == (
+            f"""\
+  + Exception Group Traceback (most recent call last):
+  |   File "{__file__}", line {local_lineno + 25}, in test_format_exception
+  |     raise_excgroup()
+  |   File "{__file__}", line {lineno + 15}, in raise_excgroup
+  |     raise exc
+  | {module_prefix}ExceptionGroup: test message (2 sub-exceptions)
+  | Displays notes attached to the group too
+  +-+---------------- 1 ----------------
+    | Traceback (most recent call last):
+    |   File "{__file__}", line {lineno + 3}, in raise_excgroup
+    |     raise ValueError("foo")
+    | ValueError: foo
+    +---------------- 2 ----------------
+    | Traceback (most recent call last):
+    |   File "{__file__}", line {lineno + 8}, in raise_excgroup
+    |     raise RuntimeError("bar")
+    | RuntimeError: bar
+    | Note from bar handler
+    +------------------------------------
+"""
+        )
+
+
+def test_format_exception_only(
+    patched: bool, old_argstyle: bool, monkeypatch: MonkeyPatch
+) -> None:
+    if not patched:
+        # Block monkey patching, then force the module to be re-imported
+        del sys.modules["traceback"]
+        del sys.modules["exceptiongroup"]
+        del sys.modules["exceptiongroup._formatting"]
+        monkeypatch.setattr(sys, "excepthook", lambda *args: sys.__excepthook__(*args))
+
+    from exceptiongroup import format_exception_only
+
+    try:
+        raise_excgroup()
+    except ExceptionGroup as exc:
+        if old_argstyle:
+            output = format_exception_only(type(exc), exc)
+        else:
+            output = format_exception_only(exc)
+
+        module_prefix = "" if sys.version_info >= (3, 11) else "exceptiongroup."
+        assert output == [
+            f"{module_prefix}ExceptionGroup: test message (2 sub-exceptions)\n",
+            "Displays notes attached to the group too\n",
+        ]
+
+
+def test_print_exception(
+    patched: bool, old_argstyle: bool, monkeypatch: MonkeyPatch, capsys: CaptureFixture
+) -> None:
+    if not patched:
+        # Block monkey patching, then force the module to be re-imported
+        del sys.modules["traceback"]
+        del sys.modules["exceptiongroup"]
+        del sys.modules["exceptiongroup._formatting"]
+        monkeypatch.setattr(sys, "excepthook", lambda *args: sys.__excepthook__(*args))
+
+    from exceptiongroup import print_exception
+
+    try:
+        raise_excgroup()
+    except ExceptionGroup as exc:
+        if old_argstyle:
+            print_exception(type(exc), exc, exc.__traceback__)
+        else:
+            print_exception(exc)
+
+        local_lineno = test_print_exception.__code__.co_firstlineno
+        lineno = raise_excgroup.__code__.co_firstlineno
+        module_prefix = "" if sys.version_info >= (3, 11) else "exceptiongroup."
+        output = capsys.readouterr().err
+        assert output == (
+            f"""\
+  + Exception Group Traceback (most recent call last):
+  |   File "{__file__}", line {local_lineno + 13}, in test_print_exception
+  |     raise_excgroup()
+  |   File "{__file__}", line {lineno + 15}, in raise_excgroup
+  |     raise exc
+  | {module_prefix}ExceptionGroup: test message (2 sub-exceptions)
+  | Displays notes attached to the group too
+  +-+---------------- 1 ----------------
+    | Traceback (most recent call last):
+    |   File "{__file__}", line {lineno + 3}, in raise_excgroup
+    |     raise ValueError("foo")
+    | ValueError: foo
+    +---------------- 2 ----------------
+    | Traceback (most recent call last):
+    |   File "{__file__}", line {lineno + 8}, in raise_excgroup
+    |     raise RuntimeError("bar")
+    | RuntimeError: bar
+    | Note from bar handler
+    +------------------------------------
+"""
+        )
+
+
+def test_print_exc(
+    patched: bool, monkeypatch: MonkeyPatch, capsys: CaptureFixture
+) -> None:
+    if not patched:
+        # Block monkey patching, then force the module to be re-imported
+        del sys.modules["traceback"]
+        del sys.modules["exceptiongroup"]
+        del sys.modules["exceptiongroup._formatting"]
+        monkeypatch.setattr(sys, "excepthook", lambda *args: sys.__excepthook__(*args))
+
+    from exceptiongroup import print_exc
+
+    try:
+        raise_excgroup()
+    except ExceptionGroup:
+        print_exc()
+        local_lineno = test_print_exc.__code__.co_firstlineno
+        lineno = raise_excgroup.__code__.co_firstlineno
+        module_prefix = "" if sys.version_info >= (3, 11) else "exceptiongroup."
+        output = capsys.readouterr().err
+        assert output == (
+            f"""\
+  + Exception Group Traceback (most recent call last):
+  |   File "{__file__}", line {local_lineno + 13}, in test_print_exc
+  |     raise_excgroup()
+  |   File "{__file__}", line {lineno + 15}, in raise_excgroup
+  |     raise exc
+  | {module_prefix}ExceptionGroup: test message (2 sub-exceptions)
+  | Displays notes attached to the group too
+  +-+---------------- 1 ----------------
+    | Traceback (most recent call last):
+    |   File "{__file__}", line {lineno + 3}, in raise_excgroup
+    |     raise ValueError("foo")
+    | ValueError: foo
+    +---------------- 2 ----------------
+    | Traceback (most recent call last):
+    |   File "{__file__}", line {lineno + 8}, in raise_excgroup
+    |     raise RuntimeError("bar")
+    | RuntimeError: bar
+    | Note from bar handler
+    +------------------------------------
+"""
+        )